commit 499020323eed1270cc4bc86be1f5f2ddba679dc2 Author: Michael W. Aziz Date: Thu Feb 27 18:08:08 2025 +0200 [0.1.0] diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..c300356 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "stable" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75dfa95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..88cea9b --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "bae5e49bc2a867403c43b2aae2de8f8c33b037e4" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 + base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 + - platform: android + create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 + base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 + - platform: ios + create_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 + base_revision: bae5e49bc2a867403c43b2aae2de8f8c33b037e4 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1395495 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": ".fvm/versions/stable" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a5a2125 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ + + +## 0.1.0 + +- **INIT**: Initial commit in the new repo. diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..0e86da1 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1 @@ +This repository is Developed, Maintained, and is property of Michael W. Aziz (Micazi). \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c6c02f --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +## Astromic: Helpers + +The helpers module of the **Astromic** Presentation System +Developed, Maintained, and is property of Michael W. Aziz (Micazi) + +### Content + +- Form Helper ☑️ +- Loading Helper ☑️ +- Sheet Helper ☑️ +- Presenting Helper ☑️ +- Dialog Helper +- Listing Helper +- SnackBar Helper diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..9062b84 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,47 @@ +include: package:lints/recommended.yaml + +linter: + rules: + # ==== Project Organization Rules ==== + # Enforces relative imports to maintain project structure and avoid unnecessary long paths + - prefer_relative_imports + + # Ensures that files are named in lowercase_with_underscores format + - file_names + + # ==== Best Practices ==== + # Enforces the closing of streams and sinks to avoid memory leaks + - close_sinks + + # Avoids empty 'else' blocks to reduce confusion and improve code readability + - avoid_empty_else + + # Prefer using 'const' constructors wherever possible for better performance and immutability + - prefer_const_constructors + + # Avoid leading underscores for local variable names to prevent conflicts and improve clarity + - no_leading_underscores_for_local_identifiers + + # ==== Code Consistency ==== + # Avoids the use of 'print' statements in production code, encouraging proper logging instead + - avoid_print + + # Encourages using 'final' for fields that are not reassigned to promote immutability + - prefer_final_fields + + # Ensures that all types are explicitly specified for better readability and type safety + - always_specify_types + + # Avoids redundant default argument values to keep the code clean + - avoid_redundant_argument_values + + # Enforces consistency by preferring single quotes over double quotes for string literals + - prefer_single_quotes + + # ==== Documentation Rules ==== + # Enforces documentation for all public classes, methods, and fields to improve API clarity + # - public_member_api_docs + + # ==== Null Safety ==== + # Avoids unnecessary null checks and encourages the use of null-aware operators + - unnecessary_null_checks diff --git a/lib/astromic_helpers.dart b/lib/astromic_helpers.dart new file mode 100644 index 0000000..49de82f --- /dev/null +++ b/lib/astromic_helpers.dart @@ -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'; diff --git a/lib/dependencies/vsync-provider/vsync_provider.dart b/lib/dependencies/vsync-provider/vsync_provider.dart new file mode 100644 index 0000000..6f4e825 --- /dev/null +++ b/lib/dependencies/vsync-provider/vsync_provider.dart @@ -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(); + if (result != null) { + return result; + } + throw FlutterError('No VsyncProvider ancestor found in the widget tree!'); + } + + //-- + @override + State createState() => VsyncProviderState(); +} + +class VsyncProviderState extends State with TickerProviderStateMixin { + @override + Widget build(BuildContext context) { + //SECTION - Build Return + return widget.child; + //!SECTION + } +} diff --git a/lib/src/form/form_helper.astromic.dart b/lib/src/form/form_helper.astromic.dart new file mode 100644 index 0000000..9dfce09 --- /dev/null +++ b/lib/src/form/form_helper.astromic.dart @@ -0,0 +1,3 @@ +export 'package:form_controller/form_controller.dart'; +export 'src/controller.dart'; +export 'src/form_field.dart'; diff --git a/lib/src/form/src/controller.dart b/lib/src/form/src/controller.dart new file mode 100644 index 0000000..68aed02 --- /dev/null +++ b/lib/src/form/src/controller.dart @@ -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 fieldStates = {}; + final Map fieldMessages = {}; + final Map _hostedValues = {}; + final Map>? streamedErrorMaps; // fieldId: {errorCode: errorMessage} + final Stream>? 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? 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(String id, T data) { + if (_hostedValues.keys.toList().contains(id)) { + _hostedValues[id] = data; + } else { + _hostedValues.addEntries(>[MapEntry(id, data)]); + } + } + + /// Get the value of a hosted state variable using it's ID. + T? getValue(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(id, AstromicFieldState.idle)]); + fieldMessages.addEntries(>[MapEntry(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 dispose() async { + _stateStreamController.close(); + super.dispose(); + } + + //SECTION - Helper Methods + _addInitialControllers() { + // Add in the initial field states... + fieldStates.addEntries(super.controllers?.entries.toList().map((MapEntry e) => MapEntry( + e.key, // controller ID + AstromicFieldState.idle, // Initial state of any new controller is Idle + )) ?? + >{}); + + // Add in the initial field messages... + fieldMessages.addEntries(super.controllers?.entries.toList().map((MapEntry e) => MapEntry( + e.key, // Controller ID + null, // The initial message it has which is Null + )) ?? + >{}); + } + + _handleErrorStream() { + errorStream!.distinct().listen((List 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 +} diff --git a/lib/src/form/src/enums/enums.exports.dart b/lib/src/form/src/enums/enums.exports.dart new file mode 100644 index 0000000..8c0f1a2 --- /dev/null +++ b/lib/src/form/src/enums/enums.exports.dart @@ -0,0 +1 @@ +export 'state.enum.dart'; diff --git a/lib/src/form/src/enums/state.enum.dart b/lib/src/form/src/enums/state.enum.dart new file mode 100644 index 0000000..a7444b5 --- /dev/null +++ b/lib/src/form/src/enums/state.enum.dart @@ -0,0 +1,30 @@ +enum AstromicFieldState { + idle, + withInfo, + withWarning, + withError, + withSuccess, +} + +extension FieldStateExtension on AstromicFieldState { + T valuefyer( + 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; + } + } +} diff --git a/lib/src/form/src/form_field.dart b/lib/src/form/src/form_field.dart new file mode 100644 index 0000000..5979a98 --- /dev/null +++ b/lib/src/form/src/form_field.dart @@ -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 extends StatefulWidget { + //SECTION - Widget Arguments + //S1 -- Shared + final AstromicFormController formController; + final String formID; + final bool? initialObscurity; + // + final AstromicFieldConfiguration? configuration; + final List? 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? 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> createState() => _AstromicFormFieldState(); +} + +class _AstromicFormFieldState extends State> { + // + //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 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 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 validators = []; + // + validators.addAll(widget.validators!); + // + Map checks = {}; + // + for (FormControllerValidator validator in validators) { + bool res = validator.checker(s); + checks.addEntries(>[MapEntry(validator, res)]); + } + // + if (checks.containsValue(false)) { + // It has an Error! + _setFieldErrorState(widget.formID, checks.entries.where((MapEntry 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 validators = []; + // + validators.addAll(widget.validators!); + // + Map checks = {}; + // + for (FormControllerValidator validator in validators) { + bool res = validator.checker(s); + checks.addEntries(>[MapEntry(validator, res)]); + } + // + if (checks.containsValue(false)) { + // It has an Error! + _setFieldErrorState(widget.formID, checks.entries.where((MapEntry 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(); + } +} diff --git a/lib/src/loading/loading_helper.astromic.dart b/lib/src/loading/loading_helper.astromic.dart new file mode 100644 index 0000000..f60e223 --- /dev/null +++ b/lib/src/loading/loading_helper.astromic.dart @@ -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 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 start(BuildContext context, {dynamic startProgress}) async { + context.loaderOverlay.show(progress: startProgress); + return await Future.delayed(_style.inDuration); + } + + /// Hide the loader. + static Future stop(BuildContext context, {dynamic startProgress}) async { + context.loaderOverlay.hide(); + return await Future.delayed(_style.outDuration); + } + + /// Update the progres in the builder. + static Future updateProgress(BuildContext context, dynamic progress) async { + context.loaderOverlay.progress(progress); + return await Future.delayed(Duration(milliseconds: (_style.inDuration.inMilliseconds / 2).floor())); + } + + /// Quickly add in a loading segment for an Async function. + static Future load( + BuildContext context, + Future Function(Future Function(dynamic p) updateProgress) process, { + dynamic startingProgress, + Function(dynamic e, StackTrace trace)? onFail, + }) async { + Completer c = Completer(); + // + 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; + } +} diff --git a/lib/src/loading/src/models/models.exports.dart b/lib/src/loading/src/models/models.exports.dart new file mode 100644 index 0000000..0688b6c --- /dev/null +++ b/lib/src/loading/src/models/models.exports.dart @@ -0,0 +1 @@ +export 'style.model.dart'; diff --git a/lib/src/loading/src/models/style.model.dart b/lib/src/loading/src/models/style.model.dart new file mode 100644 index 0000000..e41f252 --- /dev/null +++ b/lib/src/loading/src/models/style.model.dart @@ -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 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 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)'; + } +} diff --git a/lib/src/loading/src/src.exports.dart b/lib/src/loading/src/src.exports.dart new file mode 100644 index 0000000..5771d5f --- /dev/null +++ b/lib/src/loading/src/src.exports.dart @@ -0,0 +1 @@ +export 'models/models.exports.dart'; diff --git a/lib/src/presenting/presenting_helper.astromic.dart b/lib/src/presenting/presenting_helper.astromic.dart new file mode 100644 index 0000000..4d9bd94 --- /dev/null +++ b/lib/src/presenting/presenting_helper.astromic.dart @@ -0,0 +1,3 @@ +export './src/enums/enums.exports.dart'; +export './src/models/models.exports.dart'; +export './src/widgets/widgets.exports.dart'; diff --git a/lib/src/presenting/src/enums/enums.exports.dart b/lib/src/presenting/src/enums/enums.exports.dart new file mode 100644 index 0000000..4b67866 --- /dev/null +++ b/lib/src/presenting/src/enums/enums.exports.dart @@ -0,0 +1 @@ +export './presenter_state.enum.dart'; diff --git a/lib/src/presenting/src/enums/presenter_state.enum.dart b/lib/src/presenting/src/enums/presenter_state.enum.dart new file mode 100644 index 0000000..c215e36 --- /dev/null +++ b/lib/src/presenting/src/enums/presenter_state.enum.dart @@ -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 +} diff --git a/lib/src/presenting/src/helpers/helpers.exports.dart b/lib/src/presenting/src/helpers/helpers.exports.dart new file mode 100644 index 0000000..a0e80b7 --- /dev/null +++ b/lib/src/presenting/src/helpers/helpers.exports.dart @@ -0,0 +1 @@ +export './snapshot_helper.dart'; diff --git a/lib/src/presenting/src/helpers/snapshot_helper.dart b/lib/src/presenting/src/helpers/snapshot_helper.dart new file mode 100644 index 0000000..3f6acb0 --- /dev/null +++ b/lib/src/presenting/src/helpers/snapshot_helper.dart @@ -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? returnModel) stateGetter( + AsyncSnapshot 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? 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(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(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(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(initialData: previousData, oldBatchId: oldBatchId, batchId: currentBatchId); + } else { + //S3 -- The loaded data is NOT Empty + rState = AstromicPresenterState.loaded; + rModel = PresenterSuccessReturnModel(data: data, initialData: previousData, oldBatchId: oldBatchId, batchId: currentBatchId); + } + } else { + //S2 -- Done loading but there is NO data! (errored) + rState = AstromicPresenterState.error; + rModel = PresenterFailureReturnModel(error: error, initialData: previousData, oldBatchId: oldBatchId, batchId: currentBatchId); + } + } + // + return (rState, rModel); + } +} diff --git a/lib/src/presenting/src/models/models.exports.dart b/lib/src/presenting/src/models/models.exports.dart new file mode 100644 index 0000000..2e516fc --- /dev/null +++ b/lib/src/presenting/src/models/models.exports.dart @@ -0,0 +1,2 @@ +export './presenter_return.model.dart'; +export './presenter_configuration.model.dart'; diff --git a/lib/src/presenting/src/models/presenter_configuration.model.dart b/lib/src/presenting/src/models/presenter_configuration.model.dart new file mode 100644 index 0000000..3bb7c00 --- /dev/null +++ b/lib/src/presenting/src/models/presenter_configuration.model.dart @@ -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, + ); + } +} diff --git a/lib/src/presenting/src/models/presenter_return.model.dart b/lib/src/presenting/src/models/presenter_return.model.dart new file mode 100644 index 0000000..6db0eb4 --- /dev/null +++ b/lib/src/presenting/src/models/presenter_return.model.dart @@ -0,0 +1,22 @@ +class PresenterReturnModel { + 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 extends PresenterReturnModel { + PresenterSuccessReturnModel({super.initialData, super.data, super.error, required super.batchId, super.oldBatchId}); +} + +class PresenterFailureReturnModel extends PresenterReturnModel { + PresenterFailureReturnModel({super.initialData, super.data, super.error, required super.batchId, super.oldBatchId}); +} diff --git a/lib/src/presenting/src/widgets/future_presenter.widget.dart b/lib/src/presenting/src/widgets/future_presenter.widget.dart new file mode 100644 index 0000000..113f535 --- /dev/null +++ b/lib/src/presenting/src/widgets/future_presenter.widget.dart @@ -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 extends StatefulWidget { + //SECTION - Widget Arguments + final AstromicPresenterController futureController; + final String futureId; + // + final Map? r)> stateBuilder; + final AstromicPresenterConfiguration? configuration; + //!SECTION + + const AstromicFuturePresenter({ + super.key, + required this.futureController, + required this.futureId, + required this.stateBuilder, + this.configuration, + }); + + @override + State> createState() => _AstromicFuturePresenterState(); +} + +class _AstromicFuturePresenterState extends State> { + //SECTION - State Variables + //s1 --State + Future? _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 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 _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?; + } + //!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( + key: ValueKey(_refreshKey), // This ensures FutureBuilder re-runs + future: _future, + builder: (context, snapshot) { + int? oldBatchId = previousBatchId; + int currentBatchId = snapshot.hashCode; + + (AstromicPresenterState state, PresenterReturnModel? returnModel) ret = SnapshotHelper.stateGetter(snapshot, previousData, previousError, _configs, oldBatchId, currentBatchId); + AstromicPresenterState returnedState = ret.$1; + PresenterReturnModel? 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 + } +} diff --git a/lib/src/presenting/src/widgets/presenter_controller.widget.dart b/lib/src/presenting/src/widgets/presenter_controller.widget.dart new file mode 100644 index 0000000..ecd83e2 --- /dev/null +++ b/lib/src/presenting/src/widgets/presenter_controller.widget.dart @@ -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 _futures; + late final Map _streams; + + late final Map> _futureRefreshers; + + AstromicPresenterController({ + Map futures = const {}, + Map streams = const {}, + }) : _futures = futures.map((k, v) => MapEntry(k, (v.$1, v.$2))), + _futureRefreshers = futures.map((k, v) => MapEntry(k, StreamController.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? getFuture(String id) { + assert(_futures.containsKey(id), 'did you add a future with this id?'); + + return _futures[id]!.$1!() as Future?; + } + + /// Get the stream using it's ID. + Stream? getStream(String id) { + assert(_streams.containsKey(id), 'did you add a stream with this id?'); + + return _streams[id]! as Stream?; + } + + /// Get the refresh stream of a future using it's ID. + Stream getRefreshStream(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 disposeFuture(String id) async { + assert(_futures.containsKey(id), 'did you add a future with this id?'); + + return await _futureRefreshers[id]!.close(); + } +} diff --git a/lib/src/presenting/src/widgets/stream_presenter.widget.dart b/lib/src/presenting/src/widgets/stream_presenter.widget.dart new file mode 100644 index 0000000..d33e5f1 --- /dev/null +++ b/lib/src/presenting/src/widgets/stream_presenter.widget.dart @@ -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 extends StatefulWidget { + //SECTION - Widget Arguments + final AstromicPresenterController controller; + final String streamId; + // + final Map? r)> stateBuilder; + final AstromicPresenterConfiguration? configuration; + //!SECTION + // + const AstromicStreamPresenter({ + super.key, + required this.controller, + required this.streamId, + required this.stateBuilder, + this.configuration, + }); + + @override + State> createState() => _AstromicStreamPresenterState(); +} + +class _AstromicStreamPresenterState extends State> { + // + //SECTION - State Variables + //s1 --State + Stream? _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 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 _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 _initializeStream() { + _stream = widget.controller.getStream(widget.streamId)?.asyncMap((dynamic result) => result) as Stream?; + } + //!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( + stream: _stream, + builder: (BuildContext context, AsyncSnapshot snapshot) { + // + int? oldBatchId = previousBatchId; + int currentBatchId = snapshot.hashCode; + // + (AstromicPresenterState state, PresenterReturnModel? returnModel) ret = SnapshotHelper.stateGetter(snapshot, previousData, previousError, _configs, oldBatchId, currentBatchId); + AstromicPresenterState returnedState = ret.$1; + PresenterReturnModel? 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 + } +} diff --git a/lib/src/presenting/src/widgets/widgets.exports.dart b/lib/src/presenting/src/widgets/widgets.exports.dart new file mode 100644 index 0000000..c32ca69 --- /dev/null +++ b/lib/src/presenting/src/widgets/widgets.exports.dart @@ -0,0 +1,3 @@ +export 'presenter_controller.widget.dart'; +export 'future_presenter.widget.dart'; +export 'stream_presenter.widget.dart'; diff --git a/lib/src/sheet/sheet_helper.astromic.dart b/lib/src/sheet/sheet_helper.astromic.dart new file mode 100644 index 0000000..d4dad8d --- /dev/null +++ b/lib/src/sheet/sheet_helper.astromic.dart @@ -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 flex( + 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( + 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( + sheetConfiguration: sheetConfigs, + sheetStyle: sheetStyle, + // + headSection: headSection, + contentSection: contentSection, + footerSection: footerSection, + ), + ); + } + + /// Show FLEX sheet using a pre-set FlexSheetTemplate. + static Future flexTemplate(BuildContext context, AstromicFlexSheetTemplate template) async => await flex( + 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 form( + 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( + 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( + sheetType: SheetType.form, + // + sheetConfiguration: sheetConfigs, + sheetStyle: sheetStyle, + // + headSectionFormBuilder: headSectionBuilder, + contentSectionFormBuilder: contentSectionBuilder, + footerSectionFormBuilder: footerSectionBuilder, + ), + ); + } + + /// Show Form sheet using a pre-set FormSheetTemplate. + static Future formTemplate(BuildContext c, AstromicFormSheetTemplate template) async => await form( + 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 scroller( + 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( + 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) + ? [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() + : [ + 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( + 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 scrollerTemplate(BuildContext context, AstromicScrollerSheetTemplate template) async => await scroller( + context, + headSectionBuilder: template.headSectionBuilder, + contentSectionBuilder: template.contentSectionBuilder, + footerSectionBuilder: template.footerSectionBuilder, + // + configuration: template.configuration, + style: template.style, + ); +} diff --git a/lib/src/sheet/src/enums/enums.exports.dart b/lib/src/sheet/src/enums/enums.exports.dart new file mode 100644 index 0000000..76ec7b0 --- /dev/null +++ b/lib/src/sheet/src/enums/enums.exports.dart @@ -0,0 +1 @@ +export 'type.enum.dart'; diff --git a/lib/src/sheet/src/enums/type.enum.dart b/lib/src/sheet/src/enums/type.enum.dart new file mode 100644 index 0000000..ced57d4 --- /dev/null +++ b/lib/src/sheet/src/enums/type.enum.dart @@ -0,0 +1,5 @@ +enum SheetType { + flex, + form, + scroller, +} diff --git a/lib/src/sheet/src/models/Templates/flex_sheet.template.dart b/lib/src/sheet/src/models/Templates/flex_sheet.template.dart new file mode 100644 index 0000000..4310b80 --- /dev/null +++ b/lib/src/sheet/src/models/Templates/flex_sheet.template.dart @@ -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 extends AstromicSheetTemplate { + 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 copyWith({ + Widget? headSection, + Widget? contentSection, + Widget? footerSection, + AstromicSheetConfiguration? configuration, + AstromicSheetStyle? style, + }) { + return AstromicFlexSheetTemplate( + headSection: headSection ?? this.headSection, + contentSection: contentSection ?? this.contentSection, + footerSection: footerSection ?? this.footerSection, + configuration: configuration ?? this.configuration, + style: style ?? this.style, + ); + } +} diff --git a/lib/src/sheet/src/models/Templates/form_sheet.template.dart b/lib/src/sheet/src/models/Templates/form_sheet.template.dart new file mode 100644 index 0000000..dbf005f --- /dev/null +++ b/lib/src/sheet/src/models/Templates/form_sheet.template.dart @@ -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 extends AstromicSheetTemplate { + 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 copyWith({ + Widget Function(AstromicFormController)? headSectionBuilder, + Widget Function(AstromicFormController)? contentSectionBuilder, + Widget Function(AstromicFormController)? footerSectionBuilder, + AstromicSheetConfiguration? configuration, + AstromicSheetStyle? style, + }) { + return AstromicFormSheetTemplate( + headSectionBuilder: headSectionBuilder ?? this.headSectionBuilder, + contentSectionBuilder: contentSectionBuilder ?? this.contentSectionBuilder, + footerSectionBuilder: footerSectionBuilder ?? this.footerSectionBuilder, + configuration: configuration ?? this.configuration, + style: style ?? this.style, + ); + } +} diff --git a/lib/src/sheet/src/models/Templates/scroller_sheet.template.dart b/lib/src/sheet/src/models/Templates/scroller_sheet.template.dart new file mode 100644 index 0000000..dd2c3ba --- /dev/null +++ b/lib/src/sheet/src/models/Templates/scroller_sheet.template.dart @@ -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 extends AstromicSheetTemplate { + 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 copyWith({ + Widget Function(ScrollController)? headSection, + Widget Function(ScrollController, ScrollPhysics)? contentSectionBuilder, + Widget Function(ScrollController)? footerSection, + AstromicSheetConfiguration? configuration, + AstromicSheetStyle? style, + }) { + return AstromicScrollerSheetTemplate( + headSectionBuilder: headSection ?? this.headSectionBuilder, + contentSectionBuilder: contentSectionBuilder ?? this.contentSectionBuilder, + footerSectionBuilder: footerSection ?? this.footerSectionBuilder, + configuration: configuration ?? this.configuration, + style: style ?? this.style, + ); + } +} diff --git a/lib/src/sheet/src/models/Templates/template_base.dart b/lib/src/sheet/src/models/Templates/template_base.dart new file mode 100644 index 0000000..a467655 --- /dev/null +++ b/lib/src/sheet/src/models/Templates/template_base.dart @@ -0,0 +1 @@ +abstract class AstromicSheetTemplate {} diff --git a/lib/src/sheet/src/models/Templates/templates.exports.dart b/lib/src/sheet/src/models/Templates/templates.exports.dart new file mode 100644 index 0000000..760d2eb --- /dev/null +++ b/lib/src/sheet/src/models/Templates/templates.exports.dart @@ -0,0 +1,3 @@ +export 'flex_sheet.template.dart'; +export 'form_sheet.template.dart'; +export 'scroller_sheet.template.dart'; diff --git a/lib/src/sheet/src/models/configuration.model.dart b/lib/src/sheet/src/models/configuration.model.dart new file mode 100644 index 0000000..3d6953a --- /dev/null +++ b/lib/src/sheet/src/models/configuration.model.dart @@ -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, + ); + } +} diff --git a/lib/src/sheet/src/models/models.exports.dart b/lib/src/sheet/src/models/models.exports.dart new file mode 100644 index 0000000..41c0f98 --- /dev/null +++ b/lib/src/sheet/src/models/models.exports.dart @@ -0,0 +1,3 @@ +export 'configuration.model.dart'; +export 'style.model.dart'; +export 'templates/templates.exports.dart'; diff --git a/lib/src/sheet/src/models/style.model.dart b/lib/src/sheet/src/models/style.model.dart new file mode 100644 index 0000000..f31169b --- /dev/null +++ b/lib/src/sheet/src/models/style.model.dart @@ -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, + ); + } +} diff --git a/lib/src/sheet/src/widgets/base.dart b/lib/src/sheet/src/widgets/base.dart new file mode 100644 index 0000000..7c1ff0f --- /dev/null +++ b/lib/src/sheet/src/widgets/base.dart @@ -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 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> createState() => _BaseSheetWidgetState(); +} + +class _BaseSheetWidgetState extends State> { + // + //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 flexChildren = [ + 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 formChildren = [ + 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 scrollerChildren = [ + 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(); + } +} diff --git a/lib/src/sheet/src/widgets/basic.sheet.dart b/lib/src/sheet/src/widgets/basic.sheet.dart new file mode 100644 index 0000000..8a6a7f9 --- /dev/null +++ b/lib/src/sheet/src/widgets/basic.sheet.dart @@ -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 show({ + 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( + backgroundColor: Colors.transparent, + barrierColor: barrierColor, + // + enableDrag: enableSheetInteraction, + isDismissible: enableOutsideInteraction, + useRootNavigator: useRootNavigator, + // + isScrollControlled: true, + transitionAnimationController: controller, + // + context: context, + builder: (BuildContext c) { + return Wrap(children: [ClipRRect(borderRadius: BorderRadius.vertical(top: Radius.circular(radius)), child: child)]); + }, + ); + } +} diff --git a/lib/src/sheet/src/widgets/scroller.sheet.dart b/lib/src/sheet/src/widgets/scroller.sheet.dart new file mode 100644 index 0000000..dbd0820 --- /dev/null +++ b/lib/src/sheet/src/widgets/scroller.sheet.dart @@ -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 extends StatefulWidget { + // + final List? 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 createState() => ScrollerSheetState(); + + static Future show({ + 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? 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? ret; + AnimationController controller = AnimationController( + vsync: vsyncState, + ); + + controller.duration = forwardAnimationDuration; + controller.reverseDuration = reverseAnimationDuration; + controller.drive(CurveTween(curve: animationCurve ?? Curves.easeOut)); + + ret = showModalBottomSheet( + 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( + key: key, + builder: builder, + stops: stops, + initialStop: initialStop, + dragThreshold: dragThreshold, + physics: physics, + topInset: topInset, + enableSheetInteraction: enableSheetInteraction, + ), + ); + }, + ); + return ret; + } +} + +class ScrollerSheetState extends State> with SingleTickerProviderStateMixin { + List? _stops; + int? _currentStop; + int? _targetStop; + bool _dragging = false; + bool _closing = false; + double? _dragOffset; + double? _closingHeight; + ScrollController? _scrollController; + ScrollPhysics? _scrollPhysics; + Animation? _animation; + AnimationController? _animationController; + Tween? _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(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 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)), + ); + }), + ); + } +} diff --git a/lib/src/sheet/src/widgets/widgets.exports.dart b/lib/src/sheet/src/widgets/widgets.exports.dart new file mode 100644 index 0000000..fb2477d --- /dev/null +++ b/lib/src/sheet/src/widgets/widgets.exports.dart @@ -0,0 +1,3 @@ +export 'base.dart'; +export 'basic.sheet.dart'; +export 'scroller.sheet.dart'; diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..2e7e582 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,490 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + astromic_elements: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: "2f77d5d81328298952d3f07092d2aea2e6c11a44" + url: "https://git.micazi.dev/micazi/astromic_elements.git" + source: git + version: "0.1.0" + astromic_extensions: + dependency: transitive + description: + path: "." + ref: HEAD + resolved-ref: bccde3fe9f3d47b29e08ac6d287c2bc06f6d855c + url: "https://stag-git.micazi.dev/micazi/astromic_extensions.git" + source: git + version: "0.1.0" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + back_button_interceptor: + dependency: transitive + description: + name: back_button_interceptor + sha256: b85977faabf4aeb95164b3b8bf81784bed4c54ea1aef90a036ab6927ecf80c5a + url: "https://pub.dev" + source: hosted + version: "8.0.4" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + url: "https://pub.dev" + source: hosted + version: "2.0.17" + flutter_switch: + dependency: transitive + description: + name: flutter_switch + sha256: b91477f926bba135d2d203d7b24367492662d8d9c3aa6adb960b14c1087d3c41 + url: "https://pub.dev" + source: hosted + version: "0.3.2" + form_controller: + dependency: "direct main" + description: + name: form_controller + sha256: "5e0f905cc8802ac787bd295900a122fa2a0b1a26070477666de0b9c3b4263e0c" + url: "https://pub.dev" + source: hosted + version: "0.8.8+2" + http: + dependency: transitive + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + lints: + dependency: transitive + description: + name: lints + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + loader_overlay: + dependency: "direct main" + description: + name: loader_overlay + sha256: "285c9ccab9a42a392ba948bd0b14376fd0ee9ddd7b63e3018bcd54460fd3e021" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.dev" + source: hosted + version: "2.2.15" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" + url: "https://pub.dev" + source: hosted + version: "2.5.4+6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" + url: "https://pub.dev" + source: hosted + version: "2.4.1+1" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + url: "https://pub.dev" + source: hosted + version: "3.3.0+3" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + url: "https://pub.dev" + source: hosted + version: "1.1.18" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + url: "https://pub.dev" + source: hosted + version: "1.1.16" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" +sdks: + dart: ">=3.7.0-0 <4.0.0" + flutter: ">3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..802f897 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,25 @@ +name: astromic_helpers +description: The helpers module of the Astromic Presentation System +publish_to: "none" +version: 0.1.0 + +environment: + sdk: ">=3.6.0" + flutter: ">3.27.0" + +dev_dependencies: + flutter_lints: ^4.0.0 + +dependencies: + flutter: + sdk: flutter + ##S1 ~ Astromic + astromic_elements: + git: + url: https://git.micazi.dev/micazi/astromic_elements.git + ref: master + ##S1 ~ 3rd-party + ## Needed for the form helper. + form_controller: ^0.8.8+2 + ## Needed for the loading helper. + loader_overlay: ^5.0.0