diff --git a/lib/application/character/character_bloc.dart b/lib/application/character/character_bloc.dart index 6402e8367..979869d68 100644 --- a/lib/application/character/character_bloc.dart +++ b/lib/application/character/character_bloc.dart @@ -86,6 +86,41 @@ class CharacterBloc extends Bloc { final birthday = _localeService.formatCharBirthDate(char.birthday); final isInInventory = _dataService.isItemInInventory(char.key, ItemType.character); + final builds = char.builds.map((build) { + return CharacterBuildCardModel( + isRecommended: build.isRecommended, + type: build.type, + subType: build.subType, + skillPriorities: build.skillPriorities, + subStatsToFocus: build.subStatsToFocus, + weapons: build.weaponKeys.map((e) => _genshinService.getWeaponForCard(e)).toList(), + artifacts: build.artifacts.map( + (e) { + final one = e.oneKey != null ? _genshinService.getArtifactForCard(e.oneKey!) : null; + final multiples = e.multiples + .map( + (m) => CharacterBuildMultipleArtifactModel( + quantity: m.quantity, + artifact: _genshinService.getArtifactForCard(m.key), + ), + ) + .toList(); + + if (multiples.isNotEmpty) { + final count = multiples.map((e) => e.quantity).fold(0, (int p, int c) => p + c); + final diff = artifactOrder.length - count; + if (diff >= 1) { + multiples.add(CharacterBuildMultipleArtifactModel(quantity: diff, artifact: multiples.last.artifact)); + } + } + return CharacterBuildArtifactModel(one: one, multiples: _flatMultiBuild(multiples), stats: e.stats); + }, + ).toList(), + ); + }).toList(); + + builds.addAll(_dataService.customBuilds.getCustomBuildsForCharacter(char.key)); + builds.sort((x, y) => x.isRecommended ? -1 : 1); return CharacterState.loaded( key: char.key, @@ -145,39 +180,7 @@ class CharacterBloc extends Bloc { ); }).toList(), multiTalentAscensionMaterials: multiTalents, - builds: char.builds.map((build) { - return CharacterBuildCardModel( - isRecommended: build.isRecommended, - type: build.type, - subType: build.subType, - skillPriorities: build.skillPriorities, - subStatsToFocus: build.subStatsToFocus, - weapons: build.weaponKeys.map((e) => _genshinService.getWeaponForCard(e)).toList(), - artifacts: build.artifacts.map( - (e) { - final one = e.oneKey != null ? _genshinService.getArtifactForCard(e.oneKey!) : null; - final multiples = e.multiples - .map( - (m) => CharacterBuildMultipleArtifactModel( - quantity: m.quantity, - artifact: _genshinService.getArtifactForCard(m.key), - ), - ) - .toList(); - - if (multiples.isNotEmpty) { - final count = multiples.map((e) => e.quantity).fold(0, (int p, int c) => p + c); - final diff = artifactOrder.length - count; - if (diff >= 1) { - multiples.add(CharacterBuildMultipleArtifactModel(quantity: diff, artifact: multiples.last.artifact)); - } - } - return CharacterBuildArtifactModel(one: one, multiples: _flatMultiBuild(multiples), stats: e.stats); - }, - ).toList(), - ); - }).toList() - ..sort((x, y) => x.isRecommended ? -1 : 1), + builds: builds, subStatType: char.subStatType, stats: char.stats, ); diff --git a/lib/application/custom_build/custom_build_bloc.dart b/lib/application/custom_build/custom_build_bloc.dart index ba07d2d67..4cabd256c 100644 --- a/lib/application/custom_build/custom_build_bloc.dart +++ b/lib/application/custom_build/custom_build_bloc.dart @@ -33,7 +33,6 @@ class CustomBuildBloc extends Bloc { on(_handleEvent); } -//TODO: REMOVE UPCOMING CHARACTERS ? Future _handleEvent(CustomBuildEvent event, Emitter emit) async { //TODO: SHOULD I TRHOW ON INVALID REQUEST ? //IN MOST CASES THERE ARE SOME VALIDATIONS FOR THINGS LIKE @@ -160,7 +159,7 @@ class CustomBuildBloc extends Bloc { skillPriorities: build.skillPriorities, artifacts: build.artifacts..sort((x, y) => x.type.index.compareTo(y.type.index)), teamCharacters: build.teamCharacters, - subStatsSummary: _generateSubStatSummary(build.artifacts), + subStatsSummary: _genshinService.generateSubStatSummary(build.artifacts), ); } @@ -221,7 +220,16 @@ class CustomBuildBloc extends Bloc { return state; } final newCharacter = _genshinService.getCharacterForCard(e.newKey); - return state.copyWith.call(character: newCharacter); + _LoadedState updatedState = state.copyWith.call(character: newCharacter); + if (newCharacter.weaponType != state.character.weaponType) { + updatedState = updatedState.copyWith.call(weapons: []); + } + + if (updatedState.teamCharacters.any((el) => el.key == e.newKey)) { + updatedState.teamCharacters.removeWhere((el) => el.key == e.newKey); + } + + return updatedState; } CustomBuildState _addWeapon(_AddWeapon e, _LoadedState state) { @@ -304,6 +312,7 @@ class CustomBuildBloc extends Bloc { final updatedSubStats = [...old.subStats]..removeWhere((el) => el == e.statType); final updated = old.copyWith.call( type: e.type, + name: translation.name, image: img, key: e.key, rarity: fullArtifact.maxRarity, @@ -314,6 +323,7 @@ class CustomBuildBloc extends Bloc { } else { final newOne = CustomBuildArtifactModel( type: e.type, + name: translation.name, image: img, key: e.key, rarity: fullArtifact.maxRarity, @@ -341,7 +351,7 @@ class CustomBuildBloc extends Bloc { final artifacts = [...state.artifacts]; artifacts.removeAt(index); artifacts.insert(index, updated); - return state.copyWith.call(artifacts: artifacts, subStatsSummary: _generateSubStatSummary(artifacts)); + return state.copyWith.call(artifacts: artifacts, subStatsSummary: _genshinService.generateSubStatSummary(artifacts)); } CustomBuildState _deleteArtifact(_DeleteArtifact e, _LoadedState state) { @@ -351,7 +361,7 @@ class CustomBuildBloc extends Bloc { final updated = [...state.artifacts]; updated.removeWhere((el) => el.type == e.type); - return state.copyWith.call(artifacts: updated, subStatsSummary: _generateSubStatSummary(updated)); + return state.copyWith.call(artifacts: updated, subStatsSummary: _genshinService.generateSubStatSummary(updated)); } CustomBuildState _addTeamCharacter(_AddTeamCharacter e, _LoadedState state) { @@ -447,21 +457,4 @@ class CustomBuildBloc extends Bloc { _customBuildsBloc.add(const CustomBuildsEvent.load()); return _init(build.key, state.title); } - - List _generateSubStatSummary(List artifacts) { - final weightMap = {}; - - for (final artifact in artifacts) { - int weight = artifact.subStats.length; - for (var i = 0; i < artifact.subStats.length; i++) { - final subStat = artifact.subStats[i]; - final ifAbsent = weightMap.containsKey(subStat) ? i : weight; - weightMap.update(subStat, (value) => value + weight, ifAbsent: () => ifAbsent); - weight--; - } - } - - final sorted = weightMap.entries.sorted((a, b) => b.value.compareTo(a.value)); - return sorted.map((e) => e.key).toList(); - } } diff --git a/lib/application/custom_build/custom_build_event.dart b/lib/application/custom_build/custom_build_event.dart index 4467ba6bd..004bc898d 100644 --- a/lib/application/custom_build/custom_build_event.dart +++ b/lib/application/custom_build/custom_build_event.dart @@ -69,5 +69,5 @@ class CustomBuildEvent with _$CustomBuildEvent { const factory CustomBuildEvent.saveChanges() = _SaveChanges; -//TODO: SHARE, SBUSTATS, TALENETS, ARTIFACT'S PIECE BONUS +//TODO: SHARE } diff --git a/lib/domain/models/characters/character_build_card_model.dart b/lib/domain/models/characters/character_build_card_model.dart index bf061106e..b1b30b1f6 100644 --- a/lib/domain/models/characters/character_build_card_model.dart +++ b/lib/domain/models/characters/character_build_card_model.dart @@ -10,6 +10,7 @@ class CharacterBuildCardModel { final List weapons; final List artifacts; final List subStatsToFocus; + final bool isCustomBuild; CharacterBuildCardModel({ required this.isRecommended, @@ -19,6 +20,7 @@ class CharacterBuildCardModel { required this.weapons, required this.artifacts, required this.subStatsToFocus, + this.isCustomBuild = false, }); } diff --git a/lib/domain/models/custom_builds/custom_build_artifact_model.dart b/lib/domain/models/custom_builds/custom_build_artifact_model.dart index 20794e07e..4bb729bda 100644 --- a/lib/domain/models/custom_builds/custom_build_artifact_model.dart +++ b/lib/domain/models/custom_builds/custom_build_artifact_model.dart @@ -8,6 +8,7 @@ class CustomBuildArtifactModel with _$CustomBuildArtifactModel { const factory CustomBuildArtifactModel({ required String key, required ArtifactType type, + required String name, required StatType statType, required String image, required int rarity, diff --git a/lib/domain/models/custom_builds/custom_build_model.dart b/lib/domain/models/custom_builds/custom_build_model.dart index e6530879b..cf0104cb8 100644 --- a/lib/domain/models/custom_builds/custom_build_model.dart +++ b/lib/domain/models/custom_builds/custom_build_model.dart @@ -19,5 +19,6 @@ class CustomBuildModel with _$CustomBuildModel { required List teamCharacters, required List notes, required List skillPriorities, + required List subStatsSummary, }) = _CustomBuildModel; } diff --git a/lib/domain/services/genshin_service.dart b/lib/domain/services/genshin_service.dart index 802b0c5c3..941931e78 100644 --- a/lib/domain/services/genshin_service.dart +++ b/lib/domain/services/genshin_service.dart @@ -85,4 +85,5 @@ abstract class GenshinService { List getArtifactBonus(TranslationArtifactFile translation); List getArtifactRelatedParts(String fullImagePath, String image, int bonus); String getArtifactRelatedPart(String fullImagePath, String image, int bonus, ArtifactType type); + List generateSubStatSummary(List artifacts); } diff --git a/lib/domain/services/persistence/custom_builds_data_service.dart b/lib/domain/services/persistence/custom_builds_data_service.dart index 1e1f4c784..fa138d6a9 100644 --- a/lib/domain/services/persistence/custom_builds_data_service.dart +++ b/lib/domain/services/persistence/custom_builds_data_service.dart @@ -40,4 +40,6 @@ abstract class CustomBuildsDataService { ); Future deleteCustomBuild(int key); + + List getCustomBuildsForCharacter(String charKey); } diff --git a/lib/infrastructure/genshin_service.dart b/lib/infrastructure/genshin_service.dart index e9eff6a57..4fb075a30 100644 --- a/lib/infrastructure/genshin_service.dart +++ b/lib/infrastructure/genshin_service.dart @@ -782,6 +782,24 @@ class GenshinServiceImpl implements GenshinService { return imgs.firstWhere((el) => el.endsWith('$order.png')); } + @override + List generateSubStatSummary(List artifacts) { + final weightMap = {}; + + for (final artifact in artifacts) { + int weight = artifact.subStats.length; + for (var i = 0; i < artifact.subStats.length; i++) { + final subStat = artifact.subStats[i]; + final ifAbsent = weightMap.containsKey(subStat) ? i : weight; + weightMap.update(subStat, (value) => value + weight, ifAbsent: () => ifAbsent); + weight--; + } + } + + final sorted = weightMap.entries.sorted((a, b) => b.value.compareTo(a.value)); + return sorted.map((e) => e.key).toList(); + } + CharacterCardModel _toCharacterForCard(CharacterFileModel character) { final translation = getCharacterTranslation(character.key); diff --git a/lib/infrastructure/persistence/custom_builds_data_service.dart b/lib/infrastructure/persistence/custom_builds_data_service.dart index 388bb0d0f..08198e09a 100644 --- a/lib/infrastructure/persistence/custom_builds_data_service.dart +++ b/lib/infrastructure/persistence/custom_builds_data_service.dart @@ -134,6 +134,30 @@ class CustomBuildsDataServiceImpl implements CustomBuildsDataService { ]); } + @override + List getCustomBuildsForCharacter(String charKey) { + return _buildsBox.values.where((el) => el.showOnCharacterDetail && el.characterKey == charKey).map((e) { + final build = getCustomBuild(e.key as int); + final artifacts = build.artifacts.map((e) => _genshinService.getArtifactForCard(e.key)).toList(); + return CharacterBuildCardModel( + isRecommended: e.isRecommended, + isCustomBuild: true, + type: CharacterRoleType.values[e.roleType], + subType: CharacterRoleSubType.values[e.roleSubType], + skillPriorities: e.skillPriorities.map((e) => CharacterSkillType.values[e]).toList(), + subStatsToFocus: _genshinService.generateSubStatSummary(build.artifacts), + weapons: build.weapons.map((e) => _genshinService.getWeaponForCard(e.key)).toList(), + artifacts: [ + CharacterBuildArtifactModel( + one: null, + stats: build.artifacts.map((e) => e.statType).toList(), + multiples: artifacts, + ), + ], + ); + }).toList(); + } + Future _deleteCustomBuildRelatedParts(int key) { return Future.wait([ _deleteWeapons(key), @@ -205,12 +229,32 @@ class CustomBuildsDataServiceImpl implements CustomBuildsDataService { CustomBuildModel _mapToCustomBuildModel( CustomBuild build, - List notes, - List weapons, - List artifacts, - List teamCharacters, + List buildNotes, + List buildWeapons, + List buildArtifacts, + List buildTeamCharacters, ) { final character = _genshinService.getCharacterForCard(build.characterKey); + final artifacts = buildArtifacts.map((e) { + final fullArtifact = _genshinService.getArtifact(e.itemKey); + final translation = _genshinService.getArtifactTranslation(e.itemKey); + final image = _genshinService.getArtifactRelatedPart( + fullArtifact.fullImagePath, + fullArtifact.image, + translation.bonus.length, + ArtifactType.values[e.type], + ); + return CustomBuildArtifactModel( + key: e.itemKey, + name: translation.name, + type: ArtifactType.values[e.type], + statType: StatType.values[e.statType], + image: image, + rarity: fullArtifact.maxRarity, + subStats: e.subStats.map((e) => StatType.values[e]).toList(), + ); + }).toList() + ..sort((x, y) => x.type.index.compareTo(y.type.index)); return CustomBuildModel( key: build.key as int, title: build.title, @@ -219,7 +263,7 @@ class CustomBuildsDataServiceImpl implements CustomBuildsDataService { showOnCharacterDetail: build.showOnCharacterDetail, isRecommended: build.isRecommended, character: character, - weapons: weapons.map((e) { + weapons: buildWeapons.map((e) { final weapon = _genshinService.getWeaponForCard(e.weaponKey); return CustomBuildWeaponModel( key: e.weaponKey, @@ -232,28 +276,13 @@ class CustomBuildsDataServiceImpl implements CustomBuildsDataService { subStatType: weapon.subStatType, subStatValue: weapon.subStatValue, ); - }).toList(), - artifacts: artifacts.map((e) { - final fullArtifact = _genshinService.getArtifact(e.itemKey); - final translation = _genshinService.getArtifactTranslation(e.itemKey); - final image = _genshinService.getArtifactRelatedPart( - fullArtifact.fullImagePath, - fullArtifact.image, - translation.bonus.length, - ArtifactType.values[e.type], - ); - return CustomBuildArtifactModel( - key: e.itemKey, - type: ArtifactType.values[e.type], - statType: StatType.values[e.statType], - image: image, - rarity: fullArtifact.maxRarity, - subStats: e.subStats.map((e) => StatType.values[e]).toList(), - ); - }).toList(), + }).toList() + ..sort((x, y) => x.index.compareTo(y.index)), + artifacts: artifacts, + subStatsSummary: _genshinService.generateSubStatSummary(artifacts), skillPriorities: build.skillPriorities.map((e) => CharacterSkillType.values[e]).toList(), - notes: notes.map((e) => CustomBuildNoteModel(index: e.index, note: e.note)).toList()..sort((x, y) => x.index.compareTo(y.index)), - teamCharacters: teamCharacters.map((e) { + notes: buildNotes.map((e) => CustomBuildNoteModel(index: e.index, note: e.note)).toList()..sort((x, y) => x.index.compareTo(y.index)), + teamCharacters: buildTeamCharacters.map((e) { final char = _genshinService.getCharacterForCard(e.characterKey); return CustomBuildTeamCharacterModel( key: e.characterKey, diff --git a/lib/presentation/artifacts/widgets/artifact_card.dart b/lib/presentation/artifacts/widgets/artifact_card.dart index e385ff233..2360161c2 100644 --- a/lib/presentation/artifacts/widgets/artifact_card.dart +++ b/lib/presentation/artifacts/widgets/artifact_card.dart @@ -9,6 +9,8 @@ import 'package:transparent_image/transparent_image.dart'; import 'artifact_stats.dart'; +final replaceDigitRegex = RegExp(r'\d{1}'); + class ArtifactCard extends StatelessWidget { final String keyName; final String name; @@ -92,6 +94,15 @@ class ArtifactCard extends StatelessWidget { height: imgHeight, placeholder: MemoryImage(kTransparentImage), image: AssetImage(image), + imageErrorBuilder: (context, error, stack) { + //This can happen when trying to load sets like 'Prayer to xxx' + final path = image.replaceFirst(replaceDigitRegex, '4'); + return Image.asset( + path, + width: imgWidth, + height: imgHeight, + ); + }, ), Tooltip( message: name, diff --git a/lib/presentation/character/widgets/character_detail_bottom.dart b/lib/presentation/character/widgets/character_detail_bottom.dart index 34086d6a4..61d208ca8 100644 --- a/lib/presentation/character/widgets/character_detail_bottom.dart +++ b/lib/presentation/character/widgets/character_detail_bottom.dart @@ -59,6 +59,7 @@ class _PortraitLayout extends StatelessWidget { weapons: build.weapons, artifacts: build.artifacts, subStatsToFocus: build.subStatsToFocus, + isCustomBuild: build.isCustomBuild, ), ) .toList(), @@ -179,6 +180,7 @@ class _LandscapeLayout extends StatelessWidget { weapons: build.weapons, artifacts: build.artifacts, subStatsToFocus: build.subStatsToFocus, + isCustomBuild: build.isCustomBuild, ), ) .toList(), diff --git a/lib/presentation/character/widgets/character_detail_build_card.dart b/lib/presentation/character/widgets/character_detail_build_card.dart index e666770f5..ddbeb2102 100644 --- a/lib/presentation/character/widgets/character_detail_build_card.dart +++ b/lib/presentation/character/widgets/character_detail_build_card.dart @@ -5,6 +5,7 @@ import 'package:shiori/domain/extensions/iterable_extensions.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/shared/character_skill_priority.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'; @@ -12,7 +13,6 @@ import 'package:shiori/presentation/shared/styles.dart'; import 'package:shiori/presentation/shared/sub_stats_to_focus.dart'; import 'package:shiori/presentation/weapons/widgets/weapon_card.dart'; -final _replaceDigitRegex = RegExp(r'\d{1}'); const double _imgHeight = 125; class CharacterDetailBuildCard extends StatelessWidget { @@ -24,6 +24,7 @@ class CharacterDetailBuildCard extends StatelessWidget { final List weapons; final List artifacts; final List subStatsToFocus; + final bool isCustomBuild; const CharacterDetailBuildCard({ Key? key, @@ -35,6 +36,7 @@ class CharacterDetailBuildCard extends StatelessWidget { required this.weapons, required this.artifacts, required this.subStatsToFocus, + required this.isCustomBuild, }) : super(key: key); @override @@ -46,6 +48,7 @@ class CharacterDetailBuildCard extends StatelessWidget { if (subType != CharacterRoleSubType.none) { title += ' (${s.translateCharacterRoleSubType(subType)}) '; } + return Card( elevation: Styles.cardTenElevation, margin: Styles.edgeInsetAll5, @@ -55,22 +58,12 @@ class CharacterDetailBuildCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (isRecommended) - Row( - children: [ - Icon(Icons.star, color: color), - Text( - title, - style: theme.textTheme.headline6!.copyWith(color: color), - ), - ], - ) - else - Text( - title, - style: theme.textTheme.headline6!.copyWith(color: color), + _Title(title: title, isRecommended: isRecommended, isCustomBuild: isCustomBuild, color: color), + if (skillPriorities.isNotEmpty) + CharacterSkillPriority( + skillPriorities: skillPriorities, + color: color, ), - _SkillPriority(skillPriorities: skillPriorities, color: color), Container( margin: Styles.edgeInsetAll5, child: Text( @@ -86,10 +79,11 @@ class CharacterDetailBuildCard extends StatelessWidget { style: theme.textTheme.subtitle2!.copyWith(fontWeight: FontWeight.bold), ), ), - SubStatToFocus( - subStatsToFocus: subStatsToFocus, - color: color, - ), + if (subStatsToFocus.isNotEmpty) + SubStatToFocus( + subStatsToFocus: subStatsToFocus, + color: color, + ), ...artifacts.mapIndex((e, index) { final showOr = index < artifacts.length - 1; if (showOr) { @@ -104,6 +98,60 @@ class CharacterDetailBuildCard extends StatelessWidget { } } +class _Title extends StatelessWidget { + final String title; + final bool isRecommended; + final bool isCustomBuild; + final Color color; + + const _Title({ + Key? key, + required this.title, + required this.isRecommended, + required this.isCustomBuild, + required this.color, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final text = Text( + title, + style: theme.textTheme.headline6!.copyWith(color: color), + ); + + if (isRecommended && isCustomBuild) { + return Row( + children: [ + Icon(Icons.dashboard_customize, color: color), + Icon(Icons.star, color: color), + text, + ], + ); + } + + if (isRecommended) { + return Row( + children: [ + Icon(Icons.star, color: color), + text, + ], + ); + } + + if (isCustomBuild) { + return Row( + children: [ + Icon(Icons.dashboard_customize, color: color), + text, + ], + ); + } + + return text; + } +} + class _Weapons extends StatelessWidget { final List weapons; final Color color; @@ -163,7 +211,7 @@ class _ArtifactRow extends StatelessWidget { itemBuilder: (ctx, index) { final digit = artifactOrder[index]; final stat = item.stats[index]; - final path = item.one!.image.replaceFirst(_replaceDigitRegex, '$digit'); + final path = item.one!.image.replaceFirst(replaceDigitRegex, '$digit'); return ArtifactCard.withoutDetails( name: s.translateStatTypeWithoutValue(stat), image: path, @@ -185,7 +233,7 @@ class _ArtifactRow extends StatelessWidget { final multi = item.multiples[index]; final digit = artifactOrder[index]; final stat = item.stats[index]; - final path = multi.image.replaceFirst(_replaceDigitRegex, '$digit'); + final path = multi.image.replaceFirst(replaceDigitRegex, '$digit'); return ArtifactCard.withoutDetails( name: s.translateStatTypeWithoutValue(stat), image: path, @@ -197,32 +245,3 @@ class _ArtifactRow extends StatelessWidget { ); } } - -class _SkillPriority extends StatelessWidget { - final List skillPriorities; - final Color color; - - const _SkillPriority({ - Key? key, - required this.skillPriorities, - required this.color, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final s = S.of(context); - final theme = Theme.of(context); - final text = skillPriorities.map((e) => s.translateCharacterSkillType(e)).join(' > '); - return Container( - margin: Styles.edgeInsetHorizontal5, - child: Text( - '${s.talentsAscension}: $text', - style: theme.textTheme.subtitle2!.copyWith( - fontWeight: FontWeight.bold, - color: color, - fontSize: 12, - ), - ), - ); - } -} diff --git a/lib/presentation/custom_build/custom_build_page.dart b/lib/presentation/custom_build/custom_build_page.dart index 8b9ddd573..e77c8d0ef 100644 --- a/lib/presentation/custom_build/custom_build_page.dart +++ b/lib/presentation/custom_build/custom_build_page.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; @@ -15,6 +14,7 @@ import 'package:shiori/presentation/custom_build/widgets/artifact_section.dart'; import 'package:shiori/presentation/custom_build/widgets/character_section.dart'; import 'package:shiori/presentation/custom_build/widgets/team_section.dart'; import 'package:shiori/presentation/custom_build/widgets/weapon_section.dart'; +import 'package:shiori/presentation/shared/dialogs/confirm_dialog.dart'; import 'package:shiori/presentation/shared/extensions/element_type_extensions.dart'; import 'package:shiori/presentation/shared/loading.dart'; import 'package:shiori/presentation/shared/styles.dart'; @@ -34,9 +34,7 @@ class CustomBuildPage extends StatelessWidget { @override Widget build(BuildContext context) { - //TODO: SHOW THE TALENTS AND CONSTELLATIONS LIKE THIS final s = S.of(context); - //https://genshin-impact-card-generator.herokuapp.com/ return BlocProvider( create: (ctx) => Injection.getCustomBuildBloc(context.read())..add(CustomBuildEvent.load(key: itemKey, initialTitle: s.dps)), child: _Page( @@ -66,6 +64,7 @@ class _PageState extends State<_Page> { builder: (ctx, state) => state.maybeMap( loaded: (state) => Scaffold( appBar: _AppBar( + buildKey: state.key, newBuild: widget.newBuild, canSave: state.artifacts.length == ArtifactType.values.length && state.weapons.isNotEmpty, screenshotController: _screenshotController, @@ -129,12 +128,14 @@ class _PageState extends State<_Page> { } class _AppBar extends StatelessWidget implements PreferredSizeWidget { + final int? buildKey; final bool newBuild; final bool canSave; final ScreenshotController screenshotController; const _AppBar({ Key? key, + required this.buildKey, required this.newBuild, required this.canSave, required this.screenshotController, @@ -146,20 +147,41 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { return AppBar( title: Text(newBuild ? s.add : s.edit), actions: [ - IconButton( - splashRadius: Styles.mediumButtonSplashRadius, - icon: const Icon(Icons.save), - onPressed: !canSave ? null : () => context.read().add(const CustomBuildEvent.saveChanges()), + Tooltip( + message: s.save, + child: IconButton( + splashRadius: Styles.mediumButtonSplashRadius, + icon: const Icon(Icons.save), + onPressed: !canSave ? null : () => _saveChanges(context), + ), ), - IconButton( - splashRadius: Styles.mediumButtonSplashRadius, - icon: const Icon(Icons.share), - onPressed: () => _takeScreenshot(context), + if (!newBuild && buildKey != null) + Tooltip( + message: s.delete, + child: IconButton( + splashRadius: Styles.mediumButtonSplashRadius, + icon: const Icon(Icons.delete), + onPressed: () => _showDeleteDialog(context), + ), + ), + Tooltip( + message: s.share, + child: IconButton( + splashRadius: Styles.mediumButtonSplashRadius, + icon: const Icon(Icons.share), + onPressed: () => _takeScreenshot(context), + ), ), ], ); } + void _saveChanges(BuildContext context) { + final s = S.of(context); + context.read().add(const CustomBuildEvent.saveChanges()); + ToastUtils.showSucceedToast(ToastUtils.of(context), s.changeWereSuccessfullySaved); + } + Future _takeScreenshot(BuildContext context) async { final s = S.of(context); final fToast = ToastUtils.of(context); @@ -180,6 +202,24 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { } } + Future _showDeleteDialog(BuildContext context) { + final s = S.of(context); + return showDialog( + context: context, + builder: (_) => ConfirmDialog( + title: s.delete, + content: s.confirmQuestion, + onOk: () { + context.read().add(CustomBuildsEvent.delete(key: buildKey!)); + }, + ), + ).then((confirmed) { + if (confirmed == true) { + Navigator.pop(context); + } + }); + } + @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); } diff --git a/lib/presentation/custom_build/widgets/artifact_row.dart b/lib/presentation/custom_build/widgets/artifact_row.dart index fc9bee13a..9391de405 100644 --- a/lib/presentation/custom_build/widgets/artifact_row.dart +++ b/lib/presentation/custom_build/widgets/artifact_row.dart @@ -63,7 +63,7 @@ class ArtifactRow extends StatelessWidget { style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold), ), Text( - 'Archaic Petra', + artifact.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: theme.textTheme.subtitle1, @@ -170,7 +170,6 @@ class ArtifactRow extends StatelessWidget { return; } - //TODO: REMOVE THE CROWNS AND MAYBE ONLY SHOW THE SPECIFIC TYPE final selectedKey = await ArtifactsPage.forSelection(context, type: artifact.type); if (selectedKey.isNullEmptyOrWhitespace) { return; diff --git a/lib/presentation/custom_build/widgets/artifact_section.dart b/lib/presentation/custom_build/widgets/artifact_section.dart index e8a9f59c9..97355881f 100644 --- a/lib/presentation/custom_build/widgets/artifact_section.dart +++ b/lib/presentation/custom_build/widgets/artifact_section.dart @@ -122,7 +122,6 @@ class ArtifactSection extends StatelessWidget { return; } - //TODO: REMOVE THE CROWNS AND MAYBE ONLY SHOW THE SPECIFIC TYPE final selectedKey = await ArtifactsPage.forSelection(context, type: selectedType); if (selectedKey.isNullEmptyOrWhitespace) { return; diff --git a/lib/presentation/custom_build/widgets/character_section.dart b/lib/presentation/custom_build/widgets/character_section.dart index 72891f65b..6bfc6f934 100644 --- a/lib/presentation/custom_build/widgets/character_section.dart +++ b/lib/presentation/custom_build/widgets/character_section.dart @@ -45,7 +45,6 @@ class CharacterSection extends StatelessWidget { Widget build(BuildContext context) { final s = S.of(context); final theme = Theme.of(context); - final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; final height = MediaQuery.of(context).size.height; final width = MediaQuery.of(context).size.width; double imgHeight = height * 0.85; @@ -220,7 +219,6 @@ class CharacterSection extends StatelessWidget { } 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) { diff --git a/lib/presentation/custom_build/widgets/weapon_row.dart b/lib/presentation/custom_build/widgets/weapon_row.dart index 384359654..b1812ab15 100644 --- a/lib/presentation/custom_build/widgets/weapon_row.dart +++ b/lib/presentation/custom_build/widgets/weapon_row.dart @@ -17,8 +17,6 @@ enum _Options { refinements, } -//TODO: LIMIT THE NUMBER OF ROWS TO 10 -//TODO: ADD TEAMS class WeaponRow extends StatelessWidget { final CustomBuildWeaponModel weapon; final Color color; diff --git a/lib/presentation/custom_builds/custom_builds_page.dart b/lib/presentation/custom_builds/custom_builds_page.dart index dacd9c825..91176ad9e 100644 --- a/lib/presentation/custom_builds/custom_builds_page.dart +++ b/lib/presentation/custom_builds/custom_builds_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shiori/application/bloc.dart'; +import 'package:shiori/generated/l10n.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'; @@ -9,14 +10,26 @@ 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 { +class CustomBuildsPage extends StatelessWidget { const CustomBuildsPage({Key? key}) : super(key: key); @override - State createState() => _CustomBuildsPageState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => Injection.customBuildsBloc..add(const CustomBuildsEvent.load()), + child: const _Page(), + ); + } +} + +class _Page extends StatefulWidget { + const _Page({Key? key}) : super(key: key); + + @override + _PageState createState() => _PageState(); } -class _CustomBuildsPageState extends State with SingleTickerProviderStateMixin, AppFabMixin { +class _PageState extends State<_Page> with SingleTickerProviderStateMixin, AppFabMixin { @override bool get isInitiallyVisible => true; @@ -25,6 +38,7 @@ class _CustomBuildsPageState extends State with SingleTickerPr @override Widget build(BuildContext context) { + final s = S.of(context); final mq = MediaQuery.of(context); final crossAxisCount = mq.size.width > 1600 ? 4 @@ -33,55 +47,40 @@ class _CustomBuildsPageState extends State with SingleTickerPr : 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]), - // ), - ), + return Scaffold( + appBar: AppBar( + title: Text(s.customBuilds), + ), + 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: s.startByCreatingBuild) + : WaterfallFlow.builder( + itemCount: state.builds.length, + gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + ), + itemBuilder: (context, index) => CustomBuildCard(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) => const CustomBuildPage()); + final route = MaterialPageRoute( + builder: (ctx) => BlocProvider.value( + value: context.read(), + child: const 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 index bcd8e0d4b..7e9f829a7 100644 --- a/lib/presentation/custom_builds/widgets/custom_build_card.dart +++ b/lib/presentation/custom_builds/widgets/custom_build_card.dart @@ -12,6 +12,7 @@ import 'package:shiori/presentation/shared/dialogs/confirm_dialog.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/styles.dart'; +import 'package:shiori/presentation/shared/sub_stats_to_focus.dart'; import 'package:shiori/presentation/weapons/widgets/weapon_card.dart'; class CustomBuildCard extends StatelessWidget { @@ -31,17 +32,15 @@ class CustomBuildCard extends StatelessWidget { if (item.subType != CharacterRoleSubType.none) { subtitle += ' - ${s.translateCharacterRoleSubType(item.subType)}'; } + final color = item.character.elementType.getElementColorFromContext(context); return InkWell( onTap: () => _goToDetailsPage(context), child: Card( clipBehavior: Clip.hardEdge, - // shape: Styles.mainCardShape, elevation: Styles.cardTenElevation, - color: item.character.elementType.getElementColorFromContext(context), + color: color, shadowColor: Colors.transparent, - // margin: Styles.edgeInsetVertical5, child: Row( - // crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: device == DeviceScreenType.tablet ? 40 : 35, @@ -49,7 +48,7 @@ class CustomBuildCard extends StatelessWidget { name: item.character.name, image: item.character.image, rarity: item.character.stars, - height: 300, + height: 360, ), ), Expanded( @@ -62,26 +61,28 @@ class CustomBuildCard extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.headline5!.copyWith(fontWeight: FontWeight.bold), - ), - Text( - subtitle, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.headline5!.copyWith(fontWeight: FontWeight.bold), + ), + Text( + subtitle, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), ), IconButton( - onPressed: () => showDeleteDialog(context), splashRadius: Styles.smallButtonSplashRadius, icon: const Icon(Icons.delete), + onPressed: () => _showDeleteDialog(context), ), ], ), @@ -108,6 +109,12 @@ class CustomBuildCard extends StatelessWidget { ), ), Text(s.artifacts, style: const TextStyle(fontWeight: FontWeight.bold)), + if (item.subStatsSummary.isNotEmpty) + SubStatToFocus( + subStatsToFocus: item.subStatsSummary, + color: Colors.white, + fontSize: 10, + ), SizedBox( height: 110, child: ListView.builder( @@ -126,7 +133,7 @@ class CustomBuildCard extends StatelessWidget { ); }, ), - ) + ), ], ), ), @@ -147,7 +154,7 @@ class CustomBuildCard extends StatelessWidget { await Navigator.push(context, route); } - Future showDeleteDialog(BuildContext context) { + Future _showDeleteDialog(BuildContext context) { final s = S.of(context); return showDialog( context: context, diff --git a/lib/presentation/shared/character_skill_priority.dart b/lib/presentation/shared/character_skill_priority.dart new file mode 100644 index 000000000..9bfc8268a --- /dev/null +++ b/lib/presentation/shared/character_skill_priority.dart @@ -0,0 +1,34 @@ +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/item_priority.dart'; +import 'package:shiori/presentation/shared/styles.dart'; + +class CharacterSkillPriority extends StatelessWidget { + final List skillPriorities; + final Color color; + final EdgeInsetsGeometry margin; + final double fontSize; + + const CharacterSkillPriority({ + Key? key, + required this.skillPriorities, + required this.color, + this.margin = Styles.edgeInsetHorizontal5, + this.fontSize = 12, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + return ItemPriority( + title: s.talentsAscension, + items: skillPriorities, + color: color, + textResolver: (e) => s.translateCharacterSkillType(e), + margin: margin, + fontSize: fontSize, + ); + } +} diff --git a/lib/presentation/shared/dialogs/confirm_dialog.dart b/lib/presentation/shared/dialogs/confirm_dialog.dart index 710543a9f..263fb080e 100644 --- a/lib/presentation/shared/dialogs/confirm_dialog.dart +++ b/lib/presentation/shared/dialogs/confirm_dialog.dart @@ -22,13 +22,13 @@ class ConfirmDialog extends StatelessWidget { content: Text(content), actions: [ OutlinedButton( - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.pop(context, false), child: Text(s.cancel, style: TextStyle(color: theme.primaryColor)), ), ElevatedButton( onPressed: () { onOk(); - Navigator.pop(context); + Navigator.pop(context, true); }, child: Text(s.ok), ) diff --git a/lib/presentation/shared/dialogs/two_column_enum_selector_dialog.dart b/lib/presentation/shared/dialogs/two_column_enum_selector_dialog.dart index 606de7d9d..d58dfc1b6 100644 --- a/lib/presentation/shared/dialogs/two_column_enum_selector_dialog.dart +++ b/lib/presentation/shared/dialogs/two_column_enum_selector_dialog.dart @@ -18,6 +18,7 @@ class TwoColumnEnumSelectorDialog extends StatefulWidget { final List> selectedStats; final int maxNumberOfSelections; final _OnOk onOk; + final bool showMaxNumberOfSelectionsOnTitle; const TwoColumnEnumSelectorDialog({ Key? key, @@ -29,6 +30,7 @@ class TwoColumnEnumSelectorDialog extends StatefulWidget { required this.selectedStats, required this.maxNumberOfSelections, required this.onOk, + this.showMaxNumberOfSelectionsOnTitle = true, }) : assert(all.length > 0), assert(maxNumberOfSelections > 0), super(key: key); @@ -37,7 +39,6 @@ class TwoColumnEnumSelectorDialog extends StatefulWidget { State createState() => _TwoColumnEnumSelectorDialogState(); } -//TODO: IF YOU UPDATE THE MAIN STAT, YOU SHOULD REMOVE IT FROM THE SUBSTATS class _TwoColumnEnumSelectorDialogState extends State> { late ScrollController _rightController; late ScrollController _leftController; @@ -86,7 +87,7 @@ class _TwoColumnEnumSelectorDialogState extends State extends State _handleRightButtonClick(), ), Text( - widget.rightTitle, + '${widget.rightTitle} (${_selected.length} / ${widget.maxNumberOfSelections})', style: theme.textTheme.subtitle1, ), if (_selected.isNotEmpty) diff --git a/lib/presentation/shared/item_priority.dart b/lib/presentation/shared/item_priority.dart new file mode 100644 index 000000000..2c7fd0bf4 --- /dev/null +++ b/lib/presentation/shared/item_priority.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:shiori/presentation/shared/styles.dart'; + +class ItemPriority extends StatelessWidget { + final String title; + final List items; + final Color color; + final EdgeInsetsGeometry margin; + final double fontSize; + final String Function(TEnum item) textResolver; + + const ItemPriority({ + Key? key, + required this.title, + required this.items, + required this.color, + required this.textResolver, + this.margin = Styles.edgeInsetHorizontal5, + this.fontSize = 12, + }) : assert(items.length > 0), + super(key: key); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final text = items.map((e) => textResolver(e)).join(' > '); + return Container( + margin: margin, + child: Text( + '$title: $text', + style: theme.textTheme.subtitle2!.copyWith( + fontWeight: FontWeight.bold, + color: color, + fontSize: fontSize, + ), + ), + ); + } +} diff --git a/lib/presentation/shared/sub_stats_to_focus.dart b/lib/presentation/shared/sub_stats_to_focus.dart index fede60340..66081a820 100644 --- a/lib/presentation/shared/sub_stats_to_focus.dart +++ b/lib/presentation/shared/sub_stats_to_focus.dart @@ -2,6 +2,7 @@ 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/item_priority.dart'; import 'package:shiori/presentation/shared/styles.dart'; class SubStatToFocus extends StatelessWidget { @@ -21,18 +22,13 @@ class SubStatToFocus extends StatelessWidget { @override Widget build(BuildContext context) { final s = S.of(context); - final theme = Theme.of(context); - final text = subStatsToFocus.map((e) => s.translateStatTypeWithoutValue(e)).join(' > '); - return Container( + return ItemPriority( + title: s.subStats, + items: subStatsToFocus, + color: color, + textResolver: (e) => s.translateStatTypeWithoutValue(e), margin: margin, - child: Text( - '${s.subStats}: $text', - style: theme.textTheme.subtitle2!.copyWith( - fontWeight: FontWeight.bold, - color: color, - fontSize: fontSize, - ), - ), + fontSize: fontSize, ); } }