diff --git a/lib/app/app.dart b/lib/app/app.dart index 1f7f02c1..32af83be 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -1,11 +1,15 @@ +import 'dart:async'; + import 'package:aniflow/app/local/ani_flow_localizations_delegate.dart'; import 'package:aniflow/app/navigation/ani_flow_router.dart'; import 'package:aniflow/app/navigation/top_level_navigation.dart'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/data/auth_repository.dart'; import 'package:aniflow/core/data/media_information_repository.dart'; import 'package:aniflow/core/data/media_list_repository.dart'; import 'package:aniflow/core/data/user_data_repository.dart'; import 'package:aniflow/core/design_system/theme/colors.dart'; +import 'package:aniflow/core/design_system/widget/vertical_animated_scale_switcher.dart'; import 'package:aniflow/feature/discover/bloc/discover_bloc.dart'; import 'package:aniflow/feature/media_track/bloc/track_bloc.dart'; import 'package:dynamic_color/dynamic_color.dart'; @@ -103,6 +107,12 @@ class _AnimeTrackerAppScaffoldState extends State { var currentNavigation = TopLevelNavigation.discover; var needHideNavigationBar = false; + var isTopLevelNavigation = true; + late UserDataRepository userDataRepository = UserDataRepositoryImpl(); + late StreamSubscription _mediaTypeSub; + MediaType _mediaType = MediaType.anime; + + bool get isAnime => _mediaType == MediaType.anime; @override void initState() { @@ -112,8 +122,16 @@ class _AnimeTrackerAppScaffoldState extends State { currentNavigation = animeTrackerRouterDelegate.currentTopLevelNavigation; needHideNavigationBar = animeTrackerRouterDelegate.isTopRouteFullScreen; + isTopLevelNavigation = animeTrackerRouterDelegate.isTopLevelNavigation; }); }); + _mediaTypeSub = userDataRepository.getMediaTypeStream().distinct().listen( + (mediaType) { + setState(() { + _mediaType = mediaType; + }); + }, + ); } @override @@ -121,6 +139,7 @@ class _AnimeTrackerAppScaffoldState extends State { super.dispose(); animeTrackerRouterDelegate.dispose(); + _mediaTypeSub.cancel(); } @override @@ -129,43 +148,50 @@ class _AnimeTrackerAppScaffoldState extends State { return MultiBlocProvider( providers: [ BlocProvider( - create: (context) => DiscoverBloc( - userDataRepository: context.read(), - aniListRepository: context.read(), - authRepository: context.read(), - animeTrackListRepository: context.read(), - ), + create: (context) => + DiscoverBloc( + userDataRepository: context.read(), + aniListRepository: context.read(), + authRepository: context.read(), + animeTrackListRepository: context.read(), + ), ), BlocProvider( - create: (context) => TrackBloc( - animeTrackListRepository: context.read(), - authRepository: context.read(), - ), + create: (context) => + TrackBloc( + userDataRepository: context.read(), + mediaListRepository: context.read(), + authRepository: context.read(), + ), ), ], child: Scaffold( body: Router( routerDelegate: animeTrackerRouterDelegate, backButtonDispatcher: RootBackButtonDispatcher()), - bottomNavigationBar: needHideNavigationBar - ? const SizedBox() - : _animeTrackerNavigationBar( - selected: currentNavigation, - onNavigateToDestination: (navigation) async { - animeTrackerRouterDelegate.navigateToTopLevelPage(navigation); - }, - ), + floatingActionButton: isTopLevelNavigation + ? _buildTopFloatingActionButton() + : null, + bottomNavigationBar: VerticalScaleSwitcher( + visible: !needHideNavigationBar, + child: _animeTrackerNavigationBar( + selected: currentNavigation, + onNavigateToDestination: (navigation) async { + animeTrackerRouterDelegate.navigateToTopLevelPage(navigation); + }, + ), + ), ), ); } - Widget? _animeTrackerNavigationBar( - {required TopLevelNavigation selected, - required Function(TopLevelNavigation) onNavigateToDestination}) { + Widget _animeTrackerNavigationBar({required TopLevelNavigation selected, + required Function(TopLevelNavigation) onNavigateToDestination}) { final currentIndex = TopLevelNavigation.values.indexOf(selected); return NavigationBar( destinations: TopLevelNavigation.values - .map((navigation) => navigation.toBottomNavigationBarItem( + .map((navigation) => + navigation.toBottomNavigationBarItem( isSelected: navigation == selected)) .toList(), onDestinationSelected: (index) { @@ -176,4 +202,22 @@ class _AnimeTrackerAppScaffoldState extends State { selectedIndex: currentIndex, ); } + + Widget _buildTopFloatingActionButton() { + return FloatingActionButton.extended( + onPressed: () { + final repository = context.read(); + if (_mediaType == MediaType.manga) { + repository.setMediaType(MediaType.anime); + } else { + repository.setMediaType(MediaType.manga); + } + }, + isExtended: true, + icon: isAnime + ? const Icon(Icons.palette_rounded) + : const Icon(Icons.map), + label: Text(isAnime ? 'Anime' : 'Manga'), + ); + } } diff --git a/lib/app/navigation/ani_flow_router.dart b/lib/app/navigation/ani_flow_router.dart index 80d55b25..15ead877 100644 --- a/lib/app/navigation/ani_flow_router.dart +++ b/lib/app/navigation/ani_flow_router.dart @@ -27,6 +27,8 @@ class AFRouterDelegate extends RouterDelegate bool get isTopRouteFullScreen => _backStack.last.isFullScreen; + bool get isTopLevelNavigation => _backStack.last is TopLevelRoutePath; + static AFRouterDelegate of(context) => Router .of(context) diff --git a/lib/core/common/model/media_type.dart b/lib/core/common/model/media_type.dart index 2bcc8c4a..befbfa2b 100644 --- a/lib/core/common/model/media_type.dart +++ b/lib/core/common/model/media_type.dart @@ -1,5 +1,4 @@ import 'package:aniflow/core/common/model/anime_category.dart'; -import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @JsonEnum() @@ -9,12 +8,12 @@ enum MediaType { @JsonValue('MANGA') manga('MANGA'); - final String sqlTypeString; + final String jsonString; - const MediaType(this.sqlTypeString); + const MediaType(this.jsonString); - static MediaType fromString(String enumString) => - MediaType.values.firstWhere((e) => describeEnum(e) == enumString); + static MediaType fromString(String value) => + MediaType.values.firstWhere((e) => e.jsonString == value); } MediaType getMediaTypeByCategory(MediaCategory category) { diff --git a/lib/core/common/util/stream_util.dart b/lib/core/common/util/stream_util.dart index 92a27951..eee010c6 100644 --- a/lib/core/common/util/stream_util.dart +++ b/lib/core/common/util/stream_util.dart @@ -8,7 +8,10 @@ mixin StreamUtil { late StreamController controller; void listener() async { - controller.add(await getEventData()); + final data = await getEventData(); + if (!controller.isClosed) { + controller.add(data); + } } controller = StreamController(onListen: () { diff --git a/lib/core/data/media_information_repository.dart b/lib/core/data/media_information_repository.dart index c57cf4de..4b62ab1c 100644 --- a/lib/core/data/media_information_repository.dart +++ b/lib/core/data/media_information_repository.dart @@ -19,8 +19,8 @@ import 'package:aniflow/core/database/model/staff_entity.dart'; import 'package:aniflow/core/network/ani_list_data_source.dart'; import 'package:aniflow/core/network/api/airing_schedules_query_graphql.dart.dart'; import 'package:aniflow/core/network/api/media_page_query_graphql.dart'; -import 'package:aniflow/core/network/model/anime_dto.dart'; import 'package:aniflow/core/network/model/character_edge.dart'; +import 'package:aniflow/core/network/model/media_dto.dart'; import 'package:aniflow/core/network/model/media_external_links_dto.dart'; import 'package:aniflow/core/network/model/staff_edge.dart'; import 'package:aniflow/core/network/util/http_status_util.dart'; @@ -151,7 +151,7 @@ class MediaInformationRepositoryImpl extends MediaInformationRepository { Future> startFetchDetailAnimeInfo(String id) async { try { /// fetch anime info from network. - AnimeDto networkResult = await aniListDataSource.getNetworkAnime( + MediaDto networkResult = await aniListDataSource.getNetworkAnime( id: int.parse(id), ); diff --git a/lib/core/data/media_list_repository.dart b/lib/core/data/media_list_repository.dart index e48d86c1..133c84a9 100644 --- a/lib/core/data/media_list_repository.dart +++ b/lib/core/data/media_list_repository.dart @@ -1,3 +1,4 @@ +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/common/util/global_static_constants.dart'; import 'package:aniflow/core/data/load_result.dart'; import 'package:aniflow/core/data/model/anime_list_item_model.dart'; @@ -22,17 +23,25 @@ part 'model/media_list_status.dart'; abstract class MediaListRepository { Future> getUserAnimeList( {required List status, + required MediaType type, int page = 1, int? userId, int perPage = Config.defaultPerPageCount}); Stream> getUserAnimeListStream( - {required List status, required String userId}); + {required List status, + required String userId, + required MediaType type}); - Future> syncUserAnimeList({String? userId}); + Future> syncUserAnimeList( + {String? userId, + List status = const [], + MediaType? mediaType}); - Stream> getMediaListAnimeIdsByUserStream( - String userId, List status); + Stream> getMediaListMediaIdsByUserStream( + {required String userId, + required List status, + required MediaType type}); Stream getIsTrackingByUserAndIdStream( {required String userId, required String animeId}); @@ -58,6 +67,7 @@ class MediaListRepositoryImpl extends MediaListRepository { @override Future> getUserAnimeList( {required List status, + required MediaType type, int page = 1, int? userId, int perPage = Config.defaultPerPageCount}) async { @@ -68,6 +78,7 @@ class MediaListRepositoryImpl extends MediaListRepository { } final animeList = await animeTrackListDao.getMediaListByPage( + type: type, targetUserId.toString(), status, page: 1, perPage: null); @@ -77,7 +88,10 @@ class MediaListRepositoryImpl extends MediaListRepository { } @override - Future> syncUserAnimeList({String? userId}) async { + Future> syncUserAnimeList( + {String? userId, + List status = const [], + MediaType? mediaType}) async { try { final targetUserId = userId ?? (await userDataDao.getUserData())?.id; if (targetUserId == null) { @@ -90,6 +104,8 @@ class MediaListRepositoryImpl extends MediaListRepository { param: UserAnimeListPageQueryParam( page: 1, perPage: null, + mediaType: mediaType, + status: status, userId: int.parse(targetUserId.toString()), ), ); @@ -120,8 +136,10 @@ class MediaListRepositoryImpl extends MediaListRepository { @override Stream> getUserAnimeListStream( - {required List status, required String userId}) { - return animeTrackListDao.getMediaListStream(userId, status).map( + {required List status, + required String userId, + required MediaType type}) { + return animeTrackListDao.getMediaListStream(userId, status, type).map( (models) => models .map((e) => MediaListItemModel.fromDataBaseModel(e)) .toList(), @@ -129,9 +147,12 @@ class MediaListRepositoryImpl extends MediaListRepository { } @override - Stream> getMediaListAnimeIdsByUserStream( - String userId, List status) { - return animeTrackListDao.getMediaListMediaIdsByUserStream(userId, status); + Stream> getMediaListMediaIdsByUserStream( + {required String userId, + required List status, + required MediaType type}) { + return animeTrackListDao.getMediaListMediaIdsByUserStream( + userId, status, type); } @override diff --git a/lib/core/data/model/extension/media_list_item_model_extension.dart b/lib/core/data/model/extension/media_list_item_model_extension.dart index 8d209aad..5696a989 100644 --- a/lib/core/data/model/extension/media_list_item_model_extension.dart +++ b/lib/core/data/model/extension/media_list_item_model_extension.dart @@ -1,8 +1,16 @@ import 'package:aniflow/core/common/model/media_status.dart'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/data/model/anime_list_item_model.dart'; extension MediaListItemModelEx on MediaListItemModel { bool get hasNextReleasingEpisode { + if (animeModel?.type == MediaType.manga) { + /// manga has no nextAiringEpisode information. + /// simply we can regard that manga always have next episode, even if + /// the reading episode large than current episode. + return true; + } + final status = animeModel!.status; switch (status) { case MediaStatus.releasing: @@ -11,12 +19,12 @@ extension MediaListItemModelEx on MediaListItemModel { // ignore: lines_longer_than_80_chars /// sometimes, there is no nextAiringEpisode in server but status is still releasing. /// just return true if have next episode to watch. - return progress! < animeModel!.episodes!; + return progress! < (animeModel!.episodes ?? 1); } else { return progress! < nextAiringEpisode - 1; } case MediaStatus.finished: - return progress! < animeModel!.episodes!; + return progress! < (animeModel!.episodes ?? 1); case MediaStatus.notYetReleased: case null: return false; diff --git a/lib/core/data/model/media_model.dart b/lib/core/data/model/media_model.dart index 38cc5285..7a3226c4 100644 --- a/lib/core/data/model/media_model.dart +++ b/lib/core/data/model/media_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:aniflow/core/common/model/anime_season.dart'; import 'package:aniflow/core/common/model/anime_source.dart'; import 'package:aniflow/core/common/model/media_status.dart'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/data/model/character_and_voice_actor_model.dart'; import 'package:aniflow/core/data/model/media_external_link_model.dart'; import 'package:aniflow/core/data/model/media_title_modle.dart'; @@ -10,7 +11,7 @@ import 'package:aniflow/core/data/model/staff_and_role_model.dart'; import 'package:aniflow/core/data/model/trailter_model.dart'; import 'package:aniflow/core/database/model/media_entity.dart'; import 'package:aniflow/core/database/model/relations/media_with_detail_info.dart'; -import 'package:aniflow/core/network/model/anime_dto.dart'; +import 'package:aniflow/core/network/model/media_dto.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'media_model.freezed.dart'; @@ -20,6 +21,7 @@ class MediaModel with _$MediaModel { factory MediaModel({ @Default('') String id, MediaTitle? title, + @Default(MediaType.anime) MediaType type, @Default('') String coverImage, @Default('') String coverImageColor, String? description, @@ -47,6 +49,9 @@ class MediaModel with _$MediaModel { static MediaModel fromDatabaseModel(MediaEntity model) { return MediaModel( id: model.id, + type: model.type != null + ? MediaType.fromString(model.type!) + : MediaType.anime, title: MediaTitle( english: model.englishTitle, romaji: model.romajiTitle, @@ -103,7 +108,7 @@ class MediaModel with _$MediaModel { ); } - static MediaModel fromDto(AnimeDto dto) { + static MediaModel fromDto(MediaDto dto) { final entity = MediaEntity.fromNetworkModel(dto); return MediaModel.fromDatabaseModel(entity); } diff --git a/lib/core/data/model/media_model.freezed.dart b/lib/core/data/model/media_model.freezed.dart index 0194b488..aa1c98f0 100644 --- a/lib/core/data/model/media_model.freezed.dart +++ b/lib/core/data/model/media_model.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$MediaModel { String get id => throw _privateConstructorUsedError; MediaTitle? get title => throw _privateConstructorUsedError; + MediaType get type => throw _privateConstructorUsedError; String get coverImage => throw _privateConstructorUsedError; String get coverImageColor => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError; @@ -57,6 +58,7 @@ abstract class $MediaModelCopyWith<$Res> { $Res call( {String id, MediaTitle? title, + MediaType type, String coverImage, String coverImageColor, String? description, @@ -99,6 +101,7 @@ class _$MediaModelCopyWithImpl<$Res, $Val extends MediaModel> $Res call({ Object? id = null, Object? title = freezed, + Object? type = null, Object? coverImage = null, Object? coverImageColor = null, Object? description = freezed, @@ -131,6 +134,10 @@ class _$MediaModelCopyWithImpl<$Res, $Val extends MediaModel> ? _value.title : title // ignore: cast_nullable_to_non_nullable as MediaTitle?, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as MediaType, coverImage: null == coverImage ? _value.coverImage : coverImage // ignore: cast_nullable_to_non_nullable @@ -258,6 +265,7 @@ abstract class _$$_MediaModelCopyWith<$Res> $Res call( {String id, MediaTitle? title, + MediaType type, String coverImage, String coverImageColor, String? description, @@ -300,6 +308,7 @@ class __$$_MediaModelCopyWithImpl<$Res> $Res call({ Object? id = null, Object? title = freezed, + Object? type = null, Object? coverImage = null, Object? coverImageColor = null, Object? description = freezed, @@ -332,6 +341,10 @@ class __$$_MediaModelCopyWithImpl<$Res> ? _value.title : title // ignore: cast_nullable_to_non_nullable as MediaTitle?, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as MediaType, coverImage: null == coverImage ? _value.coverImage : coverImage // ignore: cast_nullable_to_non_nullable @@ -430,6 +443,7 @@ class _$_MediaModel implements _MediaModel { _$_MediaModel( {this.id = '', this.title, + this.type = MediaType.anime, this.coverImage = '', this.coverImageColor = '', this.description, @@ -466,6 +480,9 @@ class _$_MediaModel implements _MediaModel { final MediaTitle? title; @override @JsonKey() + final MediaType type; + @override + @JsonKey() final String coverImage; @override @JsonKey() @@ -549,7 +566,7 @@ class _$_MediaModel implements _MediaModel { @override String toString() { - return 'MediaModel(id: $id, title: $title, coverImage: $coverImage, coverImageColor: $coverImageColor, description: $description, source: $source, bannerImage: $bannerImage, averageScore: $averageScore, favourites: $favourites, trailerModel: $trailerModel, seasonYear: $seasonYear, season: $season, status: $status, ratedRank: $ratedRank, popularRank: $popularRank, hashtags: $hashtags, genres: $genres, episodes: $episodes, timeUntilAiring: $timeUntilAiring, nextAiringEpisode: $nextAiringEpisode, isFollowing: $isFollowing, characterAndVoiceActors: $characterAndVoiceActors, staffs: $staffs, externalLinks: $externalLinks)'; + return 'MediaModel(id: $id, title: $title, type: $type, coverImage: $coverImage, coverImageColor: $coverImageColor, description: $description, source: $source, bannerImage: $bannerImage, averageScore: $averageScore, favourites: $favourites, trailerModel: $trailerModel, seasonYear: $seasonYear, season: $season, status: $status, ratedRank: $ratedRank, popularRank: $popularRank, hashtags: $hashtags, genres: $genres, episodes: $episodes, timeUntilAiring: $timeUntilAiring, nextAiringEpisode: $nextAiringEpisode, isFollowing: $isFollowing, characterAndVoiceActors: $characterAndVoiceActors, staffs: $staffs, externalLinks: $externalLinks)'; } @override @@ -559,6 +576,7 @@ class _$_MediaModel implements _MediaModel { other is _$_MediaModel && (identical(other.id, id) || other.id == id) && (identical(other.title, title) || other.title == title) && + (identical(other.type, type) || other.type == type) && (identical(other.coverImage, coverImage) || other.coverImage == coverImage) && (identical(other.coverImageColor, coverImageColor) || @@ -604,6 +622,7 @@ class _$_MediaModel implements _MediaModel { runtimeType, id, title, + type, coverImage, coverImageColor, description, @@ -639,6 +658,7 @@ abstract class _MediaModel implements MediaModel { factory _MediaModel( {final String id, final MediaTitle? title, + final MediaType type, final String coverImage, final String coverImageColor, final String? description, @@ -667,6 +687,8 @@ abstract class _MediaModel implements MediaModel { @override MediaTitle? get title; @override + MediaType get type; + @override String get coverImage; @override String get coverImageColor; diff --git a/lib/core/data/search_repository.dart b/lib/core/data/search_repository.dart index 125f20ee..9694cab1 100644 --- a/lib/core/data/search_repository.dart +++ b/lib/core/data/search_repository.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/common/util/load_page_util.dart'; import 'package:aniflow/core/data/load_result.dart'; import 'package:aniflow/core/data/model/media_model.dart'; @@ -7,11 +8,14 @@ import 'package:aniflow/core/database/aniflow_database.dart'; import 'package:aniflow/core/database/dao/media_dao.dart'; import 'package:aniflow/core/database/model/media_entity.dart'; import 'package:aniflow/core/network/ani_list_data_source.dart'; -import 'package:aniflow/core/network/model/anime_dto.dart'; +import 'package:aniflow/core/network/model/media_dto.dart'; abstract class SearchRepository { Future>> loadMediaSearchResultByPage( - {required int page, required int perPage, required String search}); + {required int page, + required int perPage, + required String search, + required MediaType type}); } class SearchRepositoryImpl implements SearchRepository { @@ -20,17 +24,21 @@ class SearchRepositoryImpl implements SearchRepository { @override Future>> loadMediaSearchResultByPage( - {required int page, required int perPage, required String search}) { - return LoadPageUtil.loadPageWithoutDBCache( + {required int page, + required int perPage, + required String search, + required MediaType type}) { + return LoadPageUtil.loadPageWithoutDBCache( page: page, perPage: perPage, onGetNetworkRes: (int page, int perPage) => dataSource.searchAnimePage( page: page, perPage: perPage, + type: type, search: search, ), - mapDtoToModel: (AnimeDto dto) => MediaModel.fromDto(dto), - onInsertEntityToDB: (List dto) async { + mapDtoToModel: (MediaDto dto) => MediaModel.fromDto(dto), + onInsertEntityToDB: (List dto) async { final entities = dto.map((e) => MediaEntity.fromNetworkModel(e)).toList(); await dao.upsertMediaInformation(entities); diff --git a/lib/core/data/user_data_repository.dart b/lib/core/data/user_data_repository.dart index 13d19353..f6402e28 100644 --- a/lib/core/data/user_data_repository.dart +++ b/lib/core/data/user_data_repository.dart @@ -13,6 +13,8 @@ abstract class UserDataRepository { Stream getMediaTypeStream(); + MediaType getMediaType(); + Future setMediaType(MediaType type); } @@ -46,4 +48,7 @@ class UserDataRepositoryImpl implements UserDataRepository { @override Future setMediaType(MediaType type) => preferences.setCurrentMediaType(type); + + @override + MediaType getMediaType() => preferences.getCurrentMediaType(); } diff --git a/lib/core/database/aniflow_database.dart b/lib/core/database/aniflow_database.dart index ac19cb5f..b63590ea 100644 --- a/lib/core/database/aniflow_database.dart +++ b/lib/core/database/aniflow_database.dart @@ -65,6 +65,7 @@ class AniflowDatabase { await _aniflowDB! .execute('create table if not exists ${Tables.mediaTable} (' '${MediaTableColumns.id} text primary key,' + '${MediaTableColumns.type} text,' '${MediaTableColumns.englishTitle} text,' '${MediaTableColumns.romajiTitle} text,' '${MediaTableColumns.nativeTitle} text,' diff --git a/lib/core/database/dao/media_dao.dart b/lib/core/database/dao/media_dao.dart index 20efbfff..93d424ec 100644 --- a/lib/core/database/dao/media_dao.dart +++ b/lib/core/database/dao/media_dao.dart @@ -19,6 +19,7 @@ import 'package:sqflite/sqflite.dart'; /// [Tables.mediaTable] mixin MediaTableColumns { static const String id = 'id'; + static const String type = 'media_type'; static const String englishTitle = 'english_title'; static const String romajiTitle = 'romaji_title'; static const String nativeTitle = 'native_title'; diff --git a/lib/core/database/dao/media_list_dao.dart b/lib/core/database/dao/media_list_dao.dart index bda56b3e..cb3d8119 100644 --- a/lib/core/database/dao/media_list_dao.dart +++ b/lib/core/database/dao/media_list_dao.dart @@ -2,6 +2,7 @@ import 'dart:async'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/common/util/global_static_constants.dart'; import 'package:aniflow/core/common/util/stream_util.dart'; import 'package:aniflow/core/data/media_list_repository.dart'; @@ -32,16 +33,18 @@ abstract class MediaListDao { Future> getMediaListByPage( String userId, List status, - {required int page, int? perPage = Config.defaultPerPageCount}); + {required MediaType type, + required int page, + int? perPage = Config.defaultPerPageCount}); Future> getMediaListMediaIdsByUser( - String userId, List status); + String userId, List status, MediaType type); Stream> getMediaListMediaIdsByUserStream( - String userId, List status); + String userId, List status, MediaType type); Stream> getMediaListStream( - String userId, List status); + String userId, List status, MediaType type); Future getIsTrackingByUserAndId( {required String userId, required String mediaId}); @@ -72,7 +75,7 @@ class MediaListDaoImpl extends MediaListDao { @override Future> getMediaListByPage( String userId, List status, - {required int page, int? perPage}) async { + {required MediaType type, required int page, int? perPage}) async { final int? limit = perPage; final int offset = (page - 1) * (perPage ?? 0); String statusParam = ''; @@ -86,7 +89,9 @@ class MediaListDaoImpl extends MediaListDao { String sql = 'select * from ${Tables.mediaListTable} as ua ' 'left join ${Tables.mediaTable} as a ' 'on ua.${MediaListTableColumns.mediaId}=a.${MediaTableColumns.id} ' - 'where ${MediaListTableColumns.status} in ($statusParam) and ${MediaListTableColumns.userId}=\'$userId\' ' + 'where ${MediaListTableColumns.status} in ($statusParam) ' + ' and ${MediaListTableColumns.userId}=\'$userId\' ' + ' and a.${MediaTableColumns.type}=\'${type.jsonString}\' ' 'order by ${MediaListTableColumns.updatedAt} desc '; if (limit != null) { sql += 'limit $limit ' @@ -105,7 +110,7 @@ class MediaListDaoImpl extends MediaListDao { @override Future> getMediaListMediaIdsByUser( - String userId, List status) async { + String userId, List status, MediaType type) async { String statusParam = ''; for (var e in status) { statusParam += '\'${e.sqlTypeString}\''; @@ -115,9 +120,12 @@ class MediaListDaoImpl extends MediaListDao { } String sql = - 'select ${MediaListTableColumns.mediaId} from ${Tables.mediaListTable} ' - 'where ${MediaListTableColumns.status} in ($statusParam) ' - ' and ${MediaListTableColumns.userId}=\'$userId\' '; + 'select ${MediaListTableColumns.mediaId} from ${Tables.mediaListTable} as ml ' + 'join ${Tables.mediaTable} as m ' + ' on ml.${MediaListTableColumns.mediaId} = m.${MediaTableColumns.id} ' + 'where ml.${MediaListTableColumns.status} in ($statusParam) ' + ' and ml.${MediaListTableColumns.userId}=\'$userId\' ' + ' and m.${MediaTableColumns.type}=\'${type.jsonString}\' '; final List> result = await database.aniflowDB.rawQuery(sql); @@ -170,15 +178,14 @@ class MediaListDaoImpl extends MediaListDao { @override Stream> getMediaListMediaIdsByUserStream( - String userId, List status) { + String userId, List status, MediaType type) { final changeSource = _notifiers.putIfAbsent(userId, () => ValueNotifier(0)); return StreamUtil.createStream( - changeSource, () => getMediaListMediaIdsByUser(userId, status)); + changeSource, () => getMediaListMediaIdsByUser(userId, status, type)); } @override - Future insertMediaListEntities( - List entities) async { + Future insertMediaListEntities(List entities) async { final batch = database.aniflowDB.batch(); for (final entity in entities) { batch.insert( @@ -200,10 +207,10 @@ class MediaListDaoImpl extends MediaListDao { @override Stream> getMediaListStream( - String userId, List status) { + String userId, List status, MediaType type) { final changeSource = _notifiers.putIfAbsent(userId, () => ValueNotifier(0)); return StreamUtil.createStream(changeSource, - () => getMediaListByPage(userId, status, page: 1, perPage: null)); + () => getMediaListByPage(userId, status, type: type ,page: 1, perPage: null)); } @override diff --git a/lib/core/database/model/media_entity.dart b/lib/core/database/model/media_entity.dart index 9edf7f27..c8f1a3e3 100644 --- a/lib/core/database/model/media_entity.dart +++ b/lib/core/database/model/media_entity.dart @@ -4,7 +4,7 @@ import 'package:aniflow/core/common/model/anime_season.dart'; import 'package:aniflow/core/common/model/anime_source.dart'; import 'package:aniflow/core/common/model/media_status.dart'; import 'package:aniflow/core/database/dao/media_dao.dart'; -import 'package:aniflow/core/network/model/anime_dto.dart'; +import 'package:aniflow/core/network/model/media_dto.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'media_entity.freezed.dart'; @@ -15,6 +15,8 @@ part 'media_entity.g.dart'; class MediaEntity with _$MediaEntity { factory MediaEntity({ @Default('') @JsonKey(name: MediaTableColumns.id) String id, + @JsonKey(name: MediaTableColumns.type) + String? type, @Default('') @JsonKey(name: MediaTableColumns.englishTitle) String englishTitle, @@ -52,9 +54,10 @@ class MediaEntity with _$MediaEntity { factory MediaEntity.fromJson(Map json) => _$$_MediaEntityFromJson(json); - static MediaEntity fromNetworkModel(AnimeDto model) => + static MediaEntity fromNetworkModel(MediaDto model) => MediaEntity( id: model.id.toString(), + type: model.type, englishTitle: model.title?.english ?? '', romajiTitle: model.title?.romaji ?? '', nativeTitle: model.title?.native ?? '', diff --git a/lib/core/database/model/media_entity.freezed.dart b/lib/core/database/model/media_entity.freezed.dart index bd8d8212..711bd952 100644 --- a/lib/core/database/model/media_entity.freezed.dart +++ b/lib/core/database/model/media_entity.freezed.dart @@ -22,6 +22,8 @@ MediaEntity _$MediaEntityFromJson(Map json) { mixin _$MediaEntity { @JsonKey(name: MediaTableColumns.id) String get id => throw _privateConstructorUsedError; + @JsonKey(name: MediaTableColumns.type) + String? get type => throw _privateConstructorUsedError; @JsonKey(name: MediaTableColumns.englishTitle) String get englishTitle => throw _privateConstructorUsedError; @JsonKey(name: MediaTableColumns.romajiTitle) @@ -85,6 +87,7 @@ abstract class $MediaEntityCopyWith<$Res> { @useResult $Res call( {@JsonKey(name: MediaTableColumns.id) String id, + @JsonKey(name: MediaTableColumns.type) String? type, @JsonKey(name: MediaTableColumns.englishTitle) String englishTitle, @JsonKey(name: MediaTableColumns.romajiTitle) String romajiTitle, @JsonKey(name: MediaTableColumns.nativeTitle) String nativeTitle, @@ -127,6 +130,7 @@ class _$MediaEntityCopyWithImpl<$Res, $Val extends MediaEntity> @override $Res call({ Object? id = null, + Object? type = freezed, Object? englishTitle = null, Object? romajiTitle = null, Object? nativeTitle = null, @@ -157,6 +161,10 @@ class _$MediaEntityCopyWithImpl<$Res, $Val extends MediaEntity> ? _value.id : id // ignore: cast_nullable_to_non_nullable as String, + type: freezed == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String?, englishTitle: null == englishTitle ? _value.englishTitle : englishTitle // ignore: cast_nullable_to_non_nullable @@ -267,6 +275,7 @@ abstract class _$$_MediaEntityCopyWith<$Res> @useResult $Res call( {@JsonKey(name: MediaTableColumns.id) String id, + @JsonKey(name: MediaTableColumns.type) String? type, @JsonKey(name: MediaTableColumns.englishTitle) String englishTitle, @JsonKey(name: MediaTableColumns.romajiTitle) String romajiTitle, @JsonKey(name: MediaTableColumns.nativeTitle) String nativeTitle, @@ -307,6 +316,7 @@ class __$$_MediaEntityCopyWithImpl<$Res> @override $Res call({ Object? id = null, + Object? type = freezed, Object? englishTitle = null, Object? romajiTitle = null, Object? nativeTitle = null, @@ -337,6 +347,10 @@ class __$$_MediaEntityCopyWithImpl<$Res> ? _value.id : id // ignore: cast_nullable_to_non_nullable as String, + type: freezed == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String?, englishTitle: null == englishTitle ? _value.englishTitle : englishTitle // ignore: cast_nullable_to_non_nullable @@ -442,6 +456,7 @@ class __$$_MediaEntityCopyWithImpl<$Res> class _$_MediaEntity implements _MediaEntity { _$_MediaEntity( {@JsonKey(name: MediaTableColumns.id) this.id = '', + @JsonKey(name: MediaTableColumns.type) this.type, @JsonKey(name: MediaTableColumns.englishTitle) this.englishTitle = '', @JsonKey(name: MediaTableColumns.romajiTitle) this.romajiTitle = '', @JsonKey(name: MediaTableColumns.nativeTitle) this.nativeTitle = '', @@ -476,6 +491,9 @@ class _$_MediaEntity implements _MediaEntity { @JsonKey(name: MediaTableColumns.id) final String id; @override + @JsonKey(name: MediaTableColumns.type) + final String? type; + @override @JsonKey(name: MediaTableColumns.englishTitle) final String englishTitle; @override @@ -550,7 +568,7 @@ class _$_MediaEntity implements _MediaEntity { @override String toString() { - return 'MediaEntity(id: $id, englishTitle: $englishTitle, romajiTitle: $romajiTitle, nativeTitle: $nativeTitle, coverImage: $coverImage, coverImageColor: $coverImageColor, hashtag: $hashtag, description: $description, source: $source, bannerImage: $bannerImage, averageScore: $averageScore, trending: $trending, favourites: $favourites, trailerId: $trailerId, trailerSite: $trailerSite, episodes: $episodes, seasonYear: $seasonYear, season: $season, status: $status, genres: $genres, trailerThumbnail: $trailerThumbnail, popularRanking: $popularRanking, ratedRanking: $ratedRanking, timeUntilAiring: $timeUntilAiring, nextAiringEpisode: $nextAiringEpisode)'; + return 'MediaEntity(id: $id, type: $type, englishTitle: $englishTitle, romajiTitle: $romajiTitle, nativeTitle: $nativeTitle, coverImage: $coverImage, coverImageColor: $coverImageColor, hashtag: $hashtag, description: $description, source: $source, bannerImage: $bannerImage, averageScore: $averageScore, trending: $trending, favourites: $favourites, trailerId: $trailerId, trailerSite: $trailerSite, episodes: $episodes, seasonYear: $seasonYear, season: $season, status: $status, genres: $genres, trailerThumbnail: $trailerThumbnail, popularRanking: $popularRanking, ratedRanking: $ratedRanking, timeUntilAiring: $timeUntilAiring, nextAiringEpisode: $nextAiringEpisode)'; } @override @@ -559,6 +577,7 @@ class _$_MediaEntity implements _MediaEntity { (other.runtimeType == runtimeType && other is _$_MediaEntity && (identical(other.id, id) || other.id == id) && + (identical(other.type, type) || other.type == type) && (identical(other.englishTitle, englishTitle) || other.englishTitle == englishTitle) && (identical(other.romajiTitle, romajiTitle) || @@ -609,6 +628,7 @@ class _$_MediaEntity implements _MediaEntity { int get hashCode => Object.hashAll([ runtimeType, id, + type, englishTitle, romajiTitle, nativeTitle, @@ -652,6 +672,7 @@ class _$_MediaEntity implements _MediaEntity { abstract class _MediaEntity implements MediaEntity { factory _MediaEntity( {@JsonKey(name: MediaTableColumns.id) final String id, + @JsonKey(name: MediaTableColumns.type) final String? type, @JsonKey(name: MediaTableColumns.englishTitle) final String englishTitle, @JsonKey(name: MediaTableColumns.romajiTitle) final String romajiTitle, @JsonKey(name: MediaTableColumns.nativeTitle) final String nativeTitle, @@ -689,6 +710,9 @@ abstract class _MediaEntity implements MediaEntity { @JsonKey(name: MediaTableColumns.id) String get id; @override + @JsonKey(name: MediaTableColumns.type) + String? get type; + @override @JsonKey(name: MediaTableColumns.englishTitle) String get englishTitle; @override diff --git a/lib/core/database/model/media_entity.g.dart b/lib/core/database/model/media_entity.g.dart index 6ca4b678..a49f99e8 100644 --- a/lib/core/database/model/media_entity.g.dart +++ b/lib/core/database/model/media_entity.g.dart @@ -9,6 +9,7 @@ part of 'media_entity.dart'; _$_MediaEntity _$$_MediaEntityFromJson(Map json) => _$_MediaEntity( id: json['id'] as String? ?? '', + type: json['media_type'] as String?, englishTitle: json['english_title'] as String? ?? '', romajiTitle: json['romaji_title'] as String? ?? '', nativeTitle: json['native_title'] as String? ?? '', @@ -38,6 +39,7 @@ _$_MediaEntity _$$_MediaEntityFromJson(Map json) => Map _$$_MediaEntityToJson(_$_MediaEntity instance) => { 'id': instance.id, + 'media_type': instance.type, 'english_title': instance.englishTitle, 'romaji_title': instance.romajiTitle, 'native_title': instance.nativeTitle, diff --git a/lib/core/design_system/widget/media_list_item.dart b/lib/core/design_system/widget/media_list_item.dart index 3491c15f..0b26b8cc 100644 --- a/lib/core/design_system/widget/media_list_item.dart +++ b/lib/core/design_system/widget/media_list_item.dart @@ -81,13 +81,15 @@ class MediaListItem extends StatelessWidget { Widget _buildWatchingInfoLabel( BuildContext context, MediaListItemModel model) { final hasNextReleasingEpisode = model.hasNextReleasingEpisode; - String label; + String label = ''; if (hasNextReleasingEpisode) { label = 'Next Episode: EP.${model.progress! + 1}'; } else { - label = - // ignore: lines_longer_than_80_chars - 'Next Episode: EP.${model.animeModel!.nextAiringEpisode} in ${model.animeModel!.getReleasingTimeString(context)}'; + if (model.animeModel!.nextAiringEpisode != null) { + label = + // ignore: lines_longer_than_80_chars + 'Next Episode: EP.${model.animeModel!.nextAiringEpisode} in ${model.animeModel!.getReleasingTimeString(context)}'; + } } return Text( label, diff --git a/lib/core/network/ani_list_data_source.dart b/lib/core/network/ani_list_data_source.dart index 49d802db..bbceb212 100644 --- a/lib/core/network/ani_list_data_source.dart +++ b/lib/core/network/ani_list_data_source.dart @@ -1,5 +1,6 @@ // ignore_for_file: avoid_dynamic_calls +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/network/api/airing_schedules_query_graphql.dart.dart'; import 'package:aniflow/core/network/api/media_detail_query_graphql.dart'; import 'package:aniflow/core/network/api/media_list_query_graphql.dart'; @@ -9,8 +10,8 @@ import 'package:aniflow/core/network/api/query_media_character_page_graphql.dart import 'package:aniflow/core/network/api/search_query_graphql.dart'; import 'package:aniflow/core/network/client/ani_list_dio.dart'; import 'package:aniflow/core/network/model/airing_schedule_dto.dart'; -import 'package:aniflow/core/network/model/anime_dto.dart'; import 'package:aniflow/core/network/model/character_edge.dart'; +import 'package:aniflow/core/network/model/media_dto.dart'; import 'package:aniflow/core/network/model/media_list_dto.dart'; import 'package:aniflow/core/network/model/staff_edge.dart'; @@ -22,7 +23,7 @@ class AniListDataSource { AniListDataSource._(); - Future getNetworkAnime({required int id}) async { + Future getNetworkAnime({required int id}) async { final queryGraphQL = mediaDetailQueryGraphQLString; final variablesMap = { 'id': id, @@ -31,12 +32,12 @@ class AniListDataSource { data: {'query': queryGraphQL, 'variables': variablesMap}); final resultJson = response.data['data']['Media']; - final AnimeDto detailAnimeDto = AnimeDto.fromJson(resultJson); + final MediaDto detailAnimeDto = MediaDto.fromJson(resultJson); return detailAnimeDto; } - Future> getNetworkAnimePage({ + Future> getNetworkAnimePage({ required int page, required int perPage, required AnimePageQueryParam param, @@ -51,7 +52,7 @@ class AniListDataSource { final variablesMap = { 'page': page, 'perPage': perPage, - 'type': param.type.sqlTypeString, + 'type': param.type.jsonString, }; if (hasSeasonYear) { @@ -79,8 +80,8 @@ class AniListDataSource { data: {'query': queryGraphQL, 'variables': variablesMap}); final List resultJson = response.data['data']['Page']['media']; - final List animeList = - resultJson.map((e) => AnimeDto.fromJson(e)).toList(); + final List animeList = + resultJson.map((e) => MediaDto.fromJson(e)).toList(); return animeList; } @@ -134,6 +135,7 @@ class AniListDataSource { final queryGraphQL = userAnimeListGraphQLString; final hasStatus = param.status.isNotEmpty; final hasPerPage = param.perPage != null; + final hasMediaType = param.mediaType != null; final variablesMap = { 'page': param.page, 'userId': param.userId, @@ -145,6 +147,9 @@ class AniListDataSource { if (hasPerPage) { variablesMap['perPage'] = param.perPage; } + if (hasMediaType) { + variablesMap['type'] = param.mediaType!.jsonString; + } final response = await AniListDio().dio.post(AniListDio.aniListUrl, data: {'query': queryGraphQL, 'variables': variablesMap}); @@ -173,9 +178,10 @@ class AniListDataSource { return airingSchedules; } - Future> searchAnimePage({ + Future> searchAnimePage({ required int page, required int perPage, + required MediaType type, required String search, }) async { final queryGraphQL = searchQueryGraphql; @@ -183,12 +189,13 @@ class AniListDataSource { 'search': search, 'page': page, 'perPage': perPage, + 'type': type.jsonString, }; final response = await AniListDio().dio.post(AniListDio.aniListUrl, data: {'query': queryGraphQL, 'variables': variablesMap}); final List resultJson = response.data['data']['page']['media']; - final List animeList = - resultJson.map((e) => AnimeDto.fromJson(e)).toList(); + final List animeList = + resultJson.map((e) => MediaDto.fromJson(e)).toList(); return animeList; } diff --git a/lib/core/network/api/media_detail_query_graphql.dart b/lib/core/network/api/media_detail_query_graphql.dart index 06369670..fb9ffb90 100644 --- a/lib/core/network/api/media_detail_query_graphql.dart +++ b/lib/core/network/api/media_detail_query_graphql.dart @@ -8,6 +8,7 @@ query (\$id: Int) { english native } + type description(asHtml: true) episodes seasonYear diff --git a/lib/core/network/api/media_list_query_graphql.dart b/lib/core/network/api/media_list_query_graphql.dart index 7ffb7d28..e709cc59 100644 --- a/lib/core/network/api/media_list_query_graphql.dart +++ b/lib/core/network/api/media_list_query_graphql.dart @@ -1,3 +1,4 @@ +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/data/media_list_repository.dart'; class UserAnimeListPageQueryParam { @@ -5,18 +6,20 @@ class UserAnimeListPageQueryParam { final int? perPage; final int userId; final List status; + final MediaType? mediaType; UserAnimeListPageQueryParam( {required this.page, - required this.userId, this.perPage, + required this.userId, + this.perPage, + this.mediaType, this.status = const []}); } -String get userAnimeListGraphQLString => -''' -query(\$page: Int, \$perPage: Int, \$userId: Int, \$status_in: [MediaListStatus]){ +String get userAnimeListGraphQLString => ''' +query(\$page: Int, \$perPage: Int, \$userId: Int, \$status_in: [MediaListStatus], \$type: MediaType){ Page(page: \$page, perPage: \$perPage) { - mediaList(userId: \$userId, type: ANIME, status_in: \$status_in) { + mediaList(userId: \$userId, type: \$type, status_in: \$status_in) { id status score @@ -32,6 +35,7 @@ query(\$page: Int, \$perPage: Int, \$userId: Int, \$status_in: [MediaListStatus] english native } + type description(asHtml: true) episodes seasonYear diff --git a/lib/core/network/api/search_query_graphql.dart b/lib/core/network/api/search_query_graphql.dart index a22d0c98..d8a8f3fe 100644 --- a/lib/core/network/api/search_query_graphql.dart +++ b/lib/core/network/api/search_query_graphql.dart @@ -1,9 +1,9 @@ String get searchQueryGraphql => ''' -query (\$page: Int, \$perPage: Int, \$search: String) { +query (\$page: Int, \$perPage: Int, \$search: String, \$type: MediaType) { page: Page(page: \$page, perPage: \$perPage) { - media(type: ANIME, sort: [POPULARITY_DESC], search: \$search) { + media(type: \$type, sort: [POPULARITY_DESC], search: \$search) { id title { romaji diff --git a/lib/core/network/model/airing_schedule_dto.dart b/lib/core/network/model/airing_schedule_dto.dart index 71559969..ed0b0a43 100644 --- a/lib/core/network/model/airing_schedule_dto.dart +++ b/lib/core/network/model/airing_schedule_dto.dart @@ -1,4 +1,4 @@ -import 'package:aniflow/core/network/model/anime_dto.dart'; +import 'package:aniflow/core/network/model/media_dto.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'airing_schedule_dto.freezed.dart'; @@ -12,7 +12,7 @@ class AiringScheduleDto with _$AiringScheduleDto { @JsonKey(name: 'airingAt') int? airingAt, @JsonKey(name: 'timeUntilAiring') int? timeUntilAiring, @JsonKey(name: 'episode') int? episode, - @JsonKey(name: 'media') AnimeDto? media, + @JsonKey(name: 'media') MediaDto? media, }) = _AiringScheduleDto; factory AiringScheduleDto.fromJson(Map json) => diff --git a/lib/core/network/model/airing_schedule_dto.freezed.dart b/lib/core/network/model/airing_schedule_dto.freezed.dart index ba346002..decd9522 100644 --- a/lib/core/network/model/airing_schedule_dto.freezed.dart +++ b/lib/core/network/model/airing_schedule_dto.freezed.dart @@ -31,7 +31,7 @@ mixin _$AiringScheduleDto { @JsonKey(name: 'episode') int? get episode => throw _privateConstructorUsedError; @JsonKey(name: 'media') - AnimeDto? get media => throw _privateConstructorUsedError; + MediaDto? get media => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -51,9 +51,9 @@ abstract class $AiringScheduleDtoCopyWith<$Res> { @JsonKey(name: 'airingAt') int? airingAt, @JsonKey(name: 'timeUntilAiring') int? timeUntilAiring, @JsonKey(name: 'episode') int? episode, - @JsonKey(name: 'media') AnimeDto? media}); + @JsonKey(name: 'media') MediaDto? media}); - $AnimeDtoCopyWith<$Res>? get media; + $MediaDtoCopyWith<$Res>? get media; } /// @nodoc @@ -100,18 +100,18 @@ class _$AiringScheduleDtoCopyWithImpl<$Res, $Val extends AiringScheduleDto> media: freezed == media ? _value.media : media // ignore: cast_nullable_to_non_nullable - as AnimeDto?, + as MediaDto?, ) as $Val); } @override @pragma('vm:prefer-inline') - $AnimeDtoCopyWith<$Res>? get media { + $MediaDtoCopyWith<$Res>? get media { if (_value.media == null) { return null; } - return $AnimeDtoCopyWith<$Res>(_value.media!, (value) { + return $MediaDtoCopyWith<$Res>(_value.media!, (value) { return _then(_value.copyWith(media: value) as $Val); }); } @@ -131,10 +131,10 @@ abstract class _$$_AiringScheduleDtoCopyWith<$Res> @JsonKey(name: 'airingAt') int? airingAt, @JsonKey(name: 'timeUntilAiring') int? timeUntilAiring, @JsonKey(name: 'episode') int? episode, - @JsonKey(name: 'media') AnimeDto? media}); + @JsonKey(name: 'media') MediaDto? media}); @override - $AnimeDtoCopyWith<$Res>? get media; + $MediaDtoCopyWith<$Res>? get media; } /// @nodoc @@ -179,7 +179,7 @@ class __$$_AiringScheduleDtoCopyWithImpl<$Res> media: freezed == media ? _value.media : media // ignore: cast_nullable_to_non_nullable - as AnimeDto?, + as MediaDto?, )); } } @@ -215,7 +215,7 @@ class _$_AiringScheduleDto implements _AiringScheduleDto { final int? episode; @override @JsonKey(name: 'media') - final AnimeDto? media; + final MediaDto? media; @override String toString() { @@ -264,7 +264,7 @@ abstract class _AiringScheduleDto implements AiringScheduleDto { @JsonKey(name: 'airingAt') final int? airingAt, @JsonKey(name: 'timeUntilAiring') final int? timeUntilAiring, @JsonKey(name: 'episode') final int? episode, - @JsonKey(name: 'media') final AnimeDto? media}) = _$_AiringScheduleDto; + @JsonKey(name: 'media') final MediaDto? media}) = _$_AiringScheduleDto; factory _AiringScheduleDto.fromJson(Map json) = _$_AiringScheduleDto.fromJson; @@ -286,7 +286,7 @@ abstract class _AiringScheduleDto implements AiringScheduleDto { int? get episode; @override @JsonKey(name: 'media') - AnimeDto? get media; + MediaDto? get media; @override @JsonKey(ignore: true) _$$_AiringScheduleDtoCopyWith<_$_AiringScheduleDto> get copyWith => diff --git a/lib/core/network/model/airing_schedule_dto.g.dart b/lib/core/network/model/airing_schedule_dto.g.dart index 1bd1144b..0b764e2e 100644 --- a/lib/core/network/model/airing_schedule_dto.g.dart +++ b/lib/core/network/model/airing_schedule_dto.g.dart @@ -15,7 +15,7 @@ _$_AiringScheduleDto _$$_AiringScheduleDtoFromJson(Map json) => episode: json['episode'] as int?, media: json['media'] == null ? null - : AnimeDto.fromJson(json['media'] as Map), + : MediaDto.fromJson(json['media'] as Map), ); Map _$$_AiringScheduleDtoToJson( diff --git a/lib/core/network/model/anime_dto.freezed.dart b/lib/core/network/model/anime_dto.freezed.dart index c05fff64..237992c9 100644 --- a/lib/core/network/model/anime_dto.freezed.dart +++ b/lib/core/network/model/anime_dto.freezed.dart @@ -3,7 +3,7 @@ // ignore_for_file: type=lint // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark -part of 'anime_dto.dart'; +part of 'media_dto.dart'; // ************************************************************************** // FreezedGenerator diff --git a/lib/core/network/model/anime_dto.g.dart b/lib/core/network/model/anime_dto.g.dart index ec9e59e8..8a1eac64 100644 --- a/lib/core/network/model/anime_dto.g.dart +++ b/lib/core/network/model/anime_dto.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'anime_dto.dart'; +part of 'media_dto.dart'; // ************************************************************************** // JsonSerializableGenerator diff --git a/lib/core/network/model/anime_dto.dart b/lib/core/network/model/media_dto.dart similarity index 88% rename from lib/core/network/model/anime_dto.dart rename to lib/core/network/model/media_dto.dart index 53322c0c..29def9ae 100644 --- a/lib/core/network/model/anime_dto.dart +++ b/lib/core/network/model/media_dto.dart @@ -10,14 +10,15 @@ import 'package:aniflow/core/network/model/staff_connection.dart'; import 'package:aniflow/core/network/model/trailer_dto.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -part 'anime_dto.freezed.dart'; -part 'anime_dto.g.dart'; +part 'media_dto.freezed.dart'; +part 'media_dto.g.dart'; @freezed -class AnimeDto with _$AnimeDto { - factory AnimeDto({ +class MediaDto with _$MediaDto { + factory MediaDto({ @Default(-1) @JsonKey(name: 'id') int id, @JsonKey(name: 'title') MediaTitle? title, + @JsonKey(name: 'type') String? type, @Default({}) @JsonKey(name: 'coverImage') Map coverImage, @JsonKey(name: 'description') String? description, @JsonKey(name: 'status') MediaStatus? status, @@ -39,8 +40,8 @@ class AnimeDto with _$AnimeDto { @Default([]) @JsonKey(name: 'externalLinks') List externalLinks, - }) = _AnimeDto; + }) = _MediaDto; - factory AnimeDto.fromJson(Map json) => - _$$_AnimeDtoFromJson(json); + factory MediaDto.fromJson(Map json) => + _$$_MediaDtoFromJson(json); } diff --git a/lib/core/network/model/media_dto.freezed.dart b/lib/core/network/model/media_dto.freezed.dart new file mode 100644 index 00000000..41dffc3b --- /dev/null +++ b/lib/core/network/model/media_dto.freezed.dart @@ -0,0 +1,785 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'media_dto.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +MediaDto _$MediaDtoFromJson(Map json) { + return _MediaDto.fromJson(json); +} + +/// @nodoc +mixin _$MediaDto { + @JsonKey(name: 'id') + int get id => throw _privateConstructorUsedError; + @JsonKey(name: 'title') + MediaTitle? get title => throw _privateConstructorUsedError; + @JsonKey(name: 'type') + String? get type => throw _privateConstructorUsedError; + @JsonKey(name: 'coverImage') + Map get coverImage => throw _privateConstructorUsedError; + @JsonKey(name: 'description') + String? get description => throw _privateConstructorUsedError; + @JsonKey(name: 'status') + MediaStatus? get status => throw _privateConstructorUsedError; + @JsonKey(name: 'source') + AnimeSource? get source => throw _privateConstructorUsedError; + @JsonKey(name: 'episodes') + int? get episodes => throw _privateConstructorUsedError; + @JsonKey(name: 'seasonYear') + int? get seasonYear => throw _privateConstructorUsedError; + @JsonKey(name: 'season') + AnimeSeason? get season => throw _privateConstructorUsedError; + @JsonKey(name: 'hashtag') + String? get hashtag => throw _privateConstructorUsedError; + @JsonKey(name: 'bannerImage') + String? get bannerImage => throw _privateConstructorUsedError; + @JsonKey(name: 'averageScore') + int? get averageScore => throw _privateConstructorUsedError; + @JsonKey(name: 'trending') + int? get trending => throw _privateConstructorUsedError; + @JsonKey(name: 'favourites') + int? get favourites => throw _privateConstructorUsedError; + @JsonKey(name: 'genres') + List get genres => throw _privateConstructorUsedError; + @JsonKey(name: 'trailer') + TrailerDto? get trailer => throw _privateConstructorUsedError; + @JsonKey(name: 'nextAiringEpisode') + AiringScheduleDto? get nextAiringEpisode => + throw _privateConstructorUsedError; + @JsonKey(name: 'rankings') + List get rankings => throw _privateConstructorUsedError; + @JsonKey(name: 'characters') + CharacterConnection? get characters => throw _privateConstructorUsedError; + @JsonKey(name: 'staff') + StaffConnection? get staff => throw _privateConstructorUsedError; + @JsonKey(name: 'externalLinks') + List get externalLinks => + throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $MediaDtoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MediaDtoCopyWith<$Res> { + factory $MediaDtoCopyWith(MediaDto value, $Res Function(MediaDto) then) = + _$MediaDtoCopyWithImpl<$Res, MediaDto>; + @useResult + $Res call( + {@JsonKey(name: 'id') int id, + @JsonKey(name: 'title') MediaTitle? title, + @JsonKey(name: 'type') String? type, + @JsonKey(name: 'coverImage') Map coverImage, + @JsonKey(name: 'description') String? description, + @JsonKey(name: 'status') MediaStatus? status, + @JsonKey(name: 'source') AnimeSource? source, + @JsonKey(name: 'episodes') int? episodes, + @JsonKey(name: 'seasonYear') int? seasonYear, + @JsonKey(name: 'season') AnimeSeason? season, + @JsonKey(name: 'hashtag') String? hashtag, + @JsonKey(name: 'bannerImage') String? bannerImage, + @JsonKey(name: 'averageScore') int? averageScore, + @JsonKey(name: 'trending') int? trending, + @JsonKey(name: 'favourites') int? favourites, + @JsonKey(name: 'genres') List genres, + @JsonKey(name: 'trailer') TrailerDto? trailer, + @JsonKey(name: 'nextAiringEpisode') AiringScheduleDto? nextAiringEpisode, + @JsonKey(name: 'rankings') List rankings, + @JsonKey(name: 'characters') CharacterConnection? characters, + @JsonKey(name: 'staff') StaffConnection? staff, + @JsonKey(name: 'externalLinks') + List externalLinks}); + + $MediaTitleCopyWith<$Res>? get title; + $TrailerDtoCopyWith<$Res>? get trailer; + $AiringScheduleDtoCopyWith<$Res>? get nextAiringEpisode; + $CharacterConnectionCopyWith<$Res>? get characters; + $StaffConnectionCopyWith<$Res>? get staff; +} + +/// @nodoc +class _$MediaDtoCopyWithImpl<$Res, $Val extends MediaDto> + implements $MediaDtoCopyWith<$Res> { + _$MediaDtoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = freezed, + Object? type = freezed, + Object? coverImage = null, + Object? description = freezed, + Object? status = freezed, + Object? source = freezed, + Object? episodes = freezed, + Object? seasonYear = freezed, + Object? season = freezed, + Object? hashtag = freezed, + Object? bannerImage = freezed, + Object? averageScore = freezed, + Object? trending = freezed, + Object? favourites = freezed, + Object? genres = null, + Object? trailer = freezed, + Object? nextAiringEpisode = freezed, + Object? rankings = null, + Object? characters = freezed, + Object? staff = freezed, + Object? externalLinks = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as MediaTitle?, + type: freezed == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String?, + coverImage: null == coverImage + ? _value.coverImage + : coverImage // ignore: cast_nullable_to_non_nullable + as Map, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as MediaStatus?, + source: freezed == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as AnimeSource?, + episodes: freezed == episodes + ? _value.episodes + : episodes // ignore: cast_nullable_to_non_nullable + as int?, + seasonYear: freezed == seasonYear + ? _value.seasonYear + : seasonYear // ignore: cast_nullable_to_non_nullable + as int?, + season: freezed == season + ? _value.season + : season // ignore: cast_nullable_to_non_nullable + as AnimeSeason?, + hashtag: freezed == hashtag + ? _value.hashtag + : hashtag // ignore: cast_nullable_to_non_nullable + as String?, + bannerImage: freezed == bannerImage + ? _value.bannerImage + : bannerImage // ignore: cast_nullable_to_non_nullable + as String?, + averageScore: freezed == averageScore + ? _value.averageScore + : averageScore // ignore: cast_nullable_to_non_nullable + as int?, + trending: freezed == trending + ? _value.trending + : trending // ignore: cast_nullable_to_non_nullable + as int?, + favourites: freezed == favourites + ? _value.favourites + : favourites // ignore: cast_nullable_to_non_nullable + as int?, + genres: null == genres + ? _value.genres + : genres // ignore: cast_nullable_to_non_nullable + as List, + trailer: freezed == trailer + ? _value.trailer + : trailer // ignore: cast_nullable_to_non_nullable + as TrailerDto?, + nextAiringEpisode: freezed == nextAiringEpisode + ? _value.nextAiringEpisode + : nextAiringEpisode // ignore: cast_nullable_to_non_nullable + as AiringScheduleDto?, + rankings: null == rankings + ? _value.rankings + : rankings // ignore: cast_nullable_to_non_nullable + as List, + characters: freezed == characters + ? _value.characters + : characters // ignore: cast_nullable_to_non_nullable + as CharacterConnection?, + staff: freezed == staff + ? _value.staff + : staff // ignore: cast_nullable_to_non_nullable + as StaffConnection?, + externalLinks: null == externalLinks + ? _value.externalLinks + : externalLinks // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $MediaTitleCopyWith<$Res>? get title { + if (_value.title == null) { + return null; + } + + return $MediaTitleCopyWith<$Res>(_value.title!, (value) { + return _then(_value.copyWith(title: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $TrailerDtoCopyWith<$Res>? get trailer { + if (_value.trailer == null) { + return null; + } + + return $TrailerDtoCopyWith<$Res>(_value.trailer!, (value) { + return _then(_value.copyWith(trailer: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $AiringScheduleDtoCopyWith<$Res>? get nextAiringEpisode { + if (_value.nextAiringEpisode == null) { + return null; + } + + return $AiringScheduleDtoCopyWith<$Res>(_value.nextAiringEpisode!, (value) { + return _then(_value.copyWith(nextAiringEpisode: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $CharacterConnectionCopyWith<$Res>? get characters { + if (_value.characters == null) { + return null; + } + + return $CharacterConnectionCopyWith<$Res>(_value.characters!, (value) { + return _then(_value.copyWith(characters: value) as $Val); + }); + } + + @override + @pragma('vm:prefer-inline') + $StaffConnectionCopyWith<$Res>? get staff { + if (_value.staff == null) { + return null; + } + + return $StaffConnectionCopyWith<$Res>(_value.staff!, (value) { + return _then(_value.copyWith(staff: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$_MediaDtoCopyWith<$Res> implements $MediaDtoCopyWith<$Res> { + factory _$$_MediaDtoCopyWith( + _$_MediaDto value, $Res Function(_$_MediaDto) then) = + __$$_MediaDtoCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {@JsonKey(name: 'id') int id, + @JsonKey(name: 'title') MediaTitle? title, + @JsonKey(name: 'type') String? type, + @JsonKey(name: 'coverImage') Map coverImage, + @JsonKey(name: 'description') String? description, + @JsonKey(name: 'status') MediaStatus? status, + @JsonKey(name: 'source') AnimeSource? source, + @JsonKey(name: 'episodes') int? episodes, + @JsonKey(name: 'seasonYear') int? seasonYear, + @JsonKey(name: 'season') AnimeSeason? season, + @JsonKey(name: 'hashtag') String? hashtag, + @JsonKey(name: 'bannerImage') String? bannerImage, + @JsonKey(name: 'averageScore') int? averageScore, + @JsonKey(name: 'trending') int? trending, + @JsonKey(name: 'favourites') int? favourites, + @JsonKey(name: 'genres') List genres, + @JsonKey(name: 'trailer') TrailerDto? trailer, + @JsonKey(name: 'nextAiringEpisode') AiringScheduleDto? nextAiringEpisode, + @JsonKey(name: 'rankings') List rankings, + @JsonKey(name: 'characters') CharacterConnection? characters, + @JsonKey(name: 'staff') StaffConnection? staff, + @JsonKey(name: 'externalLinks') + List externalLinks}); + + @override + $MediaTitleCopyWith<$Res>? get title; + @override + $TrailerDtoCopyWith<$Res>? get trailer; + @override + $AiringScheduleDtoCopyWith<$Res>? get nextAiringEpisode; + @override + $CharacterConnectionCopyWith<$Res>? get characters; + @override + $StaffConnectionCopyWith<$Res>? get staff; +} + +/// @nodoc +class __$$_MediaDtoCopyWithImpl<$Res> + extends _$MediaDtoCopyWithImpl<$Res, _$_MediaDto> + implements _$$_MediaDtoCopyWith<$Res> { + __$$_MediaDtoCopyWithImpl( + _$_MediaDto _value, $Res Function(_$_MediaDto) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? title = freezed, + Object? type = freezed, + Object? coverImage = null, + Object? description = freezed, + Object? status = freezed, + Object? source = freezed, + Object? episodes = freezed, + Object? seasonYear = freezed, + Object? season = freezed, + Object? hashtag = freezed, + Object? bannerImage = freezed, + Object? averageScore = freezed, + Object? trending = freezed, + Object? favourites = freezed, + Object? genres = null, + Object? trailer = freezed, + Object? nextAiringEpisode = freezed, + Object? rankings = null, + Object? characters = freezed, + Object? staff = freezed, + Object? externalLinks = null, + }) { + return _then(_$_MediaDto( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + title: freezed == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as MediaTitle?, + type: freezed == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String?, + coverImage: null == coverImage + ? _value._coverImage + : coverImage // ignore: cast_nullable_to_non_nullable + as Map, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as MediaStatus?, + source: freezed == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as AnimeSource?, + episodes: freezed == episodes + ? _value.episodes + : episodes // ignore: cast_nullable_to_non_nullable + as int?, + seasonYear: freezed == seasonYear + ? _value.seasonYear + : seasonYear // ignore: cast_nullable_to_non_nullable + as int?, + season: freezed == season + ? _value.season + : season // ignore: cast_nullable_to_non_nullable + as AnimeSeason?, + hashtag: freezed == hashtag + ? _value.hashtag + : hashtag // ignore: cast_nullable_to_non_nullable + as String?, + bannerImage: freezed == bannerImage + ? _value.bannerImage + : bannerImage // ignore: cast_nullable_to_non_nullable + as String?, + averageScore: freezed == averageScore + ? _value.averageScore + : averageScore // ignore: cast_nullable_to_non_nullable + as int?, + trending: freezed == trending + ? _value.trending + : trending // ignore: cast_nullable_to_non_nullable + as int?, + favourites: freezed == favourites + ? _value.favourites + : favourites // ignore: cast_nullable_to_non_nullable + as int?, + genres: null == genres + ? _value._genres + : genres // ignore: cast_nullable_to_non_nullable + as List, + trailer: freezed == trailer + ? _value.trailer + : trailer // ignore: cast_nullable_to_non_nullable + as TrailerDto?, + nextAiringEpisode: freezed == nextAiringEpisode + ? _value.nextAiringEpisode + : nextAiringEpisode // ignore: cast_nullable_to_non_nullable + as AiringScheduleDto?, + rankings: null == rankings + ? _value._rankings + : rankings // ignore: cast_nullable_to_non_nullable + as List, + characters: freezed == characters + ? _value.characters + : characters // ignore: cast_nullable_to_non_nullable + as CharacterConnection?, + staff: freezed == staff + ? _value.staff + : staff // ignore: cast_nullable_to_non_nullable + as StaffConnection?, + externalLinks: null == externalLinks + ? _value._externalLinks + : externalLinks // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$_MediaDto implements _MediaDto { + _$_MediaDto( + {@JsonKey(name: 'id') this.id = -1, + @JsonKey(name: 'title') this.title, + @JsonKey(name: 'type') this.type, + @JsonKey(name: 'coverImage') + final Map coverImage = const {}, + @JsonKey(name: 'description') this.description, + @JsonKey(name: 'status') this.status, + @JsonKey(name: 'source') this.source, + @JsonKey(name: 'episodes') this.episodes, + @JsonKey(name: 'seasonYear') this.seasonYear, + @JsonKey(name: 'season') this.season, + @JsonKey(name: 'hashtag') this.hashtag, + @JsonKey(name: 'bannerImage') this.bannerImage, + @JsonKey(name: 'averageScore') this.averageScore, + @JsonKey(name: 'trending') this.trending, + @JsonKey(name: 'favourites') this.favourites, + @JsonKey(name: 'genres') final List genres = const [], + @JsonKey(name: 'trailer') this.trailer, + @JsonKey(name: 'nextAiringEpisode') this.nextAiringEpisode, + @JsonKey(name: 'rankings') final List rankings = const [], + @JsonKey(name: 'characters') this.characters, + @JsonKey(name: 'staff') this.staff, + @JsonKey(name: 'externalLinks') + final List externalLinks = const []}) + : _coverImage = coverImage, + _genres = genres, + _rankings = rankings, + _externalLinks = externalLinks; + + factory _$_MediaDto.fromJson(Map json) => + _$$_MediaDtoFromJson(json); + + @override + @JsonKey(name: 'id') + final int id; + @override + @JsonKey(name: 'title') + final MediaTitle? title; + @override + @JsonKey(name: 'type') + final String? type; + final Map _coverImage; + @override + @JsonKey(name: 'coverImage') + Map get coverImage { + if (_coverImage is EqualUnmodifiableMapView) return _coverImage; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_coverImage); + } + + @override + @JsonKey(name: 'description') + final String? description; + @override + @JsonKey(name: 'status') + final MediaStatus? status; + @override + @JsonKey(name: 'source') + final AnimeSource? source; + @override + @JsonKey(name: 'episodes') + final int? episodes; + @override + @JsonKey(name: 'seasonYear') + final int? seasonYear; + @override + @JsonKey(name: 'season') + final AnimeSeason? season; + @override + @JsonKey(name: 'hashtag') + final String? hashtag; + @override + @JsonKey(name: 'bannerImage') + final String? bannerImage; + @override + @JsonKey(name: 'averageScore') + final int? averageScore; + @override + @JsonKey(name: 'trending') + final int? trending; + @override + @JsonKey(name: 'favourites') + final int? favourites; + final List _genres; + @override + @JsonKey(name: 'genres') + List get genres { + if (_genres is EqualUnmodifiableListView) return _genres; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_genres); + } + + @override + @JsonKey(name: 'trailer') + final TrailerDto? trailer; + @override + @JsonKey(name: 'nextAiringEpisode') + final AiringScheduleDto? nextAiringEpisode; + final List _rankings; + @override + @JsonKey(name: 'rankings') + List get rankings { + if (_rankings is EqualUnmodifiableListView) return _rankings; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_rankings); + } + + @override + @JsonKey(name: 'characters') + final CharacterConnection? characters; + @override + @JsonKey(name: 'staff') + final StaffConnection? staff; + final List _externalLinks; + @override + @JsonKey(name: 'externalLinks') + List get externalLinks { + if (_externalLinks is EqualUnmodifiableListView) return _externalLinks; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_externalLinks); + } + + @override + String toString() { + return 'MediaDto(id: $id, title: $title, type: $type, coverImage: $coverImage, description: $description, status: $status, source: $source, episodes: $episodes, seasonYear: $seasonYear, season: $season, hashtag: $hashtag, bannerImage: $bannerImage, averageScore: $averageScore, trending: $trending, favourites: $favourites, genres: $genres, trailer: $trailer, nextAiringEpisode: $nextAiringEpisode, rankings: $rankings, characters: $characters, staff: $staff, externalLinks: $externalLinks)'; + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$_MediaDto && + (identical(other.id, id) || other.id == id) && + (identical(other.title, title) || other.title == title) && + (identical(other.type, type) || other.type == type) && + const DeepCollectionEquality() + .equals(other._coverImage, _coverImage) && + (identical(other.description, description) || + other.description == description) && + (identical(other.status, status) || other.status == status) && + (identical(other.source, source) || other.source == source) && + (identical(other.episodes, episodes) || + other.episodes == episodes) && + (identical(other.seasonYear, seasonYear) || + other.seasonYear == seasonYear) && + (identical(other.season, season) || other.season == season) && + (identical(other.hashtag, hashtag) || other.hashtag == hashtag) && + (identical(other.bannerImage, bannerImage) || + other.bannerImage == bannerImage) && + (identical(other.averageScore, averageScore) || + other.averageScore == averageScore) && + (identical(other.trending, trending) || + other.trending == trending) && + (identical(other.favourites, favourites) || + other.favourites == favourites) && + const DeepCollectionEquality().equals(other._genres, _genres) && + (identical(other.trailer, trailer) || other.trailer == trailer) && + (identical(other.nextAiringEpisode, nextAiringEpisode) || + other.nextAiringEpisode == nextAiringEpisode) && + const DeepCollectionEquality().equals(other._rankings, _rankings) && + (identical(other.characters, characters) || + other.characters == characters) && + (identical(other.staff, staff) || other.staff == staff) && + const DeepCollectionEquality() + .equals(other._externalLinks, _externalLinks)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hashAll([ + runtimeType, + id, + title, + type, + const DeepCollectionEquality().hash(_coverImage), + description, + status, + source, + episodes, + seasonYear, + season, + hashtag, + bannerImage, + averageScore, + trending, + favourites, + const DeepCollectionEquality().hash(_genres), + trailer, + nextAiringEpisode, + const DeepCollectionEquality().hash(_rankings), + characters, + staff, + const DeepCollectionEquality().hash(_externalLinks) + ]); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$_MediaDtoCopyWith<_$_MediaDto> get copyWith => + __$$_MediaDtoCopyWithImpl<_$_MediaDto>(this, _$identity); + + @override + Map toJson() { + return _$$_MediaDtoToJson( + this, + ); + } +} + +abstract class _MediaDto implements MediaDto { + factory _MediaDto( + {@JsonKey(name: 'id') final int id, + @JsonKey(name: 'title') final MediaTitle? title, + @JsonKey(name: 'type') final String? type, + @JsonKey(name: 'coverImage') final Map coverImage, + @JsonKey(name: 'description') final String? description, + @JsonKey(name: 'status') final MediaStatus? status, + @JsonKey(name: 'source') final AnimeSource? source, + @JsonKey(name: 'episodes') final int? episodes, + @JsonKey(name: 'seasonYear') final int? seasonYear, + @JsonKey(name: 'season') final AnimeSeason? season, + @JsonKey(name: 'hashtag') final String? hashtag, + @JsonKey(name: 'bannerImage') final String? bannerImage, + @JsonKey(name: 'averageScore') final int? averageScore, + @JsonKey(name: 'trending') final int? trending, + @JsonKey(name: 'favourites') final int? favourites, + @JsonKey(name: 'genres') final List genres, + @JsonKey(name: 'trailer') final TrailerDto? trailer, + @JsonKey(name: 'nextAiringEpisode') + final AiringScheduleDto? nextAiringEpisode, + @JsonKey(name: 'rankings') final List rankings, + @JsonKey(name: 'characters') final CharacterConnection? characters, + @JsonKey(name: 'staff') final StaffConnection? staff, + @JsonKey(name: 'externalLinks') + final List externalLinks}) = _$_MediaDto; + + factory _MediaDto.fromJson(Map json) = _$_MediaDto.fromJson; + + @override + @JsonKey(name: 'id') + int get id; + @override + @JsonKey(name: 'title') + MediaTitle? get title; + @override + @JsonKey(name: 'type') + String? get type; + @override + @JsonKey(name: 'coverImage') + Map get coverImage; + @override + @JsonKey(name: 'description') + String? get description; + @override + @JsonKey(name: 'status') + MediaStatus? get status; + @override + @JsonKey(name: 'source') + AnimeSource? get source; + @override + @JsonKey(name: 'episodes') + int? get episodes; + @override + @JsonKey(name: 'seasonYear') + int? get seasonYear; + @override + @JsonKey(name: 'season') + AnimeSeason? get season; + @override + @JsonKey(name: 'hashtag') + String? get hashtag; + @override + @JsonKey(name: 'bannerImage') + String? get bannerImage; + @override + @JsonKey(name: 'averageScore') + int? get averageScore; + @override + @JsonKey(name: 'trending') + int? get trending; + @override + @JsonKey(name: 'favourites') + int? get favourites; + @override + @JsonKey(name: 'genres') + List get genres; + @override + @JsonKey(name: 'trailer') + TrailerDto? get trailer; + @override + @JsonKey(name: 'nextAiringEpisode') + AiringScheduleDto? get nextAiringEpisode; + @override + @JsonKey(name: 'rankings') + List get rankings; + @override + @JsonKey(name: 'characters') + CharacterConnection? get characters; + @override + @JsonKey(name: 'staff') + StaffConnection? get staff; + @override + @JsonKey(name: 'externalLinks') + List get externalLinks; + @override + @JsonKey(ignore: true) + _$$_MediaDtoCopyWith<_$_MediaDto> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/core/network/model/media_dto.g.dart b/lib/core/network/model/media_dto.g.dart new file mode 100644 index 00000000..82ee67fb --- /dev/null +++ b/lib/core/network/model/media_dto.g.dart @@ -0,0 +1,104 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'media_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$_MediaDto _$$_MediaDtoFromJson(Map json) => _$_MediaDto( + id: json['id'] as int? ?? -1, + title: json['title'] == null + ? null + : MediaTitle.fromJson(json['title'] as Map), + type: json['type'] as String?, + coverImage: (json['coverImage'] as Map?)?.map( + (k, e) => MapEntry(k, e as String?), + ) ?? + const {}, + description: json['description'] as String?, + status: $enumDecodeNullable(_$MediaStatusEnumMap, json['status']), + source: $enumDecodeNullable(_$AnimeSourceEnumMap, json['source']), + episodes: json['episodes'] as int?, + seasonYear: json['seasonYear'] as int?, + season: $enumDecodeNullable(_$AnimeSeasonEnumMap, json['season']), + hashtag: json['hashtag'] as String?, + bannerImage: json['bannerImage'] as String?, + averageScore: json['averageScore'] as int?, + trending: json['trending'] as int?, + favourites: json['favourites'] as int?, + genres: json['genres'] as List? ?? const [], + trailer: json['trailer'] == null + ? null + : TrailerDto.fromJson(json['trailer'] as Map), + nextAiringEpisode: json['nextAiringEpisode'] == null + ? null + : AiringScheduleDto.fromJson( + json['nextAiringEpisode'] as Map), + rankings: (json['rankings'] as List?) + ?.map((e) => e == null + ? null + : AnimeRank.fromJson(e as Map)) + .toList() ?? + const [], + characters: json['characters'] == null + ? null + : CharacterConnection.fromJson( + json['characters'] as Map), + staff: json['staff'] == null + ? null + : StaffConnection.fromJson(json['staff'] as Map), + externalLinks: (json['externalLinks'] as List?) + ?.map((e) => + MediaExternalLinkDto.fromJson(e as Map)) + .toList() ?? + const [], + ); + +Map _$$_MediaDtoToJson(_$_MediaDto instance) => + { + 'id': instance.id, + 'title': instance.title, + 'type': instance.type, + 'coverImage': instance.coverImage, + 'description': instance.description, + 'status': _$MediaStatusEnumMap[instance.status], + 'source': _$AnimeSourceEnumMap[instance.source], + 'episodes': instance.episodes, + 'seasonYear': instance.seasonYear, + 'season': _$AnimeSeasonEnumMap[instance.season], + 'hashtag': instance.hashtag, + 'bannerImage': instance.bannerImage, + 'averageScore': instance.averageScore, + 'trending': instance.trending, + 'favourites': instance.favourites, + 'genres': instance.genres, + 'trailer': instance.trailer, + 'nextAiringEpisode': instance.nextAiringEpisode, + 'rankings': instance.rankings, + 'characters': instance.characters, + 'staff': instance.staff, + 'externalLinks': instance.externalLinks, + }; + +const _$MediaStatusEnumMap = { + MediaStatus.releasing: 'RELEASING', + MediaStatus.finished: 'FINISHED', + MediaStatus.notYetReleased: 'NOT_YET_RELEASED', +}; + +const _$AnimeSourceEnumMap = { + AnimeSource.original: 'ORIGINAL', + AnimeSource.manga: 'MANGA', + AnimeSource.lightNovel: 'LIGHT_NOVEL', + AnimeSource.visualNovel: 'VISUAL_NOVEL', + AnimeSource.videoGame: 'VIDEO_GAME', + AnimeSource.other: 'OTHER', +}; + +const _$AnimeSeasonEnumMap = { + AnimeSeason.winter: 'WINTER', + AnimeSeason.spring: 'SPRING', + AnimeSeason.summer: 'SUMMER', + AnimeSeason.fall: 'FALL', +}; diff --git a/lib/core/network/model/media_list_dto.dart b/lib/core/network/model/media_list_dto.dart index b01a9005..9a0b28fe 100644 --- a/lib/core/network/model/media_list_dto.dart +++ b/lib/core/network/model/media_list_dto.dart @@ -1,6 +1,6 @@ import 'package:aniflow/core/data/media_list_repository.dart'; -import 'package:aniflow/core/network/model/anime_dto.dart'; +import 'package:aniflow/core/network/model/media_dto.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'media_list_dto.freezed.dart'; @@ -15,7 +15,7 @@ class MediaListDto with _$MediaListDto { @JsonKey(name: 'status') MediaListStatus? status, @Default(-1) @JsonKey(name: 'progress') int progress, @Default(-1) @JsonKey(name: 'updatedAt') int updatedAt, - @JsonKey(name: 'media') AnimeDto? media, + @JsonKey(name: 'media') MediaDto? media, }) = _MediaListDto; factory MediaListDto.fromJson(Map json) => diff --git a/lib/core/network/model/media_list_dto.freezed.dart b/lib/core/network/model/media_list_dto.freezed.dart index 57e517ab..905f306d 100644 --- a/lib/core/network/model/media_list_dto.freezed.dart +++ b/lib/core/network/model/media_list_dto.freezed.dart @@ -33,7 +33,7 @@ mixin _$MediaListDto { @JsonKey(name: 'updatedAt') int get updatedAt => throw _privateConstructorUsedError; @JsonKey(name: 'media') - AnimeDto? get media => throw _privateConstructorUsedError; + MediaDto? get media => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -54,9 +54,9 @@ abstract class $MediaListDtoCopyWith<$Res> { @JsonKey(name: 'status') MediaListStatus? status, @JsonKey(name: 'progress') int progress, @JsonKey(name: 'updatedAt') int updatedAt, - @JsonKey(name: 'media') AnimeDto? media}); + @JsonKey(name: 'media') MediaDto? media}); - $AnimeDtoCopyWith<$Res>? get media; + $MediaDtoCopyWith<$Res>? get media; } /// @nodoc @@ -108,18 +108,18 @@ class _$MediaListDtoCopyWithImpl<$Res, $Val extends MediaListDto> media: freezed == media ? _value.media : media // ignore: cast_nullable_to_non_nullable - as AnimeDto?, + as MediaDto?, ) as $Val); } @override @pragma('vm:prefer-inline') - $AnimeDtoCopyWith<$Res>? get media { + $MediaDtoCopyWith<$Res>? get media { if (_value.media == null) { return null; } - return $AnimeDtoCopyWith<$Res>(_value.media!, (value) { + return $MediaDtoCopyWith<$Res>(_value.media!, (value) { return _then(_value.copyWith(media: value) as $Val); }); } @@ -140,10 +140,10 @@ abstract class _$$_MediaListDtoCopyWith<$Res> @JsonKey(name: 'status') MediaListStatus? status, @JsonKey(name: 'progress') int progress, @JsonKey(name: 'updatedAt') int updatedAt, - @JsonKey(name: 'media') AnimeDto? media}); + @JsonKey(name: 'media') MediaDto? media}); @override - $AnimeDtoCopyWith<$Res>? get media; + $MediaDtoCopyWith<$Res>? get media; } /// @nodoc @@ -193,7 +193,7 @@ class __$$_MediaListDtoCopyWithImpl<$Res> media: freezed == media ? _value.media : media // ignore: cast_nullable_to_non_nullable - as AnimeDto?, + as MediaDto?, )); } } @@ -233,7 +233,7 @@ class _$_MediaListDto implements _MediaListDto { final int updatedAt; @override @JsonKey(name: 'media') - final AnimeDto? media; + final MediaDto? media; @override String toString() { @@ -283,7 +283,7 @@ abstract class _MediaListDto implements MediaListDto { @JsonKey(name: 'status') final MediaListStatus? status, @JsonKey(name: 'progress') final int progress, @JsonKey(name: 'updatedAt') final int updatedAt, - @JsonKey(name: 'media') final AnimeDto? media}) = _$_MediaListDto; + @JsonKey(name: 'media') final MediaDto? media}) = _$_MediaListDto; factory _MediaListDto.fromJson(Map json) = _$_MediaListDto.fromJson; @@ -308,7 +308,7 @@ abstract class _MediaListDto implements MediaListDto { int get updatedAt; @override @JsonKey(name: 'media') - AnimeDto? get media; + MediaDto? get media; @override @JsonKey(ignore: true) _$$_MediaListDtoCopyWith<_$_MediaListDto> get copyWith => diff --git a/lib/core/network/model/media_list_dto.g.dart b/lib/core/network/model/media_list_dto.g.dart index cec00d50..78c5a4a3 100644 --- a/lib/core/network/model/media_list_dto.g.dart +++ b/lib/core/network/model/media_list_dto.g.dart @@ -16,7 +16,7 @@ _$_MediaListDto _$$_MediaListDtoFromJson(Map json) => updatedAt: json['updatedAt'] as int? ?? -1, media: json['media'] == null ? null - : AnimeDto.fromJson(json['media'] as Map), + : MediaDto.fromJson(json['media'] as Map), ); Map _$$_MediaListDtoToJson(_$_MediaListDto instance) => diff --git a/lib/core/shared_preference/aniflow_prefrences.dart b/lib/core/shared_preference/aniflow_prefrences.dart index d495e1c4..fda43eb1 100644 --- a/lib/core/shared_preference/aniflow_prefrences.dart +++ b/lib/core/shared_preference/aniflow_prefrences.dart @@ -110,7 +110,7 @@ class AniFlowPreferences { MediaType getCurrentMediaType() => MediaType.fromString( _preference.getString(UserDataKey.currentMediaType) ?? - describeEnum(MediaType.anime), + MediaType.anime.jsonString, ); Stream getCurrentMediaTypeStream() => StreamUtil.createStream( @@ -120,7 +120,7 @@ class AniFlowPreferences { Future setCurrentMediaType(MediaType mediaType) async { final isChanged = await _preference.setString( - UserDataKey.currentMediaType, describeEnum(mediaType)); + UserDataKey.currentMediaType, mediaType.jsonString); if (isChanged) { _currentMediaTypeChangeNotifier.value = diff --git a/lib/feature/anime_search/bloc/anime_search_bloc.dart b/lib/feature/anime_search/bloc/anime_search_bloc.dart index 1bc0aa32..c4b244cc 100644 --- a/lib/feature/anime_search/bloc/anime_search_bloc.dart +++ b/lib/feature/anime_search/bloc/anime_search_bloc.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/common/util/global_static_constants.dart'; import 'package:aniflow/core/data/load_result.dart'; import 'package:aniflow/core/data/model/media_model.dart'; import 'package:aniflow/core/data/search_repository.dart'; +import 'package:aniflow/core/data/user_data_repository.dart'; import 'package:aniflow/feature/common/page_loading_state.dart'; import 'package:aniflow/feature/common/paging_bloc.dart'; import 'package:bloc/bloc.dart'; @@ -17,14 +19,18 @@ class OnSearchStringCommit extends PagingEvent { class SearchPageBloc extends PagingBloc { SearchPageBloc({ required SearchRepository searchRepository, + required UserDataRepository userDataRepository, }) : _searchRepository = searchRepository, super(const PageInit(data: [])) { on>(_onSearchStringCommit); + + mediaType = userDataRepository.getMediaType(); } final SearchRepository _searchRepository; String? _searchString; + MediaType? mediaType; @override FutureOr onInit( @@ -38,6 +44,7 @@ class SearchPageBloc extends PagingBloc { return _searchRepository.loadMediaSearchResultByPage( page: page, perPage: Config.defaultPerPageCount, + type: mediaType!, search: _searchString!, ); } diff --git a/lib/feature/anime_search/media_search.dart b/lib/feature/anime_search/media_search.dart index 61e9deca..a046186f 100644 --- a/lib/feature/anime_search/media_search.dart +++ b/lib/feature/anime_search/media_search.dart @@ -1,6 +1,8 @@ import 'package:aniflow/app/navigation/ani_flow_router.dart'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/data/model/media_model.dart'; import 'package:aniflow/core/data/search_repository.dart'; +import 'package:aniflow/core/data/user_data_repository.dart'; import 'package:aniflow/core/design_system/widget/search_anime_item.dart'; import 'package:aniflow/feature/anime_search/bloc/anime_search_bloc.dart'; import 'package:aniflow/feature/common/page_loading_state.dart'; @@ -19,12 +21,13 @@ class SearchPage extends Page { } class SearchPageRoute extends PageRoute with MaterialRouteTransitionMixin { - SearchPageRoute({super.settings}): super(allowSnapshotting: false); + SearchPageRoute({super.settings}) : super(allowSnapshotting: false); @override Widget buildContent(BuildContext context) { return BlocProvider( create: (BuildContext context) => SearchPageBloc( + userDataRepository: context.read(), searchRepository: context.read(), ), child: const _MediaSearchPageContent(), @@ -42,17 +45,18 @@ class _MediaSearchPageContent extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder>>( builder: (context, state) { + final mediaType = + context.read().mediaType ?? MediaType.anime; return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.surfaceVariant, - title:_buildSearchArea(context) , + title: _buildSearchArea(context, mediaType), ), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: PagingContent( pagingState: state, - onBuildItem: (context, model) => - _buildListItems(context, model), + onBuildItem: (context, model) => _buildListItems(context, model), onRequestNewPage: () { context.read().add(OnRequestLoadPageEvent()); }, @@ -66,7 +70,7 @@ class _MediaSearchPageContent extends StatelessWidget { ); } - Widget _buildSearchArea(BuildContext context) { + Widget _buildSearchArea(BuildContext context, MediaType type) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: TextField( @@ -78,8 +82,8 @@ class _MediaSearchPageContent extends StatelessWidget { .add(OnSearchStringCommit(searchString: content)); } }, - decoration: const InputDecoration( - labelText: 'Search Anime', + decoration: InputDecoration( + labelText: type == MediaType.anime ? 'Search Anime' : 'Search Manga', ), ), ); diff --git a/lib/feature/auth/bloc/auth_bloc.dart b/lib/feature/auth/bloc/auth_bloc.dart index 9ea66084..1a445a8f 100644 --- a/lib/feature/auth/bloc/auth_bloc.dart +++ b/lib/feature/auth/bloc/auth_bloc.dart @@ -34,8 +34,10 @@ class AuthBloc extends Bloc { on(_onLogoutButtonTapped); on<_OnUserDataChanged>(_onUserDataChanged); - _userDataSub = - authRepository.getUserDataStream().listen((userDataNullable) { + _userDataSub = authRepository + .getUserDataStream() + .distinct() + .listen((userDataNullable) { add(_OnUserDataChanged(userDataNullable)); }); } diff --git a/lib/feature/discover/bloc/discover_bloc.dart b/lib/feature/discover/bloc/discover_bloc.dart index 40300d87..39826ee9 100644 --- a/lib/feature/discover/bloc/discover_bloc.dart +++ b/lib/feature/discover/bloc/discover_bloc.dart @@ -19,6 +19,7 @@ import 'package:aniflow/core/design_system/widget/aniflow_snackbar.dart'; import 'package:aniflow/feature/common/page_loading_state.dart'; import 'package:aniflow/feature/discover/bloc/discover_ui_state.dart'; import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; sealed class DiscoverEvent {} @@ -42,8 +43,8 @@ class _OnUserDataChanged extends DiscoverEvent { final UserData? userData; } -class _OnTrackingAnimeIdsChanged extends DiscoverEvent { - _OnTrackingAnimeIdsChanged(this.ids); +class _OnTrackingMediaIdsChanged extends DiscoverEvent { + _OnTrackingMediaIdsChanged(this.ids); final Set ids; } @@ -71,33 +72,31 @@ class DiscoverBloc extends Bloc { required aniListRepository, required MediaListRepository animeTrackListRepository}) : _userDataRepository = userDataRepository, - _aniListRepository = aniListRepository, - _animeTrackListRepository = animeTrackListRepository, + _authRepository = authRepository, + _mediaInfoRepository = aniListRepository, + _mediaListRepository = animeTrackListRepository, super(DiscoverUiState()) { on<_OnMediaLoaded>(_onMediaLoaded); on<_OnMediaLoadError>(_onMediaLoadError); on<_OnUserDataChanged>(_onUserDataChanged); - on<_OnTrackingAnimeIdsChanged>(_onTrackingAnimeIdsChanged); + on<_OnTrackingMediaIdsChanged>(_onTrackingMediaIdsChanged); on<_OnMediaTypeChanged>(_onMediaTypeChanged); on<_OnLoadStateChanged>(_onLoadStateChanged); - _userDataSub ??= - authRepository.getUserDataStream().listen((userDataNullable) { - add(_OnUserDataChanged(userDataNullable)); - }); - _init(); } final UserDataRepository _userDataRepository; - final MediaInformationRepository _aniListRepository; - final MediaListRepository _animeTrackListRepository; + final AuthRepository _authRepository; + final MediaInformationRepository _mediaInfoRepository; + final MediaListRepository _mediaListRepository; StreamSubscription? _userDataSub; StreamSubscription? _mediaTypeSub; StreamSubscription? _trackedMediaIdsSub; - Set _ids = {}; + Set _followingMediaIds = {}; + String? _userId; final Set _syncedMediaTypes = {}; @@ -128,6 +127,12 @@ class DiscoverBloc extends Bloc { await _userDataRepository.setAnimeSeasonParam(currentAnimeSeasonParam); } + _userDataSub ??= _authRepository.getUserDataStream().listen( + (userDataNullable) { + add(_OnUserDataChanged(userDataNullable)); + }, + ); + _mediaTypeSub = _userDataRepository.getMediaTypeStream().distinct().listen( (mediaType) { logger.d(mediaType); @@ -142,18 +147,26 @@ class DiscoverBloc extends Bloc { emit(state.copyWith(currentMediaType: type)); - // _trackedMediaIdsSub?.cancel(); - /// load all media from db cache first. - await reloadAllMedia(mediaType: type, isRefresh: false); + await _reloadAllMedia(mediaType: type, isRefresh: false); if (!_syncedMediaTypes.contains(type)) { /// Media's order may changed, refresh all media again. - unawaited(reloadAllMedia(mediaType: type, isRefresh: true)); + unawaited(_reloadAllMedia(mediaType: type, isRefresh: true)); } + + /// re-collecting following media ids, because of media type changed. + _startListenFollowingIds(); + } + + Future onPullToRefreshTriggered() { + return Future.wait([ + _reloadAllMedia(mediaType: state.currentMediaType, isRefresh: true), + if (_userId != null) _syncAllMediaList(_userId!) else Future.value(), + ]); } - Future reloadAllMedia( + Future _reloadAllMedia( {required MediaType mediaType, required bool isRefresh}) async { add(_OnLoadStateChanged(true)); @@ -186,10 +199,10 @@ class DiscoverBloc extends Bloc { {bool isRefresh = false}) async { final LoadResult result; if (isRefresh) { - result = await _aniListRepository.loadMediaPageByCategory( + result = await _mediaInfoRepository.loadMediaPageByCategory( loadType: const Refresh(), category: category); } else { - result = await _aniListRepository.loadMediaPageByCategory( + result = await _mediaInfoRepository.loadMediaPageByCategory( category: category, loadType: const Append(page: 1, perPage: Config.defaultPerPageCount), ); @@ -216,7 +229,7 @@ class DiscoverBloc extends Bloc { final DiscoverUiState newState = state.copyWith(categoryMediaMap: stateMap); - emit(DiscoverUiState.copyWithTrackedIds(newState, _ids)); + emit(DiscoverUiState.copyWithTrackedIds(newState, _followingMediaIds)); } FutureOr _onMediaLoadError( @@ -227,28 +240,22 @@ class DiscoverBloc extends Bloc { emit(state.copyWith(userData: event.userData)); if (event.userData != null) { + _userId = event.userData!.id; + /// user login, start listen following anime changed. - await _trackedMediaIdsSub?.cancel(); - _trackedMediaIdsSub = - _animeTrackListRepository.getMediaListAnimeIdsByUserStream( - event.userData!.id, - [MediaListStatus.planning, MediaListStatus.current], - ).listen((ids) { - add(_OnTrackingAnimeIdsChanged(ids)); - }); + _startListenFollowingIds(); /// post event to sync user anime list. - unawaited(_animeTrackListRepository.syncUserAnimeList( - userId: event.userData!.id)); + unawaited(_syncAllMediaList(event.userData!.id)); } else { /// user logout, cancel following stream. await _trackedMediaIdsSub?.cancel(); } } - FutureOr _onTrackingAnimeIdsChanged( - _OnTrackingAnimeIdsChanged event, Emitter emit) { - _ids = event.ids; + FutureOr _onTrackingMediaIdsChanged( + _OnTrackingMediaIdsChanged event, Emitter emit) { + _followingMediaIds = event.ids; emit(DiscoverUiState.copyWithTrackedIds(state, event.ids)); } @@ -256,4 +263,37 @@ class DiscoverBloc extends Bloc { _OnLoadStateChanged event, Emitter emit) { emit(state.copyWith(isLoading: event.isLoading)); } + + void _startListenFollowingIds() async { + final userId = _userId; + + if (userId == null) return; + + await _trackedMediaIdsSub?.cancel(); + _trackedMediaIdsSub = _mediaListRepository + .getMediaListMediaIdsByUserStream( + userId: userId, + status: [MediaListStatus.planning, MediaListStatus.current], + type: state.currentMediaType) + .distinct( + (pre, next) => const DeepCollectionEquality().equals(pre, next)) + .listen( + (ids) { + add(_OnTrackingMediaIdsChanged(ids)); + }, + ); + } + + Future _syncAllMediaList(String userId) async { + return Future.wait([ + _mediaListRepository.syncUserAnimeList( + userId: userId, + status: [MediaListStatus.current, MediaListStatus.planning], + mediaType: MediaType.manga), + _mediaListRepository.syncUserAnimeList( + userId: userId, + status: [MediaListStatus.current, MediaListStatus.planning], + mediaType: MediaType.anime), + ]); + } } diff --git a/lib/feature/discover/discover.dart b/lib/feature/discover/discover.dart index 5b63266e..349d46d2 100644 --- a/lib/feature/discover/discover.dart +++ b/lib/feature/discover/discover.dart @@ -4,7 +4,6 @@ import 'package:aniflow/core/common/model/anime_category.dart'; import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/common/util/global_static_constants.dart'; import 'package:aniflow/core/data/model/media_model.dart'; -import 'package:aniflow/core/data/user_data_repository.dart'; import 'package:aniflow/core/design_system/widget/avatar_icon.dart'; import 'package:aniflow/core/design_system/widget/loading_indicator.dart'; import 'package:aniflow/core/design_system/widget/media_preview_item.dart'; @@ -45,7 +44,6 @@ class DiscoverScreen extends StatelessWidget { builder: (BuildContext context, state) { final map = state.categoryMediaMap; final currentMediaType = state.currentMediaType; - final isAnime = state.currentMediaType == MediaType.anime; final userData = state.userData; final isLoggedIn = state.isLoggedIn; @@ -72,26 +70,11 @@ class DiscoverScreen extends StatelessWidget { ) ], ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () { - final repository = context.read(); - if (currentMediaType == MediaType.manga) { - repository.setMediaType(MediaType.anime); - } else { - repository.setMediaType(MediaType.manga); - } - }, - isExtended: true, - icon: isAnime - ? const Icon(Icons.palette_rounded) - : const Icon(Icons.map), - label: Text(isAnime ? 'Anime' : 'Manga'), - ), body: RefreshIndicator( onRefresh: () async { await context .read() - .reloadAllMedia(mediaType: MediaType.anime, isRefresh: true); + .onPullToRefreshTriggered(); }, child: CustomScrollView( cacheExtent: Config.defaultCatchExtend, diff --git a/lib/feature/media_page/bloc/media_page_bloc.dart b/lib/feature/media_page/bloc/media_page_bloc.dart index e8ddca09..e34c9dd8 100644 --- a/lib/feature/media_page/bloc/media_page_bloc.dart +++ b/lib/feature/media_page/bloc/media_page_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aniflow/core/common/model/anime_category.dart'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/common/util/global_static_constants.dart'; import 'package:aniflow/core/data/auth_repository.dart'; import 'package:aniflow/core/data/load_result.dart'; @@ -53,11 +54,15 @@ class AnimePageBloc extends PagingBloc { void _init() async { final userData = await _authRepository.getUserDataStream().first; if (userData != null) { - _trackingIdsStream = - _animeTrackListRepository.getMediaListAnimeIdsByUserStream( - userData.id, - [MediaListStatus.planning, MediaListStatus.current], - ).listen((ids) { + _trackingIdsStream = _animeTrackListRepository + .getMediaListMediaIdsByUserStream( + userId: userData.id, + status: [MediaListStatus.planning, MediaListStatus.current], +//TODO: + type: MediaType.anime, + ) + .distinct() + .listen((ids) { add(_OnTrackingAnimeIdsChanged(ids: ids)); }); } diff --git a/lib/feature/media_track/bloc/track_bloc.dart b/lib/feature/media_track/bloc/track_bloc.dart index 2dc89cdf..6471637e 100644 --- a/lib/feature/media_track/bloc/track_bloc.dart +++ b/lib/feature/media_track/bloc/track_bloc.dart @@ -1,12 +1,14 @@ import 'dart:async'; import 'package:aniflow/app/local/ani_flow_localizations.dart'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/data/auth_repository.dart'; import 'package:aniflow/core/data/load_result.dart'; import 'package:aniflow/core/data/media_list_repository.dart'; import 'package:aniflow/core/data/model/anime_list_item_model.dart'; import 'package:aniflow/core/data/model/extension/media_list_item_model_extension.dart'; import 'package:aniflow/core/data/model/user_data_model.dart'; +import 'package:aniflow/core/data/user_data_repository.dart'; import 'package:aniflow/core/design_system/widget/aniflow_snackbar.dart'; import 'package:aniflow/feature/media_track/bloc/track_ui_state.dart'; import 'package:aniflow/feature/media_track/bloc/user_anime_list_load_state.dart'; @@ -47,16 +49,26 @@ class OnAnimeMarkWatched extends TrackEvent { required this.totalEpisode}); } +class _OnMediaTypeChanged extends TrackEvent { + _OnMediaTypeChanged(this.mediaType); + + final MediaType mediaType; +} + class TrackBloc extends Bloc { TrackBloc( - {required MediaListRepository animeTrackListRepository, - required AuthRepository authRepository}) - : _animeTrackListRepository = animeTrackListRepository, + {required MediaListRepository mediaListRepository, + required AuthRepository authRepository, + required UserDataRepository userDataRepository + }) + : _animeTrackListRepository = mediaListRepository, + _userDataRepository = userDataRepository, _authRepository = authRepository, super(TrackUiState()) { on<_OnUserStateChanged>(_onUserStateChanged); on<_OnLoadStateChanged>(_onLoadStateChanged); on<_OnWatchingAnimeListChanged>(_onWatchingAnimeListChanged); + on<_OnMediaTypeChanged>(_onMediaTypeChanged); on(_onToggleShowReleasedOnly); on(_onAnimeMarkWatched); @@ -65,8 +77,11 @@ class TrackBloc extends Bloc { StreamSubscription? _userContentSub; StreamSubscription? _userStateSub; + StreamSubscription? _mediaTypeSub; final MediaListRepository _animeTrackListRepository; + final UserDataRepository _userDataRepository; final AuthRepository _authRepository; + String? _userId; var _watchingAnimeList = []; @@ -75,12 +90,19 @@ class TrackBloc extends Bloc { _userStateSub ??= _authRepository.getUserDataStream().listen((userData) { add(_OnUserStateChanged(userData: userData)); }); + + _mediaTypeSub = _userDataRepository.getMediaTypeStream().distinct().listen( + (mediaType) { + add(_OnMediaTypeChanged(mediaType)); + }, + ); } @override Future close() { _userContentSub?.cancel(); _userStateSub?.cancel(); + _mediaTypeSub?.cancel(); return super.close(); } @@ -106,15 +128,9 @@ class TrackBloc extends Bloc { emit(state.copyWith(animeLoadState: const MediaStateInitState())); } - /// start listening streams if needed. - final userData = event.userData!; - await _userContentSub?.cancel(); - _userContentSub = _animeTrackListRepository.getUserAnimeListStream( - status: [MediaListStatus.planning, MediaListStatus.current], - userId: userData.id, - ).listen((animeList) { - add(_OnWatchingAnimeListChanged(animeList: animeList)); - }); + /// start listening streams. + _userId = event.userData!.id; + _startListenFollowingMedias(); } } @@ -136,10 +152,10 @@ class TrackBloc extends Bloc { final needShowReleasedOnly = state.showReleasedOnly; /// trim anime list if needed. - final animeList = _getTrimmedAnimeList(needShowReleasedOnly); + final mediaList = _getTrimmedMediaList(needShowReleasedOnly); emit(state.copyWith( - animeLoadState: MediaStateLoaded(watchingAnimeList: animeList), + animeLoadState: MediaStateLoaded(followingMediaList: mediaList), )); } @@ -155,16 +171,16 @@ class TrackBloc extends Bloc { emit(state.copyWith(showReleasedOnly: needShowReleasedOnly)); /// trim anime list if needed. - final animeList = _getTrimmedAnimeList(needShowReleasedOnly); + final animeList = _getTrimmedMediaList(needShowReleasedOnly); emit( state.copyWith( - animeLoadState: MediaStateLoaded(watchingAnimeList: animeList), + animeLoadState: MediaStateLoaded(followingMediaList: animeList), ), ); } - List _getTrimmedAnimeList(bool needShowReleasedOnly) { + List _getTrimmedMediaList(bool needShowReleasedOnly) { List animeList; if (needShowReleasedOnly) { animeList = @@ -203,4 +219,29 @@ class TrackBloc extends Bloc { //TODO: show error msg. } } + + FutureOr _onMediaTypeChanged( + _OnMediaTypeChanged event, Emitter emit) { + final type = event.mediaType; + + emit(state.copyWith(currentMediaType: type)); + + /// re-collecting following medias, because of media type changed. + _startListenFollowingMedias(); + } + + void _startListenFollowingMedias() async { + final userId = _userId; + + if (userId == null) return; + + await _userContentSub?.cancel(); + _userContentSub = _animeTrackListRepository.getUserAnimeListStream( + status: [MediaListStatus.planning, MediaListStatus.current], + type: state.currentMediaType, + userId: userId, + ).listen((animeList) { + add(_OnWatchingAnimeListChanged(animeList: animeList)); + }); + } } diff --git a/lib/feature/media_track/bloc/track_ui_state.dart b/lib/feature/media_track/bloc/track_ui_state.dart index 8aef1c5c..d61d1109 100644 --- a/lib/feature/media_track/bloc/track_ui_state.dart +++ b/lib/feature/media_track/bloc/track_ui_state.dart @@ -1,4 +1,5 @@ +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/feature/media_track/bloc/user_anime_list_load_state.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -9,6 +10,7 @@ class TrackUiState with _$TrackUiState { factory TrackUiState({ @Default(false) bool isLoading, @Default(false) bool showReleasedOnly, + @Default(MediaType.anime) MediaType currentMediaType, @Default(MediaStateInitState()) MediaListLoadState animeLoadState, }) = _TrackUiState; } diff --git a/lib/feature/media_track/bloc/track_ui_state.freezed.dart b/lib/feature/media_track/bloc/track_ui_state.freezed.dart index 5d6779fe..5c16a0c2 100644 --- a/lib/feature/media_track/bloc/track_ui_state.freezed.dart +++ b/lib/feature/media_track/bloc/track_ui_state.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$TrackUiState { bool get isLoading => throw _privateConstructorUsedError; bool get showReleasedOnly => throw _privateConstructorUsedError; + MediaType get currentMediaType => throw _privateConstructorUsedError; MediaListLoadState get animeLoadState => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -34,6 +35,7 @@ abstract class $TrackUiStateCopyWith<$Res> { $Res call( {bool isLoading, bool showReleasedOnly, + MediaType currentMediaType, MediaListLoadState animeLoadState}); } @@ -52,6 +54,7 @@ class _$TrackUiStateCopyWithImpl<$Res, $Val extends TrackUiState> $Res call({ Object? isLoading = null, Object? showReleasedOnly = null, + Object? currentMediaType = null, Object? animeLoadState = null, }) { return _then(_value.copyWith( @@ -63,6 +66,10 @@ class _$TrackUiStateCopyWithImpl<$Res, $Val extends TrackUiState> ? _value.showReleasedOnly : showReleasedOnly // ignore: cast_nullable_to_non_nullable as bool, + currentMediaType: null == currentMediaType + ? _value.currentMediaType + : currentMediaType // ignore: cast_nullable_to_non_nullable + as MediaType, animeLoadState: null == animeLoadState ? _value.animeLoadState : animeLoadState // ignore: cast_nullable_to_non_nullable @@ -82,6 +89,7 @@ abstract class _$$_TrackUiStateCopyWith<$Res> $Res call( {bool isLoading, bool showReleasedOnly, + MediaType currentMediaType, MediaListLoadState animeLoadState}); } @@ -98,6 +106,7 @@ class __$$_TrackUiStateCopyWithImpl<$Res> $Res call({ Object? isLoading = null, Object? showReleasedOnly = null, + Object? currentMediaType = null, Object? animeLoadState = null, }) { return _then(_$_TrackUiState( @@ -109,6 +118,10 @@ class __$$_TrackUiStateCopyWithImpl<$Res> ? _value.showReleasedOnly : showReleasedOnly // ignore: cast_nullable_to_non_nullable as bool, + currentMediaType: null == currentMediaType + ? _value.currentMediaType + : currentMediaType // ignore: cast_nullable_to_non_nullable + as MediaType, animeLoadState: null == animeLoadState ? _value.animeLoadState : animeLoadState // ignore: cast_nullable_to_non_nullable @@ -123,6 +136,7 @@ class _$_TrackUiState implements _TrackUiState { _$_TrackUiState( {this.isLoading = false, this.showReleasedOnly = false, + this.currentMediaType = MediaType.anime, this.animeLoadState = const MediaStateInitState()}); @override @@ -133,11 +147,14 @@ class _$_TrackUiState implements _TrackUiState { final bool showReleasedOnly; @override @JsonKey() + final MediaType currentMediaType; + @override + @JsonKey() final MediaListLoadState animeLoadState; @override String toString() { - return 'TrackUiState(isLoading: $isLoading, showReleasedOnly: $showReleasedOnly, animeLoadState: $animeLoadState)'; + return 'TrackUiState(isLoading: $isLoading, showReleasedOnly: $showReleasedOnly, currentMediaType: $currentMediaType, animeLoadState: $animeLoadState)'; } @override @@ -149,13 +166,15 @@ class _$_TrackUiState implements _TrackUiState { other.isLoading == isLoading) && (identical(other.showReleasedOnly, showReleasedOnly) || other.showReleasedOnly == showReleasedOnly) && + (identical(other.currentMediaType, currentMediaType) || + other.currentMediaType == currentMediaType) && (identical(other.animeLoadState, animeLoadState) || other.animeLoadState == animeLoadState)); } @override - int get hashCode => - Object.hash(runtimeType, isLoading, showReleasedOnly, animeLoadState); + int get hashCode => Object.hash(runtimeType, isLoading, showReleasedOnly, + currentMediaType, animeLoadState); @JsonKey(ignore: true) @override @@ -168,6 +187,7 @@ abstract class _TrackUiState implements TrackUiState { factory _TrackUiState( {final bool isLoading, final bool showReleasedOnly, + final MediaType currentMediaType, final MediaListLoadState animeLoadState}) = _$_TrackUiState; @override @@ -175,6 +195,8 @@ abstract class _TrackUiState implements TrackUiState { @override bool get showReleasedOnly; @override + MediaType get currentMediaType; + @override MediaListLoadState get animeLoadState; @override @JsonKey(ignore: true) diff --git a/lib/feature/media_track/bloc/user_anime_list_load_state.dart b/lib/feature/media_track/bloc/user_anime_list_load_state.dart index 29f853b9..3c8f5707 100644 --- a/lib/feature/media_track/bloc/user_anime_list_load_state.dart +++ b/lib/feature/media_track/bloc/user_anime_list_load_state.dart @@ -17,10 +17,10 @@ class MediaStateNoUser extends MediaListLoadState { } class MediaStateLoaded extends MediaListLoadState { - final List watchingAnimeList; + final List followingMediaList; - const MediaStateLoaded({required this.watchingAnimeList}); + const MediaStateLoaded({required this.followingMediaList}); @override - List get props => [watchingAnimeList]; + List get props => [followingMediaList]; } diff --git a/lib/feature/media_track/media_track.dart b/lib/feature/media_track/media_track.dart index abaf16b3..fed27471 100644 --- a/lib/feature/media_track/media_track.dart +++ b/lib/feature/media_track/media_track.dart @@ -1,4 +1,5 @@ import 'package:aniflow/app/navigation/ani_flow_router.dart'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/data/model/anime_list_item_model.dart'; import 'package:aniflow/core/design_system/widget/af_toogle_button.dart'; import 'package:aniflow/core/design_system/widget/loading_indicator.dart'; @@ -19,7 +20,7 @@ class AnimeTrackPage extends Page { } class AnimeTrackRoute extends PageRoute with MaterialRouteTransitionMixin { - AnimeTrackRoute({super.settings}): super(allowSnapshotting: false); + AnimeTrackRoute({super.settings}) : super(allowSnapshotting: false); @override Widget buildContent(BuildContext context) { @@ -54,8 +55,8 @@ class _AnimeTrackPageContent extends StatelessWidget { }); } - List _buildTrackSectionContents(BuildContext context, - TrackUiState state) { + List _buildTrackSectionContents( + BuildContext context, TrackUiState state) { final animeLoadState = state.animeLoadState; if (animeLoadState is MediaStateNoUser) { return [ @@ -66,7 +67,7 @@ class _AnimeTrackPageContent extends StatelessWidget { _buildInitialDummyView(), ]; } else { - final animeList = (animeLoadState as MediaStateLoaded).watchingAnimeList; + final animeList = (animeLoadState as MediaStateLoaded).followingMediaList; final showReleasedOnly = state.showReleasedOnly; return [ SliverToBoxAdapter( @@ -74,8 +75,7 @@ class _AnimeTrackPageContent extends StatelessWidget { ), SliverList( delegate: SliverChildBuilderDelegate( - (context, index) => - _buildAnimeListItem(context, animeList[index]), + (context, index) => _buildMediaListItem(context, animeList[index]), childCount: animeList.length, ), ) @@ -83,7 +83,7 @@ class _AnimeTrackPageContent extends StatelessWidget { } } - Widget? _buildAnimeListItem(BuildContext context, MediaListItemModel item) { + Widget? _buildMediaListItem(BuildContext context, MediaListItemModel item) { return SizedBox( key: ValueKey('anime_track_list_item_${item.id}'), height: 120, @@ -124,26 +124,29 @@ class _AnimeTrackPageContent extends StatelessWidget { ); } - Widget _buildAppBar(BuildContext context, TrackUiState state) => - SliverAppBar( - title: const Text('Track'), - actions: [ - LoadingIndicator(isLoading: state.isLoading,), - const SizedBox(width: 10), - Padding( - padding: const EdgeInsets.only(right: 12.0), - child: IconButton( - icon: const Icon(Icons.calendar_month_rounded), - onPressed: () { - AFRouterDelegate.of(context) - .navigateToAiringSchedule(); - }, - ), - ), - ], - pinned: true, - automaticallyImplyLeading: false, - ); + Widget _buildAppBar(BuildContext context, TrackUiState state) { + final isAnime = state.currentMediaType == MediaType.anime; + return SliverAppBar( + title: const Text('Track'), + actions: [ + LoadingIndicator(isLoading: state.isLoading), + const SizedBox(width: 10), + isAnime + ? Padding( + padding: const EdgeInsets.only(right: 12.0), + child: IconButton( + icon: const Icon(Icons.calendar_month_rounded), + onPressed: () { + AFRouterDelegate.of(context).navigateToAiringSchedule(); + }, + ), + ) + : const SizedBox(), + ], + pinned: true, + automaticallyImplyLeading: false, + ); + } Widget _buildNoUserHint() { return const SliverToBoxAdapter( diff --git a/pubspec.lock b/pubspec.lock index c8965cc0..dfa4cf7c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -931,7 +931,7 @@ packages: source: hosted version: "1.2.0" synchronized: - dependency: transitive + dependency: "direct main" description: name: synchronized sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" diff --git a/pubspec.yaml b/pubspec.yaml index 2c366910..d06d1493 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -67,6 +67,7 @@ dependencies: collection: ^1.17.2 rxdart: ^0.27.7 country_code: ^1.0.0 + synchronized: ^3.1.0 dev_dependencies: flutter_test: diff --git a/test/core/data/repository/search_repository_test.dart b/test/core/data/repository/search_repository_test.dart index 46dc9fe9..05ef442f 100644 --- a/test/core/data/repository/search_repository_test.dart +++ b/test/core/data/repository/search_repository_test.dart @@ -1,8 +1,10 @@ +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/data/load_result.dart'; import 'package:aniflow/core/data/model/media_model.dart'; import 'package:aniflow/core/data/search_repository.dart'; import 'package:aniflow/core/database/aniflow_database.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; void main() { @@ -13,6 +15,7 @@ void main() { setUp(() async { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; + SharedPreferences.setMockInitialValues({}); await animeDatabase.initDatabase(isTest: true); searchRepository = SearchRepositoryImpl(); @@ -20,7 +23,7 @@ void main() { test('search page', () async { final res = await searchRepository.loadMediaSearchResultByPage( - page: 1, perPage: 3, search: 'Titan'); + page: 1, perPage: 3, search: 'Titan', type: MediaType.anime); expect(res.runtimeType, LoadSuccess>); }); }); diff --git a/test/core/data/repository/user_anime_list_repository_test.dart b/test/core/data/repository/user_anime_list_repository_test.dart index ab9bb945..bfdca999 100644 --- a/test/core/data/repository/user_anime_list_repository_test.dart +++ b/test/core/data/repository/user_anime_list_repository_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: lines_longer_than_80_chars import 'package:aniflow/core/common/model/anime_season.dart'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/common/util/global_static_constants.dart'; import 'package:aniflow/core/data/load_result.dart'; import 'package:aniflow/core/data/media_list_repository.dart'; @@ -27,7 +28,8 @@ void main() { repository = MediaListRepositoryImpl(); isUnitTest = true; - AuthDataSource().setTestToken('eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImFmNjI3ODk4NzQ1YzBhZWIwOGZmYmYyNjA4MTQ4NzgyYWNhMDM0YmM1NWMzZWZlYzE3OTZlNGI0NjgyMGFiNjdhNzljZTA2MTRhNDZlNTVmIn0.eyJhdWQiOiIxNDQwOSIsImp0aSI6ImFmNjI3ODk4NzQ1YzBhZWIwOGZmYmYyNjA4MTQ4NzgyYWNhMDM0YmM1NWMzZWZlYzE3OTZlNGI0NjgyMGFiNjdhNzljZTA2MTRhNDZlNTVmIiwiaWF0IjoxNjk1NjUxMjA1LCJuYmYiOjE2OTU2NTEyMDUsImV4cCI6MTcyNzI3MzYwNSwic3ViIjoiNjM3ODM5MyIsInNjb3BlcyI6W119.XGdU2Zg6tlPoKxKi6VxqqvfPSqmuwj_PW3zKagYyk9j_NIbzkrCFOh9MNLF0X1ifRmB9LcfD5mj4_mj7f63ufwJW7VLrtAaUUu3Vbg8cOwUiPibgnQfVmErv_mThDphLvw4LCEmX7ok13QqC9XMk2AVLNvx9TZW5fo6SfXS4rPySgvHhzMyPc6gbYWkvUTYCBYYTYbWPR7ZcL6gCR1NYPv4NnkVfxFDYF6KKt3nCjcZ7XY894K4GKRIlINoR2KNjflfLzVW2PcopmfQRveXRTKZBmaPeZa5HFbHclpRVe0kxoildpg_4uqqWPjPbUDkuMK3gIHahs2WjqiLc8Le0O9pMRyvaETp27iJVUcoeNFy05vLS6ZzSyoI6tFqfveTiQsrPKLodFjMAVoroTQoIZuX60r9ri-8oj1yWF3Wms4OIHQItkSyJKwezAizrIntRS8kiguuejdS_qWfNTFmMjYOxKU_-d9rcHDjYcVHxr7FPkp7HfpqN1CPVYNn8BMLtRYECMTs-Ds-kY9DDHlLQAWggOtYOR4KsU0sRqOpC4yqmkEdCCZLbdpZOwbCpp-ZLWu1jCICNGI8QEEHfrnvnBHR4KbqguMyOPIVviNXTx82DWNfI_KZTGb5PHuYMleBXacYO9wg0KTYKggVtn6ULxR0ekoA_77otU6x_bf4fW2Q'); + AuthDataSource().setTestToken( + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImFmNjI3ODk4NzQ1YzBhZWIwOGZmYmYyNjA4MTQ4NzgyYWNhMDM0YmM1NWMzZWZlYzE3OTZlNGI0NjgyMGFiNjdhNzljZTA2MTRhNDZlNTVmIn0.eyJhdWQiOiIxNDQwOSIsImp0aSI6ImFmNjI3ODk4NzQ1YzBhZWIwOGZmYmYyNjA4MTQ4NzgyYWNhMDM0YmM1NWMzZWZlYzE3OTZlNGI0NjgyMGFiNjdhNzljZTA2MTRhNDZlNTVmIiwiaWF0IjoxNjk1NjUxMjA1LCJuYmYiOjE2OTU2NTEyMDUsImV4cCI6MTcyNzI3MzYwNSwic3ViIjoiNjM3ODM5MyIsInNjb3BlcyI6W119.XGdU2Zg6tlPoKxKi6VxqqvfPSqmuwj_PW3zKagYyk9j_NIbzkrCFOh9MNLF0X1ifRmB9LcfD5mj4_mj7f63ufwJW7VLrtAaUUu3Vbg8cOwUiPibgnQfVmErv_mThDphLvw4LCEmX7ok13QqC9XMk2AVLNvx9TZW5fo6SfXS4rPySgvHhzMyPc6gbYWkvUTYCBYYTYbWPR7ZcL6gCR1NYPv4NnkVfxFDYF6KKt3nCjcZ7XY894K4GKRIlINoR2KNjflfLzVW2PcopmfQRveXRTKZBmaPeZa5HFbHclpRVe0kxoildpg_4uqqWPjPbUDkuMK3gIHahs2WjqiLc8Le0O9pMRyvaETp27iJVUcoeNFy05vLS6ZzSyoI6tFqfveTiQsrPKLodFjMAVoroTQoIZuX60r9ri-8oj1yWF3Wms4OIHQItkSyJKwezAizrIntRS8kiguuejdS_qWfNTFmMjYOxKU_-d9rcHDjYcVHxr7FPkp7HfpqN1CPVYNn8BMLtRYECMTs-Ds-kY9DDHlLQAWggOtYOR4KsU0sRqOpC4yqmkEdCCZLbdpZOwbCpp-ZLWu1jCICNGI8QEEHfrnvnBHR4KbqguMyOPIVviNXTx82DWNfI_KZTGb5PHuYMleBXacYO9wg0KTYKggVtn6ULxR0ekoA_77otU6x_bf4fW2Q'); }); tearDown(() async { @@ -39,25 +41,31 @@ void main() { }); test('get_user_anime_list_from_data_source', () async { - final res = await repository.syncUserAnimeList(userId: '1'); + final res = await repository.syncUserAnimeList( + userId: '1'); expect(res.runtimeType, equals(LoadSuccess)); }); test('ani_list_get_from_database', () async { - final res = await repository.syncUserAnimeList(userId: '1'); + final res = await repository.syncUserAnimeList( + userId: '1'); expect(res.runtimeType, equals(LoadSuccess)); final res2 = await repository.getUserAnimeList( + type: MediaType.anime, userId: 1, status: [MediaListStatus.current, MediaListStatus.planning]); expect(res2.isNotEmpty, equals(true)); }); test('ani_list_get_stream_test', () async { - final res = await repository.syncUserAnimeList(userId: '1'); + final res = await repository.syncUserAnimeList( + userId: '1'); expect(res.runtimeType, equals(LoadSuccess)); final stream = repository.getUserAnimeListStream( + type: MediaType.anime, + userId: '1', status: [MediaListStatus.current, MediaListStatus.planning]); final res2 = await stream.first; diff --git a/test/core/database/user_anime_list_dao_test.dart b/test/core/database/user_anime_list_dao_test.dart index 6463939a..ffc66ed3 100644 --- a/test/core/database/user_anime_list_dao_test.dart +++ b/test/core/database/user_anime_list_dao_test.dart @@ -1,4 +1,5 @@ import 'package:aniflow/core/common/model/anime_category.dart'; +import 'package:aniflow/core/common/model/media_type.dart'; import 'package:aniflow/core/data/media_list_repository.dart'; import 'package:aniflow/core/database/aniflow_database.dart'; import 'package:aniflow/core/database/model/media_entity.dart'; @@ -24,7 +25,10 @@ void main() { ) ]; - final dummyAnimeData = [MediaEntity(id: '33', englishTitle: 'aaaaaaaaaaa')]; + final dummyMediaData = [ + MediaEntity(id: '33', type: 'ANIME', englishTitle: 'aaaaaaaaaaa'), + MediaEntity(id: '55', type: 'MANGA', englishTitle: 'bbbbbbbbbbb') + ]; setUp(() async { sqfliteFfiInit(); @@ -50,10 +54,12 @@ void main() { test('get_list_by_user_test', () async { final dao = animeDatabase.getMediaListDao(); + final mediaDao = animeDatabase.getMediaInformationDaoDao(); await dao.insertMediaListEntities(dummyUserAnimeListEntity); + await mediaDao.upsertMediaInformation(dummyMediaData); final res = await dao.getMediaListByPage('22', [MediaListStatus.current], - page: 1); + type: MediaType.anime, page: 1); expect(res[0].mediaListEntity, equals(dummyUserAnimeListEntity[0])); }); @@ -64,41 +70,59 @@ void main() { await animeDatabase .getMediaInformationDaoDao() .insertOrIgnoreMediaByAnimeCategory(MediaCategory.movieAnime, - animeList: dummyAnimeData); + animeList: dummyMediaData); final res = await dao.getMediaListByPage('22', [MediaListStatus.current], - page: 1); - expect(res[0].mediaEntity, equals(dummyAnimeData[0])); + type: MediaType.anime, page: 1); + expect(res[0].mediaEntity, equals(dummyMediaData[0])); }); test('get_list_by_multi_status', () async { final dao = animeDatabase.getMediaListDao(); + final mediaDao = animeDatabase.getMediaInformationDaoDao(); await dao.insertMediaListEntities(dummyUserAnimeListEntity); + await mediaDao.upsertMediaInformation(dummyMediaData); final res = await dao.getMediaListByPage( '22', [MediaListStatus.current, MediaListStatus.dropped], - page: 1); - expect(res.length, equals(2)); + type: MediaType.anime, page: 1); + expect(res[0].mediaEntity, equals(dummyMediaData[0])); }); test('get_list_no_limit', () async { final dao = animeDatabase.getMediaListDao(); + final mediaDao = animeDatabase.getMediaInformationDaoDao(); await dao.insertMediaListEntities(dummyUserAnimeListEntity); + await mediaDao.upsertMediaInformation(dummyMediaData); final res = await dao.getMediaListByPage( '22', [MediaListStatus.current, MediaListStatus.dropped], - page: 1, perPage: null); - expect(res.length, equals(2)); + type: MediaType.anime, page: 1, perPage: null); + expect(res[0].mediaEntity, equals(dummyMediaData[0])); }); - test('get_list_ids_stream', () async { - final dao = animeDatabase.getMediaListDao(); - await dao.insertMediaListEntities(dummyUserAnimeListEntity); + test('get_anime_list_ids_stream', () async { + final listDao = animeDatabase.getMediaListDao(); + final mediaDao = animeDatabase.getMediaInformationDaoDao(); + await listDao.insertMediaListEntities(dummyUserAnimeListEntity); + await mediaDao.upsertMediaInformation(dummyMediaData); + + final stream = listDao.getMediaListMediaIdsByUserStream('22', + [MediaListStatus.current, MediaListStatus.dropped], MediaType.anime); + final res = await stream.first; + expect(res, equals({'33'})); + }); + + test('get_manga_list_ids_stream', () async { + final listDao = animeDatabase.getMediaListDao(); + final mediaDao = animeDatabase.getMediaInformationDaoDao(); + await listDao.insertMediaListEntities(dummyUserAnimeListEntity); + await mediaDao.upsertMediaInformation(dummyMediaData); - final stream = dao.getMediaListMediaIdsByUserStream( - '22', [MediaListStatus.current, MediaListStatus.dropped]); + final stream = listDao.getMediaListMediaIdsByUserStream('22', + [MediaListStatus.current, MediaListStatus.dropped], MediaType.manga); final res = await stream.first; - expect(res, equals({'33', '55'})); + expect(res, equals({'55'})); }); test('get_list_ids_stream', () async { diff --git a/test/core/network/model/detail_network_anime_model_test.dart b/test/core/network/model/detail_network_anime_model_test.dart index 4f1b9bef..67cba5a9 100644 --- a/test/core/network/model/detail_network_anime_model_test.dart +++ b/test/core/network/model/detail_network_anime_model_test.dart @@ -1,6 +1,6 @@ // ignore_for_file: lines_longer_than_80_chars -import 'package:aniflow/core/network/model/anime_dto.dart'; +import 'package:aniflow/core/network/model/media_dto.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -175,7 +175,7 @@ void main() { } }; test('detail_anime', () async { - AnimeDto.fromJson(dummyData); + MediaDto.fromJson(dummyData); }); }); } diff --git a/test/core/network/model/short_network_anime_model_test.dart b/test/core/network/model/short_network_anime_model_test.dart index e47a454b..4f58e897 100644 --- a/test/core/network/model/short_network_anime_model_test.dart +++ b/test/core/network/model/short_network_anime_model_test.dart @@ -1,5 +1,5 @@ import 'package:aniflow/core/data/model/media_title_modle.dart'; -import 'package:aniflow/core/network/model/anime_dto.dart'; +import 'package:aniflow/core/network/model/media_dto.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -22,9 +22,9 @@ void main() { } }; test('get_topics', () async { - final res = AnimeDto.fromJson(dummyData); + final res = MediaDto.fromJson(dummyData); expect(res, equals( - AnimeDto( + MediaDto( id: 124, title: MediaTitle( romaji: "Fushigi Yuugi: Eikoden", diff --git a/test/feature/anime_search/bloc/anime_search_bloc_test.dart b/test/feature/anime_search/bloc/anime_search_bloc_test.dart index 86aa1f69..665ab874 100644 --- a/test/feature/anime_search/bloc/anime_search_bloc_test.dart +++ b/test/feature/anime_search/bloc/anime_search_bloc_test.dart @@ -1,7 +1,10 @@ import 'package:aniflow/core/data/search_repository.dart'; +import 'package:aniflow/core/data/user_data_repository.dart'; import 'package:aniflow/core/database/aniflow_database.dart'; +import 'package:aniflow/core/shared_preference/aniflow_prefrences.dart'; import 'package:aniflow/feature/anime_search/bloc/anime_search_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; void main() { @@ -12,9 +15,13 @@ void main() { setUp(() async { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; + SharedPreferences.setMockInitialValues({}); + await AniFlowPreferences().init(); await animeDatabase.initDatabase(isTest: true); - searchPageBloc = SearchPageBloc(searchRepository: SearchRepositoryImpl()); + searchPageBloc = SearchPageBloc( + searchRepository: SearchRepositoryImpl(), + userDataRepository: UserDataRepositoryImpl()); }); test('test_search_bloc', () async {