From e37e1e4398dc09c98faefdaa16078891e3706e6e Mon Sep 17 00:00:00 2001 From: bmceachnie <60017181+McQuenji@users.noreply.github.com> Date: Fri, 10 Nov 2023 20:26:35 +0100 Subject: [PATCH 01/15] added canPatch and getInstructions to PatchingProgressProviderState --- .../providers/patching_progress_provider.dart | 2 +- .../patching_progress_provider_state.dart | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/features/update/domain/providers/patching_progress_provider.dart b/lib/features/update/domain/providers/patching_progress_provider.dart index 2ac0e006..43945a00 100644 --- a/lib/features/update/domain/providers/patching_progress_provider.dart +++ b/lib/features/update/domain/providers/patching_progress_provider.dart @@ -5,7 +5,7 @@ import 'package:riverpod/riverpod.dart'; /// /// NOTE: Resolves to `null` if no patching is in progress. /// -/// To start patching, use [patchingProgressController]. +/// To start patching or get instructions, use [patchingProgressController]. final patchingProgressProvider = StateNotifierProvider( (ref) { diff --git a/lib/features/update/domain/providers/patching_progress_provider_state.dart b/lib/features/update/domain/providers/patching_progress_provider_state.dart index ba5fe332..393f8a1b 100644 --- a/lib/features/update/domain/providers/patching_progress_provider_state.dart +++ b/lib/features/update/domain/providers/patching_progress_provider_state.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:lb_planner/features/update/update.dart'; import 'package:riverpod/riverpod.dart'; @@ -17,7 +18,9 @@ class PatchingProgressProviderState extends StateNotifier { PatchingProgressProviderState(this.patcherService, this.releaseRepository) : super(null); - /// Starts patching the current app. + /// Downloads and installs the latest version of the app. + /// + /// Throws an [UnsupportedError] if [canPatch] returns `false`. Future patch() async { var latest = await releaseRepository.getLatestRelease(); @@ -31,4 +34,17 @@ class PatchingProgressProviderState extends StateNotifier { }, ); } + + /// Whether the app can be patched automatically. + /// + /// If this returns `false`, the user will have to manually install the update. + /// + /// In this case [patch] will throw an [UnsupportedError]. Use [getInstructions] to receive instructions for manually installing the update. + bool get canPatch => patcherService.canPatch; + + /// Returns the instructions for manually installing a given release in markdown format. + /// + /// Throws an [UnsupportedError] if [canPatch] returns `true`. + String getInstructions(BuildContext context, Release release) => + patcherService.getInstructions(context, release); } From 5282f5d1a90111946e844d7f2ba2e512717da13d Mon Sep 17 00:00:00 2001 From: Can42 Date: Sun, 12 Nov 2023 03:20:53 +0100 Subject: [PATCH 02/15] cproject structure created --- assets/svg/app_icon.svg | 5 + .../update/presentation/presentation.dart | 2 + .../update/presentation/screens/screens.dart | 1 + .../presentation/screens/update_screen.dart | 123 ++++++++++++++++++ .../update/presentation/widgets/widgets.dart | 1 + lib/features/update/update.dart | 1 + .../widgets/localized_widget.dart | 30 +++++ lib/shared/presentation/widgets/markdown.dart | 105 +++++++++++++++ 8 files changed, 268 insertions(+) create mode 100644 assets/svg/app_icon.svg create mode 100644 lib/features/update/presentation/presentation.dart create mode 100644 lib/features/update/presentation/screens/screens.dart create mode 100644 lib/features/update/presentation/screens/update_screen.dart create mode 100644 lib/features/update/presentation/widgets/widgets.dart create mode 100644 lib/shared/presentation/widgets/localized_widget.dart create mode 100644 lib/shared/presentation/widgets/markdown.dart diff --git a/assets/svg/app_icon.svg b/assets/svg/app_icon.svg new file mode 100644 index 00000000..9fe6729c --- /dev/null +++ b/assets/svg/app_icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/lib/features/update/presentation/presentation.dart b/lib/features/update/presentation/presentation.dart new file mode 100644 index 00000000..998a0979 --- /dev/null +++ b/lib/features/update/presentation/presentation.dart @@ -0,0 +1,2 @@ +export 'widgets/widgets.dart'; +export 'screens/screens.dart'; diff --git a/lib/features/update/presentation/screens/screens.dart b/lib/features/update/presentation/screens/screens.dart new file mode 100644 index 00000000..c4e80770 --- /dev/null +++ b/lib/features/update/presentation/screens/screens.dart @@ -0,0 +1 @@ +export 'update_screen.dart'; diff --git a/lib/features/update/presentation/screens/update_screen.dart b/lib/features/update/presentation/screens/update_screen.dart new file mode 100644 index 00000000..0f0ab992 --- /dev/null +++ b/lib/features/update/presentation/screens/update_screen.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:lb_planner/shared/shared.dart'; + +/// Update route. + +@RoutePage() +class UpdateScreen extends StatefulWidget { + /// Update route. + const UpdateScreen({Key? key}) : super(key: key); + + /// Factor multiplied by the screen width to get the padding. + static const paddingFactor = 0.1; + + /// Size of the app icon. + static const double iconSize = 60; + + @override + State createState() => _UpdateScreenState(); +} + +class _UpdateScreenState extends State { + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, ref, _) { + var updater = ref.watch(updateController); + var update = ref.watch(updateProvider); + + void showCommand() { + WidgetsBinding.instance.addPostFrameCallback((_) { + lpShowAlertDialog( + context, + title: t.update_dialog_title, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NcBodyText( + kSetupType.canAutoUpdate + ? t.update_dialog_helpNeeded + : t.update_dialog_noAutoUpdate, + overflow: TextOverflow.visible, + ), + Spacing.large(), + LpSnippet(code: update.command), + ], + ), + ); + }); + } + + if (update.command.isNotEmpty) { + showCommand(); + } + + if (update.downloadStatus == DownloadStatus.error) { + Catcher.reportCheckedError( + kUpdateErrorKeyword + update.error, StackTrace.current); + } + + return Padding( + padding: EdgeInsets.symmetric( + horizontal: + MediaQuery.of(context).size.width * UpdateScreen.paddingFactor, + vertical: + MediaQuery.of(context).size.height * UpdateScreen.paddingFactor, + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + VectorImage('assets/svg/app_icon.svg', + height: UpdateScreen.iconSize), + Spacing.small(), + NcTitleText( + t.update_patchNotes(update.latestVersionName), + fontSize: RouteTitle.titleSize, + ), + ], + ), + if (update.downloadStatus == DownloadStatus.idle) + Button( + onPressed: updater.upgrade, + text: t.update_btn, + ), + if (update.downloadStatus == DownloadStatus.error) + Button( + onPressed: updater.upgrade, + text: t.update_btnErr, + ), + if (update.downloadStatus == DownloadStatus.downlaoding) + Row( + children: [ + NcBodyText( + t.update_downloading( + update.fileName, update.progress), + ), + Spacing.small(), + CircularProgressIndicator(), + ], + ), + if (update.downloadStatus == DownloadStatus.installing && + update.command.isEmpty) + NcBodyText(t.update_installing), + if (update.downloadStatus == DownloadStatus.installing && + update.command.isNotEmpty) + LpButton(onPressed: showCommand, text: t.update_btnInstall), + ], + ), + Spacing.large(), + Expanded( + child: Markdown(update.patchNotes), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/update/presentation/widgets/widgets.dart b/lib/features/update/presentation/widgets/widgets.dart new file mode 100644 index 00000000..205234c6 --- /dev/null +++ b/lib/features/update/presentation/widgets/widgets.dart @@ -0,0 +1 @@ +//Export widgets \ No newline at end of file diff --git a/lib/features/update/update.dart b/lib/features/update/update.dart index 9ac5c00a..a0743a91 100644 --- a/lib/features/update/update.dart +++ b/lib/features/update/update.dart @@ -1 +1,2 @@ export "domain/domain.dart"; +export 'presentation/presentation.dart'; diff --git a/lib/shared/presentation/widgets/localized_widget.dart b/lib/shared/presentation/widgets/localized_widget.dart new file mode 100644 index 00000000..771791eb --- /dev/null +++ b/lib/shared/presentation/widgets/localized_widget.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +/// Provides localized strings for the app. +abstract class LocalizedWidget extends StatefulWidget { + /// Provides localized strings for the app. + const LocalizedWidget({Key? key}) : super(key: key); + + /// Creates the widget. + Widget build(BuildContext context, AppLocalizations t); + + @override + State createState() => _LocalizedWidgetState(); +} + +class _LocalizedWidgetState extends State { + @override + Widget build(BuildContext context) => widget.build(context, t); +} + +/// Provides localized strings for the app. +extension ContextLocalization on BuildContext { + /// Provides localized strings for the app. + AppLocalizations get t => AppLocalizations.of(this)!; +} + +/// Provides localized strings for the app. +extension StateLocalization on State { + /// Provides localized strings for the app. + AppLocalizations get t => context.t; +} diff --git a/lib/shared/presentation/widgets/markdown.dart b/lib/shared/presentation/widgets/markdown.dart new file mode 100644 index 00000000..8f1b29bf --- /dev/null +++ b/lib/shared/presentation/widgets/markdown.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +/// Themed [Markdown] widget. +/// +/// Also allows for loading files from the internet. +class Markdown extends LocalizedWidget { + /// Themed [Markdown] widget. + const Markdown(this.data, {Key? key}) + : source = null, + assert(data != null), + super(key: key); + + /// Themed [Markdown] widget. + /// + /// Loads markdown from the given [source]. + const Markdown.network(this.source, {Key? key}) + : data = null, + assert(source != null), + super(key: key); + + /// The markdown source to display. + final String? data; + + /// The url to load the markdown from. + final Uri? source; + + @override + Widget build(context, t) { + return ConditionalWidget( + condition: data == null, + trueWidget: (_) => FutureBuilder( + future: http.get(source!), + builder: (context, snapshot) { + if (snapshot.hasData) { + // ignore: no-magic-number + if (snapshot.data!.statusCode == 200) + return _buildMarkdown(context, snapshot.data!.body); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + LpIcon( + FontAwesome.exclamation_triangle, + color: errorColor, + size: UpdateRoute.iconSize, + ), + NcSpacing.small(), + NcCaptionText( + t.widgets_markdown_networkError(source.toString()), + textAlign: TextAlign.center, + ), + NcSpacing.large(), + LpButton( + text: t.widgets_markdown_networkError_openInBrowser, + onPressed: () => launchUrl(source!), + ), + ], + ); + } else { + return Center( + child: LpLoadingIndicator.circular(), + ); + } + }, + ), + falseWidget: (_) => _buildMarkdown(context, data!), + ); + } + + Widget _buildMarkdown(BuildContext context, String data) { + return Markdown( + data: data, + styleSheet: MarkdownStyleSheet.fromTheme( + Theme.of(context), + ).copyWith( + blockquoteDecoration: BoxDecoration( + color: primaryColor, + borderRadius: BorderRadius.circular(kRadius), + ), + a: TextStyle(color: accentColor), + h1: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), + h2: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), + h3: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), + h4: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), + h5: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), + h6: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), + code: TextStyle( + color: textColor, + backgroundColor: primaryColor, + fontWeight: FontWeight.bold, + letterSpacing: 1, + ), + ), + onTapLink: (text, href, title) { + if (href == null) return; + + launchUrl(Uri.parse(href)); + }, + checkboxBuilder: (checked) => LpCheckbox( + value: checked, + ), + ); + } +} From e07d4356c65dfbaaf12bd140a88135295bf7fba4 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:01:06 +0100 Subject: [PATCH 03/15] implementing started --- .../presentation/screens/update_screen.dart | 17 +- .../widgets/conditional_widget.dart | 26 ++ lib/shared/presentation/widgets/dialog.dart | 357 ++++++++++++++++++ .../widgets/localized_widget.dart | 30 -- lib/shared/presentation/widgets/markdown.dart | 82 ++-- lib/shared/presentation/widgets/snippet.dart | 49 +++ lib/shared/presentation/widgets/widgets.dart | 5 + 7 files changed, 492 insertions(+), 74 deletions(-) create mode 100644 lib/shared/presentation/widgets/conditional_widget.dart create mode 100644 lib/shared/presentation/widgets/dialog.dart delete mode 100644 lib/shared/presentation/widgets/localized_widget.dart create mode 100644 lib/shared/presentation/widgets/snippet.dart diff --git a/lib/features/update/presentation/screens/update_screen.dart b/lib/features/update/presentation/screens/update_screen.dart index 0f0ab992..58a83f4a 100644 --- a/lib/features/update/presentation/screens/update_screen.dart +++ b/lib/features/update/presentation/screens/update_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lb_planner/shared/shared.dart'; +import 'package:lb_planner/features/update/update.dart'; /// Update route. @@ -24,12 +25,18 @@ class _UpdateScreenState extends State { Widget build(BuildContext context) { return Consumer( builder: (context, ref, _) { - var updater = ref.watch(updateController); - var update = ref.watch(updateProvider); + //var updater = ref.watch(updateController); + //var update = ref.watch(updateProvider); + final patchingProgressController = patchingProgressProvider.notifier; + + final isUpdateAvailableProvider = + AsyncNotifierProvider( + () => IsUpdateAvailableProviderState(), + ); void showCommand() { WidgetsBinding.instance.addPostFrameCallback((_) { - lpShowAlertDialog( + showAlertDialog( context, title: t.update_dialog_title, body: Column( @@ -42,7 +49,7 @@ class _UpdateScreenState extends State { overflow: TextOverflow.visible, ), Spacing.large(), - LpSnippet(code: update.command), + Snippet(code: update.command), ], ), ); @@ -112,7 +119,7 @@ class _UpdateScreenState extends State { ), Spacing.large(), Expanded( - child: Markdown(update.patchNotes), + child: MarkdownView(update.patchNotes), ), ], ), diff --git a/lib/shared/presentation/widgets/conditional_widget.dart b/lib/shared/presentation/widgets/conditional_widget.dart new file mode 100644 index 00000000..453872a8 --- /dev/null +++ b/lib/shared/presentation/widgets/conditional_widget.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +/// Use this to conditionally swap between widgets. +class ConditionalWidget extends StatelessWidget { + /// Constructs a new [ConditionalWidget] with the given [condition], [ifTrue] and [ifFalse]. + /// If [condition] is true, [ifTrue] will be used. + /// If [condition] is false, [ifFalse] will be used. + const ConditionalWidget( + {Key? key, + required this.condition, + required this.ifTrue, + required this.ifFalse}) + : super(key: key); + + /// The condition to check. + final bool condition; + + /// The widget to show if the condition is true. + final Widget ifTrue; + + /// The widget to show if the condition is false. + final Widget ifFalse; + + @override + Widget build(BuildContext context) => condition ? ifTrue : ifFalse; +} diff --git a/lib/shared/presentation/widgets/dialog.dart b/lib/shared/presentation/widgets/dialog.dart new file mode 100644 index 00000000..9827b681 --- /dev/null +++ b/lib/shared/presentation/widgets/dialog.dart @@ -0,0 +1,357 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:lb_planner/shared/shared.dart'; + +/// Themed [Dialog] widget. +class Dialog extends StatefulWidget { + /// Themed ConfirmDialog widget. + Dialog.confirm({ + Key? key, + required this.header, + required this.body, + this.confirmText, + this.cancelText, + required this.onConfirm, + this.onCancel, + required this.removeFromWidgetTree, + this.confirmIsBad = true, + required this.scrollable, + }) : super(key: key) { + confirmOnly = false; + } + + /// Themed [AlertDialog] widget with just one button. + Dialog.alert({ + Key? key, + required this.header, + required this.body, + this.onConfirm, + this.confirmText, + required this.removeFromWidgetTree, + required this.scrollable, + }) : super(key: key) { + confirmIsBad = false; + confirmOnly = true; + } + + /// The header of the dialog. + final Widget? header; + + /// The body of the dialog. + final Widget body; + + /// Whether the confirm button should have errorColor as it's background color. + late final bool confirmIsBad; + + /// The text of the confirm button. + final String? confirmText; + + /// The text of the cancel button. + late final String? cancelText; + + /// The boolean that determines if the dialog has just one button. + late final bool confirmOnly; + + /// Whether the dialog body should be scrollable. + final bool scrollable; + + /// The callback that is called when the user confirms the dialog. + final Function()? onConfirm; + + /// Called when the dialog has to be removed from the widget tree. + final VoidCallback removeFromWidgetTree; + + /// The callback that is called when the user cancels the dialog. + late final Function()? onCancel; + + /// The padding between the elements in the dialog. + static const double padding = 20; + + /// The width factor of the body in the dialog. + static const double widthFactor = .5; + + /// The height factor of the body in the dialog. + static const double heightFactor = .8; + + /// The font size of the buttons in the dialog. + static const double btnFontSize = 16; + + /// The padding of the buttons in the dialog. + static const double btnPadding = 14; + + /// The font size of the title. + static const double titleFontSize = 30; + + @override + State createState() => _DialogState(); +} + +class _DialogState extends State with TickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + _controller = + AnimationController(vsync: this, duration: Duration(milliseconds: 300)); + _controller.forward(); + super.initState(); + focusNode.addListener(_ensureFocus); + + focusNode.requestFocus(); + } + + Future close() async { + await _controller.reverse(); + widget.removeFromWidgetTree(); + } + + void _ensureFocus() async { + if (focusNode.hasFocus) return; + + await Future.delayed(Duration(milliseconds: 300)); + + if (!mounted) return; + + focusNode.requestFocus(); + } + + final focusNode = FocusNode(skipTraversal: true); + + @override + Widget build(BuildContext context) { + return RawKeyboardListener( + autofocus: true, + focusNode: focusNode, + onKey: (event) { + if (event.logicalKey == LogicalKeyboardKey.escape && + focusNode.hasPrimaryFocus) { + close(); + } + }, + child: FadeTransition( + // ignore: no-magic-number + opacity: Tween(begin: 0.4, end: 1).animate( + CurvedAnimation( + parent: _controller, + curve: Interval(0.0, 0.5), + ), + ), + child: ScaleTransition( + child: AlertDialog( + title: widget.header, + titlePadding: EdgeInsets.all(Dialog.padding), + buttonPadding: + EdgeInsets.only(left: Dialog.padding, right: Dialog.padding), + contentPadding: EdgeInsets.only( + bottom: Dialog.padding, + left: Dialog.padding, + right: Dialog.padding), + content: ConstrainedBox( + constraints: BoxConstraints( + minWidth: + MediaQuery.of(context).size.width * Dialog.widthFactor, + maxHeight: + MediaQuery.of(context).size.height * Dialog.heightFactor, + maxWidth: + MediaQuery.of(context).size.width * Dialog.widthFactor, + ), + child: AnimatedSize( + duration: Duration(milliseconds: 300), + curve: Curves.easeOutCubic, + child: ConditionalWrapper( + condition: widget.scrollable, + wrapper: (context, child) => SingleChildScrollView( + controller: ScrollController(), + child: GestureDetector( + onTap: () => + FocusManager.instance.primaryFocus?.unfocus(), + child: child, + ), + ), + child: widget.body, + ), + ), + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!widget.confirmOnly) + ElevatedButton( + child: Text(widget.cancelText ?? t.dialog_cancel), + style: ElevatedButton.styleFrom( + backgroundColor: widget.confirmIsBad + ? context.theme.colorScheme.primary + : context.theme.colorScheme.error, + textStyle: TextStyle(fontSize: Dialog.btnFontSize), + padding: EdgeInsets.all(Dialog.btnPadding), + ), + onPressed: () async { + await close(); + widget.onCancel?.call(); + }, + ), + Spacing.medium(), + ElevatedButton( + child: Text(widget.confirmText ?? + (widget.confirmOnly + ? t.alertDialog_confirm + : t.dialog_confirm)), + style: ElevatedButton.styleFrom( + backgroundColor: widget.confirmIsBad + ? context.theme.colorScheme.error + : context.theme.colorScheme.primary, + textStyle: TextStyle(fontSize: Dialog.btnFontSize), + padding: EdgeInsets.all(Dialog.btnPadding), + ), + onPressed: () async { + await close(); + widget.onConfirm?.call(); + }, + ), + ], + ) + ], + backgroundColor: context.theme.colorScheme.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(5)), + ), + ), + // ignore: no-magic-number + scale: Tween(begin: 1, end: 0.85).animate( + CurvedAnimation( + parent: _controller, + curve: Interval(0.0, 0.5, curve: Curves.easeOut), + ), + ), + ), + ), + ); + } +} + +/// Themed ConfirmDialog widget. +void showConfirmDialog( + BuildContext context, { + String? title, + Widget? header, + Widget? body, + String? confirmText, + String? cancelText, + Function()? onConfirm, + Function()? onCancel, + String? message, + bool confirmIsBad = true, + bool scrollable = true, +}) { + assert(body != null || message != null, + 'Either body or message must be provided.'); + assert(header != null || title != null, + 'Either header or title must be provided.'); + + var key = GlobalKey<_DialogState>(); + + OverlayEntry? dialogOverLay; + OverlayEntry background = + _generateBackground(() => key.currentState!.close()); + + var dialog = Dialog.confirm( + key: key, + header: header ?? + Text( + title!, + style: TextStyle(fontSize: Dialog.titleFontSize), + ), + body: ConditionalWidget( + condition: body != null, + ifTrue: body!, + ifFalse: Text(message!, + style: TextStyle( + overflow: TextOverflow.visible, + fontSize: 12, + letterSpacing: 0.4)), + ), + confirmText: confirmText, + cancelText: cancelText, + onConfirm: onConfirm, + onCancel: onCancel, + confirmIsBad: confirmIsBad, + scrollable: scrollable, + removeFromWidgetTree: () { + dialogOverLay!.remove(); + background.remove(); + }, + ); + + dialogOverLay = OverlayEntry( + builder: (context) => dialog, + ); + + Overlay.of(context).insert(background); + Overlay.of(context).insert(dialogOverLay); +} + +/// Themed [AlertDialog] widget. +void showAlertDialog( + BuildContext context, { + String? title, + Widget? header, + Widget? body, + String? message, + String? confirmText, + Function()? onConfirm, + bool scrollable = true, +}) { + assert(body != null || message != null, + 'Either body or message must be provided.'); + assert(header != null || title != null, + 'Either header or title must be provided.'); + var key = GlobalKey<_DialogState>(); + + OverlayEntry? dialogOverLay; + OverlayEntry background = + _generateBackground(() => key.currentState!.close()); + + var dialog = Dialog.alert( + key: key, + header: header ?? + Text( + title!, + style: TextStyle(fontSize: Dialog.titleFontSize), + ), + body: ConditionalWidget( + condition: body != null, + ifTrue: body!, + ifFalse: Text(message!, + style: TextStyle( + overflow: TextOverflow.visible, + fontSize: 12, + letterSpacing: 0.4)), + ), + onConfirm: onConfirm, + confirmText: confirmText, + scrollable: scrollable, + removeFromWidgetTree: () { + dialogOverLay!.remove(); + background.remove(); + }, + ); + + dialogOverLay = OverlayEntry( + builder: (context) => dialog, + ); + + Overlay.of(context).insert(background); + Overlay.of(context).insert(dialogOverLay); +} + +OverlayEntry _generateBackground(Function() dismiss) { + return OverlayEntry( + builder: (context) => GestureDetector( + onTap: dismiss, + child: Container( + color: Colors.black38, + ), + ), + ); +} diff --git a/lib/shared/presentation/widgets/localized_widget.dart b/lib/shared/presentation/widgets/localized_widget.dart deleted file mode 100644 index 771791eb..00000000 --- a/lib/shared/presentation/widgets/localized_widget.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Provides localized strings for the app. -abstract class LocalizedWidget extends StatefulWidget { - /// Provides localized strings for the app. - const LocalizedWidget({Key? key}) : super(key: key); - - /// Creates the widget. - Widget build(BuildContext context, AppLocalizations t); - - @override - State createState() => _LocalizedWidgetState(); -} - -class _LocalizedWidgetState extends State { - @override - Widget build(BuildContext context) => widget.build(context, t); -} - -/// Provides localized strings for the app. -extension ContextLocalization on BuildContext { - /// Provides localized strings for the app. - AppLocalizations get t => AppLocalizations.of(this)!; -} - -/// Provides localized strings for the app. -extension StateLocalization on State { - /// Provides localized strings for the app. - AppLocalizations get t => context.t; -} diff --git a/lib/shared/presentation/widgets/markdown.dart b/lib/shared/presentation/widgets/markdown.dart index 8f1b29bf..4eb70000 100644 --- a/lib/shared/presentation/widgets/markdown.dart +++ b/lib/shared/presentation/widgets/markdown.dart @@ -1,19 +1,24 @@ import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_vector_icons/flutter_vector_icons.dart'; +import 'package:lb_planner/shared/shared.dart'; +import 'package:url_launcher/url_launcher.dart'; -/// Themed [Markdown] widget. +/// Themed [MarkdownView] widget. /// /// Also allows for loading files from the internet. -class Markdown extends LocalizedWidget { - /// Themed [Markdown] widget. - const Markdown(this.data, {Key? key}) +class MarkdownView extends ConsumerWidget { + /// Themed [MarkdownView] widget. + const MarkdownView(this.data, {Key? key}) : source = null, assert(data != null), super(key: key); - /// Themed [Markdown] widget. + /// Themed [MarkdownView] widget. /// /// Loads markdown from the given [source]. - const Markdown.network(this.source, {Key? key}) + const MarkdownView.network(this.source, {Key? key}) : data = null, assert(source != null), super(key: key); @@ -25,46 +30,48 @@ class Markdown extends LocalizedWidget { final Uri? source; @override - Widget build(context, t) { + Widget build(context, ref) { + final networkService = ref.watch(networkServiceProvider); + return ConditionalWidget( condition: data == null, - trueWidget: (_) => FutureBuilder( - future: http.get(source!), + ifTrue: FutureBuilder( + future: networkService.get(source.toString()), builder: (context, snapshot) { if (snapshot.hasData) { - // ignore: no-magic-number - if (snapshot.data!.statusCode == 200) + if (snapshot.data!.statusCode == 200) { return _buildMarkdown(context, snapshot.data!.body); + } return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - LpIcon( + Icon( FontAwesome.exclamation_triangle, - color: errorColor, - size: UpdateRoute.iconSize, - ), - NcSpacing.small(), - NcCaptionText( - t.widgets_markdown_networkError(source.toString()), - textAlign: TextAlign.center, + color: context.theme.colorScheme.error, + size: 60, ), - NcSpacing.large(), - LpButton( - text: t.widgets_markdown_networkError_openInBrowser, + Spacing.small(), + Text(context.t.widgets_markdown_networkError(source.toString()), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, letterSpacing: 0.4)), + Spacing.large(), + ElevatedButton( + child: Text( + context.t.widgets_markdown_networkError_openInBrowser), onPressed: () => launchUrl(source!), - ), + ) ], ); } else { return Center( - child: LpLoadingIndicator.circular(), + child: CircularProgressIndicator(), ); } }, ), - falseWidget: (_) => _buildMarkdown(context, data!), + ifFalse: _buildMarkdown(context, data!), ); } @@ -75,19 +82,19 @@ class Markdown extends LocalizedWidget { Theme.of(context), ).copyWith( blockquoteDecoration: BoxDecoration( - color: primaryColor, - borderRadius: BorderRadius.circular(kRadius), + color: context.theme.colorScheme.primary, + borderRadius: BorderRadius.circular(5), ), - a: TextStyle(color: accentColor), - h1: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), - h2: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), - h3: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), - h4: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), - h5: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), - h6: NcBaseText.style(fontWeight: FontWeight.bold, fontSize: null), + a: TextStyle(color: context.theme.colorScheme.primary), + h1: TextStyle(fontWeight: FontWeight.bold, fontSize: null), + h2: TextStyle(fontWeight: FontWeight.bold, fontSize: null), + h3: TextStyle(fontWeight: FontWeight.bold, fontSize: null), + h4: TextStyle(fontWeight: FontWeight.bold, fontSize: null), + h5: TextStyle(fontWeight: FontWeight.bold, fontSize: null), + h6: TextStyle(fontWeight: FontWeight.bold, fontSize: null), code: TextStyle( - color: textColor, - backgroundColor: primaryColor, + color: context.theme.textTheme.bodyLarge!.color!, + backgroundColor: context.theme.colorScheme.primary, fontWeight: FontWeight.bold, letterSpacing: 1, ), @@ -97,9 +104,6 @@ class Markdown extends LocalizedWidget { launchUrl(Uri.parse(href)); }, - checkboxBuilder: (checked) => LpCheckbox( - value: checked, - ), ); } } diff --git a/lib/shared/presentation/widgets/snippet.dart b/lib/shared/presentation/widgets/snippet.dart new file mode 100644 index 00000000..d4a684ce --- /dev/null +++ b/lib/shared/presentation/widgets/snippet.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:lb_planner/shared/shared.dart'; + +/// Displays a snippet of code with a copy button. +class Snippet extends LocalizedWidget { + /// Displays a snippet of code with a copy button. + const Snippet({Key? key, required this.code}) : super(key: key); + + /// The code to display. + final String code; + + @override + Widget build(context, t) { + var color = context.theme.colorScheme.primary.withOpacity(0.5); + + return Container( + decoration: BoxDecoration( + color: context.theme.colorScheme.secondary, + borderRadius: BorderRadius.circular(5), + ), + padding: EdgeInsets.all(Spacing.smallSpacing), + child: Row( + children: [ + Expanded( + child: SingleChildScrollView( + controller: ScrollController(), + scrollDirection: Axis.horizontal, + clipBehavior: Clip.hardEdge, + child: Text( + code, + style: TextStyle( + color: context.theme.colorScheme.primary, fontSize: 16), + ), + ), + ), + Spacing.xs(), + HoverBuilder( + builder: (context, hovering) => Icon( + Icons.copy, + color: hovering ? context.theme.colorScheme.primary : color, + ), + onTap: () => Clipboard.setData(ClipboardData(text: code)), + ), + ], + ), + ); + } +} diff --git a/lib/shared/presentation/widgets/widgets.dart b/lib/shared/presentation/widgets/widgets.dart index 6e24c528..efa8ed7f 100644 --- a/lib/shared/presentation/widgets/widgets.dart +++ b/lib/shared/presentation/widgets/widgets.dart @@ -10,3 +10,8 @@ export 'scale_on_hover.dart'; export 'offset_on_hover.dart'; export 'hover_builder.dart'; export 'hoverable_widget.dart'; +export 'conditional_widget.dart'; +export 'dialog.dart'; +export 'localized_widget.dart'; +export 'markdown.dart'; +export 'snippet.dart'; From 83246f1a1ab518f25fe506b2fdf1b74667f59cd6 Mon Sep 17 00:00:00 2001 From: bmceachnie <60017181+McQuenji@users.noreply.github.com> Date: Wed, 15 Nov 2023 18:30:26 +0100 Subject: [PATCH 04/15] adjusted providers to be easier to use --- .../domain/models/patching_progress.dart | 6 +- .../models/patching_progress.freezed.dart | 56 +++++++++---------- .../is_update_available_provider_state.dart | 24 ++++++-- .../providers/patching_progress_provider.dart | 4 +- .../patching_progress_provider_state.dart | 15 ++--- 5 files changed, 59 insertions(+), 46 deletions(-) diff --git a/lib/features/update/domain/models/patching_progress.dart b/lib/features/update/domain/models/patching_progress.dart index 48887503..02aef8ed 100644 --- a/lib/features/update/domain/models/patching_progress.dart +++ b/lib/features/update/domain/models/patching_progress.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lb_planner/shared/shared.dart'; +import 'package:lb_planner/features/update/update.dart'; part 'patching_progress.freezed.dart'; @@ -8,8 +8,8 @@ part 'patching_progress.freezed.dart'; class PatchingProgress with _$PatchingProgress { /// Represents the progress of patching the current app. factory PatchingProgress({ - /// The version that is being patched. - required Version version, + /// The release that is currently being patched. + required Release release, /// The progress of the patching process. required double progress, diff --git a/lib/features/update/domain/models/patching_progress.freezed.dart b/lib/features/update/domain/models/patching_progress.freezed.dart index 7f4f77fa..625057e5 100644 --- a/lib/features/update/domain/models/patching_progress.freezed.dart +++ b/lib/features/update/domain/models/patching_progress.freezed.dart @@ -16,8 +16,8 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$PatchingProgress { - /// The version that is being patched. - Version get version => throw _privateConstructorUsedError; + /// The release that is currently being patched. + Release get release => throw _privateConstructorUsedError; /// The progress of the patching process. double get progress => throw _privateConstructorUsedError; @@ -33,9 +33,9 @@ abstract class $PatchingProgressCopyWith<$Res> { PatchingProgress value, $Res Function(PatchingProgress) then) = _$PatchingProgressCopyWithImpl<$Res, PatchingProgress>; @useResult - $Res call({Version version, double progress}); + $Res call({Release release, double progress}); - $VersionCopyWith<$Res> get version; + $ReleaseCopyWith<$Res> get release; } /// @nodoc @@ -51,14 +51,14 @@ class _$PatchingProgressCopyWithImpl<$Res, $Val extends PatchingProgress> @pragma('vm:prefer-inline') @override $Res call({ - Object? version = null, + Object? release = null, Object? progress = null, }) { return _then(_value.copyWith( - version: null == version - ? _value.version - : version // ignore: cast_nullable_to_non_nullable - as Version, + release: null == release + ? _value.release + : release // ignore: cast_nullable_to_non_nullable + as Release, progress: null == progress ? _value.progress : progress // ignore: cast_nullable_to_non_nullable @@ -68,9 +68,9 @@ class _$PatchingProgressCopyWithImpl<$Res, $Val extends PatchingProgress> @override @pragma('vm:prefer-inline') - $VersionCopyWith<$Res> get version { - return $VersionCopyWith<$Res>(_value.version, (value) { - return _then(_value.copyWith(version: value) as $Val); + $ReleaseCopyWith<$Res> get release { + return $ReleaseCopyWith<$Res>(_value.release, (value) { + return _then(_value.copyWith(release: value) as $Val); }); } } @@ -83,10 +83,10 @@ abstract class _$$_PatchingProgressCopyWith<$Res> __$$_PatchingProgressCopyWithImpl<$Res>; @override @useResult - $Res call({Version version, double progress}); + $Res call({Release release, double progress}); @override - $VersionCopyWith<$Res> get version; + $ReleaseCopyWith<$Res> get release; } /// @nodoc @@ -100,14 +100,14 @@ class __$$_PatchingProgressCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? version = null, + Object? release = null, Object? progress = null, }) { return _then(_$_PatchingProgress( - version: null == version - ? _value.version - : version // ignore: cast_nullable_to_non_nullable - as Version, + release: null == release + ? _value.release + : release // ignore: cast_nullable_to_non_nullable + as Release, progress: null == progress ? _value.progress : progress // ignore: cast_nullable_to_non_nullable @@ -119,11 +119,11 @@ class __$$_PatchingProgressCopyWithImpl<$Res> /// @nodoc class _$_PatchingProgress implements _PatchingProgress { - _$_PatchingProgress({required this.version, required this.progress}); + _$_PatchingProgress({required this.release, required this.progress}); - /// The version that is being patched. + /// The release that is currently being patched. @override - final Version version; + final Release release; /// The progress of the patching process. @override @@ -131,7 +131,7 @@ class _$_PatchingProgress implements _PatchingProgress { @override String toString() { - return 'PatchingProgress(version: $version, progress: $progress)'; + return 'PatchingProgress(release: $release, progress: $progress)'; } @override @@ -139,13 +139,13 @@ class _$_PatchingProgress implements _PatchingProgress { return identical(this, other) || (other.runtimeType == runtimeType && other is _$_PatchingProgress && - (identical(other.version, version) || other.version == version) && + (identical(other.release, release) || other.release == release) && (identical(other.progress, progress) || other.progress == progress)); } @override - int get hashCode => Object.hash(runtimeType, version, progress); + int get hashCode => Object.hash(runtimeType, release, progress); @JsonKey(ignore: true) @override @@ -156,13 +156,13 @@ class _$_PatchingProgress implements _PatchingProgress { abstract class _PatchingProgress implements PatchingProgress { factory _PatchingProgress( - {required final Version version, + {required final Release release, required final double progress}) = _$_PatchingProgress; @override - /// The version that is being patched. - Version get version; + /// The release that is currently being patched. + Release get release; @override /// The progress of the patching process. diff --git a/lib/features/update/domain/providers/is_update_available_provider_state.dart b/lib/features/update/domain/providers/is_update_available_provider_state.dart index 3575bfd2..15bfda1e 100644 --- a/lib/features/update/domain/providers/is_update_available_provider_state.dart +++ b/lib/features/update/domain/providers/is_update_available_provider_state.dart @@ -10,17 +10,33 @@ class IsUpdateAvailableProviderState extends AsyncNotifier { /// The [ReleaseRepository] instance to use. late final ReleaseRepository releaseRepository; + late Release _latestRelease; + + /// The latest release available. + Release get latestRelease => _latestRelease; + @override - FutureOr build() { + FutureOr build() async { releaseRepository = ref.watch(releaseRepositoryProvider); - return releaseRepository.isUpdateAvailable(); + return _checkForUpdates(); } - /// Checks whether an update is available. + /// Checks if a new release has been published. Future checkForUpdates() async { state = AsyncLoading(); - state = await AsyncValue.guard(releaseRepository.isUpdateAvailable); + state = await AsyncValue.guard(_checkForUpdates); + } + + /// INTERNAL USE ONLY! + /// + /// Checks whether an update is available. + /// + /// Called by [build] and [checkForUpdates]. + Future _checkForUpdates() async { + _latestRelease = await releaseRepository.getLatestRelease(); + + return releaseRepository.isUpdateAvailable(); } } diff --git a/lib/features/update/domain/providers/patching_progress_provider.dart b/lib/features/update/domain/providers/patching_progress_provider.dart index 43945a00..1f8e48d9 100644 --- a/lib/features/update/domain/providers/patching_progress_provider.dart +++ b/lib/features/update/domain/providers/patching_progress_provider.dart @@ -10,11 +10,11 @@ final patchingProgressProvider = StateNotifierProvider( (ref) { final patcherService = ref.watch(patcherServiceProvider); - final releaseRepository = ref.watch(releaseRepositoryProvider); + final updateChecker = ref.watch(updateCheckerProvider); return PatchingProgressProviderState( patcherService, - releaseRepository, + updateChecker.latestRelease, ); }, ); diff --git a/lib/features/update/domain/providers/patching_progress_provider_state.dart b/lib/features/update/domain/providers/patching_progress_provider_state.dart index 393f8a1b..a18362d6 100644 --- a/lib/features/update/domain/providers/patching_progress_provider_state.dart +++ b/lib/features/update/domain/providers/patching_progress_provider_state.dart @@ -9,26 +9,23 @@ class PatchingProgressProviderState extends StateNotifier { /// The [PatcherService] instance to use for patching. final PatcherService patcherService; - /// The [ReleaseRepository] instance to use for fetching releases. - final ReleaseRepository releaseRepository; + /// The latest [Release] available. + final Release latest; /// Provides the current [PatchingProgress]. /// /// NOTE: Resolves to `null` if no patching is in progress. - PatchingProgressProviderState(this.patcherService, this.releaseRepository) - : super(null); + PatchingProgressProviderState(this.patcherService, this.latest) : super(null); /// Downloads and installs the latest version of the app. /// /// Throws an [UnsupportedError] if [canPatch] returns `false`. Future patch() async { - var latest = await releaseRepository.getLatestRelease(); - await patcherService.patch( latest, onProgress: (progress) { state = PatchingProgress( - version: latest.version, + release: latest, progress: progress, ); }, @@ -45,6 +42,6 @@ class PatchingProgressProviderState extends StateNotifier { /// Returns the instructions for manually installing a given release in markdown format. /// /// Throws an [UnsupportedError] if [canPatch] returns `true`. - String getInstructions(BuildContext context, Release release) => - patcherService.getInstructions(context, release); + String getInstructions(BuildContext context) => + patcherService.getInstructions(context, latest); } From aebc89f0e6e58c884b6de69cf5aea653426df654 Mon Sep 17 00:00:00 2001 From: bmceachnie <60017181+McQuenji@users.noreply.github.com> Date: Wed, 15 Nov 2023 18:52:34 +0100 Subject: [PATCH 05/15] added error handling to patchinProgressProvider --- .../domain/models/patching_progress.dart | 6 ++ .../models/patching_progress.freezed.dart | 68 +++++++++++++++++-- .../patching_progress_provider_state.dart | 28 +++++--- 3 files changed, 86 insertions(+), 16 deletions(-) diff --git a/lib/features/update/domain/models/patching_progress.dart b/lib/features/update/domain/models/patching_progress.dart index 02aef8ed..05171e6d 100644 --- a/lib/features/update/domain/models/patching_progress.dart +++ b/lib/features/update/domain/models/patching_progress.dart @@ -13,5 +13,11 @@ class PatchingProgress with _$PatchingProgress { /// The progress of the patching process. required double progress, + + /// The error that occurred during patching, if any. + Object? error, + + /// The stack trace of the error that occurred during patching, if any. + StackTrace? stackTrace, }) = _PatchingProgress; } diff --git a/lib/features/update/domain/models/patching_progress.freezed.dart b/lib/features/update/domain/models/patching_progress.freezed.dart index 625057e5..7a320940 100644 --- a/lib/features/update/domain/models/patching_progress.freezed.dart +++ b/lib/features/update/domain/models/patching_progress.freezed.dart @@ -22,6 +22,12 @@ mixin _$PatchingProgress { /// The progress of the patching process. double get progress => throw _privateConstructorUsedError; + /// The error that occurred during patching, if any. + Object? get error => throw _privateConstructorUsedError; + + /// The stack trace of the error that occurred during patching, if any. + StackTrace? get stackTrace => throw _privateConstructorUsedError; + @JsonKey(ignore: true) $PatchingProgressCopyWith get copyWith => throw _privateConstructorUsedError; @@ -33,7 +39,11 @@ abstract class $PatchingProgressCopyWith<$Res> { PatchingProgress value, $Res Function(PatchingProgress) then) = _$PatchingProgressCopyWithImpl<$Res, PatchingProgress>; @useResult - $Res call({Release release, double progress}); + $Res call( + {Release release, + double progress, + Object? error, + StackTrace? stackTrace}); $ReleaseCopyWith<$Res> get release; } @@ -53,6 +63,8 @@ class _$PatchingProgressCopyWithImpl<$Res, $Val extends PatchingProgress> $Res call({ Object? release = null, Object? progress = null, + Object? error = freezed, + Object? stackTrace = freezed, }) { return _then(_value.copyWith( release: null == release @@ -63,6 +75,11 @@ class _$PatchingProgressCopyWithImpl<$Res, $Val extends PatchingProgress> ? _value.progress : progress // ignore: cast_nullable_to_non_nullable as double, + error: freezed == error ? _value.error : error, + stackTrace: freezed == stackTrace + ? _value.stackTrace + : stackTrace // ignore: cast_nullable_to_non_nullable + as StackTrace?, ) as $Val); } @@ -83,7 +100,11 @@ abstract class _$$_PatchingProgressCopyWith<$Res> __$$_PatchingProgressCopyWithImpl<$Res>; @override @useResult - $Res call({Release release, double progress}); + $Res call( + {Release release, + double progress, + Object? error, + StackTrace? stackTrace}); @override $ReleaseCopyWith<$Res> get release; @@ -102,6 +123,8 @@ class __$$_PatchingProgressCopyWithImpl<$Res> $Res call({ Object? release = null, Object? progress = null, + Object? error = freezed, + Object? stackTrace = freezed, }) { return _then(_$_PatchingProgress( release: null == release @@ -112,6 +135,11 @@ class __$$_PatchingProgressCopyWithImpl<$Res> ? _value.progress : progress // ignore: cast_nullable_to_non_nullable as double, + error: freezed == error ? _value.error : error, + stackTrace: freezed == stackTrace + ? _value.stackTrace + : stackTrace // ignore: cast_nullable_to_non_nullable + as StackTrace?, )); } } @@ -119,7 +147,11 @@ class __$$_PatchingProgressCopyWithImpl<$Res> /// @nodoc class _$_PatchingProgress implements _PatchingProgress { - _$_PatchingProgress({required this.release, required this.progress}); + _$_PatchingProgress( + {required this.release, + required this.progress, + this.error, + this.stackTrace}); /// The release that is currently being patched. @override @@ -129,9 +161,17 @@ class _$_PatchingProgress implements _PatchingProgress { @override final double progress; + /// The error that occurred during patching, if any. + @override + final Object? error; + + /// The stack trace of the error that occurred during patching, if any. + @override + final StackTrace? stackTrace; + @override String toString() { - return 'PatchingProgress(release: $release, progress: $progress)'; + return 'PatchingProgress(release: $release, progress: $progress, error: $error, stackTrace: $stackTrace)'; } @override @@ -141,11 +181,15 @@ class _$_PatchingProgress implements _PatchingProgress { other is _$_PatchingProgress && (identical(other.release, release) || other.release == release) && (identical(other.progress, progress) || - other.progress == progress)); + other.progress == progress) && + const DeepCollectionEquality().equals(other.error, error) && + (identical(other.stackTrace, stackTrace) || + other.stackTrace == stackTrace)); } @override - int get hashCode => Object.hash(runtimeType, release, progress); + int get hashCode => Object.hash(runtimeType, release, progress, + const DeepCollectionEquality().hash(error), stackTrace); @JsonKey(ignore: true) @override @@ -157,7 +201,9 @@ class _$_PatchingProgress implements _PatchingProgress { abstract class _PatchingProgress implements PatchingProgress { factory _PatchingProgress( {required final Release release, - required final double progress}) = _$_PatchingProgress; + required final double progress, + final Object? error, + final StackTrace? stackTrace}) = _$_PatchingProgress; @override @@ -168,6 +214,14 @@ abstract class _PatchingProgress implements PatchingProgress { /// The progress of the patching process. double get progress; @override + + /// The error that occurred during patching, if any. + Object? get error; + @override + + /// The stack trace of the error that occurred during patching, if any. + StackTrace? get stackTrace; + @override @JsonKey(ignore: true) _$$_PatchingProgressCopyWith<_$_PatchingProgress> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/features/update/domain/providers/patching_progress_provider_state.dart b/lib/features/update/domain/providers/patching_progress_provider_state.dart index a18362d6..a0b64b19 100644 --- a/lib/features/update/domain/providers/patching_progress_provider_state.dart +++ b/lib/features/update/domain/providers/patching_progress_provider_state.dart @@ -21,15 +21,25 @@ class PatchingProgressProviderState extends StateNotifier { /// /// Throws an [UnsupportedError] if [canPatch] returns `false`. Future patch() async { - await patcherService.patch( - latest, - onProgress: (progress) { - state = PatchingProgress( - release: latest, - progress: progress, - ); - }, - ); + try { + await patcherService.patch( + latest, + onProgress: (progress) { + state = PatchingProgress( + release: latest, + progress: progress, + ); + }, + ); + } catch (e, s) { + state = state?.copyWith(error: e, stackTrace: s) ?? + PatchingProgress( + release: latest, + progress: -1, + error: e, + stackTrace: s, + ); + } } /// Whether the app can be patched automatically. From 5e2da9591189b01349e68023411723b176d04385 Mon Sep 17 00:00:00 2001 From: bmceachnie <60017181+McQuenji@users.noreply.github.com> Date: Wed, 15 Nov 2023 19:36:29 +0100 Subject: [PATCH 06/15] (i think) fixed late init error --- .../is_update_available_provider_state.dart | 20 ++----------------- .../providers/patching_progress_provider.dart | 7 ++----- .../patching_progress_provider_state.dart | 18 +++++++++++++++-- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/lib/features/update/domain/providers/is_update_available_provider_state.dart b/lib/features/update/domain/providers/is_update_available_provider_state.dart index 15bfda1e..e1afb573 100644 --- a/lib/features/update/domain/providers/is_update_available_provider_state.dart +++ b/lib/features/update/domain/providers/is_update_available_provider_state.dart @@ -10,33 +10,17 @@ class IsUpdateAvailableProviderState extends AsyncNotifier { /// The [ReleaseRepository] instance to use. late final ReleaseRepository releaseRepository; - late Release _latestRelease; - - /// The latest release available. - Release get latestRelease => _latestRelease; - @override FutureOr build() async { releaseRepository = ref.watch(releaseRepositoryProvider); - return _checkForUpdates(); + return releaseRepository.isUpdateAvailable(); } /// Checks if a new release has been published. Future checkForUpdates() async { state = AsyncLoading(); - state = await AsyncValue.guard(_checkForUpdates); - } - - /// INTERNAL USE ONLY! - /// - /// Checks whether an update is available. - /// - /// Called by [build] and [checkForUpdates]. - Future _checkForUpdates() async { - _latestRelease = await releaseRepository.getLatestRelease(); - - return releaseRepository.isUpdateAvailable(); + state = await AsyncValue.guard(releaseRepository.isUpdateAvailable); } } diff --git a/lib/features/update/domain/providers/patching_progress_provider.dart b/lib/features/update/domain/providers/patching_progress_provider.dart index 1f8e48d9..920d1c58 100644 --- a/lib/features/update/domain/providers/patching_progress_provider.dart +++ b/lib/features/update/domain/providers/patching_progress_provider.dart @@ -10,12 +10,9 @@ final patchingProgressProvider = StateNotifierProvider( (ref) { final patcherService = ref.watch(patcherServiceProvider); - final updateChecker = ref.watch(updateCheckerProvider); + final releaseRepository = ref.watch(releaseRepositoryProvider); - return PatchingProgressProviderState( - patcherService, - updateChecker.latestRelease, - ); + return PatchingProgressProviderState(patcherService, releaseRepository); }, ); diff --git a/lib/features/update/domain/providers/patching_progress_provider_state.dart b/lib/features/update/domain/providers/patching_progress_provider_state.dart index a0b64b19..47a7575b 100644 --- a/lib/features/update/domain/providers/patching_progress_provider_state.dart +++ b/lib/features/update/domain/providers/patching_progress_provider_state.dart @@ -9,18 +9,32 @@ class PatchingProgressProviderState extends StateNotifier { /// The [PatcherService] instance to use for patching. final PatcherService patcherService; + /// The [ReleaseRepository] instance to use for fetching the latest release. + final ReleaseRepository releaseRepository; + /// The latest [Release] available. - final Release latest; + late final Release latest; /// Provides the current [PatchingProgress]. /// /// NOTE: Resolves to `null` if no patching is in progress. - PatchingProgressProviderState(this.patcherService, this.latest) : super(null); + PatchingProgressProviderState(this.patcherService, this.releaseRepository) + : super(null) { + releaseRepository.getLatestRelease().then((value) => latest = value); + } /// Downloads and installs the latest version of the app. /// /// Throws an [UnsupportedError] if [canPatch] returns `false`. + /// + /// If the patching fails, the [PatchingProgress] will contain the error and stack trace. Future patch() async { + if (!canPatch) { + throw UnsupportedError( + 'Automatic patching is not supported with ${patcherService.runtimeType}', + ); + } + try { await patcherService.patch( latest, From 9b5d3814ada6404354e584c9dc4a5a5f457ced4f Mon Sep 17 00:00:00 2001 From: Can42 Date: Wed, 15 Nov 2023 20:04:35 +0100 Subject: [PATCH 07/15] implentation finished --- lib/app_router.dart | 4 +- lib/app_router.gr.dart | 20 +++++ .../presentation/screens/update_screen.dart | 80 +++++++++---------- lib/l10n/intl_en.arb | 8 +- lib/shared/presentation/widgets/dialog.dart | 14 ++-- lib/shared/presentation/widgets/markdown.dart | 13 +-- lib/shared/presentation/widgets/snippet.dart | 8 +- lib/shared/presentation/widgets/widgets.dart | 1 - 8 files changed, 80 insertions(+), 68 deletions(-) diff --git a/lib/app_router.dart b/lib/app_router.dart index b3dab4c1..caf8a97d 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -2,6 +2,7 @@ import 'package:lb_planner/features/themes/themes.dart'; import 'package:lb_planner/shared/shared.dart'; import 'package:auto_route/auto_route.dart'; import 'package:lb_planner/features/auth/auth.dart'; +import 'package:lb_planner/features/update/update.dart'; part 'app_router.gr.dart'; @@ -111,7 +112,8 @@ class AppRouter extends _$AppRouter { DefaultRoute( page: LoginRoute.page, path: '/login', - ) + ), + DefaultRoute(page: UpdateRoute.page, path: '/update', initial: true) ]; } diff --git a/lib/app_router.gr.dart b/lib/app_router.gr.dart index 39def6dc..d133a416 100644 --- a/lib/app_router.gr.dart +++ b/lib/app_router.gr.dart @@ -27,6 +27,12 @@ abstract class _$AppRouter extends RootStackRouter { child: WrappedRoute(child: const ThemeDevelopmentScreen()), ); }, + UpdateRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const UpdateScreen(), + ); + }, }; } @@ -57,3 +63,17 @@ class ThemeDevelopmentRoute extends PageRouteInfo { static const PageInfo page = PageInfo(name); } + +/// generated route for +/// [UpdateScreen] +class UpdateRoute extends PageRouteInfo { + const UpdateRoute({List? children}) + : super( + UpdateRoute.name, + initialChildren: children, + ); + + static const String name = 'UpdateRoute'; + + static const PageInfo page = PageInfo(name); +} diff --git a/lib/features/update/presentation/screens/update_screen.dart b/lib/features/update/presentation/screens/update_screen.dart index 58a83f4a..1fe0e168 100644 --- a/lib/features/update/presentation/screens/update_screen.dart +++ b/lib/features/update/presentation/screens/update_screen.dart @@ -3,11 +3,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lb_planner/shared/shared.dart'; import 'package:lb_planner/features/update/update.dart'; -/// Update route. +/// Renders an update screen, which allows the user to update the app. @RoutePage() -class UpdateScreen extends StatefulWidget { - /// Update route. +class UpdateScreen extends ConsumerStatefulWidget { + /// Renders an update screen, which allows the user to update the app. const UpdateScreen({Key? key}) : super(key: key); /// Factor multiplied by the screen width to get the padding. @@ -17,22 +17,17 @@ class UpdateScreen extends StatefulWidget { static const double iconSize = 60; @override - State createState() => _UpdateScreenState(); + ConsumerState createState() => _UpdateScreenState(); } -class _UpdateScreenState extends State { +class _UpdateScreenState extends ConsumerState { @override Widget build(BuildContext context) { return Consumer( builder: (context, ref, _) { - //var updater = ref.watch(updateController); - //var update = ref.watch(updateProvider); - final patchingProgressController = patchingProgressProvider.notifier; - - final isUpdateAvailableProvider = - AsyncNotifierProvider( - () => IsUpdateAvailableProviderState(), - ); + final progressController = ref.watch(patchingProgressController); + final patchingProgress = ref.watch(patchingProgressProvider); + final updateChecker = ref.watch(updateCheckerProvider); void showCommand() { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -42,29 +37,24 @@ class _UpdateScreenState extends State { body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - NcBodyText( - kSetupType.canAutoUpdate + Text( + progressController.canPatch ? t.update_dialog_helpNeeded : t.update_dialog_noAutoUpdate, overflow: TextOverflow.visible, ), Spacing.large(), - Snippet(code: update.command), + Snippet(code: progressController.getInstructions(context)), ], ), ); }); } - if (update.command.isNotEmpty) { + if (progressController.getInstructions(context).isNotEmpty) { showCommand(); } - if (update.downloadStatus == DownloadStatus.error) { - Catcher.reportCheckedError( - kUpdateErrorKeyword + update.error, StackTrace.current); - } - return Padding( padding: EdgeInsets.symmetric( horizontal: @@ -82,44 +72,46 @@ class _UpdateScreenState extends State { VectorImage('assets/svg/app_icon.svg', height: UpdateScreen.iconSize), Spacing.small(), - NcTitleText( - t.update_patchNotes(update.latestVersionName), - fontSize: RouteTitle.titleSize, + Text( + t.update_patchNotes(progressController.latest.name), + style: TextStyle( + fontSize: 25, fontWeight: FontWeight.bold), ), ], ), - if (update.downloadStatus == DownloadStatus.idle) - Button( - onPressed: updater.upgrade, - text: t.update_btn, - ), - if (update.downloadStatus == DownloadStatus.error) - Button( - onPressed: updater.upgrade, - text: t.update_btnErr, + if (patchingProgress?.error != null) + ElevatedButton( + onPressed: progressController.patch, + child: Text(t.update_btnErr), ), - if (update.downloadStatus == DownloadStatus.downlaoding) + if (patchingProgress != null) Row( children: [ - NcBodyText( + Text( t.update_downloading( - update.fileName, update.progress), + patchingProgress.progress.round()), + style: TextStyle(fontWeight: FontWeight.normal), ), Spacing.small(), CircularProgressIndicator(), ], ), - if (update.downloadStatus == DownloadStatus.installing && - update.command.isEmpty) - NcBodyText(t.update_installing), - if (update.downloadStatus == DownloadStatus.installing && - update.command.isNotEmpty) - LpButton(onPressed: showCommand, text: t.update_btnInstall), + if (patchingProgress != null && + progressController.getInstructions(context).isEmpty) + Text( + t.update_installing, + style: TextStyle(fontWeight: FontWeight.normal), + ), + if (patchingProgress != null && + progressController.getInstructions(context).isNotEmpty) + ElevatedButton( + onPressed: showCommand, + child: Text(t.update_btnInstall)), ], ), Spacing.large(), Expanded( - child: MarkdownView(update.patchNotes), + child: MarkdownView(progressController.latest.changelog), ), ], ), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 9224d19d..c93839b0 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -55,11 +55,7 @@ }, "@update_downloading": { "description": "Update downloading text", - "placeholders": { - "filename": { - "example": "Setup.exe", - "type": "String" - }, + "placeholders":{ "progress": { "example": "50", "type": "int" @@ -267,7 +263,7 @@ "update_btn": "Update", "update_btnErr": "Retry", "update_btnInstall": "Install", - "update_downloading": "Downloading {filename} ({progress}%)", + "update_downloading": "Downloading {progress}%", "update_dialog_noAutoUpdate": "LB Planner cannot be updated automatically on this platform. Please run the command below to update.", "update_dialog_helpNeeded": "LB Planner needs manual intervention to update. Please run the command below to complete the update.", "update_dialog_title": "Manual steps required!", diff --git a/lib/shared/presentation/widgets/dialog.dart b/lib/shared/presentation/widgets/dialog.dart index 9827b681..4ea0c358 100644 --- a/lib/shared/presentation/widgets/dialog.dart +++ b/lib/shared/presentation/widgets/dialog.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lb_planner/shared/shared.dart'; -/// Themed [Dialog] widget. +/// Shows custom dialogs class Dialog extends StatefulWidget { - /// Themed ConfirmDialog widget. + /// Creates the structure of the confirm dialog Dialog.confirm({ Key? key, required this.header, @@ -20,7 +20,7 @@ class Dialog extends StatefulWidget { confirmOnly = false; } - /// Themed [AlertDialog] widget with just one button. + /// Creates the structure of the alert dialog Dialog.alert({ Key? key, required this.header, @@ -100,11 +100,13 @@ class _DialogState extends State with TickerProviderStateMixin { focusNode.requestFocus(); } + /// Removes the widget from the widget tree. Future close() async { await _controller.reverse(); widget.removeFromWidgetTree(); } + ///Ensures focus if the focusNode doesn't have focus. void _ensureFocus() async { if (focusNode.hasFocus) return; @@ -129,7 +131,6 @@ class _DialogState extends State with TickerProviderStateMixin { } }, child: FadeTransition( - // ignore: no-magic-number opacity: Tween(begin: 0.4, end: 1).animate( CurvedAnimation( parent: _controller, @@ -230,7 +231,7 @@ class _DialogState extends State with TickerProviderStateMixin { } } -/// Themed ConfirmDialog widget. +/// Shows an confirm dialog void showConfirmDialog( BuildContext context, { String? title, @@ -291,7 +292,7 @@ void showConfirmDialog( Overlay.of(context).insert(dialogOverLay); } -/// Themed [AlertDialog] widget. +/// Shows an alert dialog void showAlertDialog( BuildContext context, { String? title, @@ -345,6 +346,7 @@ void showAlertDialog( Overlay.of(context).insert(dialogOverLay); } +/// generates a background overlay for dismissing the dialog OverlayEntry _generateBackground(Function() dismiss) { return OverlayEntry( builder: (context) => GestureDetector( diff --git a/lib/shared/presentation/widgets/markdown.dart b/lib/shared/presentation/widgets/markdown.dart index 4eb70000..cb764a73 100644 --- a/lib/shared/presentation/widgets/markdown.dart +++ b/lib/shared/presentation/widgets/markdown.dart @@ -5,25 +5,25 @@ import 'package:flutter_vector_icons/flutter_vector_icons.dart'; import 'package:lb_planner/shared/shared.dart'; import 'package:url_launcher/url_launcher.dart'; -/// Themed [MarkdownView] widget. +/// Renders a [MarkdownView] /// -/// Also allows for loading files from the internet. +/// Also allows to loading content from the internet. class MarkdownView extends ConsumerWidget { - /// Themed [MarkdownView] widget. + /// Renders a Markdown wit the [data] const MarkdownView(this.data, {Key? key}) : source = null, assert(data != null), super(key: key); - /// Themed [MarkdownView] widget. + /// Renders a [MarkdownView] /// - /// Loads markdown from the given [source]. + /// Also allows to loading content from the internet. const MarkdownView.network(this.source, {Key? key}) : data = null, assert(source != null), super(key: key); - /// The markdown source to display. + /// The markdown content to display. final String? data; /// The url to load the markdown from. @@ -75,6 +75,7 @@ class MarkdownView extends ConsumerWidget { ); } + /// Builds the Markdown View with the specified [data] and style. Widget _buildMarkdown(BuildContext context, String data) { return Markdown( data: data, diff --git a/lib/shared/presentation/widgets/snippet.dart b/lib/shared/presentation/widgets/snippet.dart index d4a684ce..3fab6d4b 100644 --- a/lib/shared/presentation/widgets/snippet.dart +++ b/lib/shared/presentation/widgets/snippet.dart @@ -2,16 +2,16 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:lb_planner/shared/shared.dart'; -/// Displays a snippet of code with a copy button. -class Snippet extends LocalizedWidget { - /// Displays a snippet of code with a copy button. +/// Displays code snippets with copy functionality. +class Snippet extends StatelessWidget { + /// Displays code snippets with copy functionality. const Snippet({Key? key, required this.code}) : super(key: key); /// The code to display. final String code; @override - Widget build(context, t) { + Widget build(context) { var color = context.theme.colorScheme.primary.withOpacity(0.5); return Container( diff --git a/lib/shared/presentation/widgets/widgets.dart b/lib/shared/presentation/widgets/widgets.dart index efa8ed7f..55eab56b 100644 --- a/lib/shared/presentation/widgets/widgets.dart +++ b/lib/shared/presentation/widgets/widgets.dart @@ -12,6 +12,5 @@ export 'hover_builder.dart'; export 'hoverable_widget.dart'; export 'conditional_widget.dart'; export 'dialog.dart'; -export 'localized_widget.dart'; export 'markdown.dart'; export 'snippet.dart'; From 7475e203dc62651720ebe91803d23b41fed1e8c0 Mon Sep 17 00:00:00 2001 From: mcquenji <60017181+mcquenji@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:49:36 +0100 Subject: [PATCH 08/15] fixed init error ; introduced breaking changes --- .../is_update_available_provider_state.dart | 22 ++++- .../providers/patching_progress_provider.dart | 23 ++--- .../patching_progress_provider_state.dart | 86 +++++++++++-------- 3 files changed, 80 insertions(+), 51 deletions(-) diff --git a/lib/features/update/domain/providers/is_update_available_provider_state.dart b/lib/features/update/domain/providers/is_update_available_provider_state.dart index e1afb573..4366ae8d 100644 --- a/lib/features/update/domain/providers/is_update_available_provider_state.dart +++ b/lib/features/update/domain/providers/is_update_available_provider_state.dart @@ -10,17 +10,35 @@ class IsUpdateAvailableProviderState extends AsyncNotifier { /// The [ReleaseRepository] instance to use. late final ReleaseRepository releaseRepository; + late Release _latestRelease; + + /// The latest release published. + /// + /// Updates when [checkForUpdates] is called. + Release get latestRelease => _latestRelease; + @override FutureOr build() async { releaseRepository = ref.watch(releaseRepositoryProvider); - return releaseRepository.isUpdateAvailable(); + return _checkForUpdates(); } /// Checks if a new release has been published. Future checkForUpdates() async { state = AsyncLoading(); - state = await AsyncValue.guard(releaseRepository.isUpdateAvailable); + state = await AsyncValue.guard(_checkForUpdates); + } + + /// INTERNAL USE ONLY! + /// + /// Checks whether an update is available. + /// + /// Called by [build] and [checkForUpdates]. + Future _checkForUpdates() async { + _latestRelease = await releaseRepository.getLatestRelease(); + + return releaseRepository.isUpdateAvailable(); } } diff --git a/lib/features/update/domain/providers/patching_progress_provider.dart b/lib/features/update/domain/providers/patching_progress_provider.dart index 920d1c58..8e4dc592 100644 --- a/lib/features/update/domain/providers/patching_progress_provider.dart +++ b/lib/features/update/domain/providers/patching_progress_provider.dart @@ -1,22 +1,23 @@ import 'package:lb_planner/features/update/update.dart'; import 'package:riverpod/riverpod.dart'; -/// Provides the current [PatchingProgress]. +/// Provides the progress of the current patching process. +/// +/// The progess is represented as double between `0.0` and `1.0`. /// /// NOTE: Resolves to `null` if no patching is in progress. /// -/// To start patching or get instructions, use [patchingProgressController]. +/// If you want start a patching process, use [patchingProgressController]. final patchingProgressProvider = - StateNotifierProvider( - (ref) { - final patcherService = ref.watch(patcherServiceProvider); - final releaseRepository = ref.watch(releaseRepositoryProvider); - - return PatchingProgressProviderState(patcherService, releaseRepository); - }, + AsyncNotifierProvider( + () => PatchingProgressProviderState(), ); -/// Expososes the controller for [patchingProgressProvider]. +/// Exposes methods for patching the app. +/// +/// If [patchingProgressProvider] resolves to [AsyncLoading], the patcher is currently being initialized and it unsafe to use any properties or methods. +/// +/// If [patchingProgressProvider] resolves to [AsyncError], there was an error while patching or an error during setup, which also makes it unsafe to call any methods or use any properties. /// -/// To receive updates on the patching progress, use [patchingProgressProvider]. +/// If you want to read the current patching progress, use [patchingProgressProvider]. final patchingProgressController = patchingProgressProvider.notifier; diff --git a/lib/features/update/domain/providers/patching_progress_provider_state.dart b/lib/features/update/domain/providers/patching_progress_provider_state.dart index 47a7575b..8a3b8df3 100644 --- a/lib/features/update/domain/providers/patching_progress_provider_state.dart +++ b/lib/features/update/domain/providers/patching_progress_provider_state.dart @@ -1,59 +1,69 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:lb_planner/features/update/update.dart'; import 'package:riverpod/riverpod.dart'; -/// Provides the current [PatchingProgress]. +/// Provides the progress of the current patching process. +/// +/// The progess is represented as double between `0.0` and `1.0`. /// /// NOTE: Resolves to `null` if no patching is in progress. -class PatchingProgressProviderState extends StateNotifier { - /// The [PatcherService] instance to use for patching. - final PatcherService patcherService; +/// +/// If this resolves to [AsyncLoading] the patcher is currently being initialized and it unsafe to use any properties or methods. +/// +/// If this resolves to [AsyncError], there was an error while patching or an error during setup, which also makes it unsafe to call any methods or use any properties. +class PatchingProgressProviderState extends AsyncNotifier { + late PatcherService _patcherService; - /// The [ReleaseRepository] instance to use for fetching the latest release. - final ReleaseRepository releaseRepository; + late ReleaseRepository _releaseRepository; - /// The latest [Release] available. - late final Release latest; + late Release _target; + + /// The target release that should be installed. + Release get target => _target; /// Provides the current [PatchingProgress]. /// /// NOTE: Resolves to `null` if no patching is in progress. - PatchingProgressProviderState(this.patcherService, this.releaseRepository) - : super(null) { - releaseRepository.getLatestRelease().then((value) => latest = value); + PatchingProgressProviderState(); + + @override + FutureOr build() async { + _patcherService = ref.read(patcherServiceProvider); + + _releaseRepository = ref.read(releaseRepositoryProvider); + + // watch `isUpdateAvailableProvider` so we refetch the lateste release when [IsUpdateAvailableProviderState.checkForUpdates] is called + // + // this way we ensure that we always have the latest release when patching + ref.watch(isUpdateAvailableProvider); + + _target = await _releaseRepository.getLatestRelease(); + + return null; } /// Downloads and installs the latest version of the app. /// - /// Throws an [UnsupportedError] if [canPatch] returns `false`. - /// - /// If the patching fails, the [PatchingProgress] will contain the error and stack trace. + /// NOTE: [state] will resolve to an [AsyncError] if [canPatch] returns `false`. Future patch() async { - if (!canPatch) { - throw UnsupportedError( - 'Automatic patching is not supported with ${patcherService.runtimeType}', - ); - } + state = await AsyncValue.guard(() async { + if (!canPatch) { + throw UnsupportedError( + '${_patcherService.runtimeType} does not support patching.', + ); + } - try { - await patcherService.patch( - latest, + await _patcherService.patch( + target, onProgress: (progress) { - state = PatchingProgress( - release: latest, - progress: progress, - ); + state = AsyncValue.data(progress); }, ); - } catch (e, s) { - state = state?.copyWith(error: e, stackTrace: s) ?? - PatchingProgress( - release: latest, - progress: -1, - error: e, - stackTrace: s, - ); - } + + return null; + }); } /// Whether the app can be patched automatically. @@ -61,11 +71,11 @@ class PatchingProgressProviderState extends StateNotifier { /// If this returns `false`, the user will have to manually install the update. /// /// In this case [patch] will throw an [UnsupportedError]. Use [getInstructions] to receive instructions for manually installing the update. - bool get canPatch => patcherService.canPatch; + bool get canPatch => _patcherService.canPatch; /// Returns the instructions for manually installing a given release in markdown format. /// - /// Throws an [UnsupportedError] if [canPatch] returns `true`. + /// NOTE: [state] will resolve to an [AsyncError] if [canPatch] returns `true`. String getInstructions(BuildContext context) => - patcherService.getInstructions(context, latest); + _patcherService.getInstructions(context, target); } From 10d939fbef461ab8d4ed49765eaeda2778047212 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Thu, 16 Nov 2023 14:09:50 +0100 Subject: [PATCH 09/15] issues fixxed --- .../presentation/screens/update_screen.dart | 162 +++++++++--------- 1 file changed, 80 insertions(+), 82 deletions(-) diff --git a/lib/features/update/presentation/screens/update_screen.dart b/lib/features/update/presentation/screens/update_screen.dart index 1fe0e168..6759c81a 100644 --- a/lib/features/update/presentation/screens/update_screen.dart +++ b/lib/features/update/presentation/screens/update_screen.dart @@ -21,102 +21,100 @@ class UpdateScreen extends ConsumerStatefulWidget { } class _UpdateScreenState extends ConsumerState { + void showCommand() { + WidgetsBinding.instance.addPostFrameCallback((_) { + showAlertDialog( + context, + title: t.update_dialog_title, + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ref.read(patchingProgressController).canPatch + ? t.update_dialog_helpNeeded + : t.update_dialog_noAutoUpdate, + overflow: TextOverflow.visible, + ), + Spacing.large(), + Snippet( + code: ref + .read(patchingProgressController) + .getInstructions(context)), + ], + ), + ); + }); + } + @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, _) { - final progressController = ref.watch(patchingProgressController); - final patchingProgress = ref.watch(patchingProgressProvider); - final updateChecker = ref.watch(updateCheckerProvider); + final progressController = ref.watch(patchingProgressController); + final patchingProgress = ref.watch(patchingProgressProvider); - void showCommand() { - WidgetsBinding.instance.addPostFrameCallback((_) { - showAlertDialog( - context, - title: t.update_dialog_title, - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - progressController.canPatch - ? t.update_dialog_helpNeeded - : t.update_dialog_noAutoUpdate, - overflow: TextOverflow.visible, - ), - Spacing.large(), - Snippet(code: progressController.getInstructions(context)), - ], - ), - ); - }); - } + if (!progressController.canPatch) { + showCommand(); + } - if (progressController.getInstructions(context).isNotEmpty) { - showCommand(); - } + return patchingProgress.when(data: null, error: , loading: ,); - return Padding( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * UpdateScreen.paddingFactor, - vertical: - MediaQuery.of(context).size.height * UpdateScreen.paddingFactor, - ), - child: Column( + return Padding( + padding: EdgeInsets.symmetric( + horizontal: + MediaQuery.of(context).size.width * UpdateScreen.paddingFactor, + vertical: + MediaQuery.of(context).size.height * UpdateScreen.paddingFactor, + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - VectorImage('assets/svg/app_icon.svg', - height: UpdateScreen.iconSize), - Spacing.small(), - Text( - t.update_patchNotes(progressController.latest.name), - style: TextStyle( - fontSize: 25, fontWeight: FontWeight.bold), - ), - ], - ), - if (patchingProgress?.error != null) - ElevatedButton( - onPressed: progressController.patch, - child: Text(t.update_btnErr), - ), - if (patchingProgress != null) - Row( - children: [ - Text( - t.update_downloading( - patchingProgress.progress.round()), - style: TextStyle(fontWeight: FontWeight.normal), - ), - Spacing.small(), - CircularProgressIndicator(), - ], - ), - if (patchingProgress != null && - progressController.getInstructions(context).isEmpty) + VectorImage('assets/svg/app_icon.svg', + height: UpdateScreen.iconSize), + Spacing.small(), + if (patchingProgress.hasValue) Text( - t.update_installing, - style: TextStyle(fontWeight: FontWeight.normal), + t.update_patchNotes(progressController.target.name), + style: + TextStyle(fontSize: 25, fontWeight: FontWeight.bold), ), - if (patchingProgress != null && - progressController.getInstructions(context).isNotEmpty) - ElevatedButton( - onPressed: showCommand, - child: Text(t.update_btnInstall)), ], ), - Spacing.large(), - Expanded( - child: MarkdownView(progressController.latest.changelog), - ), + if (patchingProgress.hasError) + ElevatedButton( + onPressed: progressController.patch, + child: Text(t.update_btnErr), + ), + if (patchingProgress.hasValue) + Row( + children: [ + Text( + t.update_downloading(patchingProgress.value!.round()), + style: TextStyle(fontWeight: FontWeight.normal), + ), + Spacing.small(), + CircularProgressIndicator(), + ], + ), + if (patchingProgress.hasValue) + Text( + t.update_installing, + style: TextStyle(fontWeight: FontWeight.normal), + ), + if (!progressController.canPatch) + ElevatedButton( + onPressed: showCommand, child: Text(t.update_btnInstall)), ], ), - ); - }, + Spacing.large(), + if (patchingProgress.hasValue) + Expanded( + child: MarkdownView(progressController.target.changelog), + ), + ], + ), ); } } From c89e0bad54af0cc25c5227594f2d2d761e999a4f Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Thu, 16 Nov 2023 16:28:04 +0100 Subject: [PATCH 10/15] Update update_screen.dart --- lib/features/update/presentation/screens/update_screen.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/features/update/presentation/screens/update_screen.dart b/lib/features/update/presentation/screens/update_screen.dart index 6759c81a..6e4fa747 100644 --- a/lib/features/update/presentation/screens/update_screen.dart +++ b/lib/features/update/presentation/screens/update_screen.dart @@ -55,8 +55,6 @@ class _UpdateScreenState extends ConsumerState { showCommand(); } - return patchingProgress.when(data: null, error: , loading: ,); - return Padding( padding: EdgeInsets.symmetric( horizontal: From 01f1f1f14aa78e04492115d51e6804428363bec4 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 21 Nov 2023 15:03:01 +0100 Subject: [PATCH 11/15] implementation finished --- .../presentation/screens/update_screen.dart | 153 ++++++++++-------- lib/shared/presentation/widgets/dialog.dart | 2 +- 2 files changed, 85 insertions(+), 70 deletions(-) diff --git a/lib/features/update/presentation/screens/update_screen.dart b/lib/features/update/presentation/screens/update_screen.dart index 6e4fa747..b44b272e 100644 --- a/lib/features/update/presentation/screens/update_screen.dart +++ b/lib/features/update/presentation/screens/update_screen.dart @@ -21,26 +21,31 @@ class UpdateScreen extends ConsumerStatefulWidget { } class _UpdateScreenState extends ConsumerState { - void showCommand() { + void showInstructions() { WidgetsBinding.instance.addPostFrameCallback((_) { showAlertDialog( context, title: t.update_dialog_title, - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ref.read(patchingProgressController).canPatch - ? t.update_dialog_helpNeeded - : t.update_dialog_noAutoUpdate, - overflow: TextOverflow.visible, - ), - Spacing.large(), - Snippet( - code: ref - .read(patchingProgressController) - .getInstructions(context)), - ], + message: t.update_dialog_noAutoUpdate, + body: SizedBox( + height: 150, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ref.read(patchingProgressController).canPatch + ? t.update_dialog_helpNeeded + : t.update_dialog_noAutoUpdate, + overflow: TextOverflow.visible, + ), + Spacing.large(), + Expanded( + child: MarkdownView( + ref.read(patchingProgressController).getInstructions(context), + ), + ), + ], + ), ), ); }); @@ -51,67 +56,77 @@ class _UpdateScreenState extends ConsumerState { final progressController = ref.watch(patchingProgressController); final patchingProgress = ref.watch(patchingProgressProvider); - if (!progressController.canPatch) { - showCommand(); + if (patchingProgress.isLoading) { + return Container( + color: context.theme.colorScheme.background, + child: const Center( + child: CircularProgressIndicator(), + ), + ); } - return Padding( - padding: EdgeInsets.symmetric( - horizontal: - MediaQuery.of(context).size.width * UpdateScreen.paddingFactor, - vertical: - MediaQuery.of(context).size.height * UpdateScreen.paddingFactor, - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - VectorImage('assets/svg/app_icon.svg', - height: UpdateScreen.iconSize), - Spacing.small(), - if (patchingProgress.hasValue) - Text( - t.update_patchNotes(progressController.target.name), - style: - TextStyle(fontSize: 25, fontWeight: FontWeight.bold), - ), - ], - ), - if (patchingProgress.hasError) - ElevatedButton( - onPressed: progressController.patch, - child: Text(t.update_btnErr), - ), - if (patchingProgress.hasValue) + return Material( + child: Container( + color: context.theme.colorScheme.background, + padding: EdgeInsets.symmetric( + horizontal: + MediaQuery.of(context).size.width * UpdateScreen.paddingFactor, + vertical: + MediaQuery.of(context).size.height * UpdateScreen.paddingFactor, + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ Row( children: [ - Text( - t.update_downloading(patchingProgress.value!.round()), - style: TextStyle(fontWeight: FontWeight.normal), - ), + VectorImage('assets/svg/app_icon.svg', + height: UpdateScreen.iconSize), Spacing.small(), - CircularProgressIndicator(), + if (patchingProgress.hasValue) + Text( + t.update_patchNotes(progressController.target.name), + style: TextStyle( + fontSize: 25, fontWeight: FontWeight.bold), + ), ], ), - if (patchingProgress.hasValue) - Text( - t.update_installing, - style: TextStyle(fontWeight: FontWeight.normal), - ), - if (!progressController.canPatch) - ElevatedButton( - onPressed: showCommand, child: Text(t.update_btnInstall)), - ], - ), - Spacing.large(), - if (patchingProgress.hasValue) - Expanded( - child: MarkdownView(progressController.target.changelog), + if (patchingProgress.hasError) + ElevatedButton( + onPressed: progressController.patch, + child: Text(t.update_btnErr), + ), + if (patchingProgress.hasValue && patchingProgress.value != null) + Row( + children: [ + Text( + t.update_downloading( + patchingProgress.value!.round() * 100), + style: TextStyle(fontWeight: FontWeight.normal), + ), + Spacing.small(), + CircularProgressIndicator(), + ], + ), + if (patchingProgress.hasValue && patchingProgress.value != null) + Text( + t.update_installing, + style: TextStyle(fontWeight: FontWeight.normal), + ), + if (!progressController.canPatch) + ElevatedButton( + onPressed: showInstructions, + child: Text(t.update_btnInstall)), + ], ), - ], + Spacing.large(), + if (patchingProgress.hasValue) + Expanded( + child: MarkdownView(progressController.target.changelog), + ), + ], + ), ), ); } diff --git a/lib/shared/presentation/widgets/dialog.dart b/lib/shared/presentation/widgets/dialog.dart index 4ea0c358..980c075b 100644 --- a/lib/shared/presentation/widgets/dialog.dart +++ b/lib/shared/presentation/widgets/dialog.dart @@ -213,7 +213,7 @@ class _DialogState extends State with TickerProviderStateMixin { ], ) ], - backgroundColor: context.theme.colorScheme.primary, + backgroundColor: context.theme.colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(5)), ), From 2e6136cfc4c59c6c4b96b69e09c5c3238237bac8 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:28:58 +0100 Subject: [PATCH 12/15] sidebar added --- lib/app_router.dart | 9 +++++++-- .../update/presentation/screens/update_screen.dart | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/app_router.dart b/lib/app_router.dart index caf8a97d..2c60dae0 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -113,13 +113,18 @@ class AppRouter extends _$AppRouter { page: LoginRoute.page, path: '/login', ), - DefaultRoute(page: UpdateRoute.page, path: '/update', initial: true) + DefaultRoute( + page: UpdateRoute.page, + path: '/update', + initial: true, + title: (context, data) => context.t.update_btn), ]; } /// Implements [CustomRoute] with some default settings. class DefaultRoute extends CustomRoute { /// Implements [CustomRoute] with some default settings. - DefaultRoute({required super.page, required super.path, super.initial}) + DefaultRoute( + {required super.page, required super.path, super.initial, super.title}) : super(transitionsBuilder: TransitionsBuilders.noTransition); } diff --git a/lib/features/update/presentation/screens/update_screen.dart b/lib/features/update/presentation/screens/update_screen.dart index b44b272e..72e84cce 100644 --- a/lib/features/update/presentation/screens/update_screen.dart +++ b/lib/features/update/presentation/screens/update_screen.dart @@ -6,7 +6,7 @@ import 'package:lb_planner/features/update/update.dart'; /// Renders an update screen, which allows the user to update the app. @RoutePage() -class UpdateScreen extends ConsumerStatefulWidget { +class UpdateScreen extends ConsumerStatefulWidget with SidebarWrapperMixin { /// Renders an update screen, which allows the user to update the app. const UpdateScreen({Key? key}) : super(key: key); From f4e6fb73b139046130d4946fd103a14c440c7965 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 12 Mar 2024 13:50:18 +0100 Subject: [PATCH 13/15] Update screen_title_bar.dart --- lib/shared/presentation/widgets/screen_title_bar.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/shared/presentation/widgets/screen_title_bar.dart b/lib/shared/presentation/widgets/screen_title_bar.dart index a853510d..6260ce90 100644 --- a/lib/shared/presentation/widgets/screen_title_bar.dart +++ b/lib/shared/presentation/widgets/screen_title_bar.dart @@ -71,7 +71,8 @@ class ScreenTitleBar extends ConsumerWidget { width: profileImageSize, height: profileImageSize, child: CircleAvatar( - // TODO: backgroundImage: fallback image + backgroundImage: NetworkImage( + "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png"), foregroundImage: NetworkImage(user.profileImageUrl), ), ), From a26175299abb757a0a6696c6fc367e5f8beb6865 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:42:06 +0100 Subject: [PATCH 14/15] requests implemented --- lib/app_router.dart | 2 +- .../update/presentation/screens/update_screen.dart | 1 - lib/features/update/presentation/widgets/widgets.dart | 1 - lib/shared/presentation/widgets/markdown.dart | 10 ++++++---- 4 files changed, 7 insertions(+), 7 deletions(-) delete mode 100644 lib/features/update/presentation/widgets/widgets.dart diff --git a/lib/app_router.dart b/lib/app_router.dart index 2c60dae0..e417d87f 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -112,11 +112,11 @@ class AppRouter extends _$AppRouter { DefaultRoute( page: LoginRoute.page, path: '/login', + initial: true, ), DefaultRoute( page: UpdateRoute.page, path: '/update', - initial: true, title: (context, data) => context.t.update_btn), ]; } diff --git a/lib/features/update/presentation/screens/update_screen.dart b/lib/features/update/presentation/screens/update_screen.dart index 72e84cce..b40bbf19 100644 --- a/lib/features/update/presentation/screens/update_screen.dart +++ b/lib/features/update/presentation/screens/update_screen.dart @@ -4,7 +4,6 @@ import 'package:lb_planner/shared/shared.dart'; import 'package:lb_planner/features/update/update.dart'; /// Renders an update screen, which allows the user to update the app. - @RoutePage() class UpdateScreen extends ConsumerStatefulWidget with SidebarWrapperMixin { /// Renders an update screen, which allows the user to update the app. diff --git a/lib/features/update/presentation/widgets/widgets.dart b/lib/features/update/presentation/widgets/widgets.dart deleted file mode 100644 index 205234c6..00000000 --- a/lib/features/update/presentation/widgets/widgets.dart +++ /dev/null @@ -1 +0,0 @@ -//Export widgets \ No newline at end of file diff --git a/lib/shared/presentation/widgets/markdown.dart b/lib/shared/presentation/widgets/markdown.dart index cb764a73..d5d71857 100644 --- a/lib/shared/presentation/widgets/markdown.dart +++ b/lib/shared/presentation/widgets/markdown.dart @@ -39,7 +39,7 @@ class MarkdownView extends ConsumerWidget { future: networkService.get(source.toString()), builder: (context, snapshot) { if (snapshot.hasData) { - if (snapshot.data!.statusCode == 200) { + if (snapshot.data!.isOk) { return _buildMarkdown(context, snapshot.data!.body); } @@ -53,9 +53,11 @@ class MarkdownView extends ConsumerWidget { size: 60, ), Spacing.small(), - Text(context.t.widgets_markdown_networkError(source.toString()), - textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, letterSpacing: 0.4)), + Text( + context.t.widgets_markdown_networkError(source.toString()), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, letterSpacing: 0.4), + ), Spacing.large(), ElevatedButton( child: Text( From 856b71347127a0acffe39d1f552537543b636915 Mon Sep 17 00:00:00 2001 From: Can Polat <74594491+cpolat-tgm@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:07:08 +0100 Subject: [PATCH 15/15] requests implemented --- assets/blank-profile-picture-973460_1280.png | Bin 0 -> 40522 bytes .../update/presentation/presentation.dart | 1 - lib/shared/presentation/widgets/markdown.dart | 76 +++++++++--------- .../widgets/screen_title_bar.dart | 4 +- 4 files changed, 41 insertions(+), 40 deletions(-) create mode 100644 assets/blank-profile-picture-973460_1280.png diff --git a/assets/blank-profile-picture-973460_1280.png b/assets/blank-profile-picture-973460_1280.png new file mode 100644 index 0000000000000000000000000000000000000000..ad28f2edbefd19f9038259ea9fc4b893488de3c6 GIT binary patch literal 40522 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 literal 0 HcmV?d00001 diff --git a/lib/features/update/presentation/presentation.dart b/lib/features/update/presentation/presentation.dart index 998a0979..80f5a18c 100644 --- a/lib/features/update/presentation/presentation.dart +++ b/lib/features/update/presentation/presentation.dart @@ -1,2 +1 @@ -export 'widgets/widgets.dart'; export 'screens/screens.dart'; diff --git a/lib/shared/presentation/widgets/markdown.dart b/lib/shared/presentation/widgets/markdown.dart index d5d71857..f91c0046 100644 --- a/lib/shared/presentation/widgets/markdown.dart +++ b/lib/shared/presentation/widgets/markdown.dart @@ -35,44 +35,46 @@ class MarkdownView extends ConsumerWidget { return ConditionalWidget( condition: data == null, - ifTrue: FutureBuilder( - future: networkService.get(source.toString()), - builder: (context, snapshot) { - if (snapshot.hasData) { - if (snapshot.data!.isOk) { - return _buildMarkdown(context, snapshot.data!.body); - } + ifTrue: Builder(builder: (context) { + return FutureBuilder( + future: networkService.get(source.toString()), + builder: (context, snapshot) { + if (snapshot.hasData) { + if (snapshot.data!.isOk) { + return _buildMarkdown(context, snapshot.data!.body); + } - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - FontAwesome.exclamation_triangle, - color: context.theme.colorScheme.error, - size: 60, - ), - Spacing.small(), - Text( - context.t.widgets_markdown_networkError(source.toString()), - textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, letterSpacing: 0.4), - ), - Spacing.large(), - ElevatedButton( - child: Text( - context.t.widgets_markdown_networkError_openInBrowser), - onPressed: () => launchUrl(source!), - ) - ], - ); - } else { - return Center( - child: CircularProgressIndicator(), - ); - } - }, - ), + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + FontAwesome.exclamation_triangle, + color: context.theme.colorScheme.error, + size: 60, + ), + Spacing.small(), + Text( + context.t.widgets_markdown_networkError(source.toString()), + textAlign: TextAlign.center, + style: TextStyle(fontSize: 12, letterSpacing: 0.4), + ), + Spacing.large(), + ElevatedButton( + child: Text( + context.t.widgets_markdown_networkError_openInBrowser), + onPressed: () => launchUrl(source!), + ) + ], + ); + } else { + return Center( + child: CircularProgressIndicator(), + ); + } + }, + ); + }), ifFalse: _buildMarkdown(context, data!), ); } diff --git a/lib/shared/presentation/widgets/screen_title_bar.dart b/lib/shared/presentation/widgets/screen_title_bar.dart index 6260ce90..03701712 100644 --- a/lib/shared/presentation/widgets/screen_title_bar.dart +++ b/lib/shared/presentation/widgets/screen_title_bar.dart @@ -71,8 +71,8 @@ class ScreenTitleBar extends ConsumerWidget { width: profileImageSize, height: profileImageSize, child: CircleAvatar( - backgroundImage: NetworkImage( - "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png"), + backgroundImage: AssetImage( + "assets/blank-profile-picture-973460_1280.png"), foregroundImage: NetworkImage(user.profileImageUrl), ), ),