Skip to content

Commit

Permalink
Merge branch 'feat/chain-sync-v2' of github.com:input-output-hk/catal…
Browse files Browse the repository at this point in the history
…yst-voices into feat/chain-sync-v2
  • Loading branch information
stevenj committed Sep 27, 2024
2 parents 9f2498c + 68690c2 commit 2adf600
Show file tree
Hide file tree
Showing 18 changed files with 588 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import 'package:catalyst_voices/widgets/buttons/voices_filled_button.dart';
import 'dart:async';

import 'package:catalyst_cardano/catalyst_cardano.dart';
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';
import 'package:result_type/result_type.dart';

// TODO(dtscalac): define content
class SelectWalletPanel extends StatelessWidget {
const SelectWalletPanel({super.key});

Expand All @@ -12,23 +16,133 @@ class SelectWalletPanel extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Spacer(),
VoicesFilledButton(
leading: VoicesAssets.icons.wallet.buildIcon(),
const SizedBox(height: 24),
Text(
context.l10n.walletLinkSelectWalletTitle,
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 24),
Text(
context.l10n.walletLinkSelectWalletContent,
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 40),
const Expanded(child: _Wallets()),
const SizedBox(height: 24),
VoicesBackButton(
onTap: () {
RegistrationBloc.of(context).add(const PreviousStepEvent());
},
child: const Text('Previous'),
),
const SizedBox(height: 12),
VoicesFilledButton(
leading: VoicesAssets.icons.wallet.buildIcon(),
const SizedBox(height: 10),
VoicesTextButton(
trailing: VoicesAssets.icons.externalLink.buildIcon(),
onTap: () {},
child: Text(context.l10n.seeAllSupportedWallets),
),
],
);
}
}

class _Wallets extends StatefulWidget {
const _Wallets();

@override
State<_Wallets> createState() => _WalletsState();
}

class _WalletsState extends State<_Wallets> {
@override
void initState() {
super.initState();

final bloc = RegistrationBloc.of(context);
if (bloc.cardanoWallets.value == null) {
unawaited(bloc.refreshCardanoWallets());
}
}

@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: RegistrationBloc.of(context).cardanoWallets,
builder: (context, result, _) {
return switch (result) {
Success(:final value) => value.isNotEmpty
? _WalletsList(wallets: value)
: _WalletsEmpty(onRetry: _onRetry),
Failure() => _WalletsError(onRetry: _onRetry),
_ => const Center(child: VoicesCircularProgressIndicator()),
};
},
);
}

void _onRetry() {
unawaited(RegistrationBloc.of(context).refreshCardanoWallets());
}
}

class _WalletsList extends StatelessWidget {
final List<CardanoWallet> wallets;

const _WalletsList({required this.wallets});

@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: wallets.length,
itemBuilder: (context, index) {
final wallet = wallets[index];
return VoicesWalletTile(
iconSrc: wallet.icon,
name: Text(wallet.name),
onTap: () {
RegistrationBloc.of(context).add(const NextStepEvent());
},
child: const Text('Next'),
);
},
);
}
}

class _WalletsEmpty extends StatelessWidget {
final VoidCallback onRetry;

const _WalletsEmpty({required this.onRetry});

@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: SizedBox(
width: double.infinity,
child: VoicesErrorIndicator(
message: context.l10n.noWalletFound,
onRetry: onRetry,
),
],
),
);
}
}

