diff --git a/lib/injection.dart b/lib/injection.dart index 7e2e1bf15..040340bac 100644 --- a/lib/injection.dart +++ b/lib/injection.dart @@ -131,6 +131,18 @@ class Injection { return WeaponBloc(genshinService, telemetryService, dataService); } + static CustomBuildsBloc get customBuildsBloc { + final genshinService = getIt(); + final dataService = getIt(); + return CustomBuildsBloc(genshinService, dataService); + } + + static CustomBuildBloc get customBuildBloc { + final genshinService = getIt(); + final dataService = getIt(); + return CustomBuildBloc(genshinService, dataService); + } + //TODO: USE THIS PROP // static CalculatorAscMaterialsItemBloc get calculatorAscMaterialsItemBloc { // final genshinService = getIt(); diff --git a/lib/presentation/artifacts/artifacts_page.dart b/lib/presentation/artifacts/artifacts_page.dart index 774cbd26e..53db65f48 100644 --- a/lib/presentation/artifacts/artifacts_page.dart +++ b/lib/presentation/artifacts/artifacts_page.dart @@ -18,9 +18,9 @@ import 'widgets/artifact_info_card.dart'; class ArtifactsPage extends StatefulWidget { final bool isInSelectionMode; - static Future forSelection(BuildContext context, {List excludeKeys = const []}) async { + static Future forSelection(BuildContext context, {List excludeKeys = const [], ArtifactType? type}) async { final bloc = context.read(); - bloc.add(ArtifactsEvent.init(excludeKeys: excludeKeys)); + bloc.add(ArtifactsEvent.init(excludeKeys: excludeKeys, type: type)); final route = MaterialPageRoute(builder: (ctx) => const ArtifactsPage(isInSelectionMode: true)); final keyName = await Navigator.of(context).push(route); diff --git a/lib/presentation/artifacts/widgets/artifact_info_card.dart b/lib/presentation/artifacts/widgets/artifact_info_card.dart index acaed62ec..c400fbe07 100644 --- a/lib/presentation/artifacts/widgets/artifact_info_card.dart +++ b/lib/presentation/artifacts/widgets/artifact_info_card.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:shiori/domain/assets.dart'; import 'package:shiori/domain/enums/enums.dart'; import 'package:shiori/generated/l10n.dart'; import 'package:shiori/presentation/shared/bullet_list.dart'; import 'package:shiori/presentation/shared/extensions/i18n_extensions.dart'; +import 'package:shiori/presentation/shared/images/artifact_image_type.dart'; import 'package:shiori/presentation/shared/item_expansion_panel.dart'; class ArtifactInfoCard extends StatelessWidget { @@ -19,7 +19,6 @@ class ArtifactInfoCard extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - final theme = Theme.of(context); final considerations = []; final hp = s.translateStatTypeWithoutValue(StatType.hp, removeExtraSigns: true); @@ -43,22 +42,17 @@ class ArtifactInfoCard extends StatelessWidget { considerations.add( '${s.crown}: $atkPercentage / $defPercentage / $hpPercentage / $critRate / $critDmg / $elementaryMastery / ${s.healingBonus}', ); - - final panel = ItemExpansionPanel( - title: s.note, - body: BulletList( - items: considerations, - iconResolver: (index) => Image.asset( - Assets.getArtifactPathFromType(ArtifactType.values[index]), - width: 24, - height: 24, - color: theme.brightness == Brightness.dark ? Colors.white : Colors.black, + return SliverToBoxAdapter( + child: ItemExpansionPanel( + title: s.note, + body: BulletList( + items: considerations, + iconResolver: (index) => ArtifactImageType(index: index), ), + icon: const Icon(Icons.info_outline), + isCollapsed: isCollapsed, + expansionCallback: expansionCallback, ), - icon: const Icon(Icons.info_outline), - isCollapsed: isCollapsed, - expansionCallback: expansionCallback, ); - return SliverToBoxAdapter(child: panel); } } diff --git a/lib/presentation/character/widgets/character_detail_build_card.dart b/lib/presentation/character/widgets/character_detail_build_card.dart index 7b21f543b..1f55052b7 100644 --- a/lib/presentation/character/widgets/character_detail_build_card.dart +++ b/lib/presentation/character/widgets/character_detail_build_card.dart @@ -7,6 +7,7 @@ import 'package:shiori/generated/l10n.dart'; import 'package:shiori/presentation/artifacts/widgets/artifact_card.dart'; import 'package:shiori/presentation/shared/extensions/element_type_extensions.dart'; import 'package:shiori/presentation/shared/extensions/i18n_extensions.dart'; +import 'package:shiori/presentation/shared/row_column_item_or.dart'; import 'package:shiori/presentation/shared/styles.dart'; import 'package:shiori/presentation/weapons/widgets/weapon_card.dart'; @@ -91,7 +92,7 @@ class CharacterDetailBuildCard extends StatelessWidget { ...artifacts.mapIndex((e, index) { final showOr = index < artifacts.length - 1; if (showOr) { - return _ItemWithOr(widget: _ArtifactRow(item: e), color: color, useColumn: true); + return RowColumnItemOr(widget: _ArtifactRow(item: e), color: color, useColumn: true); } return _ArtifactRow(item: e); }).toList(), @@ -131,7 +132,7 @@ class _Weapons extends StatelessWidget { ); final withOr = index < weapons.length - 1; if (withOr) { - return _ItemWithOr(widget: child, color: color); + return RowColumnItemOr(widget: child, color: color); } return child; }, @@ -140,33 +141,6 @@ class _Weapons extends StatelessWidget { } } -class _ItemWithOr extends StatelessWidget { - final Widget widget; - final Color color; - final bool useColumn; - - const _ItemWithOr({ - Key? key, - required this.widget, - required this.color, - this.useColumn = false, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - if (useColumn) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [widget, _OrWidget(color: color)], - ); - } - return Row( - mainAxisSize: MainAxisSize.min, - children: [widget, _OrWidget(color: color)], - ); - } -} - class _ArtifactRow extends StatelessWidget { final CharacterBuildArtifactModel item; @@ -223,36 +197,6 @@ class _ArtifactRow extends StatelessWidget { } } -class _OrWidget extends StatelessWidget { - final Color color; - - const _OrWidget({ - Key? key, - required this.color, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final s = S.of(context); - final theme = Theme.of(context); - return Container( - margin: Styles.edgeInsetAll5, - padding: Styles.edgeInsetAll5, - decoration: BoxDecoration( - color: color, - shape: BoxShape.circle, - ), - child: Center( - child: Text( - s.or, - textAlign: TextAlign.center, - style: theme.textTheme.subtitle2!.copyWith(fontWeight: FontWeight.bold, color: Colors.white), - ), - ), - ); - } -} - class _SubStatToFocus extends StatelessWidget { final List subStatsToFocus; final Color color; diff --git a/lib/presentation/characters/characters_page.dart b/lib/presentation/characters/characters_page.dart index dfd52fa69..4d0974a1c 100644 --- a/lib/presentation/characters/characters_page.dart +++ b/lib/presentation/characters/characters_page.dart @@ -19,6 +19,7 @@ class CharactersPage extends StatefulWidget { static Future forSelection(BuildContext context, {List excludeKeys = const []}) async { final bloc = context.read(); + //TODO: RECEIVE THE EXCLUDEKEYS IN THE CONSTRUCTOR AND REMOVE THIS BLOC FROM HERE bloc.add(CharactersEvent.init(excludeKeys: excludeKeys)); final route = MaterialPageRoute(builder: (ctx) => const CharactersPage(isInSelectionMode: true)); diff --git a/lib/presentation/custom_build/custom_build_page.dart b/lib/presentation/custom_build/custom_build_page.dart new file mode 100644 index 000000000..729c63cac --- /dev/null +++ b/lib/presentation/custom_build/custom_build_page.dart @@ -0,0 +1,382 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shiori/application/bloc.dart'; +import 'package:shiori/domain/app_constants.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/artifacts/artifacts_page.dart'; +import 'package:shiori/presentation/artifacts/widgets/artifact_card.dart'; +import 'package:shiori/presentation/characters/characters_page.dart'; +import 'package:shiori/presentation/shared/character_stack_image.dart'; +import 'package:shiori/presentation/shared/dialogs/select_artifact_type_dialog.dart'; +import 'package:shiori/presentation/shared/dialogs/select_stat_type_dialog.dart'; +import 'package:shiori/presentation/shared/dropdown_button_with_title.dart'; +import 'package:shiori/presentation/shared/extensions/element_type_extensions.dart'; +import 'package:shiori/presentation/shared/extensions/i18n_extensions.dart'; +import 'package:shiori/presentation/shared/loading.dart'; +import 'package:shiori/presentation/shared/styles.dart'; +import 'package:shiori/presentation/shared/utils/enum_utils.dart'; +import 'package:shiori/presentation/weapons/weapons_page.dart'; +import 'package:shiori/presentation/weapons/widgets/weapon_card.dart'; + +class CustomBuildPage extends StatelessWidget { + final int? itemKey; + + bool get newBuild => itemKey != null; + + const CustomBuildPage({ + Key? key, + this.itemKey, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + //TODO: SHOW THE TALENTS AND CONSTELLATIONS LIKE THIS + //https://genshin-impact-card-generator.herokuapp.com/ + final theme = Theme.of(context); + final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; + return BlocProvider( + create: (ctx) => Injection.customBuildBloc..add(CustomBuildEvent.load(key: itemKey)), + child: Scaffold( + appBar: AppBar( + title: Text(newBuild ? 'Add' : s.edit), + actions: [ + IconButton( + onPressed: () {}, + splashRadius: Styles.mediumButtonSplashRadius, + icon: const Icon(Icons.save), + ), + IconButton( + onPressed: () {}, + splashRadius: Styles.mediumButtonSplashRadius, + icon: const Icon(Icons.delete), + ), + IconButton( + onPressed: () {}, + splashRadius: Styles.mediumButtonSplashRadius, + icon: const Icon(Icons.share), + ), + ], + ), + body: BlocBuilder( + builder: (ctx, state) => state.maybeMap( + loaded: (state) => SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _MainCard( + title: state.title.isNullEmptyOrWhitespace ? s.na : state.title, + type: state.type, + subType: state.subType, + showOnCharacterDetail: state.showOnCharacterDetail, + character: state.character, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _Weapons( + weapons: state.weapons, + color: state.character.elementType.getElementColorFromContext(context), + ), + ), + Expanded( + child: _Artifacts( + artifacts: state.artifacts, + color: state.character.elementType.getElementColorFromContext(context), + ), + ), + ], + ) + ], + ), + ), + orElse: () => const Loading(useScaffold: false), + ), + ), + ), + ); + } +} + +class _MainCard extends StatelessWidget { + final String title; + final CharacterRoleType type; + final CharacterRoleSubType subType; + final bool showOnCharacterDetail; + final CharacterCardModel character; + + const _MainCard({ + Key? key, + required this.title, + required this.type, + required this.subType, + required this.showOnCharacterDetail, + required this.character, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final theme = Theme.of(context); + return Container( + color: character.elementType.getElementColorFromContext(context), + child: Row( + children: [ + Expanded( + flex: 40, + child: CharacterStackImage( + name: character.name, + image: character.image, + rarity: character.stars, + onTap: () => _openCharacterPage(context), + ), + ), + const Spacer(flex: 3), + Expanded( + flex: 54, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.headline5!.copyWith(fontWeight: FontWeight.bold), + ), + ), + IconButton( + onPressed: () {}, + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.edit), + ) + ], + ), + DropdownButtonWithTitle( + margin: EdgeInsets.zero, + title: s.role, + currentValue: type, + items: EnumUtils.getTranslatedAndSortedEnum( + CharacterRoleType.values, + (val, _) => s.translateCharacterRoleType(val), + ), + onChanged: (v) => context.read().add(CustomBuildEvent.roleChanged(newValue: v)), + ), + DropdownButtonWithTitle( + margin: EdgeInsets.zero, + title: 'Sub type', + currentValue: subType, + items: EnumUtils.getTranslatedAndSortedEnum( + CharacterRoleSubType.values, + (val, _) => s.translateCharacterRoleSubType(val), + ), + onChanged: (v) => context.read().add(CustomBuildEvent.subRoleChanged(newValue: v)), + ), + SwitchListTile( + activeColor: theme.colorScheme.secondary, + contentPadding: EdgeInsets.zero, + title: Text('Show on character detail'), + value: showOnCharacterDetail, + onChanged: (v) => context.read().add(CustomBuildEvent.showOnCharacterDetailChanged(newValue: v)), + ), + ], + ), + ), + const Spacer(flex: 3), + ], + ), + ); + } + + Future _openCharacterPage(BuildContext context) async { + //TODO: EXCLUDE UPCOMING CHARACTERS ? + final bloc = context.read(); + final selectedKey = await CharactersPage.forSelection(context, excludeKeys: [character.key]); + if (selectedKey.isNullEmptyOrWhitespace) { + return; + } + + bloc.add(CustomBuildEvent.characterChanged(newKey: selectedKey!)); + } +} + +class _Weapons extends StatelessWidget { + final List weapons; + final Color color; + + const _Weapons({ + Key? key, + required this.weapons, + required this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: Styles.edgeInsetVertical10, + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: color, + border: Border(top: BorderSide(color: Colors.white)), + ), + child: Text( + s.weapons, + textAlign: TextAlign.center, + style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold), + ), + ), + Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...weapons + .map( + (e) => WeaponCard.withoutDetails( + keyName: e.key, + name: e.name, + rarity: e.rarity, + image: e.image, + isComingSoon: e.isComingSoon, + imgHeight: 60, + imgWidth: 70, + ), + ) + .toList(), + IconButton( + color: theme.colorScheme.secondary, + iconSize: 60, + splashRadius: Styles.mediumBigButtonSplashRadius, + icon: const Icon(Icons.add), + onPressed: () => _openWeaponsPage(context), + ) + ], + ), + ], + ); + } + + Future _openWeaponsPage(BuildContext context) async { + final bloc = context.read(); + final selectedKey = await WeaponsPage.forSelection(context, excludeKeys: weapons.map((e) => e.key).toList()); + if (selectedKey.isNullEmptyOrWhitespace) { + return; + } + bloc.add(CustomBuildEvent.addWeapon(key: selectedKey!)); + } +} + +class _Artifacts extends StatelessWidget { + final List artifacts; + final Color color; + + const _Artifacts({ + Key? key, + required this.artifacts, + required this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: Styles.edgeInsetVertical10, + margin: const EdgeInsets.only(bottom: 10), + decoration: BoxDecoration( + color: color, + border: Border(top: BorderSide(color: Colors.white)), + ), + child: Text( + s.artifacts, + textAlign: TextAlign.center, + style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold), + ), + ), + Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...artifacts + .map( + (e) => ArtifactCard.withoutDetails( + keyName: e.key, + name: e.name, + image: e.image, + rarity: e.rarity, + ), + ) + .toList(), + if (artifacts.length < ArtifactType.values.length) + IconButton( + color: theme.colorScheme.secondary, + iconSize: 60, + splashRadius: Styles.mediumBigButtonSplashRadius, + icon: const Icon(Icons.add), + onPressed: () => _addArtifact(context), + ), + ], + ) + ], + ); + } + + Future _addArtifact(BuildContext context) async { + final selectedType = await showDialog(context: context, builder: (ctx) => const SelectArtifactTypeDialog()); + if (selectedType == null) { + return; + } + + StatType? statType; + switch (selectedType) { + case ArtifactType.flower: + statType = StatType.hp; + break; + case ArtifactType.plume: + statType = StatType.atk; + break; + default: + statType = await showDialog( + context: context, + builder: (ctx) => SelectStatTypeDialog( + values: getArtifactPossibleMainStats(selectedType), + ), + ); + break; + } + + if (statType == null) { + return; + } + + await _openArtifactsPage(context, selectedType); + } + + Future _openArtifactsPage(BuildContext context, ArtifactType type) async { + //TODO: REMOVE THE CROWNS AND MAYBE ONLY SHOW THE SPECIFIC TYPE + final bloc = context.read(); + final selectedKey = await ArtifactsPage.forSelection(context, type: type); + if (selectedKey.isNullEmptyOrWhitespace) { + return; + } + bloc.add(CustomBuildEvent.addArtifact(key: selectedKey!, type: type)); + } +} diff --git a/lib/presentation/custom_builds/custom_builds_page.dart b/lib/presentation/custom_builds/custom_builds_page.dart new file mode 100644 index 000000000..39553beb7 --- /dev/null +++ b/lib/presentation/custom_builds/custom_builds_page.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shiori/application/bloc.dart'; +import 'package:shiori/injection.dart'; +import 'package:shiori/presentation/custom_build/custom_build_page.dart'; +import 'package:shiori/presentation/custom_builds/widgets/custom_build_card.dart'; +import 'package:shiori/presentation/shared/app_fab.dart'; +import 'package:shiori/presentation/shared/mixins/app_fab_mixin.dart'; +import 'package:shiori/presentation/shared/nothing_found_column.dart'; +import 'package:waterfall_flow/waterfall_flow.dart'; + +class CustomBuildsPage extends StatefulWidget { + const CustomBuildsPage({Key? key}) : super(key: key); + + @override + State createState() => _CustomBuildsPageState(); +} + +class _CustomBuildsPageState extends State with SingleTickerProviderStateMixin, AppFabMixin { + @override + bool get isInitiallyVisible => true; + + @override + bool get hideOnTop => false; + + @override + Widget build(BuildContext context) { + final mq = MediaQuery.of(context); + final crossAxisCount = mq.size.width > 1600 + ? 4 + : mq.size.width > 1200 + ? 3 + : mq.size.width > 620 + ? 2 + : 1; + return BlocProvider( + create: (context) => Injection.customBuildsBloc..add(const CustomBuildsEvent.load()), + child: Scaffold( + appBar: AppBar( + title: Text('Custom Builds'), + ), + floatingActionButton: AppFab( + onPressed: () => _goToDetailsPage(), + icon: const Icon(Icons.add), + hideFabAnimController: hideFabAnimController, + scrollController: scrollController, + mini: false, + ), + body: BlocBuilder( + builder: (context, state) => SafeArea( + child: state.builds.isEmpty + ? NothingFoundColumn(msg: 'Start by creating a new build') + : WaterfallFlow.builder( + itemCount: state.builds.length, + gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + ), + itemBuilder: (context, index) => CustomBuildCard(item: state.builds[index]), + ) + // : ListView.builder( + // itemCount: state.builds.length, + // itemBuilder: (context, index) => CustomBuild(item: state.builds[index]), + // ), + ), + ), + ), + ); + } + + Future _goToDetailsPage() async { + // await showModalBottomSheet( + // context: context, + // shape: Styles.modalBottomSheetShape, + // isDismissible: true, + // isScrollControlled: true, + // builder: (ctx) => CommonBottomSheet( + // titleIcon: Icons.edit, + // title: 'Algo aca', + // showOkButton: false, + // showCancelButton: false, + // child: CustomBuildPage(), + // ), + // ); + final route = MaterialPageRoute(builder: (ctx) => CustomBuildPage()); + await Navigator.push(context, route); + } +} diff --git a/lib/presentation/custom_builds/widgets/custom_build_card.dart b/lib/presentation/custom_builds/widgets/custom_build_card.dart new file mode 100644 index 000000000..0bb2d77d0 --- /dev/null +++ b/lib/presentation/custom_builds/widgets/custom_build_card.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:responsive_builder/responsive_builder.dart'; +import 'package:shiori/domain/models/models.dart'; +import 'package:shiori/generated/l10n.dart'; +import 'package:shiori/presentation/artifacts/widgets/artifact_card.dart'; +import 'package:shiori/presentation/custom_build/custom_build_page.dart'; +import 'package:shiori/presentation/shared/character_stack_image.dart'; +import 'package:shiori/presentation/shared/extensions/element_type_extensions.dart'; +import 'package:shiori/presentation/shared/styles.dart'; +import 'package:shiori/presentation/weapons/widgets/weapon_card.dart'; + +class CustomBuildCard extends StatelessWidget { + final CustomBuildModel item; + + const CustomBuildCard({ + Key? key, + required this.item, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final device = getDeviceType(MediaQuery.of(context).size); + final s = S.of(context); + final theme = Theme.of(context); + return InkWell( + onTap: () => _goToDetailsPage(context), + child: Card( + clipBehavior: Clip.hardEdge, + // shape: Styles.mainCardShape, + elevation: Styles.cardTenElevation, + color: item.character.elementType.getElementColorFromContext(context), + shadowColor: Colors.transparent, + // margin: Styles.edgeInsetVertical5, + child: Row( + // crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: device == DeviceScreenType.tablet ? 40 : 35, + child: CharacterStackImage( + name: item.character.name, + image: item.character.image, + rarity: item.character.stars, + ), + ), + Expanded( + flex: device == DeviceScreenType.tablet ? 60 : 65, + child: Padding( + padding: Styles.edgeInsetHorizontal5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + item.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.headline5!.copyWith(fontWeight: FontWeight.bold), + ), + Text('Weapons', style: TextStyle(fontWeight: FontWeight.bold)), + SizedBox( + height: 100, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: item.weapons.length, + itemBuilder: (ctx, index) { + final weapon = item.weapons[index]; + final child = WeaponCard.withoutDetails( + keyName: weapon.key, + name: weapon.name, + rarity: weapon.rarity, + image: weapon.image, + isComingSoon: weapon.isComingSoon, + imgHeight: 50, + imgWidth: 60, + ); + return child; + }, + ), + ), + Text('Artifacts', style: TextStyle(fontWeight: FontWeight.bold)), + SizedBox( + height: 110, + child: ListView.builder( + physics: const BouncingScrollPhysics(), + scrollDirection: Axis.horizontal, + itemCount: item.artifacts.length, + itemBuilder: (ctx, index) { + return ArtifactCard.withoutDetails( + name: 'Hp', + image: item.artifacts[index].image, + rarity: item.artifacts[index].rarity, + keyName: item.artifacts[index].key, + imgWidth: 55, + imgHeight: 45, + ); + }, + ), + ) + ], + ), + ), + ) + ], + ), + ), + ); + } + + Future _goToDetailsPage(BuildContext context) async { + final route = MaterialPageRoute(builder: (ctx) => CustomBuildPage(itemKey: item.key)); + await Navigator.push(context, route); + } +} diff --git a/lib/presentation/home/home_page.dart b/lib/presentation/home/home_page.dart index 013f0e08e..ee43836bb 100644 --- a/lib/presentation/home/home_page.dart +++ b/lib/presentation/home/home_page.dart @@ -4,6 +4,7 @@ import 'package:responsive_builder/responsive_builder.dart'; import 'package:shiori/application/bloc.dart'; import 'package:shiori/generated/l10n.dart'; import 'package:shiori/presentation/home/widgets/calculators_card.dart'; +import 'package:shiori/presentation/home/widgets/custom_builds_card.dart'; import 'package:shiori/presentation/home/widgets/daily_check_in_card.dart'; import 'package:shiori/presentation/home/widgets/elements_card.dart'; import 'package:shiori/presentation/home/widgets/game_codes_card.dart'; @@ -66,7 +67,7 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin< child: ListView.builder( physics: const BouncingScrollPhysics(), scrollDirection: Axis.horizontal, - itemCount: 4, + itemCount: 5, itemBuilder: (context, index) => _buildToolsSectionMenu(index), ), ), @@ -123,6 +124,8 @@ class _HomePageState extends State with AutomaticKeepAliveClientMixin< case 2: return const NotificationsCard(iconToTheLeft: true); case 3: + return const CustomBuildsCard(iconToTheLeft: true); + case 4: return const TierListCard(iconToTheLeft: true); default: throw Exception('Invalid tool section'); diff --git a/lib/presentation/home/widgets/custom_builds_card.dart b/lib/presentation/home/widgets/custom_builds_card.dart new file mode 100644 index 000000000..8eb3529f9 --- /dev/null +++ b/lib/presentation/home/widgets/custom_builds_card.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:shiori/generated/l10n.dart'; +import 'package:shiori/presentation/custom_builds/custom_builds_page.dart'; +import 'package:shiori/presentation/home/widgets/card_description.dart'; + +import 'card_item.dart'; + +class CustomBuildsCard extends StatelessWidget { + final bool iconToTheLeft; + + const CustomBuildsCard({ + Key? key, + required this.iconToTheLeft, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final s = S.of(context); + return CardItem( + title: 'Custom Builds', + iconToTheLeft: iconToTheLeft, + onClick: _gotoMaterialsPage, + icon: Icon(Icons.dashboard_customize, size: 60, color: theme.colorScheme.secondary), + children: [ + CardDescription(text: "Don't like the provided builds ? Create your custom ones!"), + ], + ); + } + + Future _gotoMaterialsPage(BuildContext context) async { + final route = MaterialPageRoute(builder: (_) => const CustomBuildsPage()); + await Navigator.push(context, route); + await route.completed; + } +} diff --git a/lib/presentation/shared/character_stack_image.dart b/lib/presentation/shared/character_stack_image.dart new file mode 100644 index 000000000..aa38f4ef9 --- /dev/null +++ b/lib/presentation/shared/character_stack_image.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:shiori/presentation/shared/images/rarity.dart'; +import 'package:shiori/presentation/shared/styles.dart'; +import 'package:transparent_image/transparent_image.dart'; + +class CharacterStackImage extends StatelessWidget { + final String name; + final String image; + final int rarity; + final double height; + final Function? onTap; + + const CharacterStackImage({ + Key? key, + required this.name, + required this.image, + required this.rarity, + this.onTap, + this.height = 280, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return InkWell( + onTap: onTap == null ? null : () => onTap!(), + child: Stack( + alignment: Alignment.bottomLeft, + fit: StackFit.passthrough, + children: [ + FadeInImage( + placeholder: MemoryImage(kTransparentImage), + height: height, + fit: BoxFit.fitHeight, + image: AssetImage(image), + ), + Container( + color: Colors.black.withOpacity(0.5), + padding: Styles.edgeInsetAll10, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Text( + name, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold), + ), + ), + Rarity(stars: rarity), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/shared/dialogs/select_artifact_type_dialog.dart b/lib/presentation/shared/dialogs/select_artifact_type_dialog.dart new file mode 100644 index 000000000..f47818913 --- /dev/null +++ b/lib/presentation/shared/dialogs/select_artifact_type_dialog.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:shiori/domain/enums/enums.dart'; +import 'package:shiori/generated/l10n.dart'; +import 'package:shiori/presentation/shared/extensions/i18n_extensions.dart'; +import 'package:shiori/presentation/shared/extensions/media_query_extensions.dart'; +import 'package:shiori/presentation/shared/images/artifact_image_type.dart'; + +class SelectArtifactTypeDialog extends StatefulWidget { + final List excluded; + + const SelectArtifactTypeDialog({ + Key? key, + this.excluded = const [], + }) : super(key: key); + + @override + _SelectArtifactTypeDialogState createState() => _SelectArtifactTypeDialogState(); +} + +class _SelectArtifactTypeDialogState extends State { + ArtifactType? currentSelectedType; + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final mq = MediaQuery.of(context); + final theme = Theme.of(context); + const values = ArtifactType.values; + if (widget.excluded.isNotEmpty) { + values.removeWhere((el) => widget.excluded.contains(el)); + } + + return AlertDialog( + title: Text(s.type), + content: SizedBox( + width: mq.getWidthForDialogs(), + height: mq.getHeightForDialogs(values.length), + child: ListView.builder( + itemCount: values.length, + itemBuilder: (ctx, index) { + final type = values[index]; + //For some reason I need to wrap this thing on a material to avoid this problem + // https://stackoverflow.com/questions/67912387/scrollable-listview-bleeds-background-color-to-adjacent-widgets + return Material( + color: Colors.transparent, + child: ListTile( + key: Key('$index'), + leading: ArtifactImageType.fromType(type: type), + title: Text( + s.translateArtifactType(type), + overflow: TextOverflow.ellipsis, + ), + selected: currentSelectedType == type, + selectedTileColor: theme.colorScheme.secondary.withOpacity(0.2), + onTap: () { + setState(() { + currentSelectedType = type; + }); + }, + ), + ); + }, + ), + ), + actions: [ + OutlinedButton( + onPressed: () => Navigator.pop(context), + child: Text(s.cancel, style: TextStyle(color: theme.primaryColor)), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, currentSelectedType), + child: Text(s.ok), + ) + ], + ); + } +} diff --git a/lib/presentation/shared/dialogs/select_stat_type_dialog.dart b/lib/presentation/shared/dialogs/select_stat_type_dialog.dart new file mode 100644 index 000000000..4669c9f87 --- /dev/null +++ b/lib/presentation/shared/dialogs/select_stat_type_dialog.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:shiori/domain/enums/enums.dart'; +import 'package:shiori/generated/l10n.dart'; +import 'package:shiori/presentation/shared/extensions/i18n_extensions.dart'; +import 'package:shiori/presentation/shared/extensions/media_query_extensions.dart'; +import 'package:shiori/presentation/shared/utils/enum_utils.dart'; + +class SelectStatTypeDialog extends StatefulWidget { + final List values; + + const SelectStatTypeDialog({ + Key? key, + required this.values, + }) : super(key: key); + + @override + State createState() => _SelectStatTypeDialogState(); +} + +class _SelectStatTypeDialogState extends State { + StatType? currentSelectedType; + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final mq = MediaQuery.of(context); + final theme = Theme.of(context); + final values = EnumUtils.getTranslatedAndSortedEnum(widget.values, (type, _) => s.translateStatTypeWithoutValue(type)); + + return AlertDialog( + title: Text(s.stats), + content: SizedBox( + width: mq.getWidthForDialogs(), + height: mq.getHeightForDialogs(widget.values.length), + child: ListView.builder( + itemCount: values.length, + itemBuilder: (ctx, index) { + final type = values[index]; + //For some reason I need to wrap this thing on a material to avoid this problem + // https://stackoverflow.com/questions/67912387/scrollable-listview-bleeds-background-color-to-adjacent-widgets + return Material( + color: Colors.transparent, + child: ListTile( + key: Key('$index'), + title: Text( + type.translation, + overflow: TextOverflow.ellipsis, + ), + selected: currentSelectedType == type.enumValue, + selectedTileColor: theme.colorScheme.secondary.withOpacity(0.2), + onTap: () { + setState(() { + currentSelectedType = type.enumValue; + }); + }, + ), + ); + }, + ), + ), + actions: [ + OutlinedButton( + onPressed: () => Navigator.pop(context), + child: Text(s.cancel, style: TextStyle(color: theme.primaryColor)), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, currentSelectedType), + child: Text(s.ok), + ) + ], + ); + } +} diff --git a/lib/presentation/shared/extensions/i18n_extensions.dart b/lib/presentation/shared/extensions/i18n_extensions.dart index d0be3a27e..fa27502a4 100644 --- a/lib/presentation/shared/extensions/i18n_extensions.dart +++ b/lib/presentation/shared/extensions/i18n_extensions.dart @@ -605,4 +605,19 @@ extension I18nExtensions on S { return shield; } } + + String translateArtifactType(ArtifactType type) { + switch (type) { + case ArtifactType.flower: + return flower; + case ArtifactType.plume: + return plume; + case ArtifactType.clock: + return clock; + case ArtifactType.goblet: + return goblet; + case ArtifactType.crown: + return crown; + } + } } diff --git a/lib/presentation/shared/images/artifact_image_type.dart b/lib/presentation/shared/images/artifact_image_type.dart new file mode 100644 index 000000000..e5631d90e --- /dev/null +++ b/lib/presentation/shared/images/artifact_image_type.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:shiori/domain/assets.dart'; +import 'package:shiori/domain/enums/enums.dart'; + +class ArtifactImageType extends StatelessWidget { + final int index; + final double width; + final double height; + + const ArtifactImageType({ + Key? key, + required this.index, + this.width = 24, + this.height = 24, + }) : super(key: key); + + ArtifactImageType.fromType({ + Key? key, + required ArtifactType type, + this.width = 24, + this.height = 24, + }) : index = type.index, + super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Image.asset( + Assets.getArtifactPathFromType(ArtifactType.values[index]), + width: width, + height: height, + color: theme.brightness == Brightness.dark ? Colors.white : Colors.black, + ); + } +} diff --git a/lib/presentation/shared/images/circle_item.dart b/lib/presentation/shared/images/circle_item.dart index f1e9fcca0..a5641f55a 100644 --- a/lib/presentation/shared/images/circle_item.dart +++ b/lib/presentation/shared/images/circle_item.dart @@ -7,6 +7,8 @@ class CircleItem extends StatelessWidget { final bool forDrag; final bool imageSizeTimesTwo; final Function(String)? onTap; + final BoxFit fit; + final Alignment alignment; const CircleItem({ Key? key, @@ -15,6 +17,8 @@ class CircleItem extends StatelessWidget { this.forDrag = false, this.imageSizeTimesTwo = true, this.onTap, + this.fit = BoxFit.cover, + this.alignment = Alignment.topCenter, }) : super(key: key); @override @@ -28,8 +32,8 @@ class CircleItem extends StatelessWidget { child: FadeInImage( placeholder: MemoryImage(kTransparentImage), image: AssetImage(image), - fit: BoxFit.cover, - alignment: Alignment.topCenter, + fit: fit, + alignment: alignment, height: size, width: size, ), diff --git a/lib/presentation/shared/images/rarity.dart b/lib/presentation/shared/images/rarity.dart index 3649616de..210e7a911 100644 --- a/lib/presentation/shared/images/rarity.dart +++ b/lib/presentation/shared/images/rarity.dart @@ -19,6 +19,10 @@ class Rarity extends StatelessWidget { widgets.add(Icon(Icons.star_sharp, color: Colors.yellow, size: starSize)); } - return Row(mainAxisAlignment: alignment, children: widgets); + return Row( + mainAxisAlignment: alignment, + mainAxisSize: MainAxisSize.min, + children: widgets, + ); } } diff --git a/lib/presentation/shared/row_column_item_or.dart b/lib/presentation/shared/row_column_item_or.dart new file mode 100644 index 000000000..da0125946 --- /dev/null +++ b/lib/presentation/shared/row_column_item_or.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:shiori/generated/l10n.dart'; +import 'package:shiori/presentation/shared/styles.dart'; + +class RowColumnItemOr extends StatelessWidget { + final Widget widget; + final Color color; + final bool useColumn; + + const RowColumnItemOr({ + Key? key, + required this.widget, + required this.color, + this.useColumn = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (useColumn) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [widget, _OrWidget(color: color)], + ); + } + return Row( + mainAxisSize: MainAxisSize.min, + children: [widget, _OrWidget(color: color)], + ); + } +} + +class _OrWidget extends StatelessWidget { + final Color color; + + const _OrWidget({ + Key? key, + required this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final theme = Theme.of(context); + return Container( + margin: Styles.edgeInsetAll5, + padding: Styles.edgeInsetAll5, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + child: Center( + child: Text( + s.or, + textAlign: TextAlign.center, + style: theme.textTheme.subtitle2!.copyWith(fontWeight: FontWeight.bold, color: Colors.white), + ), + ), + ); + } +} diff --git a/lib/presentation/shared/styles.dart b/lib/presentation/shared/styles.dart index 9810b5a13..b7a709cce 100644 --- a/lib/presentation/shared/styles.dart +++ b/lib/presentation/shared/styles.dart @@ -4,8 +4,8 @@ class Styles { static const String appIconPath = 'assets/icon/icon.png'; static const BorderRadius mainCardBorderRadius = BorderRadius.only( - bottomLeft: Radius.circular(35), - bottomRight: Radius.circular(35), + bottomLeft: Radius.circular(30), + bottomRight: Radius.circular(30), topLeft: Radius.circular(10), topRight: Radius.circular(10), ); @@ -59,6 +59,7 @@ class Styles { static const double smallButtonSplashRadius = 18; static const double mediumButtonSplashRadius = 25; + static const double mediumBigButtonSplashRadius = mediumButtonSplashRadius * 1.3; static double getIconSizeForItemPopupMenuFilter(bool forEndDrawer, bool forDefaultIcons) { if (forDefaultIcons) { diff --git a/lib/presentation/weapons/widgets/weapon_card.dart b/lib/presentation/weapons/widgets/weapon_card.dart index 5aa896e0d..601ceaefb 100644 --- a/lib/presentation/weapons/widgets/weapon_card.dart +++ b/lib/presentation/weapons/widgets/weapon_card.dart @@ -104,36 +104,42 @@ class WeaponCard extends StatelessWidget { padding: Styles.edgeInsetAll5, child: Column( children: [ - Stack( - alignment: AlignmentDirectional.topCenter, - fit: StackFit.passthrough, - children: [ - FadeInImage( - width: imgWidth, - height: imgHeight, - placeholder: MemoryImage(kTransparentImage), - image: AssetImage(image), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ComingSoonNewAvatar( - isNew: false, - isComingSoon: isComingSoon, - ), - ], - ), - ], - ), - if (!withoutDetails) - Center( - child: Tooltip( - message: name, - child: Text( - name, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold, color: Colors.white), + if (withoutDetails) + FadeInImage( + width: imgWidth, + height: imgHeight, + placeholder: MemoryImage(kTransparentImage), + image: AssetImage(image), + ) + else + Stack( + alignment: AlignmentDirectional.topCenter, + fit: StackFit.passthrough, + children: [ + FadeInImage( + width: imgWidth, + height: imgHeight, + placeholder: MemoryImage(kTransparentImage), + image: AssetImage(image), ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ComingSoonNewAvatar( + isNew: false, + isComingSoon: isComingSoon, + ), + ], + ), + ], + ), + if (!withoutDetails) + Tooltip( + message: name, + child: Text( + name, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold, color: Colors.white), ), ), Rarity(stars: rarity), diff --git a/pubspec.lock b/pubspec.lock index f92d7f228..d7bf7153a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -42,7 +42,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" bloc: dependency: transitive description: @@ -126,7 +126,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -629,7 +629,7 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" meta: dependency: transitive description: @@ -1054,21 +1054,21 @@ packages: name: test url: "https://pub.dartlang.org" source: hosted - version: "1.17.10" + version: "1.17.12" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.3" test_core: dependency: transitive description: name: test_core url: "https://pub.dartlang.org" source: hosted - version: "0.4.0" + version: "0.4.2" timezone: dependency: transitive description: @@ -1152,7 +1152,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" version_tracker: dependency: "direct main" description: