From 94676038ffcb68720e36566795b063de0c95df5d Mon Sep 17 00:00:00 2001 From: Efrain Date: Wed, 2 Jun 2021 22:30:16 -0500 Subject: [PATCH] [Presentation] Reworked the game codes page --- lib/infrastructure/data_service.dart | 1 - lib/injection.dart | 3 + lib/main.dart | 5 +- .../game_codes/game_codes_page.dart | 185 +++++++++--------- .../widgets/game_code_list_item.dart | 156 +++++++++++++++ lib/presentation/main_tab_page.dart | 1 - lib/presentation/shared/styles.dart | 1 + 7 files changed, 254 insertions(+), 98 deletions(-) create mode 100644 lib/presentation/game_codes/widgets/game_code_list_item.dart diff --git a/lib/infrastructure/data_service.dart b/lib/infrastructure/data_service.dart index fb87d32ea..ea99d81ff 100644 --- a/lib/infrastructure/data_service.dart +++ b/lib/infrastructure/data_service.dart @@ -37,7 +37,6 @@ class DataServiceImpl implements DataService { _gameCodesBox = await Hive.openBox('gameCodes'); _gameCodeRewardsBox = await Hive.openBox('gameCodeRewards'); _tierListBox = await Hive.openBox('tierList'); - await _gameCodesBox.clear(); } @override diff --git a/lib/injection.dart b/lib/injection.dart index 4ca461645..c4c5cf365 100644 --- a/lib/injection.dart +++ b/lib/injection.dart @@ -1,6 +1,7 @@ import 'package:genshindb/domain/services/calculator_service.dart'; import 'package:genshindb/domain/services/data_service.dart'; import 'package:genshindb/domain/services/device_info_service.dart'; +import 'package:genshindb/domain/services/game_code_service.dart'; import 'package:genshindb/domain/services/genshin_service.dart'; import 'package:genshindb/domain/services/locale_service.dart'; import 'package:genshindb/domain/services/logging_service.dart'; @@ -36,4 +37,6 @@ Future initInjection() async { final dataService = DataServiceImpl(getIt(), getIt()); await dataService.init(); getIt.registerSingleton(dataService); + + getIt.registerSingleton(GameCodeServiceImpl(getIt(), getIt())); } diff --git a/lib/main.dart b/lib/main.dart index 32a5b5e31..55ae821a3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'application/bloc.dart'; import 'domain/services/calculator_service.dart'; import 'domain/services/data_service.dart'; import 'domain/services/device_info_service.dart'; +import 'domain/services/game_code_service.dart'; import 'domain/services/genshin_service.dart'; import 'domain/services/locale_service.dart'; import 'domain/services/logging_service.dart'; @@ -105,7 +106,9 @@ class MyApp extends StatelessWidget { create: (ctx) { final dataService = getIt(); final telemetryService = getIt(); - return GameCodesBloc(dataService, telemetryService); + final gameCodeService = getIt(); + final networkService = getIt(); + return GameCodesBloc(dataService, telemetryService, gameCodeService, networkService); }, ), BlocProvider( diff --git a/lib/presentation/game_codes/game_codes_page.dart b/lib/presentation/game_codes/game_codes_page.dart index baa110b96..68a419d76 100644 --- a/lib/presentation/game_codes/game_codes_page.dart +++ b/lib/presentation/game_codes/game_codes_page.dart @@ -1,21 +1,29 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:genshindb/application/bloc.dart'; import 'package:genshindb/domain/models/models.dart'; import 'package:genshindb/generated/l10n.dart'; -import 'package:genshindb/presentation/shared/common_table_cell.dart'; +import 'package:genshindb/presentation/shared/bullet_list.dart'; import 'package:genshindb/presentation/shared/item_description_detail.dart'; -import 'package:genshindb/presentation/shared/styles.dart'; +import 'package:genshindb/presentation/shared/nothing_found_column.dart'; import 'package:genshindb/presentation/shared/utils/toast_utils.dart'; -import 'package:genshindb/presentation/shared/wrapped_ascension_material.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:url_launcher/url_launcher.dart'; -class GameCodesPage extends StatelessWidget { +import 'widgets/game_code_list_item.dart'; + +class GameCodesPage extends StatefulWidget { const GameCodesPage({ Key key, }) : super(key: key); + @override + _GameCodesPageState createState() => _GameCodesPageState(); +} + +class _GameCodesPageState extends State { + final RefreshController _refreshController = RefreshController(initialRefresh: false); + @override Widget build(BuildContext context) { final s = S.of(context); @@ -26,111 +34,75 @@ class GameCodesPage extends StatelessWidget { IconButton( icon: const Icon(Icons.open_in_new), onPressed: () => _launchUrl('https://genshin.mihoyo.com/en/gift'), - ) + ), + IconButton( + icon: const Icon(Icons.info), + onPressed: () => _showInfoDialog(context), + ), ], ), body: SafeArea( - child: SingleChildScrollView( - child: Padding( - padding: Styles.edgeInsetAll10, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - BlocBuilder( - builder: (ctx, state) => state.map( - loaded: (state) => _buildTableCard(s.workingCodes, state.workingGameCodes, context), - ), - ), - BlocBuilder( - builder: (ctx, state) => state.map( - loaded: (state) => _buildTableCard(s.expiredCodes, state.expiredGameCodes, context), + child: SmartRefresher( + header: const MaterialClassicHeader(), + controller: _refreshController, + onRefresh: () { + context.read().add(const GameCodesEvent.refresh()); + }, + child: BlocConsumer( + listener: (ctx, state) { + if (!state.isBusy) { + _refreshController.refreshCompleted(); + } + + if (state.isInternetAvailable == false) { + ToastUtils.showWarningToast(ToastUtils.of(context), s.noInternetConnection); + } + }, + builder: (ctx, state) => state.expiredGameCodes.isEmpty && state.workingGameCodes.isEmpty + ? state.isBusy + ? const Center(child: CircularProgressIndicator()) + : NothingFoundColumn(msg: '${s.noGameCodesHaveBeenLoaded}\n${s.pullToRefreshItems}', icon: Icons.refresh) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + margin: const EdgeInsets.only(top: 10), + child: _buildTableCard(s.workingCodes, state.isBusy, state.workingGameCodes, context), + ), + _buildTableCard(s.expiredCodes, state.isBusy, state.expiredGameCodes, context), + ], + ), ), - ), - ], - ), ), ), ), ); } - Widget _buildTableCard(String title, List codes, BuildContext context) { - final s = S.of(context); - final body = Card( - elevation: Styles.cardTenElevation, - margin: Styles.edgeInsetAll5, - shape: Styles.cardShape, - child: Padding( - padding: Styles.edgeInsetAll5, - child: Table( - columnWidths: const { - 0: FractionColumnWidth(.3), - 1: FractionColumnWidth(.4), - 2: FractionColumnWidth(.3), - }, - children: [ - TableRow( - children: [ - CommonTableCell(text: s.codes, padding: Styles.edgeInsetAll10), - CommonTableCell(text: s.rewards, padding: Styles.edgeInsetAll10), - Container(), - ], - ), - ...codes.map((e) => _buildRow(s, context, e)).toList(), - ], - ), - ), - ); + @override + void dispose() { + _refreshController.dispose(); + super.dispose(); + } + Widget _buildTableCard(String title, bool isBusy, List codes, BuildContext context) { + if (isBusy) { + return ItemDescriptionDetail( + title: title, + body: const SizedBox(height: 200, child: Center(child: CircularProgressIndicator())), + textColor: Theme.of(context).accentColor, + ); + } return ItemDescriptionDetail( title: title, - body: body, textColor: Theme.of(context).accentColor, - ); - } - - TableRow _buildRow(S s, BuildContext context, GameCodeModel model) { - final fToast = ToastUtils.of(context); - final rewards = model.rewards.map((m) => WrappedAscensionMaterial(image: m.fullImagePath, quantity: m.quantity, size: 20)).toList(); - return TableRow( - children: [ - CommonTableCell.child( - child: Center( - child: Tooltip( - message: model.code, - child: Text( - model.code, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.subtitle2.copyWith(fontSize: 12), - ), + body: codes.isEmpty + ? Container() + : Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: codes.map((e) => GameCodeListItem(item: e)).toList(), ), - ), - ), - CommonTableCell.child( - child: Wrap(alignment: WrapAlignment.center, children: rewards), - ), - CommonTableCell.child( - child: Wrap( - alignment: WrapAlignment.center, - children: [ - IconButton( - tooltip: !model.isUsed ? s.markAsUsed : s.markAsUnused, - splashRadius: 20, - icon: Icon(Icons.check, color: model.isUsed ? Colors.green : Colors.red), - onPressed: () => context.read().add(GameCodesEvent.markAsUsed(code: model.code, wasUsed: !model.isUsed)), - ), - IconButton( - tooltip: s.copy, - splashRadius: 20, - icon: const Icon(Icons.copy), - onPressed: () => Clipboard.setData(ClipboardData(text: model.code)).then( - (value) => ToastUtils.showInfoToast(fToast, s.codeXWasCopied(model.code)), - ), - ) - ], - ), - ) - ], ); } @@ -139,4 +111,27 @@ class GameCodesPage extends StatelessWidget { await launch(url); } } + + Future _showInfoDialog(BuildContext context) async { + final s = S.of(context); + //TODO: SWIPE TO SEE MORE + final explanations = [ + s.internetIsRequiredToRefreshItems, + ]; + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(s.information), + content: SingleChildScrollView( + child: BulletList(items: explanations, fontSize: 14), + ), + actions: [ + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(s.ok), + ) + ], + ), + ); + } } diff --git a/lib/presentation/game_codes/widgets/game_code_list_item.dart b/lib/presentation/game_codes/widgets/game_code_list_item.dart new file mode 100644 index 000000000..42c1f7021 --- /dev/null +++ b/lib/presentation/game_codes/widgets/game_code_list_item.dart @@ -0,0 +1,156 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:genshindb/application/bloc.dart'; +import 'package:genshindb/domain/enums/enums.dart'; +import 'package:genshindb/domain/models/models.dart'; +import 'package:genshindb/domain/utils/date_utils.dart' as utils; +import 'package:genshindb/generated/l10n.dart'; +import 'package:genshindb/presentation/shared/extensions/i18n_extensions.dart'; +import 'package:genshindb/presentation/shared/styles.dart'; +import 'package:genshindb/presentation/shared/utils/toast_utils.dart'; +import 'package:genshindb/presentation/shared/wrapped_ascension_material.dart'; + +class GameCodeListItem extends StatelessWidget { + final String code; + final DateTime discoveredOn; + final DateTime expiredOn; + final bool isUsed; + final bool isExpired; + final AppServerResetTimeType region; + + final List rewards; + + GameCodeListItem({ + Key key, + @required GameCodeModel item, + }) : code = item.code, + discoveredOn = item.discoveredOn, + expiredOn = item.expiredOn, + isUsed = item.isUsed, + isExpired = item.isExpired, + region = item.region, + rewards = item.rewards, + super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final theme = Theme.of(context); + final textCodeStyle = !isUsed + ? theme.textTheme.subtitle1 + : theme.textTheme.subtitle1.copyWith(decoration: TextDecoration.lineThrough, decorationColor: theme.accentColor, decorationThickness: 2); + + return Slidable( + actionPane: const SlidableDrawerActionPane(), + secondaryActions: [ + IconSlideAction( + caption: !isUsed ? s.markAsUsed : s.markAsUnused, + color: !isUsed ? Colors.green : Colors.red, + iconWidget: Icon(!isUsed ? Icons.check : Icons.close, color: Colors.white), + onTap: () => context.read().add(GameCodesEvent.markAsUsed(code: code, wasUsed: !isUsed)), + ), + IconSlideAction( + caption: s.copy, + icon: Icons.copy, + color: Colors.blueAccent, + onTap: () => Clipboard.setData(ClipboardData(text: code)).then( + (value) => ToastUtils.showInfoToast(ToastUtils.of(context), s.codeXWasCopied(code)), + ), + ), + ], + child: Container( + margin: Styles.edgeInsetVertical16, + padding: Styles.edgeInsetHorizontal16, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + code, + style: textCodeStyle, + overflow: TextOverflow.ellipsis, + ), + ), + Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: rewards.map((m) => WrappedAscensionMaterial(image: m.fullImagePath, quantity: m.quantity, size: 25)).toList(), + ), + if (region != null) + Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Icon( + region != null ? Icons.lock_outlined : Icons.lock_open_outlined, + color: theme.accentColor, + size: 15, + ), + Text( + s.onlyX(s.translateServerResetTimeType(region)), + style: theme.textTheme.caption, + overflow: TextOverflow.ellipsis, + ), + ], + ) + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildDateInfoRow( + s.addedOn(utils.DateUtils.formatDate(discoveredOn, format: utils.DateUtils.dayMonthYearFormat)), + context, + ), + if (isExpired) + _buildDateInfoRow( + s.expiredOn(utils.DateUtils.formatDate(expiredOn, format: utils.DateUtils.dayMonthYearFormat)), + context, + ), + if (!isExpired && expiredOn != null) + _buildDateInfoRow( + s.validUntil(utils.DateUtils.formatDate(expiredOn, format: utils.DateUtils.dayMonthYearFormat)), + context, + ), + if (!isExpired && expiredOn == null) _buildDateInfoRow(s.validUntil(s.na), context), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildDateInfoRow(String text, BuildContext context) { + final theme = Theme.of(context); + return Container( + margin: const EdgeInsets.symmetric(vertical: 3), + child: Row( + children: [ + Container( + margin: const EdgeInsets.only(right: 5), + child: const Icon(Icons.date_range, size: 13), + ), + Expanded( + child: Text( + text, + style: theme.textTheme.caption, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/main_tab_page.dart b/lib/presentation/main_tab_page.dart index 50d84a5d6..951bf7493 100644 --- a/lib/presentation/main_tab_page.dart +++ b/lib/presentation/main_tab_page.dart @@ -55,7 +55,6 @@ class _MainTabPageState extends State with SingleTickerProviderStat context.read().add(const ArtifactsEvent.init()); context.read().add(const ElementsEvent.init()); context.read().add(const SettingsEvent.init()); - context.read().add(const GameCodesEvent.init()); } @override diff --git a/lib/presentation/shared/styles.dart b/lib/presentation/shared/styles.dart index 9448138ee..6bf4c1ac0 100644 --- a/lib/presentation/shared/styles.dart +++ b/lib/presentation/shared/styles.dart @@ -26,6 +26,7 @@ class Styles { static const edgeInsetHorizontal16 = EdgeInsets.symmetric(horizontal: 16); static const edgeInsetVertical5 = EdgeInsets.symmetric(vertical: 5); static const edgeInsetHorizontal5 = EdgeInsets.symmetric(horizontal: 5); + static const edgeInsetVertical16 = EdgeInsets.symmetric(vertical: 16); static const modalBottomSheetShape = RoundedRectangleBorder( borderRadius: BorderRadius.only(