diff --git a/lib/application/custom_build/custom_build_bloc.dart b/lib/application/custom_build/custom_build_bloc.dart index 7592dac25..d530a93d1 100644 --- a/lib/application/custom_build/custom_build_bloc.dart +++ b/lib/application/custom_build/custom_build_bloc.dart @@ -7,6 +7,7 @@ import 'package:shiori/domain/extensions/string_extensions.dart'; import 'package:shiori/domain/models/models.dart'; import 'package:shiori/domain/services/data_service.dart'; import 'package:shiori/domain/services/genshin_service.dart'; +import 'package:shiori/domain/services/logging_service.dart'; import 'package:shiori/domain/services/telemetry_service.dart'; part 'custom_build_bloc.freezed.dart'; @@ -18,9 +19,10 @@ class CustomBuildBloc extends Bloc { final DataService _dataService; final TelemetryService _telemetryService; final CustomBuildsBloc _customBuildsBloc; + final LoggingService _loggingService; static int maxTitleLength = 40; - static int maxNoteLength = 100; + static int maxNoteLength = 300; static int maxNumberOfNotes = 5; static List validSkillTypes = [ CharacterSkillType.normalAttack, @@ -31,16 +33,16 @@ class CustomBuildBloc extends Bloc { static int maxNumberOfWeapons = 10; static int maxNumberOfTeamCharacters = 10; - CustomBuildBloc(this._genshinService, this._dataService, this._telemetryService, this._customBuildsBloc) : super(const CustomBuildState.loading()); + CustomBuildBloc( + this._genshinService, + this._dataService, + this._telemetryService, + this._loggingService, + this._customBuildsBloc, + ) : super(const CustomBuildState.loading()); @override Stream mapEventToState(CustomBuildEvent event) async* { - //TODO: SHOULD I TRHOW ON INVALID REQUEST ? - //IN MOST CASES THERE ARE SOME VALIDATIONS FOR THINGS LIKE - // if (!state.weapons.any((el) => el.key == e.key)) { - // return state; - // } - // WHICH SHOULD NOT HAPPEN BUT MAYBE I SHOULD THROW AN EXCEPTION IN THERE final s = await event.map( load: (e) async => _init(e.key, e.initialTitle), characterChanged: (e) async => state.maybeMap( @@ -48,23 +50,23 @@ class CustomBuildBloc extends Bloc { orElse: () => state, ), titleChanged: (e) async => state.maybeMap( - loaded: (state) => state.copyWith.call(title: e.newValue), + loaded: (state) => state.copyWith.call(title: e.newValue, readyForScreenshot: false), orElse: () => state, ), roleChanged: (e) async => state.maybeMap( - loaded: (state) => state.copyWith.call(type: e.newValue), + loaded: (state) => state.copyWith.call(type: e.newValue, readyForScreenshot: false), orElse: () => state, ), subRoleChanged: (e) async => state.maybeMap( - loaded: (state) => state.copyWith.call(subType: e.newValue), + loaded: (state) => state.copyWith.call(subType: e.newValue, readyForScreenshot: false), orElse: () => state, ), showOnCharacterDetailChanged: (e) async => state.maybeMap( - loaded: (state) => state.copyWith.call(showOnCharacterDetail: e.newValue), + loaded: (state) => state.copyWith.call(showOnCharacterDetail: e.newValue, readyForScreenshot: false), orElse: () => state, ), isRecommendedChanged: (e) async => state.maybeMap( - loaded: (state) => state.copyWith.call(isRecommended: e.newValue), + loaded: (state) => state.copyWith.call(isRecommended: e.newValue, readyForScreenshot: false), orElse: () => state, ), addNote: (e) async => state.maybeMap( @@ -100,7 +102,7 @@ class CustomBuildBloc extends Bloc { orElse: () => state, ), deleteWeapons: (e) async => state.maybeMap( - loaded: (state) => state.copyWith.call(weapons: []), + loaded: (state) => state.copyWith.call(weapons: [], readyForScreenshot: false), orElse: () => state, ), addArtifact: (e) async => state.maybeMap( @@ -116,7 +118,7 @@ class CustomBuildBloc extends Bloc { orElse: () => state, ), deleteArtifacts: (e) async => state.maybeMap( - loaded: (state) => state.copyWith.call(artifacts: [], subStatsSummary: []), + loaded: (state) => state.copyWith.call(artifacts: [], subStatsSummary: [], readyForScreenshot: false), orElse: () => state, ), addTeamCharacter: (e) async => state.maybeMap( @@ -132,13 +134,21 @@ class CustomBuildBloc extends Bloc { orElse: () => state, ), deleteTeamCharacters: (e) async => state.maybeMap( - loaded: (state) => state.copyWith.call(teamCharacters: []), + loaded: (state) => state.copyWith.call(teamCharacters: [], readyForScreenshot: false), + orElse: () => state, + ), + readyForScreenshot: (e) async => state.maybeMap( + loaded: (state) => state.copyWith.call(readyForScreenshot: e.ready), orElse: () => state, ), saveChanges: (e) async => state.maybeMap( loaded: (state) => _saveChanges(state), orElse: () async => state, ), + screenshotWasTaken: (e) async => state.maybeMap( + loaded: (state) => _onScreenShootTaken(e, state), + orElse: () async => state, + ), ); yield s; @@ -161,6 +171,7 @@ class CustomBuildBloc extends Bloc { artifacts: build.artifacts..sort((x, y) => x.type.index.compareTo(y.type.index)), teamCharacters: build.teamCharacters, subStatsSummary: _genshinService.generateSubStatSummary(build.artifacts), + readyForScreenshot: false, ); } @@ -178,6 +189,7 @@ class CustomBuildBloc extends Bloc { teamCharacters: [], skillPriorities: [], subStatsSummary: [], + readyForScreenshot: false, ); } @@ -186,7 +198,7 @@ class CustomBuildBloc extends Bloc { throw Exception('Note is not valid'); } final newNote = CustomBuildNoteModel(index: state.notes.length, note: e.note); - return state.copyWith.call(notes: [...state.notes, newNote]); + return state.copyWith.call(notes: [...state.notes, newNote], readyForScreenshot: false); } CustomBuildState _deleteNote(_DeleteNote e, _LoadedState state) { @@ -196,7 +208,7 @@ class CustomBuildBloc extends Bloc { final notes = [...state.notes]; notes.removeAt(e.index); - return state.copyWith.call(notes: notes); + return state.copyWith.call(notes: notes, readyForScreenshot: false); } CustomBuildState _addSkillPriority(_AddSkillPriority e, _LoadedState state) { @@ -206,7 +218,7 @@ class CustomBuildBloc extends Bloc { if (!validSkillTypes.contains(e.type)) { throw Exception('Skill type = ${e.type} is not valid'); } - return state.copyWith.call(skillPriorities: [...state.skillPriorities, e.type]); + return state.copyWith.call(skillPriorities: [...state.skillPriorities, e.type], readyForScreenshot: false); } CustomBuildState _deleteSkillPriority(_DeleteSkillPriority e, _LoadedState state) { @@ -216,7 +228,7 @@ class CustomBuildBloc extends Bloc { final skillPriorities = [...state.skillPriorities]; skillPriorities.removeAt(e.index); - return state.copyWith.call(skillPriorities: skillPriorities); + return state.copyWith.call(skillPriorities: skillPriorities, readyForScreenshot: false); } CustomBuildState _characterChanged(_CharacterChanged e, _LoadedState state) { @@ -224,7 +236,7 @@ class CustomBuildBloc extends Bloc { return state; } final newCharacter = _genshinService.getCharacterForCard(e.newKey); - _LoadedState updatedState = state.copyWith.call(character: newCharacter); + _LoadedState updatedState = state.copyWith.call(character: newCharacter, readyForScreenshot: false); if (newCharacter.weaponType != state.character.weaponType) { updatedState = updatedState.copyWith.call(weapons: []); } @@ -262,7 +274,7 @@ class CustomBuildBloc extends Bloc { subStatValue: weapon.subStatValue, ); final weapons = [...state.weapons, newOne]; - return state.copyWith.call(weapons: weapons); + return state.copyWith.call(weapons: weapons, readyForScreenshot: false); } CustomBuildState _weaponsOrderChanged(_WeaponsOrderChanged e, _LoadedState state) { @@ -276,7 +288,7 @@ class CustomBuildBloc extends Bloc { weapons.add(current.copyWith.call(index: i)); } - return state.copyWith.call(weapons: weapons); + return state.copyWith.call(weapons: weapons, readyForScreenshot: false); } CustomBuildState _weaponRefinementChanged(_WeaponRefinementChanged e, _LoadedState state) { @@ -300,7 +312,7 @@ class CustomBuildBloc extends Bloc { final updated = current.copyWith.call(refinement: e.newValue); weapons.insert(index, updated); - return state.copyWith.call(weapons: weapons); + return state.copyWith.call(weapons: weapons, readyForScreenshot: false); } CustomBuildState _deleteWeapon(_DeleteWeapon e, _LoadedState state) { @@ -310,7 +322,7 @@ class CustomBuildBloc extends Bloc { final updated = [...state.weapons]; updated.removeWhere((el) => el.key == e.key); - return state.copyWith.call(weapons: updated); + return state.copyWith.call(weapons: updated, readyForScreenshot: false); } CustomBuildState _addArtifact(_AddArtifact e, _LoadedState state) { @@ -349,7 +361,7 @@ class CustomBuildBloc extends Bloc { ); updatedArtifacts.add(newOne); } - return state.copyWith.call(artifacts: updatedArtifacts..sort((x, y) => x.type.index.compareTo(y.type.index))); + return state.copyWith.call(artifacts: updatedArtifacts..sort((x, y) => x.type.index.compareTo(y.type.index)), readyForScreenshot: false); } CustomBuildState _addArtifactSubStats(_AddArtifactSubStats e, _LoadedState state) { @@ -368,7 +380,7 @@ class CustomBuildBloc extends Bloc { final artifacts = [...state.artifacts]; artifacts.removeAt(index); artifacts.insert(index, updated); - return state.copyWith.call(artifacts: artifacts, subStatsSummary: _genshinService.generateSubStatSummary(artifacts)); + return state.copyWith.call(artifacts: artifacts, subStatsSummary: _genshinService.generateSubStatSummary(artifacts), readyForScreenshot: false); } CustomBuildState _deleteArtifact(_DeleteArtifact e, _LoadedState state) { @@ -378,7 +390,7 @@ class CustomBuildBloc extends Bloc { final updated = [...state.artifacts]; updated.removeWhere((el) => el.type == e.type); - return state.copyWith.call(artifacts: updated, subStatsSummary: _genshinService.generateSubStatSummary(updated)); + return state.copyWith.call(artifacts: updated, subStatsSummary: _genshinService.generateSubStatSummary(updated), readyForScreenshot: false); } CustomBuildState _addTeamCharacter(_AddTeamCharacter e, _LoadedState state) { @@ -415,7 +427,7 @@ class CustomBuildBloc extends Bloc { ); updatedTeamCharacters.add(newOne); } - return state.copyWith.call(teamCharacters: updatedTeamCharacters); + return state.copyWith.call(teamCharacters: updatedTeamCharacters, readyForScreenshot: false); } CustomBuildState _teamCharactersOrderChanged(_TeamCharactersOrderChanged e, _LoadedState state) { @@ -429,7 +441,7 @@ class CustomBuildBloc extends Bloc { teamCharacters.add(current.copyWith.call(index: i)); } - return state.copyWith.call(teamCharacters: teamCharacters); + return state.copyWith.call(teamCharacters: teamCharacters, readyForScreenshot: false); } CustomBuildState _deleteTeamCharacter(_DeleteTeamCharacter e, _LoadedState state) { @@ -439,10 +451,11 @@ class CustomBuildBloc extends Bloc { final updated = [...state.teamCharacters]; updated.removeWhere((el) => el.key == e.key); - return state.copyWith.call(teamCharacters: updated); + return state.copyWith.call(teamCharacters: updated, readyForScreenshot: false); } Future _saveChanges(_LoadedState state) async { + _LoadedState updatedState; if (state.key != null) { await _dataService.customBuilds.updateCustomBuild( state.key!, @@ -457,27 +470,37 @@ class CustomBuildBloc extends Bloc { state.teamCharacters, state.skillPriorities, ); - - await _telemetryService.trackCustomBuildSaved(state.character.key, state.type, state.subType); - _customBuildsBloc.add(const CustomBuildsEvent.load()); - return _init(state.key, state.title); + updatedState = _init(state.key, state.title) as _LoadedState; + } else { + final build = await _dataService.customBuilds.saveCustomBuild( + state.character.key, + state.title, + state.type, + state.subType, + state.showOnCharacterDetail, + state.isRecommended, + state.notes, + state.weapons, + state.artifacts, + state.teamCharacters, + state.skillPriorities, + ); + updatedState = _init(build.key, state.title) as _LoadedState; } - final build = await _dataService.customBuilds.saveCustomBuild( - state.character.key, - state.title, - state.type, - state.subType, - state.showOnCharacterDetail, - state.isRecommended, - state.notes, - state.weapons, - state.artifacts, - state.teamCharacters, - state.skillPriorities, - ); await _telemetryService.trackCustomBuildSaved(state.character.key, state.type, state.subType); _customBuildsBloc.add(const CustomBuildsEvent.load()); - return _init(build.key, state.title); + return updatedState.copyWith.call(readyForScreenshot: true); + } + + Future _onScreenShootTaken(_ScreenshotWasTaken e, _LoadedState state) async { + if (e.succeed) { + await _telemetryService.trackCustomBuildScreenShootTaken(state.character.key, state.type, state.subType); + return state.copyWith.call(readyForScreenshot: false); + } else { + _loggingService.error(runtimeType, 'Something went wrong while taking the tier list builder screenshot', e.ex, e.trace); + } + + return state; } } diff --git a/lib/application/custom_build/custom_build_event.dart b/lib/application/custom_build/custom_build_event.dart index 004bc898d..b47892f73 100644 --- a/lib/application/custom_build/custom_build_event.dart +++ b/lib/application/custom_build/custom_build_event.dart @@ -67,7 +67,13 @@ class CustomBuildEvent with _$CustomBuildEvent { const factory CustomBuildEvent.deleteTeamCharacters() = _DeleteTeamCharacters; - const factory CustomBuildEvent.saveChanges() = _SaveChanges; + const factory CustomBuildEvent.readyForScreenshot({required bool ready}) = _ReadyForScreenshot; + + const factory CustomBuildEvent.screenshotWasTaken({ + required bool succeed, + Object? ex, + StackTrace? trace, + }) = _ScreenshotWasTaken; -//TODO: SHARE + const factory CustomBuildEvent.saveChanges() = _SaveChanges; } diff --git a/lib/application/custom_build/custom_build_state.dart b/lib/application/custom_build/custom_build_state.dart index 5ce6a485b..a7c59efac 100644 --- a/lib/application/custom_build/custom_build_state.dart +++ b/lib/application/custom_build/custom_build_state.dart @@ -18,5 +18,6 @@ class CustomBuildState with _$CustomBuildState { required List notes, required List skillPriorities, required List subStatsSummary, + required bool readyForScreenshot, }) = _LoadedState; } diff --git a/lib/domain/enums/character_role_subtype.dart b/lib/domain/enums/character_role_subtype.dart index ccbf83b6f..7caebe255 100644 --- a/lib/domain/enums/character_role_subtype.dart +++ b/lib/domain/enums/character_role_subtype.dart @@ -12,4 +12,5 @@ enum CharacterRoleSubType { melt, freeze, shield, + healer, } diff --git a/lib/domain/services/telemetry_service.dart b/lib/domain/services/telemetry_service.dart index 2c069e4fa..12e9db175 100644 --- a/lib/domain/services/telemetry_service.dart +++ b/lib/domain/services/telemetry_service.dart @@ -55,4 +55,6 @@ abstract class TelemetryService { Future trackNotificationStopped(AppNotificationType type); Future trackCustomBuildSaved(String charKey, CharacterRoleType roleType, CharacterRoleSubType subType); + + Future trackCustomBuildScreenShootTaken(String charKey, CharacterRoleType roleType, CharacterRoleSubType subType); } diff --git a/lib/infrastructure/telemetry/telemetry_service.dart b/lib/infrastructure/telemetry/telemetry_service.dart index b79e46f0e..75c120a24 100644 --- a/lib/infrastructure/telemetry/telemetry_service.dart +++ b/lib/infrastructure/telemetry/telemetry_service.dart @@ -150,4 +150,14 @@ class TelemetryServiceImpl implements TelemetryService { 'SubType': EnumToString.convertToString(subType), }, ); + + @override + Future trackCustomBuildScreenShootTaken(String charKey, CharacterRoleType roleType, CharacterRoleSubType subType) => trackEventAsync( + 'Custom-Build-ScreenShootTaken', + { + 'CharKey': charKey, + 'RoleType': EnumToString.convertToString(roleType), + 'SubType': EnumToString.convertToString(subType), + }, + ); } diff --git a/lib/injection.dart b/lib/injection.dart index 8a15fd0f2..5462b7b5c 100644 --- a/lib/injection.dart +++ b/lib/injection.dart @@ -176,7 +176,8 @@ class Injection { final genshinService = getIt(); final dataService = getIt(); final telemetryService = getIt(); - return CustomBuildBloc(genshinService, dataService, telemetryService, bloc); + final loggingService = getIt(); + return CustomBuildBloc(genshinService, dataService, telemetryService, loggingService, bloc); } static Future init() async { diff --git a/lib/presentation/custom_build/custom_build_page.dart b/lib/presentation/custom_build/custom_build_page.dart index f8565c70b..e776e69ab 100644 --- a/lib/presentation/custom_build/custom_build_page.dart +++ b/lib/presentation/custom_build/custom_build_page.dart @@ -82,15 +82,34 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { loaded: (state) => AppBar( title: Text(newBuild ? s.add : s.edit), actions: [ - Tooltip( - message: s.save, - child: IconButton( - splashRadius: Styles.mediumButtonSplashRadius, - icon: const Icon(Icons.save), - onPressed: !(state.artifacts.length == ArtifactType.values.length && state.weapons.isNotEmpty) ? null : () => _saveChanges(context), + if (!state.readyForScreenshot) + Tooltip( + message: s.save, + child: IconButton( + splashRadius: Styles.mediumButtonSplashRadius, + icon: const Icon(Icons.save), + onPressed: !(state.artifacts.length == ArtifactType.values.length && state.weapons.isNotEmpty) ? null : () => _saveChanges(context), + ), ), - ), - if (!newBuild && state.key != null) + if (state.readyForScreenshot) + Tooltip( + message: s.save, + child: IconButton( + splashRadius: Styles.mediumButtonSplashRadius, + icon: const Icon(Icons.save_alt), + onPressed: () => _takeScreenshot(context), + ), + ), + if (state.readyForScreenshot) + Tooltip( + message: s.cancel, + child: IconButton( + splashRadius: Styles.mediumButtonSplashRadius, + icon: const Icon(Icons.undo), + onPressed: () => context.read().add(const CustomBuildEvent.readyForScreenshot(ready: false)), + ), + ), + if (!newBuild && state.key != null && !state.readyForScreenshot) Tooltip( message: s.delete, child: IconButton( @@ -99,14 +118,35 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { onPressed: () => _showDeleteDialog(context, state.key), ), ), - Tooltip( - message: s.share, - child: IconButton( - splashRadius: Styles.mediumButtonSplashRadius, - icon: const Icon(Icons.share), - onPressed: () => _takeScreenshot(context), + if (!state.readyForScreenshot) + PopupMenuButton( + onSelected: (e) { + switch (e) { + case 0: + context.read().add(CustomBuildEvent.showOnCharacterDetailChanged(newValue: !state.showOnCharacterDetail)); + break; + default: + throw Exception('Invalid option'); + } + }, + tooltip: s.options, + itemBuilder: (BuildContext context) { + return [0].map((int choice) { + switch (choice) { + case 0: + return PopupMenuItem( + value: choice, + child: ListTile( + title: Text(s.showOnCharacterDetail), + leading: Icon(state.showOnCharacterDetail ? Icons.check_box : Icons.check_box_outline_blank), + ), + ); + default: + throw Exception('Invalid option'); + } + }).toList(); + }, ), - ), ], ), orElse: () => AppBar( @@ -125,7 +165,7 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { Future _takeScreenshot(BuildContext context) async { final s = S.of(context); final fToast = ToastUtils.of(context); - // final bloc = context.read(); + final bloc = context.read(); try { if (!await Permission.storage.request().isGranted) { ToastUtils.showInfoToast(fToast, s.acceptToSaveImg); @@ -135,10 +175,10 @@ class _AppBar extends StatelessWidget implements PreferredSizeWidget { final bytes = await screenshotController.capture(pixelRatio: 1.5); await ImageGallerySaver.saveImage(bytes!, quality: 100); ToastUtils.showSucceedToast(fToast, s.imgSavedSuccessfully); - // bloc.add(const TierListEvent.screenshotTaken(succeed: true)); + bloc.add(const CustomBuildEvent.screenshotWasTaken(succeed: true)); } catch (e, trace) { ToastUtils.showErrorToast(fToast, s.unknownError); - // bloc.add(TierListEvent.screenshotTaken(succeed: false, ex: e, trace: trace)); + bloc.add(CustomBuildEvent.screenshotWasTaken(succeed: false, ex: e, trace: trace)); } } @@ -208,15 +248,17 @@ class _LandscapeLayout extends StatelessWidget { Expanded( child: WeaponSection( maxItemImageWidth: _maxItemImageWidth, + useBoxDecoration: false, ), ), Expanded( child: ArtifactSection( maxItemImageWidth: _maxItemImageWidth, + useBoxDecoration: false, ), ), Expanded( - child: TeamSection(), + child: TeamSection(useBoxDecoration: false), ), ], ), @@ -241,11 +283,13 @@ class _WeaponsAndArtifacts extends StatelessWidget { children: const [ WeaponSection( maxItemImageWidth: _maxItemImageWidth, + useBoxDecoration: true, ), ArtifactSection( maxItemImageWidth: _maxItemImageWidth, + useBoxDecoration: true, ), - TeamSection(), + TeamSection(useBoxDecoration: true), ], ); } @@ -259,16 +303,18 @@ class _WeaponsAndArtifacts extends StatelessWidget { Expanded( child: WeaponSection( maxItemImageWidth: _maxItemImageWidth, + useBoxDecoration: true, ), ), Expanded( child: ArtifactSection( maxItemImageWidth: _maxItemImageWidth, + useBoxDecoration: true, ), ), ], ), - const TeamSection(), + const TeamSection(useBoxDecoration: true), ], ); } diff --git a/lib/presentation/custom_build/widgets/artifact_row.dart b/lib/presentation/custom_build/widgets/artifact_row.dart index 9391de405..320b2069b 100644 --- a/lib/presentation/custom_build/widgets/artifact_row.dart +++ b/lib/presentation/custom_build/widgets/artifact_row.dart @@ -25,12 +25,14 @@ class ArtifactRow extends StatelessWidget { final CustomBuildArtifactModel artifact; final Color color; final double maxImageWidth; + final bool readyForScreenshot; const ArtifactRow({ Key? key, required this.artifact, required this.color, required this.maxImageWidth, + required this.readyForScreenshot, }) : super(key: key); @override @@ -79,50 +81,51 @@ class ArtifactRow extends StatelessWidget { ), ), ), - ItemPopupMenuFilter<_Options>.withoutSelectedValue( - values: _Options.values, - tooltipText: s.options, - icon: const Icon(Icons.more_vert), - onSelected: (type) => _handleOptionSelected(context, type), - childBuilder: (e) { - Widget icon; - switch (e.enumValue) { - case _Options.subStats: - icon = const Icon(Icons.menu); - break; - case _Options.delete: - icon = const Icon(Icons.delete); - break; - case _Options.update: - icon = const Icon(Icons.edit); - break; - default: - throw Exception('The provided artifact option type = ${e.enumValue} is not valid'); - } + if (!readyForScreenshot) + ItemPopupMenuFilter<_Options>.withoutSelectedValue( + values: _Options.values, + tooltipText: s.options, + icon: const Icon(Icons.more_vert), + onSelected: (type) => _handleOptionSelected(context, type), + childBuilder: (e) { + Widget icon; + switch (e.enumValue) { + case _Options.subStats: + icon = const Icon(Icons.menu); + break; + case _Options.delete: + icon = const Icon(Icons.delete); + break; + case _Options.update: + icon = const Icon(Icons.edit); + break; + default: + throw Exception('The provided artifact option type = ${e.enumValue} is not valid'); + } - return Row( - children: [ - icon, - Container( - margin: const EdgeInsets.only(left: 10), - child: Text(e.translation, overflow: TextOverflow.ellipsis), - ), - ], - ); - }, - itemText: (type, _) { - switch (type) { - case _Options.subStats: - return s.subStats; - case _Options.delete: - return s.delete; - case _Options.update: - return s.update; - default: - throw Exception('The provided artifact option type = $type is not valid'); - } - }, - ), + return Row( + children: [ + icon, + Container( + margin: const EdgeInsets.only(left: 10), + child: Text(e.translation, overflow: TextOverflow.ellipsis), + ), + ], + ); + }, + itemText: (type, _) { + switch (type) { + case _Options.subStats: + return s.subStats; + case _Options.delete: + return s.delete; + case _Options.update: + return s.update; + default: + throw Exception('The provided artifact option type = $type is not valid'); + } + }, + ), ], ); } diff --git a/lib/presentation/custom_build/widgets/artifact_section.dart b/lib/presentation/custom_build/widgets/artifact_section.dart index c5d1c1522..e5ccfa758 100644 --- a/lib/presentation/custom_build/widgets/artifact_section.dart +++ b/lib/presentation/custom_build/widgets/artifact_section.dart @@ -17,17 +17,18 @@ import 'package:shiori/presentation/shared/sub_stats_to_focus.dart'; class ArtifactSection extends StatelessWidget { final double maxItemImageWidth; + final bool useBoxDecoration; const ArtifactSection({ Key? key, required this.maxItemImageWidth, + required this.useBoxDecoration, }) : super(key: key); @override Widget build(BuildContext context) { final s = S.of(context); final theme = Theme.of(context); - final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; return BlocBuilder( builder: (context, state) => state.maybeMap( loaded: (state) { @@ -38,46 +39,54 @@ class ArtifactSection extends StatelessWidget { children: [ Container( padding: Styles.edgeInsetVertical10, - // margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: color, - border: isPortrait ? const Border(top: BorderSide(color: Colors.white)) : null, + border: useBoxDecoration ? const Border(top: BorderSide(color: Colors.white)) : null, ), child: Text( - '${s.artifacts} (${state.artifacts.length} / ${ArtifactType.values.length})', + state.readyForScreenshot ? s.artifacts : '${s.artifacts} (${state.artifacts.length} / ${ArtifactType.values.length})', textAlign: TextAlign.center, style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold), ), ), - ButtonBar( - buttonPadding: EdgeInsets.zero, - children: [ - Tooltip( - message: s.add, - child: IconButton( - padding: EdgeInsets.zero, - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.add), - onPressed: state.artifacts.length < ArtifactType.values.length - ? () => _addArtifact(context, state.artifacts.map((e) => e.type).toList()) - : null, + if (!state.readyForScreenshot) + ButtonBar( + buttonPadding: EdgeInsets.zero, + children: [ + Tooltip( + message: s.add, + child: IconButton( + padding: EdgeInsets.zero, + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.add), + onPressed: state.artifacts.length < ArtifactType.values.length + ? () => _addArtifact(context, state.artifacts.map((e) => e.type).toList()) + : null, + ), ), - ), - Tooltip( - message: s.clearAll, - child: IconButton( - padding: EdgeInsets.zero, - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.clear_all), - onPressed: state.artifacts.isEmpty ? null : () => context.read().add(const CustomBuildEvent.deleteArtifacts()), + Tooltip( + message: s.clearAll, + child: IconButton( + padding: EdgeInsets.zero, + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.clear_all), + onPressed: + state.artifacts.isEmpty ? null : () => context.read().add(const CustomBuildEvent.deleteArtifacts()), + ), ), - ), - ], - ), + ], + ), if (state.artifacts.isEmpty) NothingFound(msg: s.startByAddingArtifacts) else - ...state.artifacts.map((e) => ArtifactRow(artifact: e, color: color, maxImageWidth: maxItemImageWidth)), + ...state.artifacts.map( + (e) => ArtifactRow( + artifact: e, + color: color, + maxImageWidth: maxItemImageWidth, + readyForScreenshot: state.readyForScreenshot, + ), + ), if (state.subStatsSummary.isNotEmpty) SubStatToFocus( subStatsToFocus: state.subStatsSummary, diff --git a/lib/presentation/custom_build/widgets/character_section.dart b/lib/presentation/custom_build/widgets/character_section.dart index 6d3f5a21c..8ace5c679 100644 --- a/lib/presentation/custom_build/widgets/character_section.dart +++ b/lib/presentation/custom_build/widgets/character_section.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; 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'; @@ -25,61 +26,69 @@ class CharacterSection extends StatelessWidget { Widget build(BuildContext context) { final s = S.of(context); final theme = Theme.of(context); + final size = MediaQuery.of(context).size; final height = MediaQuery.of(context).size.height; final width = MediaQuery.of(context).size.width; + final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; double imgHeight = height * 0.85; if (imgHeight > 1000) { imgHeight = 1000; } + + if (!isPortrait && imgHeight < 350) { + imgHeight = 600; + } final flexA = width < 400 ? 55 : 45; final flexB = width < 400 ? 45 : 55; + + final deviceType = getDeviceType(size); + final useRowOnTalentsAndNotes = deviceType != DeviceScreenType.mobile && !isPortrait; + return BlocBuilder( builder: (context, state) => state.maybeMap( - loaded: (state) { - final canAddNotes = state.notes.map((e) => e.note.length).sum < 300 && state.notes.length < CustomBuildBloc.maxNumberOfNotes; - final canAddSkillPriorities = CustomBuildBloc.validSkillTypes.length == state.skillPriorities.length; - return Container( - color: state.character.elementType.getElementColorFromContext(context), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: flexA, - child: CharacterStackImage( - name: state.character.name, - image: state.character.image, - rarity: state.character.stars, - height: imgHeight, - onTap: () => _openCharacterPage(context, state.character.key), - ), + loaded: (state) => Container( + color: state.character.elementType.getElementColorFromContext(context), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: flexA, + child: CharacterStackImage( + name: state.character.name, + image: state.character.image, + rarity: state.character.stars, + height: imgHeight, + onTap: () => _openCharacterPage(context, state.character.key), ), - Expanded( - flex: flexB, - child: Padding( - padding: Styles.edgeInsetHorizontal5, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - Expanded( - child: Text( - state.title, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.headline5!.copyWith(fontWeight: FontWeight.bold), - ), + ), + Expanded( + flex: flexB, + child: Padding( + padding: Styles.edgeInsetHorizontal5, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + state.title, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.headline5!.copyWith(fontWeight: FontWeight.bold), ), - Tooltip( - message: s.recommended, - child: IconButton( - splashRadius: Styles.smallButtonSplashRadius, - icon: Icon(state.isRecommended ? Icons.star : Icons.star_border_outlined), - onPressed: () => context.read().add( - CustomBuildEvent.isRecommendedChanged(newValue: !state.isRecommended), - ), - ), + ), + Tooltip( + message: s.recommended, + child: IconButton( + splashRadius: Styles.smallButtonSplashRadius, + icon: Icon(state.isRecommended ? Icons.star : Icons.star_border_outlined), + onPressed: () => context.read().add( + CustomBuildEvent.isRecommendedChanged(newValue: !state.isRecommended), + ), ), + ), + if (!state.readyForScreenshot) Tooltip( message: s.edit, child: IconButton( @@ -96,8 +105,41 @@ class CharacterSection extends StatelessWidget { ), ), ) + ], + ), + if (!isPortrait) + Row( + children: [ + Expanded( + flex: 48, + child: DropdownButtonWithTitle( + margin: EdgeInsets.zero, + title: s.role, + currentValue: state.type, + items: EnumUtils.getTranslatedAndSortedEnum( + CharacterRoleType.values.where((el) => el != CharacterRoleType.na).toList(), + (val, _) => s.translateCharacterRoleType(val), + ), + onChanged: (v) => context.read().add(CustomBuildEvent.roleChanged(newValue: v)), + ), + ), + const Spacer(flex: 4), + Expanded( + flex: 48, + child: DropdownButtonWithTitle( + margin: EdgeInsets.zero, + title: s.subType, + currentValue: state.subType, + items: EnumUtils.getTranslatedAndSortedEnum( + CharacterRoleSubType.values, + (val, _) => s.translateCharacterRoleSubType(val), + ), + onChanged: (v) => context.read().add(CustomBuildEvent.subRoleChanged(newValue: v)), + ), + ), ], - ), + ) + else ...[ DropdownButtonWithTitle( margin: EdgeInsets.zero, title: s.role, @@ -118,90 +160,31 @@ class CharacterSection extends StatelessWidget { ), onChanged: (v) => context.read().add(CustomBuildEvent.subRoleChanged(newValue: v)), ), - SwitchListTile( - activeColor: theme.colorScheme.secondary, - contentPadding: EdgeInsets.zero, - title: Text(s.showOnCharacterDetail), - value: state.showOnCharacterDetail, - onChanged: (v) => context.read().add(CustomBuildEvent.showOnCharacterDetailChanged(newValue: v)), - ), + ], + if (useRowOnTalentsAndNotes) Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Text( - s.talentPriority, - style: theme.textTheme.subtitle1, - ), + child: _TalentPriorityRow(skillPriorities: state.skillPriorities, readyToShare: state.readyForScreenshot), ), - IconButton( - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.add), - onPressed: canAddSkillPriorities - ? null - : () => showDialog( - context: context, - builder: (_) => SelectCharacterSkillTypeDialog( - excluded: CustomBuildBloc.excludedSkillTypes, - selectedValues: state.skillPriorities, - onSave: (type) { - if (type == null) { - return; - } - - context.read().add(CustomBuildEvent.addSkillPriority(type: type)); - }, - ), - ), - ), - ], - ), - BulletList( - iconSize: 14, - items: state.skillPriorities.map((e) => s.translateCharacterSkillType(e)).toList(), - iconResolver: (index) => Text('#${index + 1}', style: theme.textTheme.subtitle2!.copyWith(fontSize: 12)), - fontSize: 10, - padding: const EdgeInsets.only(right: 16, left: 5, bottom: 5, top: 5), - onDelete: (index) => context.read().add(CustomBuildEvent.deleteSkillPriority(index: index)), - ), - Row( - children: [ Expanded( - child: Text( - s.notes, - style: theme.textTheme.subtitle1, - ), - ), - IconButton( - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.add), - onPressed: !canAddNotes - ? null - : () => showDialog( - context: context, - builder: (_) => TextDialog.create( - title: s.note, - onSave: (note) => context.read().add(CustomBuildEvent.addNote(note: note)), - maxLength: CustomBuildBloc.maxNoteLength, - ), - ), + child: _NoteRow(notes: state.notes.map((e) => e.note).toList(), readyToShare: state.readyForScreenshot), ), ], - ), - BulletList( - iconSize: 14, - items: state.notes.map((e) => e.note).toList(), - fontSize: 10, - padding: const EdgeInsets.only(right: 16, left: 5, bottom: 5, top: 5), - onDelete: (index) => context.read().add(CustomBuildEvent.deleteNote(index: index)), - ), - ], - ), + ) + else ...[ + _TalentPriorityRow(skillPriorities: state.skillPriorities, readyToShare: state.readyForScreenshot), + _NoteRow(notes: state.notes.map((e) => e.note).toList(), readyToShare: state.readyForScreenshot), + ] + ], ), ), - ], - ), - ); - }, + ), + ], + ), + ), orElse: () => const Loading(useScaffold: false), ), ); @@ -217,3 +200,121 @@ class CharacterSection extends StatelessWidget { bloc.add(CustomBuildEvent.characterChanged(newKey: selectedKey!)); } } + +class _TalentPriorityRow extends StatelessWidget { + final List skillPriorities; + final bool readyToShare; + + const _TalentPriorityRow({ + Key? key, + required this.skillPriorities, + required this.readyToShare, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final theme = Theme.of(context); + final canAddSkillPriorities = CustomBuildBloc.validSkillTypes.length == skillPriorities.length; + return Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + s.talentPriority, + style: theme.textTheme.subtitle1, + ), + ), + if (!readyToShare) + IconButton( + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.add), + onPressed: canAddSkillPriorities + ? null + : () => showDialog( + context: context, + builder: (_) => SelectCharacterSkillTypeDialog( + excluded: CustomBuildBloc.excludedSkillTypes, + selectedValues: skillPriorities, + onSave: (type) { + if (type == null) { + return; + } + + context.read().add(CustomBuildEvent.addSkillPriority(type: type)); + }, + ), + ), + ), + ], + ), + BulletList( + iconSize: 14, + items: skillPriorities.map((e) => s.translateCharacterSkillType(e)).toList(), + iconResolver: (index) => Text('#${index + 1}', style: theme.textTheme.subtitle2!.copyWith(fontSize: 12)), + fontSize: 10, + addTooltip: false, + padding: const EdgeInsets.only(right: 16, left: 5, bottom: 5, top: 5), + onDelete: readyToShare ? null : (index) => context.read().add(CustomBuildEvent.deleteSkillPriority(index: index)), + ), + ], + ); + } +} + +class _NoteRow extends StatelessWidget { + final List notes; + final bool readyToShare; + + const _NoteRow({ + Key? key, + required this.notes, + required this.readyToShare, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final s = S.of(context); + final theme = Theme.of(context); + final canAddNotes = notes.map((e) => e.length).sum < (CustomBuildBloc.maxNumberOfNotes * CustomBuildBloc.maxNoteLength) && + notes.length < CustomBuildBloc.maxNumberOfNotes; + return Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + s.notes, + style: theme.textTheme.subtitle1, + ), + ), + if (!readyToShare) + IconButton( + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.add), + onPressed: !canAddNotes + ? null + : () => showDialog( + context: context, + builder: (_) => TextDialog.create( + title: s.note, + onSave: (note) => context.read().add(CustomBuildEvent.addNote(note: note)), + maxLength: CustomBuildBloc.maxNoteLength, + ), + ), + ), + ], + ), + BulletList( + iconSize: 14, + items: notes, + fontSize: 10, + addTooltip: false, + padding: const EdgeInsets.only(right: 16, left: 5, bottom: 5, top: 5), + onDelete: readyToShare ? null : (index) => context.read().add(CustomBuildEvent.deleteNote(index: index)), + ), + ], + ); + } +} diff --git a/lib/presentation/custom_build/widgets/team_character_row.dart b/lib/presentation/custom_build/widgets/team_character_row.dart index 60db422af..b3ef39fe6 100644 --- a/lib/presentation/custom_build/widgets/team_character_row.dart +++ b/lib/presentation/custom_build/widgets/team_character_row.dart @@ -20,12 +20,14 @@ class TeamCharacterRow extends StatelessWidget { final CustomBuildTeamCharacterModel character; final int teamCount; final Color color; + final bool readyToShare; const TeamCharacterRow({ Key? key, required this.character, required this.teamCount, required this.color, + required this.readyToShare, }) : super(key: key); @override @@ -68,45 +70,46 @@ class TeamCharacterRow extends StatelessWidget { ), ), ), - ItemPopupMenuFilter<_Options>.withoutSelectedValue( - values: _Options.values, - tooltipText: s.options, - icon: const Icon(Icons.more_vert), - onSelected: (type) => _handleOptionSelected(context, type), - childBuilder: (e) { - Widget icon; - switch (e.enumValue) { - case _Options.delete: - icon = const Icon(Icons.delete); - break; - case _Options.update: - icon = const Icon(Icons.edit); - break; - default: - throw Exception('The provided team character option type = ${e.enumValue} is not valid'); - } + if (!readyToShare) + ItemPopupMenuFilter<_Options>.withoutSelectedValue( + values: _Options.values, + tooltipText: s.options, + icon: const Icon(Icons.more_vert), + onSelected: (type) => _handleOptionSelected(context, type), + childBuilder: (e) { + Widget icon; + switch (e.enumValue) { + case _Options.delete: + icon = const Icon(Icons.delete); + break; + case _Options.update: + icon = const Icon(Icons.edit); + break; + default: + throw Exception('The provided team character option type = ${e.enumValue} is not valid'); + } - return Row( - children: [ - icon, - Container( - margin: const EdgeInsets.only(left: 10), - child: Text(e.translation, overflow: TextOverflow.ellipsis), - ), - ], - ); - }, - itemText: (type, _) { - switch (type) { - case _Options.delete: - return s.delete; - case _Options.update: - return s.update; - default: - throw Exception('The provided team character option type = $type is not valid'); - } - }, - ), + return Row( + children: [ + icon, + Container( + margin: const EdgeInsets.only(left: 10), + child: Text(e.translation, overflow: TextOverflow.ellipsis), + ), + ], + ); + }, + itemText: (type, _) { + switch (type) { + case _Options.delete: + return s.delete; + case _Options.update: + return s.update; + default: + throw Exception('The provided team character option type = $type is not valid'); + } + }, + ), ], ); } diff --git a/lib/presentation/custom_build/widgets/team_section.dart b/lib/presentation/custom_build/widgets/team_section.dart index b924044dd..2a0520f59 100644 --- a/lib/presentation/custom_build/widgets/team_section.dart +++ b/lib/presentation/custom_build/widgets/team_section.dart @@ -16,13 +16,17 @@ import 'package:shiori/presentation/shared/nothing_found.dart'; import 'package:shiori/presentation/shared/styles.dart'; class TeamSection extends StatelessWidget { - const TeamSection({Key? key}) : super(key: key); + final bool useBoxDecoration; + + const TeamSection({ + Key? key, + required this.useBoxDecoration, + }) : super(key: key); @override Widget build(BuildContext context) { final s = S.of(context); final theme = Theme.of(context); - final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; return BlocBuilder( builder: (context, state) => state.maybeMap( loaded: (state) { @@ -33,71 +37,82 @@ class TeamSection extends StatelessWidget { children: [ Container( padding: Styles.edgeInsetVertical10, - // margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: color, - border: isPortrait ? const Border(top: BorderSide(color: Colors.white)) : null, + border: useBoxDecoration ? const Border(top: BorderSide(color: Colors.white)) : null, ), child: Text( - '${s.teamComposition} (${state.teamCharacters.length} / ${CustomBuildBloc.maxNumberOfTeamCharacters})', + state.readyForScreenshot + ? s.teamComposition + : '${s.teamComposition} (${state.teamCharacters.length} / ${CustomBuildBloc.maxNumberOfTeamCharacters})', textAlign: TextAlign.center, style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold), ), ), - ButtonBar( - buttonPadding: EdgeInsets.zero, - children: [ - Tooltip( - message: s.add, - child: IconButton( - padding: EdgeInsets.zero, - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.add), - onPressed: state.teamCharacters.length <= CustomBuildBloc.maxNumberOfTeamCharacters - ? () => _addTeamCharacter(context, state.teamCharacters.map((e) => e.key).toList(), state.character.key) - : null, + if (!state.readyForScreenshot) + ButtonBar( + buttonPadding: EdgeInsets.zero, + children: [ + Tooltip( + message: s.add, + child: IconButton( + padding: EdgeInsets.zero, + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.add), + onPressed: state.teamCharacters.length <= CustomBuildBloc.maxNumberOfTeamCharacters + ? () => _addTeamCharacter(context, state.teamCharacters.map((e) => e.key).toList(), state.character.key) + : null, + ), ), - ), - Tooltip( - message: s.sort, - child: IconButton( - padding: EdgeInsets.zero, - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.sort), - onPressed: state.teamCharacters.length < 2 - ? null - : () => showDialog( - context: context, - builder: (_) => SortItemsDialog( - items: state.teamCharacters.map((e) => SortableItem(e.key, e.name)).toList(), - onSave: (result) { - if (!result.somethingChanged) { - return; - } + Tooltip( + message: s.sort, + child: IconButton( + padding: EdgeInsets.zero, + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.sort), + onPressed: state.teamCharacters.length < 2 + ? null + : () => showDialog( + context: context, + builder: (_) => SortItemsDialog( + items: state.teamCharacters.map((e) => SortableItem(e.key, e.name)).toList(), + onSave: (result) { + if (!result.somethingChanged) { + return; + } - context.read().add(CustomBuildEvent.teamCharactersOrderChanged(characters: result.items)); - }, + context.read().add(CustomBuildEvent.teamCharactersOrderChanged(characters: result.items)); + }, + ), ), - ), + ), ), - ), - Tooltip( - message: s.clearAll, - child: IconButton( - padding: EdgeInsets.zero, - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.clear_all), - onPressed: state.teamCharacters.isEmpty - ? null - : () => context.read().add(const CustomBuildEvent.deleteTeamCharacters()), + Tooltip( + message: s.clearAll, + child: IconButton( + padding: EdgeInsets.zero, + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.clear_all), + onPressed: state.teamCharacters.isEmpty + ? null + : () => context.read().add(const CustomBuildEvent.deleteTeamCharacters()), + ), ), - ), - ], - ), + ], + ), if (state.teamCharacters.isEmpty) NothingFound(msg: s.startByAddingCharacters) else - ...state.teamCharacters.map((e) => TeamCharacterRow(character: e, teamCount: state.teamCharacters.length, color: color)).toList(), + ...state.teamCharacters + .map( + (e) => TeamCharacterRow( + character: e, + teamCount: state.teamCharacters.length, + color: color, + readyToShare: state.readyForScreenshot, + ), + ) + .toList(), ], ); }, diff --git a/lib/presentation/custom_build/widgets/weapon_row.dart b/lib/presentation/custom_build/widgets/weapon_row.dart index b1812ab15..fa0679cac 100644 --- a/lib/presentation/custom_build/widgets/weapon_row.dart +++ b/lib/presentation/custom_build/widgets/weapon_row.dart @@ -22,6 +22,7 @@ class WeaponRow extends StatelessWidget { final Color color; final double maxImageWidth; final int weaponCount; + final bool readyForScreenshot; const WeaponRow({ Key? key, @@ -29,6 +30,7 @@ class WeaponRow extends StatelessWidget { required this.color, required this.maxImageWidth, required this.weaponCount, + required this.readyForScreenshot, }) : super(key: key); @override @@ -107,51 +109,52 @@ class WeaponRow extends StatelessWidget { ), ), ), - ItemPopupMenuFilter<_Options>.withoutSelectedValue( - values: _Options.values, - tooltipText: s.options, - icon: const Icon(Icons.more_vert), - onSelected: (type) => _handleOptionSelected(context, type), - isItemEnabled: (type) { - if (type == _Options.refinements && !canWeaponBeRefined(weapon.rarity)) { - return false; - } - return true; - }, - childBuilder: (e) { - Widget icon; - switch (e.enumValue) { - case _Options.delete: - icon = const Icon(Icons.delete); - break; - case _Options.refinements: - icon = const Icon(Icons.notes); - break; - default: - throw Exception('The provided weapon option type = ${e.enumValue} is not valid'); - } + if (!readyForScreenshot) + ItemPopupMenuFilter<_Options>.withoutSelectedValue( + values: _Options.values, + tooltipText: s.options, + icon: const Icon(Icons.more_vert), + onSelected: (type) => _handleOptionSelected(context, type), + isItemEnabled: (type) { + if (type == _Options.refinements && !canWeaponBeRefined(weapon.rarity)) { + return false; + } + return true; + }, + childBuilder: (e) { + Widget icon; + switch (e.enumValue) { + case _Options.delete: + icon = const Icon(Icons.delete); + break; + case _Options.refinements: + icon = const Icon(Icons.notes); + break; + default: + throw Exception('The provided weapon option type = ${e.enumValue} is not valid'); + } - return Row( - children: [ - icon, - Container( - margin: const EdgeInsets.only(left: 10), - child: Text(e.translation, overflow: TextOverflow.ellipsis), - ), - ], - ); - }, - itemText: (type, _) { - switch (type) { - case _Options.delete: - return s.delete; - case _Options.refinements: - return s.refinements; - default: - throw Exception('The provided weapon option type = $type is not valid'); - } - }, - ), + return Row( + children: [ + icon, + Container( + margin: const EdgeInsets.only(left: 10), + child: Text(e.translation, overflow: TextOverflow.ellipsis), + ), + ], + ); + }, + itemText: (type, _) { + switch (type) { + case _Options.delete: + return s.delete; + case _Options.refinements: + return s.refinements; + default: + throw Exception('The provided weapon option type = $type is not valid'); + } + }, + ), ], ); } diff --git a/lib/presentation/custom_build/widgets/weapon_section.dart b/lib/presentation/custom_build/widgets/weapon_section.dart index 45d7fdddb..6f1953bc0 100644 --- a/lib/presentation/custom_build/widgets/weapon_section.dart +++ b/lib/presentation/custom_build/widgets/weapon_section.dart @@ -15,17 +15,18 @@ import 'package:shiori/presentation/weapons/weapons_page.dart'; class WeaponSection extends StatelessWidget { final double maxItemImageWidth; + final bool useBoxDecoration; const WeaponSection({ Key? key, required this.maxItemImageWidth, + required this.useBoxDecoration, }) : super(key: key); @override Widget build(BuildContext context) { final s = S.of(context); final theme = Theme.of(context); - final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; return BlocBuilder( builder: (context, state) => state.maybeMap( loaded: (state) { @@ -36,63 +37,63 @@ class WeaponSection extends StatelessWidget { children: [ Container( padding: Styles.edgeInsetVertical10, - // margin: const EdgeInsets.only(bottom: 10), decoration: BoxDecoration( color: color, - border: isPortrait ? const Border(top: BorderSide(color: Colors.white)) : null, + border: useBoxDecoration ? const Border(top: BorderSide(color: Colors.white)) : null, ), child: Text( - '${s.weapons} (${state.weapons.length} / ${CustomBuildBloc.maxNumberOfWeapons})', + state.readyForScreenshot ? s.weapons : '${s.weapons} (${state.weapons.length} / ${CustomBuildBloc.maxNumberOfWeapons})', textAlign: TextAlign.center, style: theme.textTheme.subtitle1!.copyWith(fontWeight: FontWeight.bold), ), ), - ButtonBar( - buttonPadding: EdgeInsets.zero, - children: [ - Tooltip( - message: s.add, - child: IconButton( - padding: EdgeInsets.zero, - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.add), - onPressed: () => _openWeaponsPage(context, state.weapons.map((e) => e.key).toList(), state.character.weaponType), + if (!state.readyForScreenshot) + ButtonBar( + buttonPadding: EdgeInsets.zero, + children: [ + Tooltip( + message: s.add, + child: IconButton( + padding: EdgeInsets.zero, + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.add), + onPressed: () => _openWeaponsPage(context, state.weapons.map((e) => e.key).toList(), state.character.weaponType), + ), ), - ), - Tooltip( - message: s.sort, - child: IconButton( - padding: EdgeInsets.zero, - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.sort), - onPressed: state.weapons.length < 2 - ? null - : () => showDialog( - context: context, - builder: (_) => SortItemsDialog( - items: state.weapons.map((e) => SortableItem(e.key, e.name)).toList(), - onSave: (result) { - if (!result.somethingChanged) { - return; - } + Tooltip( + message: s.sort, + child: IconButton( + padding: EdgeInsets.zero, + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.sort), + onPressed: state.weapons.length < 2 + ? null + : () => showDialog( + context: context, + builder: (_) => SortItemsDialog( + items: state.weapons.map((e) => SortableItem(e.key, e.name)).toList(), + onSave: (result) { + if (!result.somethingChanged) { + return; + } - context.read().add(CustomBuildEvent.weaponsOrderChanged(weapons: result.items)); - }, + context.read().add(CustomBuildEvent.weaponsOrderChanged(weapons: result.items)); + }, + ), ), - ), + ), ), - ), - Tooltip( - message: s.clearAll, - child: IconButton( - padding: EdgeInsets.zero, - splashRadius: Styles.smallButtonSplashRadius, - icon: const Icon(Icons.clear_all), - onPressed: state.weapons.isEmpty ? null : () => context.read().add(const CustomBuildEvent.deleteWeapons()), + Tooltip( + message: s.clearAll, + child: IconButton( + padding: EdgeInsets.zero, + splashRadius: Styles.smallButtonSplashRadius, + icon: const Icon(Icons.clear_all), + onPressed: state.weapons.isEmpty ? null : () => context.read().add(const CustomBuildEvent.deleteWeapons()), + ), ), - ), - ], - ), + ], + ), if (state.weapons.isEmpty) NothingFound(msg: s.startByAddingWeapons) else @@ -103,6 +104,7 @@ class WeaponSection extends StatelessWidget { color: color, maxImageWidth: maxItemImageWidth, weaponCount: state.weapons.length, + readyForScreenshot: state.readyForScreenshot, ), ) .toList(), diff --git a/lib/presentation/shared/extensions/i18n_extensions.dart b/lib/presentation/shared/extensions/i18n_extensions.dart index fa27502a4..8fb522622 100644 --- a/lib/presentation/shared/extensions/i18n_extensions.dart +++ b/lib/presentation/shared/extensions/i18n_extensions.dart @@ -603,6 +603,8 @@ extension I18nExtensions on S { return freeze; case CharacterRoleSubType.shield: return shield; + case CharacterRoleSubType.healer: + return healer; } } diff --git a/test/application/custom_build/custom_build_bloc_test.dart b/test/application/custom_build/custom_build_bloc_test.dart index a073d3de2..6d62cd119 100644 --- a/test/application/custom_build/custom_build_bloc_test.dart +++ b/test/application/custom_build/custom_build_bloc_test.dart @@ -6,6 +6,7 @@ import 'package:shiori/domain/enums/enums.dart'; import 'package:shiori/domain/models/models.dart'; import 'package:shiori/domain/services/data_service.dart'; import 'package:shiori/domain/services/genshin_service.dart'; +import 'package:shiori/domain/services/logging_service.dart'; import 'package:shiori/domain/services/telemetry_service.dart'; import 'package:shiori/infrastructure/infrastructure.dart'; @@ -18,6 +19,7 @@ void main() { late GenshinService _genshinService; late DataService _dataService; late TelemetryService _telemetryService; + late LoggingService _loggingService; late CustomBuildsBloc _customBuildsBloc; const _keqingKey = 'keqing'; @@ -32,6 +34,7 @@ void main() { _genshinService = GenshinServiceImpl(localeService); _dataService = DataServiceImpl(_genshinService, CalculatorServiceImpl(_genshinService)); _telemetryService = MockTelemetryService(); + _loggingService = MockLoggingService(); _customBuildsBloc = CustomBuildsBloc(_dataService); return Future(() async { @@ -47,6 +50,8 @@ void main() { }); }); + CustomBuildBloc _getBloc() => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _loggingService, _customBuildsBloc); + Future _saveCustomBuild(String charKey) async { final artifact = _genshinService.getArtifactForCard(_thunderingFuryKey); final weapon = _genshinService.getWeaponForCard(_aquilaFavoniaKey); @@ -171,13 +176,13 @@ void main() { test( 'Initial state', - () => expect(CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc).state, const CustomBuildState.loading()), + () => expect(_getBloc().state, const CustomBuildState.loading()), ); group('Load', () { blocTest( 'create', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc.add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')), verify: (bloc) => bloc.state.maybeMap( loaded: (state) { @@ -206,7 +211,7 @@ void main() { final build = await _saveCustomBuild(_keqingKey); _buildKey = build.key; }, - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc.add(CustomBuildEvent.load(initialTitle: 'XXX', key: _buildKey)), verify: (bloc) => bloc.state.maybeMap( loaded: (state) { @@ -231,7 +236,7 @@ void main() { group('General', () { blocTest( 'character changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.characterChanged(newKey: _ganyuKey)), @@ -245,7 +250,7 @@ void main() { blocTest( 'title changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.titleChanged(newValue: 'KEQING PRO')), @@ -259,7 +264,7 @@ void main() { blocTest( 'role changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.roleChanged(newValue: CharacterRoleType.offFieldDps)), @@ -273,7 +278,7 @@ void main() { blocTest( 'sub role changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.subRoleChanged(newValue: CharacterRoleSubType.cryo)), @@ -287,7 +292,7 @@ void main() { blocTest( 'show on character detail changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.showOnCharacterDetailChanged(newValue: false)), @@ -301,7 +306,7 @@ void main() { blocTest( 'is recommended changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.isRecommendedChanged(newValue: true)), @@ -317,7 +322,7 @@ void main() { group('Notes', () { blocTest( 'add', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addNote(note: 'This build needs 200 ER')) @@ -332,7 +337,7 @@ void main() { blocTest( 'add, note is not valid', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addNote(note: 'This build needs 200 ER')) @@ -342,7 +347,7 @@ void main() { blocTest( 'delete', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addNote(note: 'This build needs 200 ER')) @@ -357,7 +362,7 @@ void main() { blocTest( 'delete, index is not valid', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addNote(note: 'This build needs 200 ER')) @@ -369,7 +374,7 @@ void main() { group('Skill priorities', () { blocTest( 'add', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addSkillPriority(type: CharacterSkillType.elementalBurst)) @@ -384,7 +389,7 @@ void main() { blocTest( 'add, skill already exist', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addSkillPriority(type: CharacterSkillType.elementalBurst)) @@ -400,7 +405,7 @@ void main() { blocTest( 'add, skill is not valid', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addSkillPriority(type: CharacterSkillType.elementalBurst)) @@ -410,7 +415,7 @@ void main() { blocTest( 'delete', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addSkillPriority(type: CharacterSkillType.elementalBurst)) @@ -426,7 +431,7 @@ void main() { blocTest( 'delete, index is not valid', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addSkillPriority(type: CharacterSkillType.elementalBurst)) @@ -439,7 +444,7 @@ void main() { group('Weapons', () { blocTest( 'add', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addWeapon(key: _aquilaFavoniaKey)), @@ -454,7 +459,7 @@ void main() { blocTest( 'add, weapon already exists', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addWeapon(key: _aquilaFavoniaKey)) @@ -464,7 +469,7 @@ void main() { blocTest( 'add, weapon is not valid for current character', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.characterChanged(newKey: _ganyuKey)) @@ -474,7 +479,7 @@ void main() { blocTest( 'refinement changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addWeapon(key: _aquilaFavoniaKey)) @@ -489,7 +494,7 @@ void main() { blocTest( 'refinement changed, weapon does not exist', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.weaponRefinementChanged(key: _aquilaFavoniaKey, newValue: 5)), @@ -498,7 +503,7 @@ void main() { blocTest( 'refinement changed, refinement has not changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addWeapon(key: _aquilaFavoniaKey)) @@ -514,7 +519,7 @@ void main() { blocTest( 'refinement changed, invalid value', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.weaponRefinementChanged(key: _aquilaFavoniaKey, newValue: 6)), @@ -523,7 +528,7 @@ void main() { blocTest( 'delete', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.weaponRefinementChanged(key: _aquilaFavoniaKey, newValue: 5)) @@ -538,7 +543,7 @@ void main() { blocTest( 'delete, weapon does not exist', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addWeapon(key: _aquilaFavoniaKey)) @@ -550,7 +555,7 @@ void main() { blocTest( 'delete all weapons', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addWeapon(key: _aquilaFavoniaKey)) @@ -565,7 +570,7 @@ void main() { blocTest( 'order changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.characterChanged(newKey: _keqingKey)) @@ -593,7 +598,7 @@ void main() { group('Artifacts', () { blocTest( 'add', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addArtifact(key: _thunderingFuryKey, type: ArtifactType.flower, statType: StatType.hp)), @@ -612,7 +617,7 @@ void main() { blocTest( 'add, type already exists', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addArtifact(key: _thunderingFuryKey, type: ArtifactType.crown, statType: StatType.hp)) @@ -632,7 +637,7 @@ void main() { blocTest( 'add sub stats', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addArtifact(key: _thunderingFuryKey, type: ArtifactType.flower, statType: StatType.hp)) @@ -668,7 +673,7 @@ void main() { blocTest( 'add sub stats, artifact does not exist', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add( @@ -679,7 +684,7 @@ void main() { blocTest( 'add sub-stats, sub-stat is not valid', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add( @@ -690,7 +695,7 @@ void main() { blocTest( 'add sub-stats, sub-stat is not valid', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add( @@ -701,7 +706,7 @@ void main() { blocTest( 'delete', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addArtifact(key: _thunderingFuryKey, type: ArtifactType.flower, statType: StatType.hp)) @@ -717,7 +722,7 @@ void main() { blocTest( 'delete, type does not exist', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addArtifact(key: _thunderingFuryKey, type: ArtifactType.flower, statType: StatType.hp)) @@ -727,7 +732,7 @@ void main() { blocTest( 'delete all artifacts', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.addArtifact(key: _thunderingFuryKey, type: ArtifactType.flower, statType: StatType.hp)) @@ -745,7 +750,7 @@ void main() { group('Team characters', () { blocTest( 'add', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add( @@ -764,7 +769,7 @@ void main() { blocTest( 'add, team character is the same as the main one', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.characterChanged(newKey: _ganyuKey)) @@ -776,7 +781,7 @@ void main() { blocTest( 'add the same character multiple times', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add( @@ -795,7 +800,7 @@ void main() { blocTest( 'order changed', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add( @@ -829,7 +834,7 @@ void main() { blocTest( 'delete', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add( @@ -846,7 +851,7 @@ void main() { blocTest( 'delete, team character does not exist', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.deleteTeamCharacter(key: _ganyuKey)), @@ -857,7 +862,7 @@ void main() { group('Save', () { blocTest( 'all stuff was set', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.characterChanged(newKey: _keqingKey)) @@ -903,7 +908,7 @@ void main() { blocTest( 'nothing was set', - build: () => CustomBuildBloc(_genshinService, _dataService, _telemetryService, _customBuildsBloc), + build: () => _getBloc(), act: (bloc) => bloc ..add(const CustomBuildEvent.load(initialTitle: 'DPS PRO')) ..add(const CustomBuildEvent.characterChanged(newKey: _keqingKey))