diff --git a/lib/application/bloc.dart b/lib/application/bloc.dart index 81ed67a1e..bd7a14396 100644 --- a/lib/application/bloc.dart +++ b/lib/application/bloc.dart @@ -10,6 +10,7 @@ export 'main/main_bloc.dart'; export 'main_tab/main_tab_bloc.dart'; export 'materials/materials_bloc.dart'; export 'settings/settings_bloc.dart'; +export 'tierlist/tier_list_bloc.dart'; export 'url_page/url_page_bloc.dart'; export 'weapon/weapon_bloc.dart'; export 'weapons/weapons_bloc.dart'; diff --git a/lib/application/tierlist/tier_list_bloc.dart b/lib/application/tierlist/tier_list_bloc.dart new file mode 100644 index 000000000..a1a70b962 --- /dev/null +++ b/lib/application/tierlist/tier_list_bloc.dart @@ -0,0 +1,153 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:genshindb/domain/models/models.dart'; +import 'package:genshindb/domain/services/genshin_service.dart'; +import 'package:genshindb/domain/services/telemetry_service.dart'; +import 'package:meta/meta.dart'; + +part 'tier_list_bloc.freezed.dart'; +part 'tier_list_event.dart'; +part 'tier_list_state.dart'; + +class TierListBloc extends Bloc { + final GenshinService _genshinService; + final TelemetryService _telemetryService; + final List defaultColors = [ + 0xfff44336, + 0xffff9800, + 0xffffc107, + 0xffffeb3b, + 0xff8bc34a, + ]; + + _LoadedState get currentState => state as _LoadedState; + + TierListBloc(this._genshinService, this._telemetryService) : super(const TierListState.loading()); + + @override + Stream mapEventToState(TierListEvent event) async* { + if (event is _Init) { + yield const TierListState.loading(); + } + + final s = await event.map( + init: (_) async => _init(), + rowTextChanged: (e) async => _rowTextChanged(e.index, e.newValue), + rowPositionChanged: (e) async => _rowPositionChanged(e.index, e.newIndex), + rowColorChanged: (e) async => _rowColorChanged(e.index, e.newColor), + addNewRow: (e) async => _addNewRow(e.index, e.above), + deleteRow: (e) async => _deleteRow(e.index), + clearRow: (e) async => _clearRow(e.index), + clearAllRows: (_) async => _clearAllRows(), + addCharacterToRow: (e) async => _addCharacterToRow(e.index, e.charImg), + deleteCharacterFromRow: (e) async => _deleteCharacterFromRow(e.index, e.charImg), + readyToSave: (e) async => currentState.copyWith.call(readyToSave: e.ready), + ); + + yield s; + } + + Future _init() async { + await _telemetryService.trackTierListOpened(); + final defaultTierList = _genshinService.getDefaultCharacterTierList(defaultColors); + return TierListState.loaded(rows: defaultTierList, charsAvailable: [], readyToSave: false); + } + + TierListState _rowTextChanged(int index, String newValue) { + final updated = currentState.rows.elementAt(index).copyWith.call(tierText: newValue); + final rows = _updateRows(updated, index, index); + return currentState.copyWith.call(rows: rows); + } + + TierListState _rowPositionChanged(int index, int newIndex) { + final updated = currentState.rows.elementAt(index); + final rows = _updateRows(updated, newIndex, index); + return currentState.copyWith.call(rows: rows); + } + + TierListState _rowColorChanged(int index, int newColor) { + final updated = currentState.rows.elementAt(index).copyWith.call(tierColor: newColor); + final rows = _updateRows(updated, index, index); + return currentState.copyWith.call(rows: rows); + } + + TierListState _addNewRow(int index, bool above) { + final colorsCopy = [...defaultColors]; + final color = (colorsCopy..shuffle()).first; + final newIndex = above ? index : index + 1; + + final newRow = TierListRowModel.row(tierText: (currentState.rows.length + 1).toString(), tierColor: color, charImgs: []); + final rows = [...currentState.rows]; + rows.insert(newIndex, newRow); + return currentState.copyWith.call(rows: rows); + } + + TierListState _deleteRow(int index) { + if (currentState.rows.length == 1) { + return currentState; + } + final rows = [...currentState.rows]; + final row = rows.elementAt(index); + final chars = _updateAvailableChars([...currentState.charsAvailable, ...row.charImgs], []); + rows.removeAt(index); + return currentState.copyWith.call(rows: rows, charsAvailable: chars); + } + + TierListState _clearRow(int index) { + final row = currentState.rows.elementAt(index); + final updated = row.copyWith.call(charImgs: []); + final rows = _updateRows(updated, index, index); + final chars = _updateAvailableChars([...currentState.charsAvailable, ...row.charImgs], []); + return currentState.copyWith.call(rows: rows, charsAvailable: chars); + } + + TierListState _clearAllRows() { + final chars = _updateAvailableChars(_genshinService.getDefaultCharacterTierList(defaultColors).expand((row) => row.charImgs).toList(), []); + final updatedRows = currentState.rows.map((row) => row.copyWith.call(charImgs: [])).toList(); + return currentState.copyWith.call(rows: updatedRows, charsAvailable: chars, readyToSave: false); + } + + TierListState _addCharacterToRow(int index, String charImg) { + final row = currentState.rows.elementAt(index); + final updated = row.copyWith.call(charImgs: [...row.charImgs, charImg]); + final updatedChars = _updateAvailableChars(currentState.charsAvailable, [charImg]); + return currentState.copyWith.call(rows: _updateRows(updated, index, index), charsAvailable: updatedChars); + } + + TierListState _deleteCharacterFromRow(int index, String charImg) { + final row = currentState.rows.elementAt(index); + final updated = row.copyWith.call(charImgs: row.charImgs.where((img) => img != charImg).toList()); + final updatedChars = _updateAvailableChars([...currentState.charsAvailable, charImg], []); + return currentState.copyWith.call(rows: _updateRows(updated, index, index), charsAvailable: updatedChars, readyToSave: false); + } + + List _updateRows(TierListRowModel updated, int newIndex, int excludeIndex) { + final rows = []; + + if (newIndex < 0 || newIndex == currentState.rows.length) { + return currentState.rows; + } + + for (int i = 0; i < currentState.rows.length; i++) { + if (i == excludeIndex) { + continue; + } + final row = currentState.rows[i]; + rows.add(row); + } + + rows.insert(newIndex, updated); +// rows.sort((x, y) => x.index - y.index); + return rows; + } + + List _updateAvailableChars(List from, List exclude) { + var chars = from; + if (exclude.isNotEmpty) { + chars = chars.where((img) => !exclude.contains(img)).toList(); + } + return chars..sort((x, y) => x.compareTo(y)); + } +} diff --git a/lib/application/tierlist/tier_list_event.dart b/lib/application/tierlist/tier_list_event.dart new file mode 100644 index 000000000..921fd23d1 --- /dev/null +++ b/lib/application/tierlist/tier_list_event.dart @@ -0,0 +1,48 @@ +part of 'tier_list_bloc.dart'; + +@freezed +abstract class TierListEvent with _$TierListEvent { + const factory TierListEvent.init() = _Init; + + const factory TierListEvent.rowTextChanged({ + @required int index, + @required String newValue, + }) = _RowTextChanged; + + const factory TierListEvent.rowPositionChanged({ + @required int index, + @required int newIndex, + }) = _RowPositionChanged; + + const factory TierListEvent.rowColorChanged({ + @required int index, + @required int newColor, + }) = _RowColorChanged; + + const factory TierListEvent.addNewRow({ + @required int index, + @required bool above, + }) = _AddRow; + + const factory TierListEvent.deleteRow({ + @required int index, + }) = _DeleteRow; + + const factory TierListEvent.clearRow({ + @required int index, + }) = _ClearRow; + + const factory TierListEvent.clearAllRows() = _ClearAllRows; + + const factory TierListEvent.addCharacterToRow({ + @required int index, + @required String charImg, + }) = _AddCharacterToRow; + + const factory TierListEvent.deleteCharacterFromRow({ + @required int index, + @required String charImg, + }) = _DeleteCharacterFromRow; + + const factory TierListEvent.readyToSave({@required bool ready}) = _ReadyToSave; +} diff --git a/lib/application/tierlist/tier_list_state.dart b/lib/application/tierlist/tier_list_state.dart new file mode 100644 index 000000000..aaceb65f7 --- /dev/null +++ b/lib/application/tierlist/tier_list_state.dart @@ -0,0 +1,12 @@ +part of 'tier_list_bloc.dart'; + +@freezed +abstract class TierListState with _$TierListState { + const factory TierListState.loading() = _LoadingState; + + const factory TierListState.loaded({ + @required List rows, + @required List charsAvailable, + @required bool readyToSave, + }) = _LoadedState; +} diff --git a/lib/domain/models/db/characters/character_file_model.dart b/lib/domain/models/db/characters/character_file_model.dart index 34b94b320..44f602950 100644 --- a/lib/domain/models/db/characters/character_file_model.dart +++ b/lib/domain/models/db/characters/character_file_model.dart @@ -29,6 +29,7 @@ abstract class CharacterFileModel implements _$CharacterFileModel { @required bool isComingSoon, @required bool isNew, @required CharacterType role, + @required String tier, String birthday, @required List ascensionMaterials, @required List talentAscensionMaterials, diff --git a/lib/domain/models/models.dart b/lib/domain/models/models.dart index f80579a0f..3c49336a1 100644 --- a/lib/domain/models/models.dart +++ b/lib/domain/models/models.dart @@ -33,5 +33,6 @@ export 'home/today_weapon_ascension_material_model.dart'; export 'items/item_ascension_material_model.dart'; export 'language_model.dart'; export 'settings/app_settings.dart'; +export 'tierlist/tierlist_row_model.dart'; export 'weapons/weapon_card_model.dart'; export 'weapons/weapon_file_refinement_model.dart'; diff --git a/lib/domain/models/tierlist/tierlist_row_model.dart b/lib/domain/models/tierlist/tierlist_row_model.dart new file mode 100644 index 000000000..50f813ff5 --- /dev/null +++ b/lib/domain/models/tierlist/tierlist_row_model.dart @@ -0,0 +1,12 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'tierlist_row_model.freezed.dart'; + +@freezed +abstract class TierListRowModel with _$TierListRowModel { + factory TierListRowModel.row({ + String tierText, + int tierColor, + List charImgs, + }) = _Row; +} diff --git a/lib/domain/services/genshin_service.dart b/lib/domain/services/genshin_service.dart index bfcad8659..3beb7f368 100644 --- a/lib/domain/services/genshin_service.dart +++ b/lib/domain/services/genshin_service.dart @@ -14,6 +14,7 @@ abstract class GenshinService { CharacterFileModel getCharacter(String key); CharacterFileModel getCharacterByImg(String img); List getCharactersForBirthday(DateTime date); + List getDefaultCharacterTierList(List colors); List getWeaponsForCard(); WeaponCardModel getWeaponForCardByImg(String image); diff --git a/lib/domain/services/telemetry_service.dart b/lib/domain/services/telemetry_service.dart index 1c916ba13..32cb407cc 100644 --- a/lib/domain/services/telemetry_service.dart +++ b/lib/domain/services/telemetry_service.dart @@ -23,4 +23,6 @@ abstract class TelemetryService { Future trackUrlOpened(bool loadMap, bool loadWishSimulator, bool networkAvailable); Future trackCalculatorItemAscMaterialLoaded(String item); + + Future trackTierListOpened(); } diff --git a/lib/infrastructure/genshin_service.dart b/lib/infrastructure/genshin_service.dart index 5471aea1f..7e2738e79 100644 --- a/lib/infrastructure/genshin_service.dart +++ b/lib/infrastructure/genshin_service.dart @@ -130,6 +130,40 @@ class GenshinServiceImpl implements GenshinService { }).toList(); } + @override + List getDefaultCharacterTierList(List colors) { + assert(colors.length == 5); + + final sTier = _charactersFile.characters + .where((char) => !char.isComingSoon && char.tier == 's') + .map((char) => Assets.getCharacterPath(char.image)) + .toList(); + final aTier = _charactersFile.characters + .where((char) => !char.isComingSoon && char.tier == 'a') + .map((char) => Assets.getCharacterPath(char.image)) + .toList(); + final bTier = _charactersFile.characters + .where((char) => !char.isComingSoon && char.tier == 'b') + .map((char) => Assets.getCharacterPath(char.image)) + .toList(); + final cTier = _charactersFile.characters + .where((char) => !char.isComingSoon && char.tier == 'c') + .map((char) => Assets.getCharacterPath(char.image)) + .toList(); + final dTier = _charactersFile.characters + .where((char) => !char.isComingSoon && char.tier == 'd') + .map((char) => Assets.getCharacterPath(char.image)) + .toList(); + + return [ + TierListRowModel.row(tierText: 'S', tierColor: colors.first, charImgs: sTier), + TierListRowModel.row(tierText: 'A', tierColor: colors[1], charImgs: aTier), + TierListRowModel.row(tierText: 'B', tierColor: colors[2], charImgs: bTier), + TierListRowModel.row(tierText: 'C', tierColor: colors[3], charImgs: cTier), + TierListRowModel.row(tierText: 'D', tierColor: colors.last, charImgs: dTier), + ]; + } + @override List getWeaponsForCard() { return _weaponsFile.weapons.map( diff --git a/lib/infrastructure/telemetry/telemetry_service.dart b/lib/infrastructure/telemetry/telemetry_service.dart index 8b4b085a2..b66bdd5e1 100644 --- a/lib/infrastructure/telemetry/telemetry_service.dart +++ b/lib/infrastructure/telemetry/telemetry_service.dart @@ -70,4 +70,9 @@ class TelemetryServiceImpl implements TelemetryService { 'Name': item, }); } + + @override + Future trackTierListOpened() async { + await trackEventAsync('TierList-Opened'); + } }