From 85aa687606018d15cab02c29006d80ed7b5ee5aa Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Wed, 11 Sep 2024 17:35:12 +0200 Subject: [PATCH] added feed feature --- lib/l10n/app_en.arb | 2 + lib/models/feed.dart | 5 + lib/models/feed.g.dart | 210 +++++++++- lib/modules/feed/feed_screen.dart | 361 ++++++++++++++++++ .../history/providers/isar_providers.dart | 12 + .../history/providers/isar_providers.g.dart | 129 +++++++ lib/modules/library/library_screen.dart | 6 + lib/modules/main_view/main_screen.dart | 89 ++++- .../update_manga_detail_providers.dart | 5 +- .../update_manga_detail_providers.g.dart | 2 +- .../backup_and_restore/providers/restore.dart | 21 + .../providers/restore.g.dart | 2 +- lib/router/router.dart | 10 + lib/services/sync_server.dart | 59 ++- lib/services/sync_server.g.dart | 2 +- 15 files changed, 893 insertions(+), 22 deletions(-) create mode 100644 lib/modules/feed/feed_screen.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 139a5f8..241a9f2 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -3,6 +3,7 @@ "library": "Library", "updates": "Updates", "history": "History", + "feed": "Feed", "browse": "Browse", "more": "More", "open_random_entry": "Open random entry", @@ -39,6 +40,7 @@ "no_recent_updates": "No recent updates", "remove_everything": "Remove everything", "remove_everything_msg": "Are you sure? All history will be lost", + "remove_all_feed_msg": "Are you sure? The whole feed will be cleared", "ok": "OK", "cancel": "Cancel", "remove": "Remove", diff --git a/lib/models/feed.dart b/lib/models/feed.dart index 8dba0b2..7ef4dca 100644 --- a/lib/models/feed.dart +++ b/lib/models/feed.dart @@ -9,6 +9,8 @@ class Feed { int? mangaId; + String? chapterName; + final chapter = IsarLink(); String? date; @@ -16,18 +18,21 @@ class Feed { Feed({ this.id = Isar.autoIncrement, required this.mangaId, + required this.chapterName, required this.date, }); Feed.fromJson(Map json) { id = json['id']; mangaId = json['mangaId']; + mangaId = json['chapterName']; date = json['date']; } Map toJson() => { 'id': id, 'mangaId': mangaId, + 'chapterName': chapterName, 'date': date, }; } diff --git a/lib/models/feed.g.dart b/lib/models/feed.g.dart index eb1694b..e8c8ce2 100644 --- a/lib/models/feed.g.dart +++ b/lib/models/feed.g.dart @@ -17,13 +17,18 @@ const FeedSchema = CollectionSchema( name: r'Feed', id: 8879644747771893978, properties: { - r'date': PropertySchema( + r'chapterName': PropertySchema( id: 0, + name: r'chapterName', + type: IsarType.string, + ), + r'date': PropertySchema( + id: 1, name: r'date', type: IsarType.string, ), r'mangaId': PropertySchema( - id: 1, + id: 2, name: r'mangaId', type: IsarType.long, ) @@ -55,6 +60,12 @@ int _feedEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; + { + final value = object.chapterName; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final value = object.date; if (value != null) { @@ -70,8 +81,9 @@ void _feedSerialize( List offsets, Map> allOffsets, ) { - writer.writeString(offsets[0], object.date); - writer.writeLong(offsets[1], object.mangaId); + writer.writeString(offsets[0], object.chapterName); + writer.writeString(offsets[1], object.date); + writer.writeLong(offsets[2], object.mangaId); } Feed _feedDeserialize( @@ -81,9 +93,10 @@ Feed _feedDeserialize( Map> allOffsets, ) { final object = Feed( - date: reader.readStringOrNull(offsets[0]), + chapterName: reader.readStringOrNull(offsets[0]), + date: reader.readStringOrNull(offsets[1]), id: id, - mangaId: reader.readLongOrNull(offsets[1]), + mangaId: reader.readLongOrNull(offsets[2]), ); return object; } @@ -98,6 +111,8 @@ P _feedDeserializeProp

( case 0: return (reader.readStringOrNull(offset)) as P; case 1: + return (reader.readStringOrNull(offset)) as P; + case 2: return (reader.readLongOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -193,6 +208,152 @@ extension FeedQueryWhere on QueryBuilder { } extension FeedQueryFilter on QueryBuilder { + QueryBuilder chapterNameIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'chapterName', + )); + }); + } + + QueryBuilder chapterNameIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'chapterName', + )); + }); + } + + QueryBuilder chapterNameEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'chapterName', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'chapterName', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'chapterName', + value: '', + )); + }); + } + + QueryBuilder chapterNameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'chapterName', + value: '', + )); + }); + } + QueryBuilder dateIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -492,6 +653,18 @@ extension FeedQueryLinks on QueryBuilder { } extension FeedQuerySortBy on QueryBuilder { + QueryBuilder sortByChapterName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'chapterName', Sort.asc); + }); + } + + QueryBuilder sortByChapterNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'chapterName', Sort.desc); + }); + } + QueryBuilder sortByDate() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'date', Sort.asc); @@ -518,6 +691,18 @@ extension FeedQuerySortBy on QueryBuilder { } extension FeedQuerySortThenBy on QueryBuilder { + QueryBuilder thenByChapterName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'chapterName', Sort.asc); + }); + } + + QueryBuilder thenByChapterNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'chapterName', Sort.desc); + }); + } + QueryBuilder thenByDate() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'date', Sort.asc); @@ -556,6 +741,13 @@ extension FeedQuerySortThenBy on QueryBuilder { } extension FeedQueryWhereDistinct on QueryBuilder { + QueryBuilder distinctByChapterName( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'chapterName', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByDate( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -577,6 +769,12 @@ extension FeedQueryProperty on QueryBuilder { }); } + QueryBuilder chapterNameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'chapterName'); + }); + } + QueryBuilder dateProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'date'); diff --git a/lib/modules/feed/feed_screen.dart b/lib/modules/feed/feed_screen.dart new file mode 100644 index 0000000..11ddcda --- /dev/null +++ b/lib/modules/feed/feed_screen.dart @@ -0,0 +1,361 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:grouped_list/sliver_grouped_list.dart'; + +import 'package:isar/isar.dart'; +import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/feed.dart'; +import 'package:mangayomi/models/history.dart'; +import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/modules/history/providers/isar_providers.dart'; +import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/utils/cached_network.dart'; +import 'package:mangayomi/utils/constant.dart'; +import 'package:mangayomi/utils/date.dart'; +import 'package:mangayomi/utils/extensions/chapter.dart'; +import 'package:mangayomi/utils/headers.dart'; +import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart'; +import 'package:mangayomi/modules/widgets/error_text.dart'; +import 'package:mangayomi/modules/widgets/progress_center.dart'; + +class FeedScreen extends ConsumerStatefulWidget { + const FeedScreen({super.key}); + + @override + ConsumerState createState() => _FeedScreenState(); +} + +class _FeedScreenState extends ConsumerState + with TickerProviderStateMixin { + late TabController _tabBarController; + + @override + void initState() { + _tabBarController = TabController(length: 2, vsync: this); + _tabBarController.animateTo(0); + _tabBarController.addListener(() { + setState(() { + _textEditingController.clear(); + _isSearch = false; + }); + }); + super.initState(); + } + + final _textEditingController = TextEditingController(); + bool _isSearch = false; + List entriesData = []; + @override + Widget build(BuildContext context) { + final l10n = l10nLocalizations(context)!; + return DefaultTabController( + animationDuration: Duration.zero, + length: 2, + child: Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.transparent, + title: _isSearch + ? null + : Text( + l10n.feed, + style: TextStyle(color: Theme.of(context).hintColor), + ), + actions: [ + _isSearch + ? SeachFormTextField( + onChanged: (value) { + setState(() {}); + }, + onSuffixPressed: () { + _textEditingController.clear(); + setState(() {}); + }, + onPressed: () { + setState(() { + _isSearch = false; + }); + _textEditingController.clear(); + }, + controller: _textEditingController, + ) + : IconButton( + splashRadius: 20, + onPressed: () { + setState(() { + _isSearch = true; + }); + }, + icon: + Icon(Icons.search, color: Theme.of(context).hintColor)), + IconButton( + splashRadius: 20, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + l10n.remove_everything, + ), + content: Text(l10n.remove_all_feed_msg), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(l10n.cancel)), + const SizedBox( + width: 15, + ), + TextButton( + onPressed: () { + List feeds = isar.feeds + .filter() + .idIsNotNull() + .chapter((q) => q.manga((q) => + q.isMangaEqualTo( + _tabBarController.index == + 0))) + .findAllSync() + .toList(); + isar.writeTxnSync(() { + for (var feed in feeds) { + isar.feeds.deleteSync(feed.id!); + } + }); + if (mounted) { + Navigator.pop(context); + } + }, + child: Text(l10n.ok)), + ], + ) + ], + ); + }); + }, + icon: Icon(Icons.delete_sweep_outlined, + color: Theme.of(context).hintColor)), + ], + bottom: TabBar( + indicatorSize: TabBarIndicatorSize.tab, + controller: _tabBarController, + tabs: [ + Tab(text: l10n.manga), + Tab(text: l10n.anime), + ], + ), + ), + body: Padding( + padding: const EdgeInsets.only(top: 10), + child: TabBarView(controller: _tabBarController, children: [ + FeedTab( + isManga: true, + query: _textEditingController.text, + ), + FeedTab( + isManga: false, + query: _textEditingController.text, + ) + ]), + ), + ), + ); + } +} + +class FeedTab extends ConsumerStatefulWidget { + final String query; + final bool isManga; + const FeedTab({required this.isManga, required this.query, super.key}); + + @override + ConsumerState createState() => _FeedTabState(); +} + +class _FeedTabState extends ConsumerState { + @override + Widget build(BuildContext context) { + final l10n = l10nLocalizations(context)!; + final feed = + ref.watch(getAllFeedStreamProvider(isManga: widget.isManga)); + return Scaffold( + body: feed.when( + data: (data) { + final entries = data + .where((element) => widget.query.isNotEmpty + ? element.chapter.value!.manga.value!.name! + .toLowerCase() + .contains(widget.query.toLowerCase()) + : true) + .toList(); + + if (entries.isNotEmpty) { + return CustomScrollView( + slivers: [ + SliverGroupedListView( + elements: entries, + groupBy: (element) => dateFormat(element.date!, + context: context, + ref: ref, + forHistoryValue: true, + useRelativeTimesTamps: false), + groupSeparatorBuilder: (String groupByValue) => Padding( + padding: const EdgeInsets.only(bottom: 8, left: 12), + child: Row( + children: [ + Text(dateFormat( + null, + context: context, + stringDate: groupByValue, + ref: ref, + )), + ], + ), + ), + itemBuilder: (context, Feed element) { + final manga = element.chapter.value!.manga.value!; + final chapter = element.chapter.value!; + return ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(0), + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(0)), + elevation: 0, + shadowColor: Colors.transparent), + onPressed: () { + chapter.pushToReaderView(context); + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SizedBox( + height: 105, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 60, + height: 90, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(7)), + ), + onPressed: () { + context.push('/manga-reader/detail', + extra: manga.id); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(7), + child: manga.customCoverImage != null + ? Image.memory( + manga.customCoverImage as Uint8List) + : cachedNetworkImage( + headers: ref.watch(headersProvider( + source: manga.source!, + lang: manga.lang!)), + imageUrl: toImgUrl( + manga.customCoverFromTracker ?? + manga.imageUrl ?? + ""), + width: 60, + height: 90, + fit: BoxFit.cover), + ), + ), + ), + Flexible( + child: Row( + children: [ + Expanded( + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + manga.name!, + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .textTheme + .bodyLarge! + .color, + fontWeight: FontWeight.bold), + textAlign: TextAlign.start, + ), + Wrap( + crossAxisAlignment: + WrapCrossAlignment.end, + children: [ + Text( + chapter.name!, + style: TextStyle( + fontSize: 11, + color: Theme.of(context) + .textTheme + .bodyLarge! + .color, + ), + ), + Text( + " - ${dateFormatHour(element.date!, context)}", + style: TextStyle( + fontSize: 11, + color: Theme.of(context) + .textTheme + .bodyLarge! + .color, + fontWeight: + FontWeight.w400), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ) + ], + ), + ), + ), + ); + }, + itemComparator: (item1, item2) => + item1.date!.compareTo(item2.date!), + order: GroupedListOrder.DESC, + ), + ], + ); + } + return Center( + child: Text(l10n.nothing_read_recently), + ); + }, + error: (Object error, StackTrace stackTrace) { + return ErrorText(error); + }, + loading: () { + return const ProgressCenter(); + }, + )); + } +} diff --git a/lib/modules/history/providers/isar_providers.dart b/lib/modules/history/providers/isar_providers.dart index b950e0a..a08b3b9 100644 --- a/lib/modules/history/providers/isar_providers.dart +++ b/lib/modules/history/providers/isar_providers.dart @@ -1,6 +1,7 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/feed.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -16,3 +17,14 @@ Stream> getAllHistoryStream(GetAllHistoryStreamRef ref, .chapter((q) => q.manga((q) => q.isMangaEqualTo(isManga))) .watch(fireImmediately: true); } + +@riverpod +Stream> getAllFeedStream(GetAllFeedStreamRef ref, + {required bool isManga}) async* { + yield* isar.feeds + .filter() + .idIsNotNull() + .and() + .chapter((q) => q.manga((q) => q.isMangaEqualTo(isManga))) + .watch(fireImmediately: true); +} diff --git a/lib/modules/history/providers/isar_providers.g.dart b/lib/modules/history/providers/isar_providers.g.dart index 6518fcb..f370b20 100644 --- a/lib/modules/history/providers/isar_providers.g.dart +++ b/lib/modules/history/providers/isar_providers.g.dart @@ -157,5 +157,134 @@ class _GetAllHistoryStreamProviderElement @override bool get isManga => (origin as GetAllHistoryStreamProvider).isManga; } + +String _$getAllFeedStreamHash() => r'3d60bca5377bf6fc2aee36e7bec5b319b2377add'; + +/// See also [getAllFeedStream]. +@ProviderFor(getAllFeedStream) +const getAllFeedStreamProvider = GetAllFeedStreamFamily(); + +/// See also [getAllFeedStream]. +class GetAllFeedStreamFamily extends Family>> { + /// See also [getAllFeedStream]. + const GetAllFeedStreamFamily(); + + /// See also [getAllFeedStream]. + GetAllFeedStreamProvider call({ + required bool isManga, + }) { + return GetAllFeedStreamProvider( + isManga: isManga, + ); + } + + @override + GetAllFeedStreamProvider getProviderOverride( + covariant GetAllFeedStreamProvider provider, + ) { + return call( + isManga: provider.isManga, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'getAllFeedStreamProvider'; +} + +/// See also [getAllFeedStream]. +class GetAllFeedStreamProvider extends AutoDisposeStreamProvider> { + /// See also [getAllFeedStream]. + GetAllFeedStreamProvider({ + required bool isManga, + }) : this._internal( + (ref) => getAllFeedStream( + ref as GetAllFeedStreamRef, + isManga: isManga, + ), + from: getAllFeedStreamProvider, + name: r'getAllFeedStreamProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$getAllFeedStreamHash, + dependencies: GetAllFeedStreamFamily._dependencies, + allTransitiveDependencies: + GetAllFeedStreamFamily._allTransitiveDependencies, + isManga: isManga, + ); + + GetAllFeedStreamProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.isManga, + }) : super.internal(); + + final bool isManga; + + @override + Override overrideWith( + Stream> Function(GetAllFeedStreamRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: GetAllFeedStreamProvider._internal( + (ref) => create(ref as GetAllFeedStreamRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + isManga: isManga, + ), + ); + } + + @override + AutoDisposeStreamProviderElement> createElement() { + return _GetAllFeedStreamProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is GetAllFeedStreamProvider && other.isManga == isManga; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, isManga.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin GetAllFeedStreamRef on AutoDisposeStreamProviderRef> { + /// The parameter `isManga` of this provider. + bool get isManga; +} + +class _GetAllFeedStreamProviderElement + extends AutoDisposeStreamProviderElement> + with GetAllFeedStreamRef { + _GetAllFeedStreamProviderElement(super.provider); + + @override + bool get isManga => (origin as GetAllFeedStreamProvider).isManga; +} // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/modules/library/library_screen.dart b/lib/modules/library/library_screen.dart index 5f09c80..89985dd 100644 --- a/lib/modules/library/library_screen.dart +++ b/lib/modules/library/library_screen.dart @@ -16,6 +16,7 @@ import 'package:mangayomi/models/download.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; +import 'package:mangayomi/models/feed.dart'; import 'package:mangayomi/modules/library/providers/add_torrent.dart'; import 'package:mangayomi/modules/library/providers/local_archive.dart'; import 'package:mangayomi/modules/manga/detail/providers/update_manga_detail_providers.dart'; @@ -1164,6 +1165,11 @@ class _LibraryScreenState extends ConsumerState .notifier) .addUpdatedChapter( chapter, true, false); + isar.feeds + .filter() + .mangaIdEqualTo(chapter.mangaId) + .chapterNameEqualTo(chapter.name) + .deleteAllSync(); isar.chapters.deleteSync(chapter.id!); } ref diff --git a/lib/modules/main_view/main_screen.dart b/lib/modules/main_view/main_screen.dart index 2fe7bb5..d46f9fd 100644 --- a/lib/modules/main_view/main_screen.dart +++ b/lib/modules/main_view/main_screen.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/feed.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/modules/widgets/loading_icon.dart'; import 'package:mangayomi/services/fetch_anime_sources.dart'; @@ -45,8 +46,9 @@ class MainScreen extends ConsumerWidget { '/MangaLibrary' => 0, '/AnimeLibrary' => 1, '/history' => 2, - '/browse' => 3, - _ => 4, + '/feed' => 3, + '/browse' => 4, + _ => 5, }; final incognitoMode = ref.watch(incognitoModeStateProvider); @@ -96,6 +98,7 @@ class MainScreen extends ConsumerWidget { != '/MangaLibrary' && != '/AnimeLibrary' && != '/history' && + != '/feed' && != '/browse' && != '/more' => 0, @@ -141,6 +144,36 @@ class MainScreen extends ConsumerWidget { padding: const EdgeInsets.only(top: 5), child: Text(l10n.history))), + NavigationRailDestination( + selectedIcon: Stack( + children: [ + const Icon(Icons.rss_feed), + Positioned( + right: 0, + top: 0, + child: _feedTotalNumbers( + ref, false)) + ], + ), + icon: Stack( + children: [ + const Icon( + Icons.rss_feed_outlined), + Positioned( + right: 0, + top: 0, + child: _feedTotalNumbers( + ref, false)) + ], + ), + label: Padding( + padding: + const EdgeInsets.only(top: 5), + child: Stack( + children: [ + Text(l10n.feed), + ], + ))), NavigationRailDestination( selectedIcon: const Icon(Icons.explore), @@ -169,8 +202,10 @@ class MainScreen extends ConsumerWidget { } else if (newIndex == 2) { route.go('/history'); } else if (newIndex == 3) { - route.go('/browse'); + route.go('/feed'); } else if (newIndex == 4) { + route.go('/browse'); + } else if (newIndex == 5) { route.go('/more'); } }, @@ -199,6 +234,7 @@ class MainScreen extends ConsumerWidget { != '/MangaLibrary' && != '/AnimeLibrary' && != '/history' && + != '/feed' && != '/browse' && != '/more' => 0, @@ -231,6 +267,18 @@ class MainScreen extends ConsumerWidget { selectedIcon: const Icon(Icons.history), icon: const Icon(Icons.history_outlined), label: l10n.history), + Stack( + children: [ + NavigationDestination( + selectedIcon: const Icon(Icons.rss_feed), + icon: const Icon(Icons.rss_feed_outlined), + label: l10n.feed), + Positioned( + right: 14, + top: 3, + child: _feedTotalNumbers(ref, true)), + ], + ), Stack( children: [ NavigationDestination( @@ -315,3 +363,38 @@ Widget _extensionUpdateTotalNumbers(WidgetRef ref) { return Container(); }); } + +Widget _feedTotalNumbers(WidgetRef ref, bool mobile) { + return StreamBuilder( + stream: isar.feeds.filter().idIsNotNull().watch(fireImmediately: true), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + final entries = snapshot.data!.where((element) { + if (!element.chapter.isLoaded) { + element.chapter.loadSync(); + } + return !(element.chapter.value?.isRead ?? false); + }).toList(); + return entries.isEmpty + ? Container() + : Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: const Color.fromARGB(255, 176, 46, 37)), + child: Padding( + padding: mobile + ? const EdgeInsets.symmetric(horizontal: 5, vertical: 3) + : const EdgeInsets.symmetric( + horizontal: 3, vertical: 1), + child: Text( + entries.length.toString(), + style: TextStyle( + fontSize: 10, + color: Theme.of(context).textTheme.bodySmall!.color), + ), + ), + ); + } + return Container(); + }); +} diff --git a/lib/modules/manga/detail/providers/update_manga_detail_providers.dart b/lib/modules/manga/detail/providers/update_manga_detail_providers.dart index 6f2b631..d1370e9 100644 --- a/lib/modules/manga/detail/providers/update_manga_detail_providers.dart +++ b/lib/modules/manga/detail/providers/update_manga_detail_providers.dart @@ -86,7 +86,10 @@ Future updateMangaDetail(UpdateMangaDetailRef ref, chap.manga.saveSync(); final savedChapter = isar.chapters.getSync(chap.id!); if (savedChapter != null) { - final feed = Feed(mangaId: mangaId, date: savedChapter.dateUpload) + final feed = Feed( + mangaId: mangaId, + chapterName: savedChapter.name, + date: savedChapter.dateUpload) ..chapter.value = savedChapter; isar.feeds.putSync(feed); feed.chapter.saveSync(); diff --git a/lib/modules/manga/detail/providers/update_manga_detail_providers.g.dart b/lib/modules/manga/detail/providers/update_manga_detail_providers.g.dart index 4a7e8e5..c314898 100644 --- a/lib/modules/manga/detail/providers/update_manga_detail_providers.g.dart +++ b/lib/modules/manga/detail/providers/update_manga_detail_providers.g.dart @@ -6,7 +6,7 @@ part of 'update_manga_detail_providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$updateMangaDetailHash() => r'36381242c5d911c4c656e57f72135f5f1e377198'; +String _$updateMangaDetailHash() => r'c21ac4f7725b5ac4403902bac07a3b5462488bbd'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/more/backup_and_restore/providers/restore.dart b/lib/modules/more/backup_and_restore/providers/restore.dart index ba74807..8aab257 100644 --- a/lib/modules/more/backup_and_restore/providers/restore.dart +++ b/lib/modules/more/backup_and_restore/providers/restore.dart @@ -2,12 +2,14 @@ import 'dart:convert'; import 'package:archive/archive_io.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; +import 'package:isar/isar.dart'; import 'package:mangayomi/eval/dart/model/m_bridge.dart'; import 'package:mangayomi/eval/dart/model/source_preference.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/category.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; +import 'package:mangayomi/models/feed.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; @@ -59,6 +61,8 @@ void doRestore(DoRestoreRef ref, final extensionsPref = (backup["extensions_preferences"] as List?) ?.map((e) => SourcePreference.fromJson(e)) .toList(); + final feeds = + (backup["feeds"] as List?)?.map((e) => Feed.fromJson(e)).toList(); isar.writeTxnSync(() { isar.mangas.clearSync(); @@ -95,6 +99,23 @@ void doRestore(DoRestoreRef ref, } } } + + isar.feeds.clearSync(); + if (feeds != null) { + final tempChapters = + isar.chapters.filter().idIsNotNull().findAllSync().toList(); + for (var feed in feeds) { + final matchingChapter = tempChapters + .where((chapter) => + chapter.mangaId == feed.mangaId && + chapter.name == feed.chapterName) + .firstOrNull; + if (matchingChapter != null) { + isar.feeds.putSync(feed..chapter.value = matchingChapter); + feed.chapter.saveSync(); + } + } + } } isar.categorys.clearSync(); diff --git a/lib/modules/more/backup_and_restore/providers/restore.g.dart b/lib/modules/more/backup_and_restore/providers/restore.g.dart index 2789683..d14af08 100644 --- a/lib/modules/more/backup_and_restore/providers/restore.g.dart +++ b/lib/modules/more/backup_and_restore/providers/restore.g.dart @@ -6,7 +6,7 @@ part of 'restore.dart'; // RiverpodGenerator // ************************************************************************** -String _$doRestoreHash() => r'3c88ad8ba80c245a4b511961111f7ab79c0d330f'; +String _$doRestoreHash() => r'823b26bade20d89ae7b7b56a7eb7c25020795b45'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/router/router.dart b/lib/router/router.dart index 328ddd2..d056590 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -10,6 +10,7 @@ import 'package:mangayomi/modules/browse/extension/edit_code.dart'; import 'package:mangayomi/modules/browse/extension/extension_detail.dart'; import 'package:mangayomi/modules/browse/extension/widgets/create_extension.dart'; import 'package:mangayomi/modules/browse/sources/sources_filter_screen.dart'; +import 'package:mangayomi/modules/feed/feed_screen.dart'; import 'package:mangayomi/modules/more/backup_and_restore/backup_and_restore.dart'; import 'package:mangayomi/modules/more/categories/categories_screen.dart'; import 'package:mangayomi/modules/more/settings/downloads/downloads_screen.dart'; @@ -115,6 +116,15 @@ class RouterNotifier extends ChangeNotifier { child: const HistoryScreen(), ), ), + GoRoute( + name: "feed", + path: '/feed', + builder: (context, state) => const FeedScreen(), + pageBuilder: (context, state) => transitionPage( + key: state.pageKey, + child: const FeedScreen(), + ), + ), GoRoute( name: "browse", path: '/browse', diff --git a/lib/services/sync_server.dart b/lib/services/sync_server.dart index 3ddbe9c..7a4c48a 100644 --- a/lib/services/sync_server.dart +++ b/lib/services/sync_server.dart @@ -1,11 +1,10 @@ -import 'dart:developer'; - import 'package:crypto/crypto.dart'; import 'package:isar/isar.dart'; import 'package:mangayomi/eval/dart/model/m_bridge.dart'; import 'package:mangayomi/eval/dart/model/source_preference.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/changed_items.dart'; +import 'package:mangayomi/models/feed.dart'; import 'package:mangayomi/models/sync_preference.dart'; import 'package:mangayomi/models/track.dart'; import 'package:mangayomi/models/manga.dart'; @@ -184,10 +183,9 @@ class SyncServer extends _$SyncServer { return; } var jsonData = jsonDecode(response.body) as Map; - _restore( - jsonData["backupData"] is String - ? jsonDecode(jsonData["backupData"]) - : jsonData["backupData"]); + _restore(jsonData["backupData"] is String + ? jsonDecode(jsonData["backupData"]) + : jsonData["backupData"]); ref .read(synchingProvider(syncId: syncId).notifier) .setLastDownload(DateTime.now().millisecondsSinceEpoch); @@ -208,6 +206,7 @@ class SyncServer extends _$SyncServer { datas["chapters"] = data["chapters"]; datas["tracks"] = data["tracks"]; datas["history"] = data["history"]; + datas["feeds"] = data["feeds"]; var encodedJson = jsonEncode(datas); return sha256.convert(utf8.encode(encodedJson)).toString(); } @@ -297,6 +296,13 @@ class SyncServer extends _$SyncServer { .map((e) => e.toJson()) .toList(); datas.addAll({"extensions_preferences": sourcePreferences}); + final feeds = isar.feeds + .filter() + .idIsNotNull() + .findAllSync() + .map((e) => e.toJson()) + .toList(); + datas.addAll({"feeds": feeds}); return datas; } @@ -316,6 +322,8 @@ class SyncServer extends _$SyncServer { final history = (backup["history"] as List?) ?.map((e) => History.fromJson(e)) .toList(); + final feeds = + (backup["feeds"] as List?)?.map((e) => Feed.fromJson(e)).toList(); isar.writeTxnSync(() { isar.mangas.clearSync(); @@ -341,6 +349,23 @@ class SyncServer extends _$SyncServer { } } } + + isar.feeds.clearSync(); + if (feeds != null) { + final tempChapters = + isar.chapters.filter().idIsNotNull().findAllSync().toList(); + for (var feed in feeds) { + final matchingChapter = tempChapters + .where((chapter) => + chapter.mangaId == feed.mangaId && + chapter.name == feed.chapterName) + .firstOrNull; + if (matchingChapter != null) { + isar.feeds.putSync(feed..chapter.value = matchingChapter); + feed.chapter.saveSync(); + } + } + } } isar.categorys.clearSync(); @@ -369,7 +394,6 @@ class SyncServer extends _$SyncServer { void _restore(Map backup) { if (backup['version'] == "1") { try { - log("DEBUG: ${jsonEncode(backup["version"])}"); final manga = (backup["manga"] as List?)?.map((e) => Manga.fromJson(e)).toList(); final chapters = (backup["chapters"] as List?) @@ -392,8 +416,8 @@ class SyncServer extends _$SyncServer { final extensionsPref = (backup["extensions_preferences"] as List?) ?.map((e) => SourcePreference.fromJson(e)) .toList(); - log("DEBUG 1: ${jsonEncode(backup["manga"])}"); - log("DEBUG 2: ${jsonEncode(manga)}"); + final feeds = + (backup["feeds"] as List?)?.map((e) => Feed.fromJson(e)).toList(); isar.writeTxnSync(() { isar.mangas.clearSync(); @@ -419,6 +443,23 @@ class SyncServer extends _$SyncServer { } } } + + isar.feeds.clearSync(); + if (feeds != null) { + final tempChapters = + isar.chapters.filter().idIsNotNull().findAllSync().toList(); + for (var feed in feeds) { + final matchingChapter = tempChapters + .where((chapter) => + chapter.mangaId == feed.mangaId && + chapter.name == feed.chapterName) + .firstOrNull; + if (matchingChapter != null) { + isar.feeds.putSync(feed..chapter.value = matchingChapter); + feed.chapter.saveSync(); + } + } + } } isar.categorys.clearSync(); diff --git a/lib/services/sync_server.g.dart b/lib/services/sync_server.g.dart index 3292433..87b7a54 100644 --- a/lib/services/sync_server.g.dart +++ b/lib/services/sync_server.g.dart @@ -6,7 +6,7 @@ part of 'sync_server.dart'; // RiverpodGenerator // ************************************************************************** -String _$syncServerHash() => r'0eda7252078d155750e8adc4fa9cefec80041faf'; +String _$syncServerHash() => r'e019e8870184d25f7a2659e35f6c3969bc683b50'; /// Copied from Dart SDK class _SystemHash {