Skip to content

Commit

Permalink
Merge pull request #40 from andannn/full_support_for_manga
Browse files Browse the repository at this point in the history
Full support for manga
  • Loading branch information
andannn authored Oct 21, 2023
2 parents 5b27fbc + f4f5c0d commit e9e83c1
Show file tree
Hide file tree
Showing 53 changed files with 1,486 additions and 275 deletions.
88 changes: 66 additions & 22 deletions lib/app/app.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -103,6 +107,12 @@ class _AnimeTrackerAppScaffoldState extends State<AnimeTrackerAppScaffold> {

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() {
Expand All @@ -112,15 +122,24 @@ class _AnimeTrackerAppScaffoldState extends State<AnimeTrackerAppScaffold> {
currentNavigation =
animeTrackerRouterDelegate.currentTopLevelNavigation;
needHideNavigationBar = animeTrackerRouterDelegate.isTopRouteFullScreen;
isTopLevelNavigation = animeTrackerRouterDelegate.isTopLevelNavigation;
});
});
_mediaTypeSub = userDataRepository.getMediaTypeStream().distinct().listen(
(mediaType) {
setState(() {
_mediaType = mediaType;
});
},
);
}

@override
void dispose() {
super.dispose();

animeTrackerRouterDelegate.dispose();
_mediaTypeSub.cancel();
}

@override
Expand All @@ -129,43 +148,50 @@ class _AnimeTrackerAppScaffoldState extends State<AnimeTrackerAppScaffold> {
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => DiscoverBloc(
userDataRepository: context.read<UserDataRepository>(),
aniListRepository: context.read<MediaInformationRepository>(),
authRepository: context.read<AuthRepository>(),
animeTrackListRepository: context.read<MediaListRepository>(),
),
create: (context) =>
DiscoverBloc(
userDataRepository: context.read<UserDataRepository>(),
aniListRepository: context.read<MediaInformationRepository>(),
authRepository: context.read<AuthRepository>(),
animeTrackListRepository: context.read<MediaListRepository>(),
),
),
BlocProvider(
create: (context) => TrackBloc(
animeTrackListRepository: context.read<MediaListRepository>(),
authRepository: context.read<AuthRepository>(),
),
create: (context) =>
TrackBloc(
userDataRepository: context.read<UserDataRepository>(),
mediaListRepository: context.read<MediaListRepository>(),
authRepository: context.read<AuthRepository>(),
),
),
],
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) {
Expand All @@ -176,4 +202,22 @@ class _AnimeTrackerAppScaffoldState extends State<AnimeTrackerAppScaffold> {
selectedIndex: currentIndex,
);
}

Widget _buildTopFloatingActionButton() {
return FloatingActionButton.extended(
onPressed: () {
final repository = context.read<UserDataRepository>();
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'),
);
}
}
2 changes: 2 additions & 0 deletions lib/app/navigation/ani_flow_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class AFRouterDelegate extends RouterDelegate<AniFlowRoutePath>

bool get isTopRouteFullScreen => _backStack.last.isFullScreen;

bool get isTopLevelNavigation => _backStack.last is TopLevelRoutePath;

