From 88ded5d0a962132c6113b262d94cd3e3036bb430 Mon Sep 17 00:00:00 2001 From: areille Date: Mon, 6 Feb 2023 12:33:13 +0100 Subject: [PATCH 1/2] :sparkles: Use task either for brew info --- lib/constants/brew_exe.dart | 1 - lib/constants/constants.dart | 6 + lib/exceptions/exceptions.dart | 22 + .../commands/data/commands_repository.dart | 4 +- .../commands/state/command_state.freezed.dart | 10 +- lib/features/info/data/info_repository.dart | 21 +- lib/features/info/state/info_provider.dart | 28 +- lib/features/info/state/info_state.dart | 11 + .../info/state/info_state.freezed.dart | 466 ++++++++++++++++++ .../widgets/package_info_view.dart | 4 +- .../list/repository/list_repository.dart | 7 +- lib/utils/process.dart | 13 + pubspec.lock | 8 + pubspec.yaml | 1 + 14 files changed, 579 insertions(+), 23 deletions(-) delete mode 100644 lib/constants/brew_exe.dart create mode 100644 lib/constants/constants.dart create mode 100644 lib/exceptions/exceptions.dart create mode 100644 lib/features/info/state/info_state.dart create mode 100644 lib/features/info/state/info_state.freezed.dart create mode 100644 lib/utils/process.dart diff --git a/lib/constants/brew_exe.dart b/lib/constants/brew_exe.dart deleted file mode 100644 index ff98c50..0000000 --- a/lib/constants/brew_exe.dart +++ /dev/null @@ -1 +0,0 @@ -const kBrewExecutable = 'brew'; diff --git a/lib/constants/constants.dart b/lib/constants/constants.dart new file mode 100644 index 0000000..cc19882 --- /dev/null +++ b/lib/constants/constants.dart @@ -0,0 +1,6 @@ +class Constants { + const Constants._(); + + static const brewExecutable = 'brew'; + static const brewInfoCmd = 'info'; +} diff --git a/lib/exceptions/exceptions.dart b/lib/exceptions/exceptions.dart new file mode 100644 index 0000000..60e65ff --- /dev/null +++ b/lib/exceptions/exceptions.dart @@ -0,0 +1,22 @@ +abstract class BrewException { + String get message; +} + +class ProcessLaunchException implements BrewException { + ProcessLaunchException(this.args, this.error); + + final List args; + final Object error; + + @override + String get message => 'Cannot launch brew with args $args : $error'; +} + +class ProcessStdoutReadingException implements BrewException { + ProcessStdoutReadingException(this.error); + + final Object error; + + @override + String get message => 'Cannot parse process stdout : $error'; +} diff --git a/lib/features/commands/data/commands_repository.dart b/lib/features/commands/data/commands_repository.dart index 811c3c1..17cedd1 100644 --- a/lib/features/commands/data/commands_repository.dart +++ b/lib/features/commands/data/commands_repository.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:brew_flutter/constants/brew_exe.dart'; +import 'package:brew_flutter/constants/constants.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; final brewCommandsRepositoryProvider = @@ -12,7 +12,7 @@ class BrewCommandsRepository { late Process process; Future launch(List args) async { - process = await Process.start(kBrewExecutable, args); + process = await Process.start(Constants.brewExecutable, args); stdout = process.stdout.map(String.fromCharCodes); } diff --git a/lib/features/commands/state/command_state.freezed.dart b/lib/features/commands/state/command_state.freezed.dart index 133a84b..b001631 100644 --- a/lib/features/commands/state/command_state.freezed.dart +++ b/lib/features/commands/state/command_state.freezed.dart @@ -1,7 +1,7 @@ // coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark part of 'command_state.dart'; @@ -110,7 +110,7 @@ class _$Running implements Running { abstract class Running implements CommandState { const factory Running(final String data) = _$Running; - String get data => throw _privateConstructorUsedError; + String get data; } /// @nodoc @@ -154,8 +154,8 @@ abstract class Error implements CommandState { const factory Error(final Object error, {final StackTrace? stackTrace}) = _$Error; - Object get error => throw _privateConstructorUsedError; - StackTrace? get stackTrace => throw _privateConstructorUsedError; + Object get error; + StackTrace? get stackTrace; } /// @nodoc @@ -196,5 +196,5 @@ class _$Done implements Done { abstract class Done implements CommandState { const factory Done(final String data) = _$Done; - String get data => throw _privateConstructorUsedError; + String get data; } diff --git a/lib/features/info/data/info_repository.dart b/lib/features/info/data/info_repository.dart index 5e2de4e..5c5845f 100644 --- a/lib/features/info/data/info_repository.dart +++ b/lib/features/info/data/info_repository.dart @@ -1,11 +1,18 @@ import 'dart:io'; -import 'package:brew_flutter/constants/brew_exe.dart'; +import 'package:brew_flutter/constants/constants.dart'; +import 'package:brew_flutter/exceptions/exceptions.dart'; import 'package:brew_flutter/features/info/model/package_info.dart'; +import 'package:brew_flutter/utils/process.dart'; +import 'package:fpdart/fpdart.dart'; -Future runBrewInfo(String package) async { - final process = await Process.start(kBrewExecutable, ['info', package]); - return PackageInfo.fromRaw( - await process.stdout.map(String.fromCharCodes).join('\n'), - ); -} +TaskEither brewInfoTE(String package) => + startProcessTask([Constants.brewInfoCmd, package]) + .flatMap(processStdoutTE) + .map(PackageInfo.fromRaw); // IMPURE + +TaskEither processStdoutTE(Process process) => + TaskEither.tryCatch( + () => process.stdout.map(String.fromCharCodes).join('\n'), + (err, __) => ProcessStdoutReadingException(err), + ); diff --git a/lib/features/info/state/info_provider.dart b/lib/features/info/state/info_provider.dart index 46ece03..1cb09d7 100644 --- a/lib/features/info/state/info_provider.dart +++ b/lib/features/info/state/info_provider.dart @@ -1,7 +1,29 @@ import 'package:brew_flutter/features/info/data/info_repository.dart'; -import 'package:brew_flutter/features/info/model/package_info.dart'; +import 'package:brew_flutter/features/info/state/info_state.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -final packageInfoProvider = FutureProvider.family( - (_, package) => runBrewInfo(package), +typedef PackageName = String; + +class PackageInfoState extends StateNotifier { + PackageInfoState(this.packageName) : super(const InfoState.loading()) { + init(); + } + + final PackageName packageName; + + Future init() async { + state = const InfoState.loading(); + final infoTask = brewInfoTE(packageName); + state = (await infoTask.run()).match( + (error) => InfoState.error(error.message), + InfoState.success, + ); + return unit; + } +} + +final packageInfoProvider = + StateNotifierProviderFamily( + (_, packageName) => PackageInfoState(packageName), ); diff --git a/lib/features/info/state/info_state.dart b/lib/features/info/state/info_state.dart new file mode 100644 index 0000000..72dd187 --- /dev/null +++ b/lib/features/info/state/info_state.dart @@ -0,0 +1,11 @@ +import 'package:brew_flutter/features/info/model/package_info.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'info_state.freezed.dart'; + +@freezed +class InfoState with _$InfoState { + const factory InfoState.loading() = LoadingInfoState; + const factory InfoState.error(String string) = ErrorInfoState; + const factory InfoState.success(PackageInfo info) = SuccessInfoState; +} diff --git a/lib/features/info/state/info_state.freezed.dart b/lib/features/info/state/info_state.freezed.dart new file mode 100644 index 0000000..892e1b0 --- /dev/null +++ b/lib/features/info/state/info_state.freezed.dart @@ -0,0 +1,466 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'info_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$InfoState { + @optionalTypeArgs + TResult when({ + required TResult Function() loading, + required TResult Function(String string) error, + required TResult Function(PackageInfo info) success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? loading, + TResult? Function(String string)? error, + TResult? Function(PackageInfo info)? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? loading, + TResult Function(String string)? error, + TResult Function(PackageInfo info)? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(LoadingInfoState value) loading, + required TResult Function(ErrorInfoState value) error, + required TResult Function(SuccessInfoState value) success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LoadingInfoState value)? loading, + TResult? Function(ErrorInfoState value)? error, + TResult? Function(SuccessInfoState value)? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LoadingInfoState value)? loading, + TResult Function(ErrorInfoState value)? error, + TResult Function(SuccessInfoState value)? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $InfoStateCopyWith<$Res> { + factory $InfoStateCopyWith(InfoState value, $Res Function(InfoState) then) = + _$InfoStateCopyWithImpl<$Res, InfoState>; +} + +/// @nodoc +class _$InfoStateCopyWithImpl<$Res, $Val extends InfoState> + implements $InfoStateCopyWith<$Res> { + _$InfoStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$LoadingInfoStateCopyWith<$Res> { + factory _$$LoadingInfoStateCopyWith( + _$LoadingInfoState value, $Res Function(_$LoadingInfoState) then) = + __$$LoadingInfoStateCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingInfoStateCopyWithImpl<$Res> + extends _$InfoStateCopyWithImpl<$Res, _$LoadingInfoState> + implements _$$LoadingInfoStateCopyWith<$Res> { + __$$LoadingInfoStateCopyWithImpl( + _$LoadingInfoState _value, $Res Function(_$LoadingInfoState) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$LoadingInfoState implements LoadingInfoState { + const _$LoadingInfoState(); + + @override + String toString() { + return 'InfoState.loading()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadingInfoState); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() loading, + required TResult Function(String string) error, + required TResult Function(PackageInfo info) success, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? loading, + TResult? Function(String string)? error, + TResult? Function(PackageInfo info)? success, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? loading, + TResult Function(String string)? error, + TResult Function(PackageInfo info)? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(LoadingInfoState value) loading, + required TResult Function(ErrorInfoState value) error, + required TResult Function(SuccessInfoState value) success, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LoadingInfoState value)? loading, + TResult? Function(ErrorInfoState value)? error, + TResult? Function(SuccessInfoState value)? success, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LoadingInfoState value)? loading, + TResult Function(ErrorInfoState value)? error, + TResult Function(SuccessInfoState value)? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class LoadingInfoState implements InfoState { + const factory LoadingInfoState() = _$LoadingInfoState; +} + +/// @nodoc +abstract class _$$ErrorInfoStateCopyWith<$Res> { + factory _$$ErrorInfoStateCopyWith( + _$ErrorInfoState value, $Res Function(_$ErrorInfoState) then) = + __$$ErrorInfoStateCopyWithImpl<$Res>; + @useResult + $Res call({String string}); +} + +/// @nodoc +class __$$ErrorInfoStateCopyWithImpl<$Res> + extends _$InfoStateCopyWithImpl<$Res, _$ErrorInfoState> + implements _$$ErrorInfoStateCopyWith<$Res> { + __$$ErrorInfoStateCopyWithImpl( + _$ErrorInfoState _value, $Res Function(_$ErrorInfoState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? string = null, + }) { + return _then(_$ErrorInfoState( + null == string + ? _value.string + : string // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ErrorInfoState implements ErrorInfoState { + const _$ErrorInfoState(this.string); + + @override + final String string; + + @override + String toString() { + return 'InfoState.error(string: $string)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ErrorInfoState && + (identical(other.string, string) || other.string == string)); + } + + @override + int get hashCode => Object.hash(runtimeType, string); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ErrorInfoStateCopyWith<_$ErrorInfoState> get copyWith => + __$$ErrorInfoStateCopyWithImpl<_$ErrorInfoState>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() loading, + required TResult Function(String string) error, + required TResult Function(PackageInfo info) success, + }) { + return error(string); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? loading, + TResult? Function(String string)? error, + TResult? Function(PackageInfo info)? success, + }) { + return error?.call(string); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? loading, + TResult Function(String string)? error, + TResult Function(PackageInfo info)? success, + required TResult orElse(), + }) { + if (error != null) { + return error(string); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(LoadingInfoState value) loading, + required TResult Function(ErrorInfoState value) error, + required TResult Function(SuccessInfoState value) success, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LoadingInfoState value)? loading, + TResult? Function(ErrorInfoState value)? error, + TResult? Function(SuccessInfoState value)? success, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LoadingInfoState value)? loading, + TResult Function(ErrorInfoState value)? error, + TResult Function(SuccessInfoState value)? success, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class ErrorInfoState implements InfoState { + const factory ErrorInfoState(final String string) = _$ErrorInfoState; + + String get string; + @JsonKey(ignore: true) + _$$ErrorInfoStateCopyWith<_$ErrorInfoState> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$SuccessInfoStateCopyWith<$Res> { + factory _$$SuccessInfoStateCopyWith( + _$SuccessInfoState value, $Res Function(_$SuccessInfoState) then) = + __$$SuccessInfoStateCopyWithImpl<$Res>; + @useResult + $Res call({PackageInfo info}); +} + +/// @nodoc +class __$$SuccessInfoStateCopyWithImpl<$Res> + extends _$InfoStateCopyWithImpl<$Res, _$SuccessInfoState> + implements _$$SuccessInfoStateCopyWith<$Res> { + __$$SuccessInfoStateCopyWithImpl( + _$SuccessInfoState _value, $Res Function(_$SuccessInfoState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? info = null, + }) { + return _then(_$SuccessInfoState( + null == info + ? _value.info + : info // ignore: cast_nullable_to_non_nullable + as PackageInfo, + )); + } +} + +/// @nodoc + +class _$SuccessInfoState implements SuccessInfoState { + const _$SuccessInfoState(this.info); + + @override + final PackageInfo info; + + @override + String toString() { + return 'InfoState.success(info: $info)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SuccessInfoState && + (identical(other.info, info) || other.info == info)); + } + + @override + int get hashCode => Object.hash(runtimeType, info); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SuccessInfoStateCopyWith<_$SuccessInfoState> get copyWith => + __$$SuccessInfoStateCopyWithImpl<_$SuccessInfoState>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() loading, + required TResult Function(String string) error, + required TResult Function(PackageInfo info) success, + }) { + return success(info); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? loading, + TResult? Function(String string)? error, + TResult? Function(PackageInfo info)? success, + }) { + return success?.call(info); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? loading, + TResult Function(String string)? error, + TResult Function(PackageInfo info)? success, + required TResult orElse(), + }) { + if (success != null) { + return success(info); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(LoadingInfoState value) loading, + required TResult Function(ErrorInfoState value) error, + required TResult Function(SuccessInfoState value) success, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LoadingInfoState value)? loading, + TResult? Function(ErrorInfoState value)? error, + TResult? Function(SuccessInfoState value)? success, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LoadingInfoState value)? loading, + TResult Function(ErrorInfoState value)? error, + TResult Function(SuccessInfoState value)? success, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class SuccessInfoState implements InfoState { + const factory SuccessInfoState(final PackageInfo info) = _$SuccessInfoState; + + PackageInfo get info; + @JsonKey(ignore: true) + _$$SuccessInfoStateCopyWith<_$SuccessInfoState> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/list/presentation/widgets/package_info_view.dart b/lib/features/list/presentation/widgets/package_info_view.dart index c36f818..8465a76 100644 --- a/lib/features/list/presentation/widgets/package_info_view.dart +++ b/lib/features/list/presentation/widgets/package_info_view.dart @@ -17,12 +17,12 @@ class PackageInfoView extends ConsumerWidget { final packageInfo = ref.watch(packageInfoProvider(selectedPackage)); return packageInfo.when( - data: (info) => PackageInfoScreen( + success: (info) => PackageInfoScreen( packageInfo: info, onUninstall: () => ref.read(packagesListProvider.notifier).uninstallPackage(info.name), ), - error: (err, _) => Center(child: Text(err.toString())), + error: (err) => Center(child: Text(err)), loading: () => const Center(child: ProgressCircle()), ); } diff --git a/lib/features/list/repository/list_repository.dart b/lib/features/list/repository/list_repository.dart index 99ab20a..8195db9 100644 --- a/lib/features/list/repository/list_repository.dart +++ b/lib/features/list/repository/list_repository.dart @@ -1,15 +1,16 @@ import 'dart:io'; -import 'package:brew_flutter/constants/brew_exe.dart'; +import 'package:brew_flutter/constants/constants.dart'; import 'package:rxdart/rxdart.dart'; Future> runBrewList() async { - final process = await Process.start(kBrewExecutable, ['list']); + final process = await Process.start(Constants.brewExecutable, ['list']); return String.fromCharCodes(await process.stdout.last).split('\n'); } Future runBrewUninstall(String package) async { - final process = await Process.start(kBrewExecutable, ['uninstall', package]); + final process = + await Process.start(Constants.brewExecutable, ['uninstall', package]); process.stdout.doOnDone(() { return; }); diff --git a/lib/utils/process.dart b/lib/utils/process.dart new file mode 100644 index 0000000..cb1cbdc --- /dev/null +++ b/lib/utils/process.dart @@ -0,0 +1,13 @@ +import 'dart:io'; + +import 'package:brew_flutter/constants/constants.dart'; +import 'package:brew_flutter/exceptions/exceptions.dart'; +import 'package:fpdart/fpdart.dart'; + +TaskEither startProcessTask( + List args, +) => + TaskEither.tryCatch( + () => Process.start(Constants.brewExecutable, args), + (err, __) => ProcessLaunchException(args, err), + ); diff --git a/pubspec.lock b/pubspec.lock index 61e06e5..8480a44 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -240,6 +240,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fpdart: + dependency: "direct main" + description: + name: fpdart + sha256: "19db038cf3bb49bfe28b2a0b363ce84cf2e6a0f471a93ae09271cfef03dae935" + url: "https://pub.dev" + source: hosted + version: "0.4.0" freezed: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5c9ba1e..0b44f5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,6 +15,7 @@ dependencies: flutter: sdk: flutter flutter_hooks: ^0.18.5+1 + fpdart: ^0.4.0 freezed_annotation: ^2.0.3 hooks_riverpod: ^2.1.3 linkable: ^3.0.1 From 057e774005f2f6ca67c023d263f25949b2d8c686 Mon Sep 17 00:00:00 2001 From: areille Date: Mon, 6 Feb 2023 14:55:54 +0100 Subject: [PATCH 2/2] :sparkles: use fp for packages list --- lib/constants/constants.dart | 2 + lib/exceptions/exceptions.dart | 18 + lib/features/info/data/info_repository.dart | 4 +- lib/features/info/model/package_info.dart | 4 +- lib/features/info/state/info_provider.dart | 3 +- .../presentation/widgets/packages_list.dart | 14 +- .../list/presentation/widgets/search.dart | 2 +- .../list/repository/list_repository.dart | 38 +- lib/features/list/state/list_provider.dart | 50 +- lib/features/list/state/list_state.dart | 10 + .../list/state/list_state.freezed.dart | 475 ++++++++++++++++++ 11 files changed, 584 insertions(+), 36 deletions(-) create mode 100644 lib/features/list/state/list_state.dart create mode 100644 lib/features/list/state/list_state.freezed.dart diff --git a/lib/constants/constants.dart b/lib/constants/constants.dart index cc19882..5060aed 100644 --- a/lib/constants/constants.dart +++ b/lib/constants/constants.dart @@ -3,4 +3,6 @@ class Constants { static const brewExecutable = 'brew'; static const brewInfoCmd = 'info'; + static const brewListCmd = 'list'; + static const brewUninstallCmd = 'uninstall'; } diff --git a/lib/exceptions/exceptions.dart b/lib/exceptions/exceptions.dart index 60e65ff..faaee95 100644 --- a/lib/exceptions/exceptions.dart +++ b/lib/exceptions/exceptions.dart @@ -20,3 +20,21 @@ class ProcessStdoutReadingException implements BrewException { @override String get message => 'Cannot parse process stdout : $error'; } + +class ProcessStdoutLastException implements BrewException { + ProcessStdoutLastException(this.error); + + final Object error; + + @override + String get message => 'Cannot read last from stdout : $error'; +} + +class ProcessStdoutOnDoneException implements BrewException { + ProcessStdoutOnDoneException(this.error); + + final Object error; + + @override + String get message => 'Cannot wait for stdout to complete : $error'; +} diff --git a/lib/features/info/data/info_repository.dart b/lib/features/info/data/info_repository.dart index 5c5845f..84b9bfd 100644 --- a/lib/features/info/data/info_repository.dart +++ b/lib/features/info/data/info_repository.dart @@ -8,10 +8,10 @@ import 'package:fpdart/fpdart.dart'; TaskEither brewInfoTE(String package) => startProcessTask([Constants.brewInfoCmd, package]) - .flatMap(processStdoutTE) + .flatMap(_processStdoutTE) .map(PackageInfo.fromRaw); // IMPURE -TaskEither processStdoutTE(Process process) => +TaskEither _processStdoutTE(Process process) => TaskEither.tryCatch( () => process.stdout.map(String.fromCharCodes).join('\n'), (err, __) => ProcessStdoutReadingException(err), diff --git a/lib/features/info/model/package_info.dart b/lib/features/info/model/package_info.dart index 9bc63c8..fdb2015 100644 --- a/lib/features/info/model/package_info.dart +++ b/lib/features/info/model/package_info.dart @@ -1,3 +1,5 @@ +typedef PackageName = String; + class PackageInfo { PackageInfo({ required this.name, @@ -24,7 +26,7 @@ class PackageInfo { ); } - final String name; + final PackageName name; final String version; final String description; final String url; diff --git a/lib/features/info/state/info_provider.dart b/lib/features/info/state/info_provider.dart index 1cb09d7..1d61d3f 100644 --- a/lib/features/info/state/info_provider.dart +++ b/lib/features/info/state/info_provider.dart @@ -1,10 +1,9 @@ import 'package:brew_flutter/features/info/data/info_repository.dart'; +import 'package:brew_flutter/features/info/model/package_info.dart'; import 'package:brew_flutter/features/info/state/info_state.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -typedef PackageName = String; - class PackageInfoState extends StateNotifier { PackageInfoState(this.packageName) : super(const InfoState.loading()) { init(); diff --git a/lib/features/list/presentation/widgets/packages_list.dart b/lib/features/list/presentation/widgets/packages_list.dart index b0b8318..676f9a4 100644 --- a/lib/features/list/presentation/widgets/packages_list.dart +++ b/lib/features/list/presentation/widgets/packages_list.dart @@ -1,4 +1,5 @@ import 'package:brew_flutter/features/list/state/list_provider.dart'; +import 'package:brew_flutter/features/list/state/list_state.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:macos_ui/macos_ui.dart'; @@ -14,15 +15,16 @@ class PackagesList extends ConsumerWidget { // Set selected package as the first of the list ref.listen(packagesListProvider, (previous, next) { - if ((previous is AsyncLoading && next is AsyncData>) || - next is AsyncData> && - !next.value.contains(ref.read(selectedPackageProvider))) { - ref.read(selectedPackageProvider.notifier).state = next.value.first; + if ((previous is LoadingListState && next is SuccessListState) || + next is SuccessListState && + !next.packagesList.contains(ref.read(selectedPackageProvider))) { + ref.read(selectedPackageProvider.notifier).state = + next.packagesList.first; } }); final selectedPackage = ref.watch(selectedPackageProvider); return packages.when( - data: (packages) { + success: (packages) { return SidebarItems( scrollController: scrollController, currentIndex: packages.contains(selectedPackage) @@ -36,7 +38,7 @@ class PackagesList extends ConsumerWidget { ], ); }, - error: (err, _) => Center(child: Text(err.toString())), + error: (err) => Center(child: Text(err)), loading: () => const Center(child: ProgressCircle()), ); } diff --git a/lib/features/list/presentation/widgets/search.dart b/lib/features/list/presentation/widgets/search.dart index 61fc8b2..b0ec33e 100644 --- a/lib/features/list/presentation/widgets/search.dart +++ b/lib/features/list/presentation/widgets/search.dart @@ -13,7 +13,7 @@ class Search extends HookConsumerWidget { final list = ref.watch(filteredPackageListProvider); return list.maybeWhen( - data: (packages) => MacosSearchField( + success: (packages) => MacosSearchField( controller: searchFieldController, results: packages.map(SearchResultItem.new).toList(), onResultSelected: (res) { diff --git a/lib/features/list/repository/list_repository.dart b/lib/features/list/repository/list_repository.dart index 8195db9..f3a26d3 100644 --- a/lib/features/list/repository/list_repository.dart +++ b/lib/features/list/repository/list_repository.dart @@ -1,17 +1,31 @@ import 'dart:io'; import 'package:brew_flutter/constants/constants.dart'; -import 'package:rxdart/rxdart.dart'; +import 'package:brew_flutter/exceptions/exceptions.dart'; +import 'package:brew_flutter/features/info/model/package_info.dart'; +import 'package:brew_flutter/utils/process.dart'; +import 'package:fpdart/fpdart.dart'; -Future> runBrewList() async { - final process = await Process.start(Constants.brewExecutable, ['list']); - return String.fromCharCodes(await process.stdout.last).split('\n'); -} +TaskEither brewUninstallTE(PackageName packageName) => + startProcessTask([Constants.brewUninstallCmd, packageName]) + .flatMap(_stdoutDone); -Future runBrewUninstall(String package) async { - final process = - await Process.start(Constants.brewExecutable, ['uninstall', package]); - process.stdout.doOnDone(() { - return; - }); -} +TaskEither _stdoutDone(Process process) => + TaskEither.tryCatch( + process.stdout.listen(null).asFuture, + (error, _) => ProcessStdoutOnDoneException(error), + ); + +TaskEither> brewListTE() => + startProcessTask([Constants.brewListCmd]) + .flatMap(_lastFromStdoutTE) + .map(String.fromCharCodes) + .map(_splitWithNewLines); + +TaskEither> _lastFromStdoutTE(Process process) => + TaskEither.tryCatch( + () => process.stdout.last, + (error, _) => ProcessStdoutLastException(error), + ); + +List _splitWithNewLines(String data) => data.split('\n'); diff --git a/lib/features/list/state/list_provider.dart b/lib/features/list/state/list_provider.dart index 5d13328..a3562f8 100644 --- a/lib/features/list/state/list_provider.dart +++ b/lib/features/list/state/list_provider.dart @@ -1,28 +1,54 @@ +import 'package:brew_flutter/features/info/model/package_info.dart'; import 'package:brew_flutter/features/list/repository/list_repository.dart'; +import 'package:brew_flutter/features/list/state/list_state.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; final selectedPackageProvider = StateProvider((ref) => ''); final searchInputProvider = StateProvider((ref) => ''); -final filteredPackageListProvider = FutureProvider>((ref) async { +final filteredPackageListProvider = Provider((ref) { final search = ref.watch(searchInputProvider); - final packages = await ref.watch(packagesListProvider.future); - return packages.where((p) => p.contains(search)).toList(); + final packages = ref.watch(packagesListProvider); + + return packages.when( + loading: ListState.loading, + error: ListState.error, + success: (data) => + ListState.success(data.where((p) => p.contains(search)).toList()), + ); }); final packagesListProvider = - AsyncNotifierProvider>( - PackagesListNotifier.new, + StateNotifierProvider( + (ref) => PackagesListState(), ); -class PackagesListNotifier extends AsyncNotifier> { - @override - Future> build() => runBrewList(); +class PackagesListState extends StateNotifier { + PackagesListState() : super(const ListState.loading()) { + init(); + } + + Future init() async { + state = const ListState.loading(); + final listTask = brewListTE(); + state = (await listTask.run()).match( + (error) => ListState.error(error.message), + ListState.success, + ); + return unit; + } - Future uninstallPackage(String package) async { - await runBrewUninstall(package); - state = AsyncValue.data( - state.value!..removeWhere((element) => element == package), + Future uninstallPackage(PackageName package) async { + final uninstallTask = brewUninstallTE(package); + final result = await uninstallTask.run(); + result.match( + (error) => unit, + (_) => state = ListState.success( + (state as SuccessListState).packagesList + ..removeWhere((element) => element == package), + ), ); + return unit; } } diff --git a/lib/features/list/state/list_state.dart b/lib/features/list/state/list_state.dart new file mode 100644 index 0000000..55dd7c3 --- /dev/null +++ b/lib/features/list/state/list_state.dart @@ -0,0 +1,10 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'list_state.freezed.dart'; + +@freezed +class ListState with _$ListState { + const factory ListState.loading() = LoadingListState; + const factory ListState.error(String string) = ErrorListState; + const factory ListState.success(List packagesList) = SuccessListState; +} diff --git a/lib/features/list/state/list_state.freezed.dart b/lib/features/list/state/list_state.freezed.dart new file mode 100644 index 0000000..b9da0b6 --- /dev/null +++ b/lib/features/list/state/list_state.freezed.dart @@ -0,0 +1,475 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'list_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +mixin _$ListState { + @optionalTypeArgs + TResult when({ + required TResult Function() loading, + required TResult Function(String string) error, + required TResult Function(List packagesList) success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? loading, + TResult? Function(String string)? error, + TResult? Function(List packagesList)? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? loading, + TResult Function(String string)? error, + TResult Function(List packagesList)? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(LoadingListState value) loading, + required TResult Function(ErrorListState value) error, + required TResult Function(SuccessListState value) success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LoadingListState value)? loading, + TResult? Function(ErrorListState value)? error, + TResult? Function(SuccessListState value)? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LoadingListState value)? loading, + TResult Function(ErrorListState value)? error, + TResult Function(SuccessListState value)? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ListStateCopyWith<$Res> { + factory $ListStateCopyWith(ListState value, $Res Function(ListState) then) = + _$ListStateCopyWithImpl<$Res, ListState>; +} + +/// @nodoc +class _$ListStateCopyWithImpl<$Res, $Val extends ListState> + implements $ListStateCopyWith<$Res> { + _$ListStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$LoadingListStateCopyWith<$Res> { + factory _$$LoadingListStateCopyWith( + _$LoadingListState value, $Res Function(_$LoadingListState) then) = + __$$LoadingListStateCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoadingListStateCopyWithImpl<$Res> + extends _$ListStateCopyWithImpl<$Res, _$LoadingListState> + implements _$$LoadingListStateCopyWith<$Res> { + __$$LoadingListStateCopyWithImpl( + _$LoadingListState _value, $Res Function(_$LoadingListState) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$LoadingListState implements LoadingListState { + const _$LoadingListState(); + + @override + String toString() { + return 'ListState.loading()'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is _$LoadingListState); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() loading, + required TResult Function(String string) error, + required TResult Function(List packagesList) success, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? loading, + TResult? Function(String string)? error, + TResult? Function(List packagesList)? success, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? loading, + TResult Function(String string)? error, + TResult Function(List packagesList)? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(LoadingListState value) loading, + required TResult Function(ErrorListState value) error, + required TResult Function(SuccessListState value) success, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LoadingListState value)? loading, + TResult? Function(ErrorListState value)? error, + TResult? Function(SuccessListState value)? success, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LoadingListState value)? loading, + TResult Function(ErrorListState value)? error, + TResult Function(SuccessListState value)? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class LoadingListState implements ListState { + const factory LoadingListState() = _$LoadingListState; +} + +/// @nodoc +abstract class _$$ErrorListStateCopyWith<$Res> { + factory _$$ErrorListStateCopyWith( + _$ErrorListState value, $Res Function(_$ErrorListState) then) = + __$$ErrorListStateCopyWithImpl<$Res>; + @useResult + $Res call({String string}); +} + +/// @nodoc +class __$$ErrorListStateCopyWithImpl<$Res> + extends _$ListStateCopyWithImpl<$Res, _$ErrorListState> + implements _$$ErrorListStateCopyWith<$Res> { + __$$ErrorListStateCopyWithImpl( + _$ErrorListState _value, $Res Function(_$ErrorListState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? string = null, + }) { + return _then(_$ErrorListState( + null == string + ? _value.string + : string // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$ErrorListState implements ErrorListState { + const _$ErrorListState(this.string); + + @override + final String string; + + @override + String toString() { + return 'ListState.error(string: $string)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ErrorListState && + (identical(other.string, string) || other.string == string)); + } + + @override + int get hashCode => Object.hash(runtimeType, string); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ErrorListStateCopyWith<_$ErrorListState> get copyWith => + __$$ErrorListStateCopyWithImpl<_$ErrorListState>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() loading, + required TResult Function(String string) error, + required TResult Function(List packagesList) success, + }) { + return error(string); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? loading, + TResult? Function(String string)? error, + TResult? Function(List packagesList)? success, + }) { + return error?.call(string); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? loading, + TResult Function(String string)? error, + TResult Function(List packagesList)? success, + required TResult orElse(), + }) { + if (error != null) { + return error(string); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(LoadingListState value) loading, + required TResult Function(ErrorListState value) error, + required TResult Function(SuccessListState value) success, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LoadingListState value)? loading, + TResult? Function(ErrorListState value)? error, + TResult? Function(SuccessListState value)? success, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LoadingListState value)? loading, + TResult Function(ErrorListState value)? error, + TResult Function(SuccessListState value)? success, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class ErrorListState implements ListState { + const factory ErrorListState(final String string) = _$ErrorListState; + + String get string; + @JsonKey(ignore: true) + _$$ErrorListStateCopyWith<_$ErrorListState> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$SuccessListStateCopyWith<$Res> { + factory _$$SuccessListStateCopyWith( + _$SuccessListState value, $Res Function(_$SuccessListState) then) = + __$$SuccessListStateCopyWithImpl<$Res>; + @useResult + $Res call({List packagesList}); +} + +/// @nodoc +class __$$SuccessListStateCopyWithImpl<$Res> + extends _$ListStateCopyWithImpl<$Res, _$SuccessListState> + implements _$$SuccessListStateCopyWith<$Res> { + __$$SuccessListStateCopyWithImpl( + _$SuccessListState _value, $Res Function(_$SuccessListState) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? packagesList = null, + }) { + return _then(_$SuccessListState( + null == packagesList + ? _value._packagesList + : packagesList // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$SuccessListState implements SuccessListState { + const _$SuccessListState(final List packagesList) + : _packagesList = packagesList; + + final List _packagesList; + @override + List get packagesList { + if (_packagesList is EqualUnmodifiableListView) return _packagesList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_packagesList); + } + + @override + String toString() { + return 'ListState.success(packagesList: $packagesList)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SuccessListState && + const DeepCollectionEquality() + .equals(other._packagesList, _packagesList)); + } + + @override + int get hashCode => Object.hash( + runtimeType, const DeepCollectionEquality().hash(_packagesList)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SuccessListStateCopyWith<_$SuccessListState> get copyWith => + __$$SuccessListStateCopyWithImpl<_$SuccessListState>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() loading, + required TResult Function(String string) error, + required TResult Function(List packagesList) success, + }) { + return success(packagesList); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? loading, + TResult? Function(String string)? error, + TResult? Function(List packagesList)? success, + }) { + return success?.call(packagesList); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? loading, + TResult Function(String string)? error, + TResult Function(List packagesList)? success, + required TResult orElse(), + }) { + if (success != null) { + return success(packagesList); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(LoadingListState value) loading, + required TResult Function(ErrorListState value) error, + required TResult Function(SuccessListState value) success, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(LoadingListState value)? loading, + TResult? Function(ErrorListState value)? error, + TResult? Function(SuccessListState value)? success, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(LoadingListState value)? loading, + TResult Function(ErrorListState value)? error, + TResult Function(SuccessListState value)? success, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class SuccessListState implements ListState { + const factory SuccessListState(final List packagesList) = + _$SuccessListState; + + List get packagesList; + @JsonKey(ignore: true) + _$$SuccessListStateCopyWith<_$SuccessListState> get copyWith => + throw _privateConstructorUsedError; +}