class _WalletsError extends StatelessWidget {
final VoidCallback onRetry;

const _WalletsError({required this.onRetry});

@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.topCenter,
child: SizedBox(
width: double.infinity,
child: VoicesErrorIndicator(
message: context.l10n.somethingWentWrong,
onRetry: onRetry,
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import 'package:catalyst_voices/widgets/indicators/voices_circular_progress_indicator.dart';
import 'package:catalyst_voices/widgets/indicators/voices_error_indicator.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
import 'package:flutter/material.dart';

/// A callback that generates a new [Future] of type [T].
typedef VoicesFutureProvider<T extends Object> = Future<T> Function();

/// A callback that builds a widget from a [T] value.
///
/// Call [onRetry] if your data state contains
/// the retry button, it will reload the widget.
typedef VoicesFutureDataBuilder<T> = Widget Function(
BuildContext context,
T value,
VoidCallback onRetry,
);

/// A callback that builds a widget in an error state.
///
/// Call [onRetry] if your error state contains
/// the retry button, it will reload the widget.
typedef VoicesFutureErrorBuilder = Widget Function(
BuildContext context,
Object? error,
VoidCallback onRetry,
);

/// A [FutureBuilder] which simplifies handling a [Future] gently.
class VoicesFutureBuilder<T extends Object> extends StatefulWidget {
/// The future provider, make sure to return a fresh future
/// each time it is called, the widget takes care of caching
/// the future internally.
final VoicesFutureProvider<T> future;

/// The builder called to build a child
/// when the [future] finishes successfully.
final VoicesFutureDataBuilder<T> dataBuilder;

/// The builder called to build a child
/// when the [future] finishes with an error.
///
/// If not provided then [VoicesErrorIndicator] is used instead.
final VoicesFutureErrorBuilder errorBuilder;

/// The builder called to build a child
/// when the [future] hasn't finished yet.
///
/// If not provided then a centered [VoicesCircularProgressIndicator]
/// is used instead.
final WidgetBuilder loaderBuilder;

/// The minimum duration during which the loader state is shown.
///
/// It is useful to delay a future which finishes in a split second
/// as this results in jumpy UI, not giving the user enough time to see
/// the loader state before data or error states are shown.
///
/// Pass [Duration.zero] to disable it.
final Duration minimumDelay;

const VoicesFutureBuilder({
super.key,
required this.future,
required this.dataBuilder,
this.errorBuilder = _defaultErrorBuilder,
this.loaderBuilder = _defaultLoaderBuilder,
this.minimumDelay = const Duration(milliseconds: 300),
});

@override
State<VoicesFutureBuilder> createState() => _VoicesFutureBuilderState<T>();
}

class _VoicesFutureBuilderState<T extends Object>
extends State<VoicesFutureBuilder<T>> {
Future<T>? _future;

@override
void initState() {
super.initState();

// ignore: discarded_futures
_future = _makeDelayedFuture();
}

@override
void dispose() {
_future = null;
super.dispose();
}

@override
void didUpdateWidget(VoicesFutureBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);

if (widget.future != oldWidget.future) {
// ignore: discarded_futures
_future = _makeDelayedFuture();
}
}

@override
Widget build(BuildContext context) {
return FutureBuilder<T>(
future: _future,
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(context, snapshot.error, _onRetry);
}

final data = snapshot.data;
if (data == null) {
return widget.loaderBuilder(context);
}

return widget.dataBuilder(context, data, _onRetry);
},
);
}

void _onRetry() {
setState(() {
// ignore: discarded_futures
_future = _makeDelayedFuture();
});
}

Future<T> _makeDelayedFuture() async {
return widget.future().withMinimumDelay(widget.minimumDelay);
}
}

Widget _defaultErrorBuilder(
BuildContext context,
Object? error,
VoidCallback onRetry,
) {
return _Error(onRetry: onRetry);
}

Widget _defaultLoaderBuilder(BuildContext context) {
return const _Loader();
}

class _Error extends StatelessWidget {
final VoidCallback onRetry;

const _Error({required this.onRetry});

@override
Widget build(BuildContext context) {
return VoicesErrorIndicator(
message: context.l10n.somethingWentWrong,
onRetry: onRetry,
);
}
}

class _Loader extends StatelessWidget {
const _Loader();

@override
Widget build(BuildContext context) {
return const Center(child: VoicesCircularProgressIndicator());
}
}
59 changes: 59 additions & 0 deletions catalyst_voices/lib/widgets/indicators/voices_error_indicator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'package:catalyst_voices/widgets/widgets.dart';
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';

/// A generic error state with optional retry button.
class VoicesErrorIndicator extends StatelessWidget {
/// The description of the error.
final String message;

/// The callback called when refresh button is tapped.
///
/// If null then retry button is hidden.
final VoidCallback? onRetry;

const VoicesErrorIndicator({
super.key,
required this.message,
this.onRetry,
});

@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16),
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).colors.outlineBorderVariant!,
width: 1,
),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
VoicesAssets.icons.exclamation.buildIcon(
color: Theme.of(context).colors.iconsError,
size: 20,
),
const SizedBox(width: 10),
Text(
message,
style: Theme.of(context).textTheme.titleSmall?.copyWith(height: 1),
),
if (onRetry != null) ...[
const SizedBox(width: 10),
VoicesTextButton(
leading: VoicesAssets.icons.refresh.buildIcon(),
onTap: onRetry,
child: Text(context.l10n.retry),
),
],
],
),
);
}
}
1 change: 1 addition & 0 deletions catalyst_voices/lib/widgets/widgets.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export 'headers/section_header.dart';
export 'headers/segment_header.dart';
export 'indicators/process_progress_indicator.dart';
export 'indicators/voices_circular_progress_indicator.dart';
export 'indicators/voices_error_indicator.dart';
export 'indicators/voices_linear_progress_indicator.dart';
export 'indicators/voices_no_internet_connection_banner.dart';
export 'indicators/voices_password_strength_indicator.dart';
Expand Down
Loading

0 comments on commit 2adf600

Please sign in to comment.