diff --git a/assets/blank-profile-picture-973460_1280.png b/assets/blank-profile-picture-973460_1280.png
new file mode 100644
index 00000000..ad28f2ed
Binary files /dev/null and b/assets/blank-profile-picture-973460_1280.png differ
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/app_router.dart b/lib/app_router.dart
index b3dab4c1..e417d87f 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,13 +112,19 @@ class AppRouter extends _$AppRouter {
DefaultRoute(
page: LoginRoute.page,
path: '/login',
- )
+ initial: true,
+ ),
+ DefaultRoute(
+ page: UpdateRoute.page,
+ path: '/update',
+ 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/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/domain/models/patching_progress.dart b/lib/features/update/domain/models/patching_progress.dart
index 48887503..05171e6d 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,10 +8,16 @@ 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,
+
+ /// 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 7f4f77fa..7a320940 100644
--- a/lib/features/update/domain/models/patching_progress.freezed.dart
+++ b/lib/features/update/domain/models/patching_progress.freezed.dart
@@ -16,12 +16,18 @@ 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;
+ /// 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,9 +39,13 @@ 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,
+ Object? error,
+ StackTrace? stackTrace});
- $VersionCopyWith<$Res> get version;
+ $ReleaseCopyWith<$Res> get release;
}
/// @nodoc
@@ -51,26 +61,33 @@ class _$PatchingProgressCopyWithImpl<$Res, $Val extends PatchingProgress>
@pragma('vm:prefer-inline')
@override
$Res call({
- Object? version = null,
+ Object? release = null,
Object? progress = null,
+ Object? error = freezed,
+ Object? stackTrace = freezed,
}) {
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
as double,
+ error: freezed == error ? _value.error : error,
+ stackTrace: freezed == stackTrace
+ ? _value.stackTrace
+ : stackTrace // ignore: cast_nullable_to_non_nullable
+ as StackTrace?,
) as $Val);
}
@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 +100,14 @@ abstract class _$$_PatchingProgressCopyWith<$Res>
__$$_PatchingProgressCopyWithImpl<$Res>;
@override
@useResult
- $Res call({Version version, double progress});
+ $Res call(
+ {Release release,
+ double progress,
+ Object? error,
+ StackTrace? stackTrace});
@override
- $VersionCopyWith<$Res> get version;
+ $ReleaseCopyWith<$Res> get release;
}
/// @nodoc
@@ -100,18 +121,25 @@ class __$$_PatchingProgressCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
- Object? version = null,
+ Object? release = null,
Object? progress = null,
+ Object? error = freezed,
+ Object? stackTrace = freezed,
}) {
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
as double,
+ error: freezed == error ? _value.error : error,
+ stackTrace: freezed == stackTrace
+ ? _value.stackTrace
+ : stackTrace // ignore: cast_nullable_to_non_nullable
+ as StackTrace?,
));
}
}
@@ -119,19 +147,31 @@ class __$$_PatchingProgressCopyWithImpl<$Res>
/// @nodoc
class _$_PatchingProgress implements _PatchingProgress {
- _$_PatchingProgress({required this.version, required this.progress});
+ _$_PatchingProgress(
+ {required this.release,
+ required this.progress,
+ this.error,
+ this.stackTrace});
- /// 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
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(version: $version, progress: $progress)';
+ return 'PatchingProgress(release: $release, progress: $progress, error: $error, stackTrace: $stackTrace)';
}
@override
@@ -139,13 +179,17 @@ 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));
+ other.progress == progress) &&
+ const DeepCollectionEquality().equals(other.error, error) &&
+ (identical(other.stackTrace, stackTrace) ||
+ other.stackTrace == stackTrace));
}
@override
- int get hashCode => Object.hash(runtimeType, version, progress);
+ int get hashCode => Object.hash(runtimeType, release, progress,
+ const DeepCollectionEquality().hash(error), stackTrace);
@JsonKey(ignore: true)
@override
@@ -156,18 +200,28 @@ class _$_PatchingProgress implements _PatchingProgress {
abstract class _PatchingProgress implements PatchingProgress {
factory _PatchingProgress(
- {required final Version version,
- required final double progress}) = _$_PatchingProgress;
+ {required final Release release,
+ required final double progress,
+ final Object? error,
+ final StackTrace? stackTrace}) = _$_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.
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/is_update_available_provider_state.dart b/lib/features/update/domain/providers/is_update_available_provider_state.dart
index 3575bfd2..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() {
+ 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 2ac0e006..8e4dc592 100644
--- a/lib/features/update/domain/providers/patching_progress_provider.dart
+++ b/lib/features/update/domain/providers/patching_progress_provider.dart
@@ -1,25 +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, 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 ba5fe332..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,34 +1,81 @@
+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;
+
+ late ReleaseRepository _releaseRepository;
- /// The [ReleaseRepository] instance to use for fetching releases.
- final ReleaseRepository releaseRepository;
+ 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);
+ 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();
- /// Starts patching the current app.
+ return null;
+ }
+
+ /// Downloads and installs the latest version of the app.
+ ///
+ /// NOTE: [state] will resolve to an [AsyncError] if [canPatch] returns `false`.
Future patch() async {
- var latest = await releaseRepository.getLatestRelease();
-
- await patcherService.patch(
- latest,
- onProgress: (progress) {
- state = PatchingProgress(
- version: latest.version,
- progress: progress,
+ state = await AsyncValue.guard(() async {
+ if (!canPatch) {
+ throw UnsupportedError(
+ '${_patcherService.runtimeType} does not support patching.',
);
- },
- );
+ }
+
+ await _patcherService.patch(
+ target,
+ onProgress: (progress) {
+ state = AsyncValue.data(progress);
+ },
+ );
+
+ return null;
+ });
}
+
+ /// 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.
+ ///
+ /// NOTE: [state] will resolve to an [AsyncError] if [canPatch] returns `true`.
+ String getInstructions(BuildContext context) =>
+ _patcherService.getInstructions(context, target);
}
diff --git a/lib/features/update/presentation/presentation.dart b/lib/features/update/presentation/presentation.dart
new file mode 100644
index 00000000..80f5a18c
--- /dev/null
+++ b/lib/features/update/presentation/presentation.dart
@@ -0,0 +1 @@
+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..b40bbf19
--- /dev/null
+++ b/lib/features/update/presentation/screens/update_screen.dart
@@ -0,0 +1,132 @@
+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';
+
+/// 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.
+ 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
+ ConsumerState createState() => _UpdateScreenState();
+}
+
+class _UpdateScreenState extends ConsumerState {
+ void showInstructions() {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ showAlertDialog(
+ context,
+ title: t.update_dialog_title,
+ 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),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final progressController = ref.watch(patchingProgressController);
+ final patchingProgress = ref.watch(patchingProgressProvider);
+
+ if (patchingProgress.isLoading) {
+ return Container(
+ color: context.theme.colorScheme.background,
+ child: const Center(
+ child: CircularProgressIndicator(),
+ ),
+ );
+ }
+
+ 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: [
+ 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 && 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/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/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/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..980c075b
--- /dev/null
+++ b/lib/shared/presentation/widgets/dialog.dart
@@ -0,0 +1,359 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:lb_planner/shared/shared.dart';
+
+/// Shows custom dialogs
+class Dialog extends StatefulWidget {
+ /// Creates the structure of the confirm dialog
+ 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;
+ }
+
+ /// Creates the structure of the alert dialog
+ 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