added feed feature

This commit is contained in:
Schnitzel5 2024-09-11 17:35:12 +02:00
parent 45a0d5c9db
commit 85aa687606
15 changed files with 893 additions and 22 deletions

View file

@ -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",

View file

@ -9,6 +9,8 @@ class Feed {
int? mangaId;
String? chapterName;
final chapter = IsarLink<Chapter>();
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<String, dynamic> json) {
id = json['id'];
mangaId = json['mangaId'];
mangaId = json['chapterName'];
date = json['date'];
}
Map<String, dynamic> toJson() => {
'id': id,
'mangaId': mangaId,
'chapterName': chapterName,
'date': date,
};
}

View file

@ -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<Type, List<int>> 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<int> offsets,
Map<Type, List<int>> 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<Type, List<int>> 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<P>(
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<Feed, Feed, QWhereClause> {
}
extension FeedQueryFilter on QueryBuilder<Feed, Feed, QFilterCondition> {
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapterNameIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'chapterName',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapterNameIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'chapterName',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapterNameEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'chapterName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> 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<Feed, Feed, QAfterFilterCondition> 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<Feed, Feed, QAfterFilterCondition> 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<Feed, Feed, QAfterFilterCondition> chapterNameStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'chapterName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapterNameEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'chapterName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapterNameContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'chapterName',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapterNameMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'chapterName',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapterNameIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'chapterName',
value: '',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapterNameIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'chapterName',
value: '',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@ -492,6 +653,18 @@ extension FeedQueryLinks on QueryBuilder<Feed, Feed, QFilterCondition> {
}
extension FeedQuerySortBy on QueryBuilder<Feed, Feed, QSortBy> {
QueryBuilder<Feed, Feed, QAfterSortBy> sortByChapterName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'chapterName', Sort.asc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> sortByChapterNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'chapterName', Sort.desc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> sortByDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.asc);
@ -518,6 +691,18 @@ extension FeedQuerySortBy on QueryBuilder<Feed, Feed, QSortBy> {
}
extension FeedQuerySortThenBy on QueryBuilder<Feed, Feed, QSortThenBy> {
QueryBuilder<Feed, Feed, QAfterSortBy> thenByChapterName() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'chapterName', Sort.asc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> thenByChapterNameDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'chapterName', Sort.desc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> thenByDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.asc);
@ -556,6 +741,13 @@ extension FeedQuerySortThenBy on QueryBuilder<Feed, Feed, QSortThenBy> {
}
extension FeedQueryWhereDistinct on QueryBuilder<Feed, Feed, QDistinct> {
QueryBuilder<Feed, Feed, QDistinct> distinctByChapterName(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'chapterName', caseSensitive: caseSensitive);
});
}
QueryBuilder<Feed, Feed, QDistinct> distinctByDate(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@ -577,6 +769,12 @@ extension FeedQueryProperty on QueryBuilder<Feed, Feed, QQueryProperty> {
});
}
QueryBuilder<Feed, String?, QQueryOperations> chapterNameProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'chapterName');
});
}
QueryBuilder<Feed, String?, QQueryOperations> dateProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'date');

View file

@ -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<FeedScreen> createState() => _FeedScreenState();
}
class _FeedScreenState extends ConsumerState<FeedScreen>
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<History> 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<Feed> 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<FeedTab> createState() => _FeedTabState();
}
class _FeedTabState extends ConsumerState<FeedTab> {
@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<Feed, String>(
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();
},
));
}
}

View file

@ -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<List<History>> getAllHistoryStream(GetAllHistoryStreamRef ref,
.chapter((q) => q.manga((q) => q.isMangaEqualTo(isManga)))
.watch(fireImmediately: true);
}
@riverpod
Stream<List<Feed>> getAllFeedStream(GetAllFeedStreamRef ref,
{required bool isManga}) async* {
yield* isar.feeds
.filter()
.idIsNotNull()
.and()
.chapter((q) => q.manga((q) => q.isMangaEqualTo(isManga)))
.watch(fireImmediately: true);
}

View file

@ -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<AsyncValue<List<Feed>>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'getAllFeedStreamProvider';
}
/// See also [getAllFeedStream].
class GetAllFeedStreamProvider extends AutoDisposeStreamProvider<List<Feed>> {
/// 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<List<Feed>> 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<List<Feed>> 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<List<Feed>> {
/// The parameter `isManga` of this provider.
bool get isManga;
}
class _GetAllFeedStreamProviderElement
extends AutoDisposeStreamProviderElement<List<Feed>>
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

View file

@ -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<LibraryScreen>
.notifier)
.addUpdatedChapter(
chapter, true, false);
isar.feeds
.filter()
.mangaIdEqualTo(chapter.mangaId)
.chapterNameEqualTo(chapter.name)
.deleteAllSync();
isar.chapters.deleteSync(chapter.id!);
}
ref

View file

@ -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();
});
}

View file

@ -86,7 +86,10 @@ Future<dynamic> 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();

View file

@ -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 {

View file

@ -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();

View file

@ -6,7 +6,7 @@ part of 'restore.dart';
// RiverpodGenerator
// **************************************************************************
String _$doRestoreHash() => r'3c88ad8ba80c245a4b511961111f7ab79c0d330f';
String _$doRestoreHash() => r'823b26bade20d89ae7b7b56a7eb7c25020795b45';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -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',

View file

@ -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<String, dynamic>;
_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<String, dynamic> 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();

View file

@ -6,7 +6,7 @@ part of 'sync_server.dart';
// RiverpodGenerator
// **************************************************************************
String _$syncServerHash() => r'0eda7252078d155750e8adc4fa9cefec80041faf';
String _$syncServerHash() => r'e019e8870184d25f7a2659e35f6c3969bc683b50';
/// Copied from Dart SDK
class _SystemHash {