diff --git a/lib/infrastructure/genshin_service.dart b/lib/infrastructure/genshin_service.dart index 2a1189b1c..b13289842 100644 --- a/lib/infrastructure/genshin_service.dart +++ b/lib/infrastructure/genshin_service.dart @@ -918,6 +918,25 @@ class GenshinServiceImpl implements GenshinService { .toList() ..sort((x, y) => x.from.compareTo(y.from)); + @override + List getItemReleaseHistory(String itemKey) { + final history = _bannerHistoryFile.banners + .where((el) => el.itemKeys.contains(itemKey)) + .map((e) => ItemReleaseHistoryModel(version: e.version, dates: [ItemReleaseHistoryDatesModel(from: e.from, until: e.until)])) + .toList(); + + if (history.isEmpty) { + throw Exception('There is no banner history associated to itemKey = $itemKey'); + } + + return history + .groupListsBy((el) => el.version) + .entries + .map((e) => ItemReleaseHistoryModel(version: e.key, dates: e.value.expand((el) => el.dates).toList())) + .toList() + ..sort((x, y) => x.version.compareTo(y.version)); + } + CharacterCardModel _toCharacterForCard(CharacterFileModel character) { final translation = getCharacterTranslation(character.key); diff --git a/lib/infrastructure/telemetry/telemetry_service.dart b/lib/infrastructure/telemetry/telemetry_service.dart index b0d470d11..d21bb6bdb 100644 --- a/lib/infrastructure/telemetry/telemetry_service.dart +++ b/lib/infrastructure/telemetry/telemetry_service.dart @@ -165,4 +165,7 @@ class TelemetryServiceImpl implements TelemetryService { @override Future trackBannerHistoryItemOpened(double version) => trackEventAsync('Banner-History-Item-Opened', {'Version': '$version'}); + + @override + Future trackItemReleaseHistoryOpened(String itemKey) => trackEventAsync('Banner-History-Item-Release-History-Opened', {'ItemKey': itemKey}); } diff --git a/lib/injection.dart b/lib/injection.dart index ace32ad2a..451e630e3 100644 --- a/lib/injection.dart +++ b/lib/injection.dart @@ -156,6 +156,12 @@ class Injection { return BannerHistoryItemBloc(genshinService, telemetryService); } + static ItemReleaseHistoryBloc get itemReleaseHistoryBloc { + final genshinService = getIt(); + final telemetryService = getIt(); + return ItemReleaseHistoryBloc(genshinService, telemetryService); + } + //TODO: USE THIS PROP // static CalculatorAscMaterialsItemBloc get calculatorAscMaterialsItemBloc { // final genshinService = getIt(); diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index a84cbe03d..1817831ec 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -425,5 +425,8 @@ "nameAsc": "Name asc.", "nameDesc": "Name desc.", "versionAsc": "Version asc.", - "versionDesc": "Version desc." + "versionDesc": "Version desc.", + "releaseHistory": "Release History", + "selectAnOption": "Select an option", + "details": "Details" } diff --git a/lib/presentation/banner_history/banner_history_page.dart b/lib/presentation/banner_history/banner_history_page.dart index 7f8342e2b..088399fd9 100644 --- a/lib/presentation/banner_history/banner_history_page.dart +++ b/lib/presentation/banner_history/banner_history_page.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:responsive_builder/responsive_builder.dart'; import 'package:shiori/application/bloc.dart'; import 'package:shiori/domain/enums/enums.dart'; +import 'package:shiori/domain/extensions/string_extensions.dart'; +import 'package:shiori/domain/models/models.dart'; import 'package:shiori/generated/l10n.dart'; import 'package:shiori/injection.dart'; import 'package:shiori/presentation/banner_history/widgets/content.dart'; @@ -13,9 +16,11 @@ import 'package:shiori/presentation/shared/item_popupmenu_filter.dart'; import 'package:shiori/presentation/shared/mixins/app_fab_mixin.dart'; import 'package:shiori/presentation/shared/sync_controller.dart'; -const double _firstCellWidth = 150; +const double _tabletFirstCellWidth = 150; +const double _mobileFirstCellWidth = 120; const double _firstCellHeight = 70; -const double _cellWidth = 100; +const double _tabletCellWidth = 100; +const double _mobileCellWidth = 80; const double _cellHeight = 120; class BannerHistoryPage extends StatefulWidget { @@ -41,6 +46,12 @@ class _BannerHistoryPageState extends State with SingleTicker @override Widget build(BuildContext context) { const margin = EdgeInsets.all(4.0); + double firstCellWidth = _tabletFirstCellWidth; + double cellWidth = _tabletCellWidth; + if (getDeviceType(MediaQuery.of(context).size) == DeviceScreenType.mobile) { + firstCellWidth = _mobileFirstCellWidth; + cellWidth = _mobileCellWidth; + } return BlocProvider( create: (_) => Injection.bannerHistoryBloc..add(const BannerHistoryEvent.init()), child: Scaffold( @@ -62,9 +73,9 @@ class _BannerHistoryPageState extends State with SingleTicker versions: state.versions, selectedVersions: state.selectedVersions, margin: margin, - firstCellWidth: _firstCellWidth, + firstCellWidth: firstCellWidth, firstCellHeight: _firstCellHeight, - cellWidth: _cellWidth, + cellWidth: cellWidth, cellHeight: 60, ), ), @@ -79,7 +90,7 @@ class _BannerHistoryPageState extends State with SingleTicker BlocBuilder( builder: (ctx, state) => FixedLeftColumn( margin: margin, - cellWidth: _firstCellWidth, + cellWidth: firstCellWidth, cellHeight: _cellHeight, items: state.banners, ), @@ -98,7 +109,7 @@ class _BannerHistoryPageState extends State with SingleTicker banners: state.banners, versions: state.versions, margin: margin, - cellWidth: _cellWidth, + cellWidth: cellWidth, cellHeight: _cellHeight, ), ), @@ -131,6 +142,21 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { builder: (ctx, state) => AppBar( title: Text(s.bannerHistory), actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () => showSearch>( + context: context, + delegate: _AppBarSearchDelegate( + ctx.read().getItemsForSearch(), + [...state.selectedItemKeys], + ), + ).then((keys) { + if (keys == null) { + return; + } + context.read().add(BannerHistoryEvent.itemsSelected(keys: keys)); + }), + ), ItemPopupMenuFilter( tooltipText: s.bannerType, selectedValue: state.type, @@ -155,3 +181,65 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); } + +class _AppBarSearchDelegate extends SearchDelegate> { + final List items; + final List selected; + + _AppBarSearchDelegate(this.items, this.selected); + + @override + List buildActions(BuildContext context) => [ + IconButton( + icon: const Icon(Icons.check, color: Colors.green), + onPressed: () => close(context, selected), + ), + IconButton( + icon: const Icon(Icons.clear, color: Colors.red), + onPressed: () { + if (query.isNullEmptyOrWhitespace) { + close(context, []); + } else { + query = ''; + } + }, + ) + ]; + + @override + Widget buildLeading(BuildContext context) => IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => close(context, selected), + ); + + @override + Widget buildResults(BuildContext context) => Text(query); + + @override + Widget buildSuggestions(BuildContext context) { + final possibilities = query.isNullEmptyOrWhitespace ? items : items.where((el) => el.name.toLowerCase().contains(query.toLowerCase())).toList(); + possibilities.sort((x, y) => x.name.compareTo(y.name)); + + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) => ListView.builder( + itemCount: possibilities.length, + itemBuilder: (ctx, index) { + final item = possibilities[index]; + final isSelected = selected.any((el) => el == item.key); + return ListTile( + title: Text(item.name), + leading: isSelected ? const Icon(Icons.check) : null, + minLeadingWidth: 10, + onTap: () { + if (isSelected) { + setState(() => selected.remove(item.key)); + } else { + setState(() => selected.add(item.key)); + } + }, + ); + }, + ), + ); + } +} diff --git a/lib/presentation/banner_history/widgets/content.dart b/lib/presentation/banner_history/widgets/content.dart index 8e9090869..5d2bf675b 100644 --- a/lib/presentation/banner_history/widgets/content.dart +++ b/lib/presentation/banner_history/widgets/content.dart @@ -65,7 +65,6 @@ class _ContentCard extends StatelessWidget { return SizedBox.fromSize(size: Size(cellWidth + margin.horizontal, cellHeight + margin.vertical)); } final theme = Theme.of(context); - //TODO: LIGHT COLOR return SizedBox( child: Container( width: cellWidth, @@ -74,7 +73,7 @@ class _ContentCard extends StatelessWidget { child: Container( alignment: Alignment.center, margin: const EdgeInsets.only(top: 15, bottom: 15, left: 10, right: 10), - color: theme.colorScheme.background.withOpacity(0.2), + color: theme.brightness == Brightness.dark ? theme.colorScheme.background.withOpacity(0.2) : theme.dividerColor, child: number != null ? Text( '$number', diff --git a/lib/presentation/banner_history/widgets/fixed_header_row.dart b/lib/presentation/banner_history/widgets/fixed_header_row.dart index 362bb1aa5..bb2ebdced 100644 --- a/lib/presentation/banner_history/widgets/fixed_header_row.dart +++ b/lib/presentation/banner_history/widgets/fixed_header_row.dart @@ -2,20 +2,10 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:intl/intl.dart'; import 'package:shiori/application/bloc.dart'; import 'package:shiori/domain/enums/enums.dart'; -import 'package:shiori/domain/models/models.dart'; import 'package:shiori/generated/l10n.dart'; -import 'package:shiori/injection.dart'; -import 'package:shiori/presentation/shared/extensions/media_query_extensions.dart'; -import 'package:shiori/presentation/shared/extensions/rarity_extensions.dart'; -import 'package:shiori/presentation/shared/images/circle_character.dart'; -import 'package:shiori/presentation/shared/images/circle_weapon.dart'; -import 'package:shiori/presentation/shared/loading.dart'; - -const _dateFormat = 'yyyy/MM/dd'; +import 'package:shiori/presentation/banner_history/widgets/version_details_dialog.dart'; class FixedHeaderRow extends StatelessWidget { final BannerHistoryItemType type; @@ -146,11 +136,11 @@ class _VersionCard extends StatelessWidget { final theme = Theme.of(context); return InkWell( onTap: () => context.read().add(BannerHistoryEvent.versionSelected(version: version)), - onLongPress: () => showDialog(context: context, builder: (_) => _VersionDetailsDialog(version: version)), + onLongPress: () => showDialog(context: context, builder: (_) => VersionDetailsDialog(version: version)), child: Card( margin: margin, color: isSelected ? theme.colorScheme.primary.withOpacity(0.45) : theme.colorScheme.primary, - elevation: isSelected ? 0 : 10, + elevation: isSelected ? 0 : 5, child: Container( alignment: Alignment.center, width: cellWidth, @@ -159,150 +149,9 @@ class _VersionCard extends StatelessWidget { '$version', textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, - style: theme.textTheme.titleLarge!.copyWith(fontWeight: FontWeight.bold), - ), - ), - ), - ); - } -} - -class _VersionDetailsDialog extends StatelessWidget { - final double version; - - const _VersionDetailsDialog({ - Key? key, - required this.version, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final mq = MediaQuery.of(context); - final s = S.of(context); - return BlocProvider( - create: (context) => Injection.bannerHistoryItemBloc..add(BannerHistoryItemEvent.init(version: version)), - child: AlertDialog( - title: Text(s.appVersion(version)), - content: SizedBox( - width: mq.getWidthForDialogs(), - child: SingleChildScrollView( - child: BlocBuilder( - builder: (context, state) => state.maybeMap( - loadedState: (state) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: state.items - .groupListsBy((el) => '${DateFormat(_dateFormat).format(el.from)}_${DateFormat(_dateFormat).format(el.until)}') - .values - .map( - (e) { - final group = e.first; - - return _VersionDetailPeriod( - from: group.from, - until: group.until, - items: e.expand((el) => el.items).toList(), - ); - }, - ).toList(), - ), - orElse: () => const Loading(useScaffold: false), - ), - ), + style: theme.textTheme.titleLarge!.copyWith(fontWeight: FontWeight.bold, color: Colors.white), ), ), - actions: [ - ElevatedButton( - onPressed: () => Navigator.pop(context), - child: Text(s.ok), - ) - ], - ), - ); - } -} - -class _VersionDetailPeriod extends StatelessWidget { - final DateTime from; - final DateTime until; - final List items; - - const _VersionDetailPeriod({ - Key? key, - required this.from, - required this.until, - required this.items, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final s = S.of(context); - final theme = Theme.of(context); - final from = DateFormat(_dateFormat).format(this.from); - final until = DateFormat(_dateFormat).format(this.until); - final characters = items.where((el) => el.type == ItemType.character).toList()..sort((x, y) => y.rarity.compareTo(x.rarity)); - final weapons = items.where((el) => el.type == ItemType.weapon).toList()..sort((x, y) => y.rarity.compareTo(x.rarity)); - - return Container( - margin: const EdgeInsets.only(bottom: 20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(s.fromDate(from), style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold)), - Text(s.untilDate(until), style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold)), - ], - ), - Divider(color: theme.colorScheme.primary), - if (characters.isNotEmpty) Text(s.characters, style: theme.textTheme.subtitle1), - if (characters.isNotEmpty) - _Items( - type: BannerHistoryItemType.character, - items: characters, - ), - if (weapons.isNotEmpty) Text(s.weapons, style: theme.textTheme.subtitle1), - if (weapons.isNotEmpty) - _Items( - type: BannerHistoryItemType.weapon, - items: weapons, - ), - ], - ), - ); - } -} - -class _Items extends StatelessWidget { - final BannerHistoryItemType type; - final List items; - - const _Items({ - Key? key, - required this.type, - required this.items, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return SizedBox( - height: type == BannerHistoryItemType.character ? 80 : 70, - child: ListView.builder( - physics: const BouncingScrollPhysics(), - scrollDirection: Axis.horizontal, - itemCount: items.length, - itemBuilder: (ctx, index) { - final item = items[index]; - final gradient = item.rarity.getRarityGradient(); - switch (type) { - case BannerHistoryItemType.character: - return CircleCharacter(itemKey: item.key, image: item.image, gradient: gradient); - case BannerHistoryItemType.weapon: - return CircleWeapon(itemKey: item.key, image: item.image, gradient: gradient); - default: - throw Exception('Banner history item type = $type is not valid'); - } - }, ), ); } diff --git a/lib/presentation/banner_history/widgets/fixed_left_column.dart b/lib/presentation/banner_history/widgets/fixed_left_column.dart index 0a4b110eb..2961c32c7 100644 --- a/lib/presentation/banner_history/widgets/fixed_left_column.dart +++ b/lib/presentation/banner_history/widgets/fixed_left_column.dart @@ -1,10 +1,19 @@ import 'package:flutter/material.dart'; import 'package:shiori/domain/enums/enums.dart'; import 'package:shiori/domain/models/models.dart'; +import 'package:shiori/generated/l10n.dart'; +import 'package:shiori/presentation/banner_history/widgets/item_release_history_dialog.dart'; +import 'package:shiori/presentation/character/character_page.dart'; import 'package:shiori/presentation/shared/extensions/rarity_extensions.dart'; import 'package:shiori/presentation/shared/images/circle_character.dart'; import 'package:shiori/presentation/shared/images/circle_weapon.dart'; import 'package:shiori/presentation/shared/styles.dart'; +import 'package:shiori/presentation/weapon/weapon_page.dart'; + +enum _ItemOptionsType { + details, + releaseHistory, +} class FixedLeftColumn extends StatelessWidget { final List items; @@ -76,9 +85,9 @@ class _ItemCard extends StatelessWidget { const double radius = 10; return InkWell( borderRadius: const BorderRadius.all(Radius.circular(radius)), - onTap: () { - //show some details here ? - }, + onTap: () => showDialog<_ItemOptionsType>(context: context, builder: (_) => const _OptionsDialog()).then( + (value) async => _handleOptionSelected(value, context), + ), child: Card( margin: margin, elevation: 10, @@ -108,7 +117,7 @@ class _ItemCard extends StatelessWidget { child: Text( '$number', overflow: TextOverflow.ellipsis, - style: theme.textTheme.subtitle2!.copyWith(fontWeight: FontWeight.bold), + style: theme.textTheme.subtitle2!.copyWith(fontWeight: FontWeight.bold, color: Colors.white), ), ), ), @@ -126,7 +135,7 @@ class _ItemCard extends StatelessWidget { name, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, - style: theme.textTheme.subtitle2!.copyWith(fontWeight: FontWeight.bold), + style: theme.textTheme.subtitle2!.copyWith(fontWeight: FontWeight.bold, color: Colors.white), ), ), ], @@ -137,4 +146,59 @@ class _ItemCard extends StatelessWidget { ), ); } + + Future _handleOptionSelected(_ItemOptionsType? value, BuildContext context) async { + if (value == null) { + return; + } + + switch (value) { + case _ItemOptionsType.details: + switch (type) { + case BannerHistoryItemType.character: + await CharacterPage.route(itemKey, context); + break; + case BannerHistoryItemType.weapon: + await WeaponPage.route(itemKey, context); + break; + } + break; + case _ItemOptionsType.releaseHistory: + await showDialog(context: context, builder: (_) => ItemReleaseHistoryDialog(itemKey: itemKey)); + break; + } + } +} + +class _OptionsDialog extends StatelessWidget { + const _OptionsDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return AlertDialog( + title: Text(s.selectAnOption), + actions: [ + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: Text(s.cancel), + ) + ], + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ListTile( + title: Text(s.details), + onTap: () => Navigator.pop(context, _ItemOptionsType.details), + ), + ListTile( + title: Text(s.releaseHistory), + onTap: () => Navigator.pop(context, _ItemOptionsType.releaseHistory), + ), + ], + ), + ), + ); + } } diff --git a/lib/presentation/banner_history/widgets/item_release_history_dialog.dart b/lib/presentation/banner_history/widgets/item_release_history_dialog.dart new file mode 100644 index 000000000..ee4f11e8a --- /dev/null +++ b/lib/presentation/banner_history/widgets/item_release_history_dialog.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:shiori/application/bloc.dart'; +import 'package:shiori/domain/models/models.dart'; +import 'package:shiori/generated/l10n.dart'; +import 'package:shiori/injection.dart'; +import 'package:shiori/presentation/shared/loading.dart'; + +const _dateFormat = 'yyyy/MM/dd'; + +class ItemReleaseHistoryDialog extends StatelessWidget { + final String itemKey; + + const ItemReleaseHistoryDialog({ + Key? key, + required this.itemKey, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return BlocProvider( + create: (context) => Injection.itemReleaseHistoryBloc..add(ItemReleaseHistoryEvent.init(itemKey: itemKey)), + child: AlertDialog( + title: Text(s.bannerHistory), + actions: [ + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: Text(s.ok), + ) + ], + content: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) => state.map( + loading: (_) => const Loading(useScaffold: false), + initial: (state) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: state.history.map((e) => _ReleasedOn(history: e)).toList(), + ), + ), + ), + ), + ), + ); + } +} + +class _ReleasedOn extends StatelessWidget { + final ItemReleaseHistoryModel history; + + const _ReleasedOn({ + Key? key, + required this.history, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final theme = Theme.of(context); + final dateFormat = DateFormat(_dateFormat); + return Container( + margin: const EdgeInsets.only(bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + s.appVersion(history.version), + style: theme.textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold), + ), + ...history.dates.map( + (e) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(s.fromDate(dateFormat.format(e.from))), + Text(s.untilDate(dateFormat.format(e.until))), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/banner_history/widgets/version_details_dialog.dart b/lib/presentation/banner_history/widgets/version_details_dialog.dart new file mode 100644 index 000000000..3c58b9f91 --- /dev/null +++ b/lib/presentation/banner_history/widgets/version_details_dialog.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; +import 'package:shiori/application/bloc.dart'; +import 'package:shiori/domain/enums/enums.dart'; +import 'package:shiori/domain/models/models.dart'; +import 'package:shiori/generated/l10n.dart'; +import 'package:shiori/injection.dart'; +import 'package:shiori/presentation/shared/extensions/media_query_extensions.dart'; +import 'package:shiori/presentation/shared/extensions/rarity_extensions.dart'; +import 'package:shiori/presentation/shared/images/circle_character.dart'; +import 'package:shiori/presentation/shared/images/circle_weapon.dart'; +import 'package:shiori/presentation/shared/loading.dart'; + +const _dateFormat = 'yyyy/MM/dd'; + +class VersionDetailsDialog extends StatelessWidget { + final double version; + + const VersionDetailsDialog({ + Key? key, + required this.version, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final mq = MediaQuery.of(context); + final s = S.of(context); + return BlocProvider( + create: (context) => Injection.bannerHistoryItemBloc..add(BannerHistoryItemEvent.init(version: version)), + child: AlertDialog( + title: Text(s.appVersion(version)), + content: SizedBox( + width: mq.getWidthForDialogs(), + child: SingleChildScrollView( + child: BlocBuilder( + builder: (context, state) => state.maybeMap( + loadedState: (state) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: state.items + .groupListsBy((el) => '${DateFormat(_dateFormat).format(el.from)}_${DateFormat(_dateFormat).format(el.until)}') + .values + .map( + (e) { + final group = e.first; + + return _VersionDetailPeriod( + from: group.from, + until: group.until, + items: e.expand((el) => el.items).toList(), + ); + }, + ).toList(), + ), + orElse: () => const Loading(useScaffold: false), + ), + ), + ), + ), + actions: [ + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: Text(s.ok), + ) + ], + ), + ); + } +} + +class _VersionDetailPeriod extends StatelessWidget { + final DateTime from; + final DateTime until; + final List items; + + const _VersionDetailPeriod({ + Key? key, + required this.from, + required this.until, + required this.items, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final theme = Theme.of(context); + final from = DateFormat(_dateFormat).format(this.from); + final until = DateFormat(_dateFormat).format(this.until); + final characters = items.where((el) => el.type == ItemType.character).toList()..sort((x, y) => y.rarity.compareTo(x.rarity)); + final weapons = items.where((el) => el.type == ItemType.weapon).toList()..sort((x, y) => y.rarity.compareTo(x.rarity)); + + return Container( + margin: const EdgeInsets.only(bottom: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(s.fromDate(from), style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold)), + Text(s.untilDate(until), style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold)), + ], + ), + Divider(color: theme.colorScheme.primary), + if (characters.isNotEmpty) Text(s.characters, style: theme.textTheme.subtitle1), + if (characters.isNotEmpty) + _Items( + type: BannerHistoryItemType.character, + items: characters, + ), + if (weapons.isNotEmpty) Text(s.weapons, style: theme.textTheme.subtitle1), + if (weapons.isNotEmpty) + _Items( + type: BannerHistoryItemType.weapon, + items: weapons, + ), + ], + ), + ); + } +} + +class _Items extends StatelessWidget { + final BannerHistoryItemType type; + final List items; + + const _Items({ + Key? key, + required this.type, + required this.items, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: type == BannerHistoryItemType.character ? 80 : 70, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: items.length, + itemBuilder: (ctx, index) { + final item = items[index]; + final gradient = item.rarity.getRarityGradient(); + switch (type) { + case BannerHistoryItemType.character: + return CircleCharacter(itemKey: item.key, image: item.image, gradient: gradient); + case BannerHistoryItemType.weapon: + return CircleWeapon(itemKey: item.key, image: item.image, gradient: gradient); + default: + throw Exception('Banner history item type = $type is not valid'); + } + }, + ), + ); + } +} diff --git a/lib/presentation/character/character_page.dart b/lib/presentation/character/character_page.dart index c1972233a..84ab6c1c6 100644 --- a/lib/presentation/character/character_page.dart +++ b/lib/presentation/character/character_page.dart @@ -11,6 +11,12 @@ class CharacterPage extends StatelessWidget { const CharacterPage({Key? key, required this.itemKey}) : super(key: key); + static Future route(String itemKey, BuildContext context) async { + final route = MaterialPageRoute(builder: (c) => CharacterPage(itemKey: itemKey)); + await Navigator.push(context, route); + await route.completed; + } + @override Widget build(BuildContext context) { final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; diff --git a/lib/presentation/shared/images/circle_character.dart b/lib/presentation/shared/images/circle_character.dart index 312068827..dde2a3692 100644 --- a/lib/presentation/shared/images/circle_character.dart +++ b/lib/presentation/shared/images/circle_character.dart @@ -38,15 +38,9 @@ class CircleCharacter extends StatelessWidget { return CircleItem( image: image, forDrag: forDrag, - onTap: (img) => onTap != null ? onTap!(img) : _gotoCharacterPage(context), + onTap: (img) => onTap != null ? onTap!(img) : CharacterPage.route(itemKey, context), radius: radius, gradient: gradient, ); } - - Future _gotoCharacterPage(BuildContext context) async { - final route = MaterialPageRoute(builder: (c) => CharacterPage(itemKey: itemKey)); - await Navigator.push(context, route); - await route.completed; - } } diff --git a/lib/presentation/shared/images/circle_weapon.dart b/lib/presentation/shared/images/circle_weapon.dart index 25c3d82a7..f8c8e8403 100644 --- a/lib/presentation/shared/images/circle_weapon.dart +++ b/lib/presentation/shared/images/circle_weapon.dart @@ -39,14 +39,8 @@ class CircleWeapon extends StatelessWidget { image: image, radius: radius, forDrag: forDrag, - onTap: (img) => onTap != null ? onTap!(img) : _gotoWeaponPage(context), + onTap: (img) => onTap != null ? onTap!(img) : WeaponPage.route(itemKey, context), gradient: gradient, ); } - - Future _gotoWeaponPage(BuildContext context) async { - final route = MaterialPageRoute(builder: (c) => WeaponPage(itemKey: itemKey)); - await Navigator.push(context, route); - await route.completed; - } } diff --git a/lib/presentation/weapon/weapon_page.dart b/lib/presentation/weapon/weapon_page.dart index e9093a00e..a990434ea 100644 --- a/lib/presentation/weapon/weapon_page.dart +++ b/lib/presentation/weapon/weapon_page.dart @@ -12,6 +12,12 @@ class WeaponPage extends StatelessWidget { const WeaponPage({Key? key, required this.itemKey}) : super(key: key); + static Future route(String itemKey, BuildContext context) async { + final route = MaterialPageRoute(builder: (c) => WeaponPage(itemKey: itemKey)); + await Navigator.push(context, route); + await route.completed; + } + @override Widget build(BuildContext context) { final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;