static AFRouterDelegate of(context) =>
Router
.of(context)
Expand Down
9 changes: 4 additions & 5 deletions lib/core/common/model/media_type.dart
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion lib/core/common/util/stream_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ mixin StreamUtil {
late StreamController<T> controller;

void listener() async {
controller.add(await getEventData());
final data = await getEventData();
if (!controller.isClosed) {
controller.add(data);
}
}

controller = StreamController(onListen: () {
Expand Down
4 changes: 2 additions & 2 deletions lib/core/data/media_information_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -151,7 +151,7 @@ class MediaInformationRepositoryImpl extends MediaInformationRepository {
Future<LoadResult<void>> startFetchDetailAnimeInfo(String id) async {
try {
/// fetch anime info from network.
AnimeDto networkResult = await aniListDataSource.getNetworkAnime(
MediaDto networkResult = await aniListDataSource.getNetworkAnime(
id: int.parse(id),
);

Expand Down
41 changes: 31 additions & 10 deletions lib/core/data/media_list_repository.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -22,17 +23,25 @@ part 'model/media_list_status.dart';
abstract class MediaListRepository {
Future<List<MediaListItemModel>> getUserAnimeList(
{required List<MediaListStatus> status,
required MediaType type,
int page = 1,
int? userId,
int perPage = Config.defaultPerPageCount});

Stream<List<MediaListItemModel>> getUserAnimeListStream(
{required List<MediaListStatus> status, required String userId});
{required List<MediaListStatus> status,
required String userId,
required MediaType type});

Future<LoadResult<void>> syncUserAnimeList({String? userId});
Future<LoadResult<void>> syncUserAnimeList(
{String? userId,
List<MediaListStatus> status = const [],
MediaType? mediaType});

Stream<Set<String>> getMediaListAnimeIdsByUserStream(
String userId, List<MediaListStatus> status);
Stream<Set<String>> getMediaListMediaIdsByUserStream(
{required String userId,
required List<MediaListStatus> status,
required MediaType type});

Stream<bool> getIsTrackingByUserAndIdStream(
{required String userId, required String animeId});
Expand All @@ -58,6 +67,7 @@ class MediaListRepositoryImpl extends MediaListRepository {
@override
Future<List<MediaListItemModel>> getUserAnimeList(
{required List<MediaListStatus> status,
required MediaType type,
int page = 1,
int? userId,
int perPage = Config.defaultPerPageCount}) async {
Expand All @@ -68,6 +78,7 @@ class MediaListRepositoryImpl extends MediaListRepository {
}

final animeList = await animeTrackListDao.getMediaListByPage(
type: type,
targetUserId.toString(), status,
page: 1, perPage: null);

Expand All @@ -77,7 +88,10 @@ class MediaListRepositoryImpl extends MediaListRepository {
}

@override
Future<LoadResult<void>> syncUserAnimeList({String? userId}) async {
Future<LoadResult<void>> syncUserAnimeList(
{String? userId,
List<MediaListStatus> status = const [],
MediaType? mediaType}) async {
try {
final targetUserId = userId ?? (await userDataDao.getUserData())?.id;
if (targetUserId == null) {
Expand All @@ -90,6 +104,8 @@ class MediaListRepositoryImpl extends MediaListRepository {
param: UserAnimeListPageQueryParam(
page: 1,
perPage: null,
mediaType: mediaType,
status: status,
userId: int.parse(targetUserId.toString()),
),
);
Expand Down Expand Up @@ -120,18 +136,23 @@ class MediaListRepositoryImpl extends MediaListRepository {

@override
Stream<List<MediaListItemModel>> getUserAnimeListStream(
{required List<MediaListStatus> status, required String userId}) {
return animeTrackListDao.getMediaListStream(userId, status).map(
{required List<MediaListStatus> status,
required String userId,
required MediaType type}) {
return animeTrackListDao.getMediaListStream(userId, status, type).map(
(models) => models
.map((e) => MediaListItemModel.fromDataBaseModel(e))
.toList(),
);
}

@override
Stream<Set<String>> getMediaListAnimeIdsByUserStream(
String userId, List<MediaListStatus> status) {
return animeTrackListDao.getMediaListMediaIdsByUserStream(userId, status);
Stream<Set<String>> getMediaListMediaIdsByUserStream(
{required String userId,
required List<MediaListStatus> status,
required MediaType type}) {
return animeTrackListDao.getMediaListMediaIdsByUserStream(
userId, status, type);
}

@override
Expand Down
12 changes: 10 additions & 2 deletions lib/core/data/model/extension/media_list_item_model_extension.dart
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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;
Expand Down
9 changes: 7 additions & 2 deletions lib/core/data/model/media_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ 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';
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';
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down
Loading

0 comments on commit e9e83c1

Please sign in to comment.