Merge pull request #253 from Schnitzel5/feature/sync-server

testing first version of sync feature
This commit is contained in:
Moustapha Kodjo Amadou 2024-09-12 16:50:04 +01:00 committed by GitHub
commit 1a03a4ec79
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 7072 additions and 37 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",
@ -180,6 +182,28 @@
"one_tracker": "1 tracker",
"n_tracker": "{n} trackers",
"tracking": "Tracking",
"syncing": "Sync",
"sync_logged": "Login successful",
"syncing_subtitle": "Sync your progress across multiple devices via a self-hosted \nserver. Make sure to upload first if this is your first time \nsyncing or download before using (auto) sync on this device!",
"last_sync": "Last sync at: ",
"last_upload": "Last upload at: ",
"last_download": "Last download at: ",
"sync_server": "Sync Server Address",
"sync_login_invalid_creds": "Invalid email or password",
"sync_checking": "Checking for sync...",
"sync_uploading": "Upload started...",
"sync_downloading": "Download started...",
"sync_upload_finished": "Upload finished",
"sync_download_finished": "Download finished",
"sync_up_to_date": "Sync up to date",
"sync_upload_failed": "Upload failed",
"sync_download_failed": "Download failed",
"sync_button_sync": "Sync progress",
"sync_button_upload": "Full upload",
"sync_button_download": "Full download",
"sync_confirm_upload": "A full upload will completely replace the remote data with your current one!",
"sync_confirm_download": "A full download will completely replace your current data with the remote one!",
"dialog_confirm": "Confirm",
"description": "Description",
"episode_progress": "Progress: {n}",
"n_episodes": "{n} episodes",
@ -274,6 +298,8 @@
"default_skip_intro_length": "Default Skip intro length",
"default_playback_speed_length": "Default Playback speed length",
"updateProgressAfterReading": "Update progress after reading",
"syncAfterReading": "Sync after reading or watching",
"syncOnAppLaunch": "Sync when opening the app",
"no_sources_installed": "No sources installed!",
"show_extensions": "Show extensions",
"default_skip_forward_skip_length": "Default skip forward skip length",
@ -334,5 +360,12 @@
"or": "OR",
"advanced": "Advanced",
"use_native_http_client": "Use native http client",
"use_native_http_client_info": "it automatically supports platform features such VPNs, support more HTTP features such as HTTP/3 and custom redirect handling"
"use_native_http_client_info": "it automatically supports platform features such VPNs, support more HTTP features such as HTTP/3 and custom redirect handling",
"n_hour_ago": "{hour} hour ago",
"n_hours_ago": "{hours} hours ago",
"n_minute_ago": "{minute} minute ago",
"n_minutes_ago": "{minutes} minutes ago",
"n_day_ago": "{day} day ago",
"now": "now",
"library_last_updated": "Library last updated: {lastUpdated}"
}

View file

@ -17,6 +17,8 @@ import 'package:mangayomi/modules/more/settings/appearance/providers/blend_level
import 'package:mangayomi/modules/more/settings/appearance/providers/flex_scheme_color_state_provider.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/services/sync_server.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:mangayomi/src/rust/frb_generated.dart';
import 'package:media_kit/media_kit.dart';
@ -68,6 +70,10 @@ class _MyAppState extends ConsumerState<MyApp> {
@override
Widget build(BuildContext context) {
final syncOnAppLaunch = ref.watch(syncOnAppLaunchStateProvider);
if (syncOnAppLaunch) {
ref.read(syncServerProvider(syncId: 1).notifier).checkForSync(true);
}
final isDarkTheme = ref.watch(themeModeStateProvider);
final blendLevel = ref.watch(blendLevelStateProvider);
final appFontFamily = ref.watch(appFontFamilyProvider);

View file

@ -0,0 +1,86 @@
import 'package:isar/isar.dart';
part 'changed_items.g.dart';
@collection
@Name("Changed Items")
class ChangedItems {
Id? id;
List<DeletedManga>? deletedMangas;
List<UpdatedChapter>? updatedChapters;
List<DeletedCategory>? deletedCategories;
ChangedItems(
{this.id = Isar.autoIncrement,
this.deletedMangas = const [],
this.updatedChapters = const [],
this.deletedCategories = const []});
ChangedItems.fromJson(Map<String, dynamic> json) {
id = json['id'];
deletedMangas = json['deletedMangas'];
updatedChapters = json['updatedChapters'];
deletedCategories = json['deletedCategories'];
}
Map<String, dynamic> toJson() => {
'id': id,
'deletedMangas': deletedMangas,
'updatedChapters': updatedChapters,
'deletedCategories': deletedCategories
};
}
@embedded
class DeletedManga {
int? mangaId;
DeletedManga({this.mangaId});
DeletedManga.fromJson(Map<String, dynamic> json) {
mangaId = json['mangaId'];
}
Map<String, dynamic> toJson() => {'mangaId': mangaId};
}
@embedded
class UpdatedChapter {
int? chapterId;
int? mangaId;
bool? isBookmarked;
bool? isRead;
String? lastPageRead;
bool? deleted;
UpdatedChapter(
{this.chapterId,
this.mangaId,
this.isBookmarked,
this.isRead,
this.lastPageRead,
this.deleted});
UpdatedChapter.fromJson(Map<String, dynamic> json) {
chapterId = json['chapterId'];
mangaId = json['mangaId'];
isBookmarked = json['isBookmarked'];
isRead = json['isRead'];
lastPageRead = json['lastPageRead'];
deleted = json['deleted'];
}
Map<String, dynamic> toJson() => {
'chapterId': chapterId,
'mangaId': mangaId,
'isBookmarked': isBookmarked,
'isRead': isRead,
'lastPageRead': lastPageRead,
'deleted': deleted
};
}
@embedded
class DeletedCategory {
int? categoryId;
DeletedCategory({this.categoryId});
DeletedCategory.fromJson(Map<String, dynamic> json) {
categoryId = json['categoryId'];
}
Map<String, dynamic> toJson() => {'categoryId': categoryId};
}

File diff suppressed because it is too large Load diff

38
lib/models/feed.dart Normal file
View file

@ -0,0 +1,38 @@
import 'package:isar/isar.dart';
import 'package:mangayomi/models/chapter.dart';
part 'feed.g.dart';
@collection
@Name("Feed")
class Feed {
Id? id;
int? mangaId;
String? chapterName;
final chapter = IsarLink<Chapter>();
String? date;
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,
};
}

789
lib/models/feed.g.dart Normal file
View file

@ -0,0 +1,789 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'feed.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetFeedCollection on Isar {
IsarCollection<Feed> get feeds => this.collection();
}
const FeedSchema = CollectionSchema(
name: r'Feed',
id: 8879644747771893978,
properties: {
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: 2,
name: r'mangaId',
type: IsarType.long,
)
},
estimateSize: _feedEstimateSize,
serialize: _feedSerialize,
deserialize: _feedDeserialize,
deserializeProp: _feedDeserializeProp,
idName: r'id',
indexes: {},
links: {
r'chapter': LinkSchema(
id: 8037684855892205613,
name: r'chapter',
target: r'Chapter',
single: true,
)
},
embeddedSchemas: {},
getId: _feedGetId,
getLinks: _feedGetLinks,
attach: _feedAttach,
version: '3.1.0+1',
);
int _feedEstimateSize(
Feed object,
List<int> offsets,
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) {
bytesCount += 3 + value.length * 3;
}
}
return bytesCount;
}
void _feedSerialize(
Feed object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.chapterName);
writer.writeString(offsets[1], object.date);
writer.writeLong(offsets[2], object.mangaId);
}
Feed _feedDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = Feed(
chapterName: reader.readStringOrNull(offsets[0]),
date: reader.readStringOrNull(offsets[1]),
id: id,
mangaId: reader.readLongOrNull(offsets[2]),
);
return object;
}
P _feedDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
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');
}
}
Id _feedGetId(Feed object) {
return object.id ?? Isar.autoIncrement;
}
List<IsarLinkBase<dynamic>> _feedGetLinks(Feed object) {
return [object.chapter];
}
void _feedAttach(IsarCollection<dynamic> col, Id id, Feed object) {
object.id = id;
object.chapter.attach(col, col.isar.collection<Chapter>(), r'chapter', id);
}
extension FeedQueryWhereSort on QueryBuilder<Feed, Feed, QWhere> {
QueryBuilder<Feed, Feed, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension FeedQueryWhere on QueryBuilder<Feed, Feed, QWhereClause> {
QueryBuilder<Feed, Feed, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: id,
upper: id,
));
});
}
QueryBuilder<Feed, Feed, QAfterWhereClause> idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<Feed, Feed, QAfterWhereClause> idGreaterThan(Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<Feed, Feed, QAfterWhereClause> idLessThan(Id id,
{bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<Feed, Feed, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
));
});
}
}
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(
property: r'date',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'date',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateEqualTo(
String? value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'date',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateGreaterThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'date',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateLessThan(
String? value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'date',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateBetween(
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'date',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'date',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'date',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateContains(String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'date',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateMatches(String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'date',
wildcard: pattern,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'date',
value: '',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> dateIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'date',
value: '',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> idIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'id',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> idIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'id',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> idEqualTo(Id? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'id',
value: value,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> idGreaterThan(
Id? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> idLessThan(
Id? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> idBetween(
Id? lower,
Id? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> mangaIdIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'mangaId',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> mangaIdIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'mangaId',
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> mangaIdEqualTo(int? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'mangaId',
value: value,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> mangaIdGreaterThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'mangaId',
value: value,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> mangaIdLessThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'mangaId',
value: value,
));
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> mangaIdBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
property: r'mangaId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
));
});
}
}
extension FeedQueryObject on QueryBuilder<Feed, Feed, QFilterCondition> {}
extension FeedQueryLinks on QueryBuilder<Feed, Feed, QFilterCondition> {
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapter(
FilterQuery<Chapter> q) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'chapter');
});
}
QueryBuilder<Feed, Feed, QAfterFilterCondition> chapterIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'chapter', 0, true, 0, true);
});
}
}
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);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> sortByDateDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.desc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> sortByMangaId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'mangaId', Sort.asc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> sortByMangaIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'mangaId', Sort.desc);
});
}
}
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);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> thenByDateDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.desc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> thenByMangaId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'mangaId', Sort.asc);
});
}
QueryBuilder<Feed, Feed, QAfterSortBy> thenByMangaIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'mangaId', Sort.desc);
});
}
}
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) {
return query.addDistinctBy(r'date', caseSensitive: caseSensitive);
});
}
QueryBuilder<Feed, Feed, QDistinct> distinctByMangaId() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'mangaId');
});
}
}
extension FeedQueryProperty on QueryBuilder<Feed, Feed, QQueryProperty> {
QueryBuilder<Feed, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
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');
});
}
QueryBuilder<Feed, int?, QQueryOperations> mangaIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'mangaId');
});
}
}

View file

@ -137,6 +137,10 @@ class Settings {
List<int>? backupFrequencyOptions;
bool? syncOnAppLaunch;
bool? syncAfterReading;
String? autoBackupLocation;
bool? usePageTapZones;
@ -246,6 +250,8 @@ class Settings {
this.personalPageModeList,
this.backupFrequency,
this.backupFrequencyOptions,
this.syncOnAppLaunch,
this.syncAfterReading,
this.autoBackupLocation,
this.startDatebackup,
this.usePageTapZones = true,
@ -390,13 +396,15 @@ class Settings {
userAgent = json['userAgent'];
backupFrequency = json['backupFrequency'];
backupFrequencyOptions = json['backupFrequencyOptions']?.cast<int>();
syncOnAppLaunch = json['syncOnAppLaunch'];
syncAfterReading = json['syncAfterReading'];
autoBackupLocation = json['autoBackupLocation'];
startDatebackup = json['startDatebackup'];
usePageTapZones = json['usePageTapZones'];
markEpisodeAsSeenType = json['markEpisodeAsSeenType'];
defaultSkipIntroLength = json['defaultSkipIntroLength'];
defaultDoubleTapToSkipLength = json['defaultDoubleTapToSkipLength'];
defaultPlayBackSpeed = json['defaultPlayBackSpeed'];
defaultPlayBackSpeed = json['defaultPlayBackSpeed'] is double ? json['defaultPlayBackSpeed'] : (json['defaultPlayBackSpeed'] as int).toDouble();
updateProgressAfterReading = json['updateProgressAfterReading'];
enableAniSkip = json['enableAniSkip'];
enableAutoSkip = json['enableAutoSkip'];
@ -410,7 +418,7 @@ class Settings {
colorFilterBlendMode = ColorFilterBlendMode
.values[json['colorFilterBlendMode'] ?? ColorFilterBlendMode.none];
playerSubtitleSettings = json['playerSubtitleSettings'] != null
? PlayerSubtitleSettings.fromJson(json['customColorFilter'])
? PlayerSubtitleSettings.fromJson(json['playerSubtitleSettings'])
: null;
mangaHomeDisplayType = DisplayType.values[
json['mangaHomeDisplayType'] ?? DisplayType.comfortableGrid.index];
@ -503,6 +511,8 @@ class Settings {
'userAgent': userAgent,
'backupFrequency': backupFrequency,
'backupFrequencyOptions': backupFrequencyOptions,
'syncOnAppLaunch': syncOnAppLaunch,
'syncAfterReading': syncAfterReading,
'autoBackupLocation': autoBackupLocation,
'startDatebackup': startDatebackup,
'usePageTapZones': usePageTapZones,

View file

@ -441,28 +441,38 @@ const SettingsSchema = CollectionSchema(
name: r'startDatebackup',
type: IsarType.long,
),
r'themeIsDark': PropertySchema(
r'syncAfterReading': PropertySchema(
id: 80,
name: r'syncAfterReading',
type: IsarType.bool,
),
r'syncOnAppLaunch': PropertySchema(
id: 81,
name: r'syncOnAppLaunch',
type: IsarType.bool,
),
r'themeIsDark': PropertySchema(
id: 82,
name: r'themeIsDark',
type: IsarType.bool,
),
r'updateProgressAfterReading': PropertySchema(
id: 81,
id: 83,
name: r'updateProgressAfterReading',
type: IsarType.bool,
),
r'useLibass': PropertySchema(
id: 82,
id: 84,
name: r'useLibass',
type: IsarType.bool,
),
r'usePageTapZones': PropertySchema(
id: 83,
id: 85,
name: r'usePageTapZones',
type: IsarType.bool,
),
r'userAgent': PropertySchema(
id: 84,
id: 86,
name: r'userAgent',
type: IsarType.string,
)
@ -914,11 +924,13 @@ void _settingsSerialize(
object.sortLibraryManga,
);
writer.writeLong(offsets[79], object.startDatebackup);
writer.writeBool(offsets[80], object.themeIsDark);
writer.writeBool(offsets[81], object.updateProgressAfterReading);
writer.writeBool(offsets[82], object.useLibass);
writer.writeBool(offsets[83], object.usePageTapZones);
writer.writeString(offsets[84], object.userAgent);
writer.writeBool(offsets[80], object.syncAfterReading);
writer.writeBool(offsets[81], object.syncOnAppLaunch);
writer.writeBool(offsets[82], object.themeIsDark);
writer.writeBool(offsets[83], object.updateProgressAfterReading);
writer.writeBool(offsets[84], object.useLibass);
writer.writeBool(offsets[85], object.usePageTapZones);
writer.writeString(offsets[86], object.userAgent);
}
Settings _settingsDeserialize(
@ -1077,11 +1089,13 @@ Settings _settingsDeserialize(
allOffsets,
),
startDatebackup: reader.readLongOrNull(offsets[79]),
themeIsDark: reader.readBoolOrNull(offsets[80]),
updateProgressAfterReading: reader.readBoolOrNull(offsets[81]),
useLibass: reader.readBoolOrNull(offsets[82]),
usePageTapZones: reader.readBoolOrNull(offsets[83]),
userAgent: reader.readStringOrNull(offsets[84]),
syncAfterReading: reader.readBoolOrNull(offsets[80]),
syncOnAppLaunch: reader.readBoolOrNull(offsets[81]),
themeIsDark: reader.readBoolOrNull(offsets[82]),
updateProgressAfterReading: reader.readBoolOrNull(offsets[83]),
useLibass: reader.readBoolOrNull(offsets[84]),
usePageTapZones: reader.readBoolOrNull(offsets[85]),
userAgent: reader.readStringOrNull(offsets[86]),
);
object.chapterFilterBookmarkedList =
reader.readObjectList<ChapterFilterBookmarked>(
@ -1375,6 +1389,10 @@ P _settingsDeserializeProp<P>(
case 83:
return (reader.readBoolOrNull(offset)) as P;
case 84:
return (reader.readBoolOrNull(offset)) as P;
case 85:
return (reader.readBoolOrNull(offset)) as P;
case 86:
return (reader.readStringOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@ -6759,6 +6777,62 @@ extension SettingsQueryFilter
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
syncAfterReadingIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'syncAfterReading',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
syncAfterReadingIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'syncAfterReading',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
syncAfterReadingEqualTo(bool? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'syncAfterReading',
value: value,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
syncOnAppLaunchIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
property: r'syncOnAppLaunch',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
syncOnAppLaunchIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNotNull(
property: r'syncOnAppLaunch',
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition>
syncOnAppLaunchEqualTo(bool? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'syncOnAppLaunch',
value: value,
));
});
}
QueryBuilder<Settings, Settings, QAfterFilterCondition> themeIsDarkIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const FilterCondition.isNull(
@ -8017,6 +8091,30 @@ extension SettingsQuerySortBy on QueryBuilder<Settings, Settings, QSortBy> {
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortBySyncAfterReading() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncAfterReading', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortBySyncAfterReadingDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncAfterReading', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortBySyncOnAppLaunch() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncOnAppLaunch', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortBySyncOnAppLaunchDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncOnAppLaunch', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> sortByThemeIsDark() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'themeIsDark', Sort.asc);
@ -8917,6 +9015,30 @@ extension SettingsQuerySortThenBy
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenBySyncAfterReading() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncAfterReading', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenBySyncAfterReadingDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncAfterReading', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenBySyncOnAppLaunch() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncOnAppLaunch', Sort.asc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenBySyncOnAppLaunchDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'syncOnAppLaunch', Sort.desc);
});
}
QueryBuilder<Settings, Settings, QAfterSortBy> thenByThemeIsDark() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'themeIsDark', Sort.asc);
@ -9404,6 +9526,18 @@ extension SettingsQueryWhereDistinct
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctBySyncAfterReading() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'syncAfterReading');
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctBySyncOnAppLaunch() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'syncOnAppLaunch');
});
}
QueryBuilder<Settings, Settings, QDistinct> distinctByThemeIsDark() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'themeIsDark');
@ -9980,6 +10114,18 @@ extension SettingsQueryProperty
});
}
QueryBuilder<Settings, bool?, QQueryOperations> syncAfterReadingProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'syncAfterReading');
});
}
QueryBuilder<Settings, bool?, QQueryOperations> syncOnAppLaunchProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'syncOnAppLaunch');
});
}
QueryBuilder<Settings, bool?, QQueryOperations> themeIsDarkProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'themeIsDark');

View file

@ -0,0 +1,50 @@
import 'package:isar/isar.dart';
part 'sync_preference.g.dart';
@collection
@Name("Sync Preference")
class SyncPreference {
Id? syncId;
String? email;
String? authToken;
int? lastSync;
int? lastUpload;
int? lastDownload;
String? server;
SyncPreference({
this.syncId,
this.email,
this.authToken,
this.lastSync,
this.lastUpload,
this.lastDownload,
this.server,
});
SyncPreference.fromJson(Map<String, dynamic> json) {
syncId = json['syncId'];
email = json['email'];
authToken = json['authToken'];
lastSync = json['lastSync'];
lastUpload = json['lastUpload'];
lastDownload = json['lastDownload'];
server = json['server'];
}
Map<String, dynamic> toJson() => {
'syncId': syncId,
'email': email,
'authToken': authToken,
'lastSync': lastSync,
'lastUpload': lastUpload,
'lastDownload': lastDownload,
'server': server
};
}

File diff suppressed because it is too large Load diff

View file

@ -7,6 +7,7 @@ import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/services/aniskip.dart';
import 'package:mangayomi/utils/chapter_recognition.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -164,10 +165,14 @@ class AnimeStreamController extends _$AnimeStreamController {
isar.writeTxnSync(() {
ep.isRead = isWatch;
ep.lastPageRead = (duration.inMilliseconds).toString();
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.addUpdatedChapter(ep, false, false);
isar.chapters.putSync(ep);
});
if (isWatch) {
episode.updateTrackChapterRead(ref);
episode.syncProgressAfterChapterRead(ref);
}
}
}

View file

@ -7,7 +7,7 @@ part of 'anime_player_controller_provider.dart';
// **************************************************************************
String _$animeStreamControllerHash() =>
r'24639a8644ea9820458658807035e4cffb1b1644';
r'afa475dbb6f73d33b2495dd6d4502f3df1ab931d';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -0,0 +1,262 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/feed/widgets/feed_chapter_list_tile_widget.dart';
import 'package:mangayomi/modules/history/providers/isar_providers.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/date.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';
import 'package:mangayomi/utils/extensions/build_context_extensions.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();
final lastUpdatedList =
data.map((e) => e.chapter.value!.manga.value!.lastUpdate!).toList();
lastUpdatedList.sort((a, b) => a.compareTo(b));
final lastUpdated = lastUpdatedList.firstOrNull;
if (entries.isNotEmpty) {
return CustomScrollView(
slivers: [
if (lastUpdated != null)
SliverPadding(
padding: const EdgeInsets.only(
left: 10, right: 10, top: 10, bottom: 20),
sliver: SliverList(
delegate: SliverChildListDelegate.fixed([
Text(
l10n.library_last_updated(dateFormat(
lastUpdated.toString(),
ref: ref,
context: context,
showHOURorMINUTE: true)),
style: TextStyle(
fontStyle: FontStyle.italic,
color: context.secondaryColor))
])),
),
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 chapter = element.chapter.value!;
return FeedChapterListTileWidget(
chapter: chapter, sourceExist: true);
},
itemComparator: (item1, item2) =>
item1.date!.compareTo(item2.date!),
order: GroupedListOrder.DESC,
),
],
);
}
return Center(
child: Text(l10n.no_recent_updates),
);
},
error: (Object error, StackTrace stackTrace) {
return ErrorText(error);
},
loading: () {
return const ProgressCenter();
},
));
}
}

View file

@ -0,0 +1,112 @@
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:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
import 'package:mangayomi/utils/constant.dart';
import 'package:mangayomi/modules/manga/download/download_page_widget.dart';
import 'package:mangayomi/utils/extensions/chapter.dart';
import 'package:mangayomi/utils/headers.dart';
class FeedChapterListTileWidget extends ConsumerWidget {
final Chapter chapter;
final bool sourceExist;
const FeedChapterListTileWidget({
required this.chapter,
required this.sourceExist,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final manga = chapter.manga.value!;
return Material(
borderRadius: BorderRadius.circular(5),
color: Colors.transparent,
clipBehavior: Clip.antiAliasWithSaveLayer,
child: InkWell(
onTap: () async {
chapter.pushToReaderView(context, ignoreIsRead: true);
},
onLongPress: () {},
onSecondaryTap: () {},
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 5),
child: Container(
height: 45,
decoration: BoxDecoration(borderRadius: BorderRadius.circular(5)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Material(
child: GestureDetector(
onTap: () {
context.push('/manga-reader/detail',
extra: manga.id);
},
child: Ink.image(
fit: BoxFit.cover,
width: 40,
height: 45,
image: manga.customCoverImage != null
? MemoryImage(
manga.customCoverImage as Uint8List)
as ImageProvider
: CustomExtendedNetworkImageProvider(
toImgUrl(manga.customCoverFromTracker ??
manga.imageUrl!),
headers: ref.watch(headersProvider(
source: manga.source!,
lang: manga.lang!)),
),
child: InkWell(child: Container()),
),
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(manga.name!,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.textTheme
.bodyLarge!
.color)),
Text(chapter.name!,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
color: Theme.of(context)
.textTheme
.bodyLarge!
.color)),
],
),
),
)
],
),
),
if (sourceExist) ChapterPageDownload(chapter: chapter)
],
),
),
),
),
);
}
}

View file

@ -10,6 +10,7 @@ import 'package:mangayomi/models/chapter.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/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/cached_network.dart';
import 'package:mangayomi/utils/constant.dart';
@ -357,6 +358,10 @@ class _HistoryTabState extends ConsumerState<HistoryTab> {
),
TextButton(
onPressed: () async {
await manga.chapters
.load();
final chapters =
manga.chapters;
await isar.writeTxn(
() async {
await isar
@ -364,6 +369,34 @@ class _HistoryTabState extends ConsumerState<HistoryTab> {
.delete(
element
.id!);
for (var chapter
in chapters) {
await ref
.read(changedItemsManagerProvider(
managerId:
1)
.notifier)
.addUpdatedChapterAsync(
chapter,
true,
false);
await isar
.chapters
.delete(
chapter
.id!);
}
await ref
.read(changedItemsManagerProvider(
managerId:
1)
.notifier)
.addDeletedMangaAsync(
manga,
false);
await isar.mangas
.delete(manga
.id!);
});
if (context
.mounted) {

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,10 +16,12 @@ 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';
import 'package:mangayomi/modules/more/categories/providers/isar_providers.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
@ -1157,8 +1159,24 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
}
for (var chapter in manga.chapters) {
ref
.read(changedItemsManagerProvider(
managerId: 1)
.notifier)
.addUpdatedChapter(
chapter, true, false);
isar.feeds
.filter()
.mangaIdEqualTo(chapter.mangaId)
.chapterNameEqualTo(chapter.name)
.deleteAllSync();
isar.chapters.deleteSync(chapter.id!);
}
ref
.read(changedItemsManagerProvider(
managerId: 1)
.notifier)
.addDeletedManga(manga, false);
isar.mangas.deleteSync(manga.id!);
} else {
manga.favorite = false;
@ -1792,7 +1810,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
});
} else if (value == 2) {
_importLocal(context, widget.isManga);
} else if (value == 3 && !widget.isManga){
} else if (value == 3 && !widget.isManga) {
addTorrent(context);
}
}),

View file

@ -4,6 +4,7 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'library_state_provider.g.dart';
@ -650,10 +651,14 @@ class MangasSetIsReadState extends _$MangasSetIsReadState {
for (var chapter in chapters) {
chapter.isRead = true;
chapter.lastPageRead = "1";
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.addUpdatedChapter(chapter, false, false);
isar.chapters.putSync(chapter..manga.value = manga);
chapter.manga.saveSync();
}
});
chapters.last.syncProgressAfterChapterRead(ref);
}
}
@ -674,6 +679,9 @@ class MangasSetUnReadState extends _$MangasSetUnReadState {
isar.writeTxnSync(() {
for (var chapter in chapters) {
chapter.isRead = false;
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.addUpdatedChapter(chapter, false, false);
isar.chapters.putSync(chapter..manga.value = manga);
chapter.manga.saveSync();
}

View file

@ -2520,7 +2520,7 @@ final isLongPressedMangaStateProvider =
typedef _$IsLongPressedMangaState = AutoDisposeNotifier<bool>;
String _$mangasSetIsReadStateHash() =>
r'8f86296f588a48747de625e0471048978ee9bdeb';
r'926659caebb85b15952f66f437f773a4b660bb78';
abstract class _$MangasSetIsReadState
extends BuildlessAutoDisposeNotifier<void> {
@ -2665,7 +2665,7 @@ class _MangasSetIsReadStateProviderElement
}
String _$mangasSetUnReadStateHash() =>
r'3413e731b2fd8476a4032d3e47b943ca12f25090';
r'7b2f4c579f9cb392830ed4d70aff9ccc3e7952a0';
abstract class _$MangasSetUnReadState
extends BuildlessAutoDisposeNotifier<void> {

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(
@ -256,8 +304,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');
}
},
@ -315,3 +365,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

@ -22,6 +22,7 @@ import 'package:mangayomi/modules/manga/detail/widgets/tracker_search_widget.dar
import 'package:mangayomi/modules/manga/detail/widgets/tracker_widget.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
@ -708,6 +709,11 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
isar.writeTxnSync(() {
for (var chapter in chapters) {
chapter.isBookmarked = !chapter.isBookmarked!;
ref
.read(changedItemsManagerProvider(
managerId: 1)
.notifier)
.addUpdatedChapter(chapter, false, false);
isar.chapters.putSync(
chapter..manga.value = widget.manga);
chapter.manga.saveSync();
@ -748,11 +754,17 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
if (!chapter.isRead!) {
chapter.lastPageRead = "1";
}
ref
.read(changedItemsManagerProvider(
managerId: 1)
.notifier)
.addUpdatedChapter(chapter, false, false);
isar.chapters.putSync(
chapter..manga.value = widget.manga);
chapter.manga.saveSync();
if (chapter.isRead!) {
chapter.updateTrackChapterRead(ref);
chapter.syncProgressAfterChapterRead(ref);
}
}
});
@ -793,6 +805,12 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
if (!chapters[i].isRead!) {
chapters[i].isRead = true;
chapters[i].lastPageRead = "1";
ref
.read(changedItemsManagerProvider(
managerId: 1)
.notifier)
.addUpdatedChapter(
chapters[i], false, false);
isar.chapters.putSync(chapters[i]
..manga.value = widget.manga);
chapters[i].manga.saveSync();
@ -805,6 +823,8 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
.read(chaptersListStateProvider.notifier)
.clear();
});
chapters[index + 1]
.syncProgressAfterChapterRead(ref);
},
child: Stack(
children: [

View file

@ -5,6 +5,7 @@ import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/manga/download/providers/download_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'state_providers.g.dart';
@ -292,6 +293,9 @@ class ChapterSetIsBookmarkState extends _$ChapterSetIsBookmarkState {
isar.writeTxnSync(() {
for (var chapter in chapters) {
chapter.isBookmarked = !chapter.isBookmarked!;
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.addUpdatedChapter(chapter, false, false);
isar.chapters.putSync(chapter..manga.value = manga);
chapter.manga.saveSync();
}
@ -311,6 +315,9 @@ class ChapterSetIsReadState extends _$ChapterSetIsReadState {
isar.writeTxnSync(() {
for (var chapter in chapters) {
chapter.isRead = !chapter.isRead!;
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.addUpdatedChapter(chapter, false, false);
isar.chapters.putSync(chapter..manga.value = manga);
chapter.manga.saveSync();
}

View file

@ -806,7 +806,7 @@ class _ChapterFilterResultStateProviderElement
}
String _$chapterSetIsBookmarkStateHash() =>
r'113131bb13e50566390ee3e34aa2f08820a8870c';
r'48d4f203ba51616e9d1142e0dd482d3ae065a4f4';
abstract class _$ChapterSetIsBookmarkState
extends BuildlessAutoDisposeNotifier<void> {
@ -951,7 +951,7 @@ class _ChapterSetIsBookmarkStateProviderElement
}
String _$chapterSetIsReadStateHash() =>
r'c319f81ec30565ad81a28cb0a8ce7fddcb47cd77';
r'1e219dd68898fc30b6cb64d294377776516775d4';
abstract class _$ChapterSetIsReadState
extends BuildlessAutoDisposeNotifier<void> {

View file

@ -2,7 +2,9 @@ import 'package:mangayomi/eval/dart/model/m_bridge.dart';
import 'package:mangayomi/eval/dart/model/m_manga.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/feed.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/services/get_detail.dart';
import 'package:mangayomi/utils/utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -77,8 +79,20 @@ Future<dynamic> updateMangaDetail(UpdateMangaDetailRef ref,
}
if (chapters.isNotEmpty) {
for (var chap in chapters.reversed.toList()) {
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.addUpdatedChapter(chap, false, false);
isar.chapters.putSync(chap);
chap.manga.saveSync();
if (manga.chapters.isNotEmpty) {
final feed = Feed(
mangaId: mangaId,
chapterName: chap.name,
date: DateTime.now().millisecondsSinceEpoch.toString())
..chapter.value = chap;
isar.feeds.putSync(feed);
feed.chapter.saveSync();
}
}
}
final oldChapers =
@ -93,6 +107,9 @@ Future<dynamic> updateMangaDetail(UpdateMangaDetailRef ref,
newChap.name == oldChap.name) {
oldChap.url = newChap.url;
oldChap.scanlator = newChap.scanlator;
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.addUpdatedChapter(oldChap, false, false);
isar.chapters.putSync(oldChap);
oldChap.manga.saveSync();
}

View file

@ -6,7 +6,7 @@ part of 'update_manga_detail_providers.dart';
// RiverpodGenerator
// **************************************************************************
String _$updateMangaDetailHash() => r'7733551fd578f5d093c4ca11427015a3f3af415a';
String _$updateMangaDetailHash() => r'c21ac4f7725b5ac4403902bac07a3b5462488bbd';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -11,6 +11,7 @@ import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
import 'package:mangayomi/utils/chapter_recognition.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -177,6 +178,7 @@ class ReaderController extends _$ReaderController {
.filter()
.mangaIdEqualTo(getManga().id)
.findFirstSync())!
..chapterId = chapter.id
..chapter.value = chapter
..date = DateTime.now().millisecondsSinceEpoch.toString();
}
@ -192,6 +194,9 @@ class ReaderController extends _$ReaderController {
final chap = chapter;
isar.writeTxnSync(() {
chap.isBookmarked = !isBookmarked;
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.addUpdatedChapter(chap, false, false);
isar.chapters.putSync(chap);
});
}
@ -329,10 +334,14 @@ class ReaderController extends _$ReaderController {
getIsarSetting()..chapterPageIndexList = chapterPageIndexs);
chap.isRead = isRead;
chap.lastPageRead = isRead ? '1' : (newIndex + 1).toString();
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.addUpdatedChapter(chap, false, false);
isar.chapters.putSync(chap);
});
if (isRead) {
chapter.updateTrackChapterRead(ref);
chapter.syncProgressAfterChapterRead(ref);
}
}
}
@ -396,6 +405,14 @@ extension ChapterExtensions on Chapter {
}
}
}
void syncProgressAfterChapterRead(dynamic ref) {
if (!(ref is WidgetRef || ref is AutoDisposeNotifierProviderRef)) return;
final syncAfterReading = ref.watch(syncAfterReadingStateProvider);
if (!syncAfterReading) return;
checkForSyncIndependentProvider.call(true);
// ref.read(syncServerProvider(syncId: 1).notifier).checkForSync(ref, true);
}
}
extension MangaExtensions on Manga {

View file

@ -169,7 +169,7 @@ class _CurrentIndexProviderElement
Chapter get chapter => (origin as CurrentIndexProvider).chapter;
}
String _$readerControllerHash() => r'b334d7b508df43c66ce57f0d2bf7c59ea4bf3ff7';
String _$readerControllerHash() => r'f615f15a622ac5ccdb16318dcdefc097a175fd33';
abstract class _$ReaderController extends BuildlessAutoDisposeNotifier<void> {
late final Chapter chapter;

View file

@ -4,6 +4,7 @@ import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/utils/date.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
@ -155,8 +156,11 @@ class _ChapterListTileState extends State<ChapterListTile> {
setState(() {
isBookmarked = !isBookmarked;
});
isar.writeTxnSync(() =>
isar.chapters.putSync(chapter..isBookmarked = isBookmarked));
isar.writeTxnSync(() => {
addUpdatedChapterIndependentProvider.call(
chapter, false, false),
isar.chapters.putSync(chapter..isBookmarked = isBookmarked),
});
},
icon: Icon(isBookmarked ? Icons.bookmark : Icons.bookmark_outline,
color: context.primaryColor),

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

@ -4,6 +4,7 @@ import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/category.dart';
import 'package:mangayomi/modules/more/categories/providers/isar_providers.dart';
import 'package:mangayomi/modules/more/categories/widgets/custom_textfield.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
@ -177,13 +178,22 @@ class _CategoriesTabState extends ConsumerState<CategoriesTab> {
onPressed: () async {
await isar.writeTxn(
() async {
await ref
.read(changedItemsManagerProvider(
managerId:
1)
.notifier)
.addDeletedCategoryAsync(
_entries[
index], false);
await isar
.categorys
.delete(_entries[
index]
.id!);
});
if (context.mounted) {
if (context
.mounted) {
Navigator.pop(
context);
}

View file

@ -41,6 +41,11 @@ class SettingsScreen extends StatelessWidget {
subtitle: "",
icon: Icons.sync_outlined,
onTap: () => context.push('/track')),
ListTileWidget(
title: l10n.syncing,
subtitle: l10n.syncing_subtitle,
icon: Icons.cloud_sync_outlined,
onTap: () => context.push('/sync')),
ListTileWidget(
title: l10n.browse,
subtitle: l10n.browse_subtitle,

View file

@ -0,0 +1,24 @@
class JWToken {
String? uuid;
String? email;
int? iat;
int? exp;
JWToken({this.uuid, this.email, this.iat, this.exp});
JWToken.fromJson(Map<String, dynamic> json) {
email = json['email'];
uuid = json['uuid'];
iat = (json['iat'] as int) * 1000;
exp = (json['exp'] as int) * 1000;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['uuid'] = uuid;
data['email'] = email;
data['iat'] = iat;
data['exp'] = exp;
return data;
}
}

View file

@ -0,0 +1,283 @@
import 'dart:convert';
import 'dart:developer';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/category.dart';
import 'package:mangayomi/models/changed_items.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/sync_preference.dart';
import 'package:mangayomi/services/sync_server.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'sync_providers.g.dart';
@riverpod
void addUpdatedChapterIndependent(AddUpdatedChapterIndependentRef ref,
Chapter chapter, bool deleted, bool txn) {
final changedItems = isar.changedItems.getSync(1) ?? ChangedItems();
bool updated = false;
changedItems.updatedChapters = changedItems.updatedChapters?.map((e) {
if (e.chapterId == chapter.id) {
e.isBookmarked = chapter.isBookmarked;
e.isRead = chapter.isRead;
e.lastPageRead = chapter.lastPageRead;
e.deleted = deleted;
updated = true;
}
return e;
}).toList();
if (!updated) {
final updatedChapter = UpdatedChapter(
chapterId: chapter.id,
isBookmarked: chapter.isBookmarked,
isRead: chapter.isRead,
lastPageRead: chapter.lastPageRead,
deleted: deleted);
changedItems.updatedChapters = changedItems.updatedChapters?.toList()
?..add(updatedChapter);
}
if (!txn) {
isar.changedItems.putSync(changedItems);
} else {
isar.writeTxnSync(() {
isar.changedItems.putSync(changedItems);
});
}
}
@riverpod
void checkForSyncIndependent(CheckForSyncIndependentRef ref, bool silent) {
ref.read(SyncServerProvider(syncId: 1).notifier).checkForSync(silent);
}
@riverpod
class ChangedItemsManager extends _$ChangedItemsManager {
@override
ChangedItems? build({required int? managerId}) {
return isar.changedItems.getSync(managerId!);
}
void cleanChangedItems(bool txn) {
final changedItems =
isar.changedItems.getSync(managerId!) ?? ChangedItems(id: managerId);
changedItems.deletedMangas = [];
changedItems.updatedChapters = [];
changedItems.deletedCategories = [];
if (!txn) {
isar.changedItems.putSync(changedItems);
} else {
isar.writeTxnSync(() {
isar.changedItems.putSync(changedItems);
});
}
}
void addDeletedManga(Manga manga, bool txn) {
final changedItems =
isar.changedItems.getSync(managerId!) ?? ChangedItems(id: managerId);
log("DEBUG");
log(jsonEncode(changedItems));
final deletedManga = DeletedManga(mangaId: manga.id);
changedItems.deletedMangas = changedItems.deletedMangas?.toList()
?..add(deletedManga);
if (!txn) {
isar.changedItems.putSync(changedItems);
} else {
isar.writeTxnSync(() {
isar.changedItems.putSync(changedItems);
});
}
}
Future addDeletedMangaAsync(Manga manga, bool txn) async {
final changedItems =
await isar.changedItems.get(managerId!) ?? ChangedItems(id: managerId);
final deletedManga = DeletedManga(mangaId: manga.id);
changedItems.deletedMangas = changedItems.deletedMangas?.toList()
?..add(deletedManga);
if (!txn) {
await isar.changedItems.put(changedItems);
} else {
await isar.writeTxn(() async {
await isar.changedItems.put(changedItems);
});
}
}
void addUpdatedChapter(Chapter chapter, bool deleted, bool txn) {
final changedItems =
isar.changedItems.getSync(managerId!) ?? ChangedItems(id: managerId);
bool updated = false;
changedItems.updatedChapters = changedItems.updatedChapters?.map((e) {
if (e.chapterId == chapter.id && e.mangaId == chapter.mangaId) {
e.isBookmarked = chapter.isBookmarked;
e.isRead = chapter.isRead;
e.lastPageRead = chapter.lastPageRead;
e.deleted = deleted;
updated = true;
}
return e;
}).toList();
if (!updated) {
final updatedChapter = UpdatedChapter(
chapterId: chapter.id,
mangaId: chapter.mangaId,
isBookmarked: chapter.isBookmarked,
isRead: chapter.isRead,
lastPageRead: chapter.lastPageRead,
deleted: deleted);
changedItems.updatedChapters = changedItems.updatedChapters?.toList()
?..add(updatedChapter);
}
if (!txn) {
isar.changedItems.putSync(changedItems);
} else {
isar.writeTxnSync(() {
isar.changedItems.putSync(changedItems);
});
}
}
Future addUpdatedChapterAsync(Chapter chapter, bool deleted, bool txn) async {
final changedItems =
await isar.changedItems.get(managerId!) ?? ChangedItems(id: managerId);
bool updated = false;
changedItems.updatedChapters = changedItems.updatedChapters?.map((e) {
if (e.chapterId == chapter.id && e.mangaId == chapter.mangaId) {
e.isBookmarked = chapter.isBookmarked;
e.isRead = chapter.isRead;
e.lastPageRead = chapter.lastPageRead;
e.deleted = deleted;
updated = true;
}
return e;
}).toList();
if (!updated) {
final updatedChapter = UpdatedChapter(
chapterId: chapter.id,
mangaId: chapter.mangaId,
isBookmarked: chapter.isBookmarked,
isRead: chapter.isRead,
lastPageRead: chapter.lastPageRead,
deleted: deleted);
changedItems.updatedChapters = changedItems.updatedChapters?.toList()
?..add(updatedChapter);
}
if (!txn) {
await isar.changedItems.put(changedItems);
} else {
await isar.writeTxn(() async {
await isar.changedItems.put(changedItems);
});
}
}
void addDeletedCategory(Category category, bool txn) {
final changedItems =
isar.changedItems.getSync(managerId!) ?? ChangedItems(id: managerId);
final deletedCategory = DeletedCategory(categoryId: category.id);
changedItems.deletedCategories = changedItems.deletedCategories?.toList()
?..add(deletedCategory);
if (!txn) {
isar.changedItems.putSync(changedItems);
} else {
isar.writeTxnSync(() {
isar.changedItems.putSync(changedItems);
});
}
}
Future addDeletedCategoryAsync(Category category, bool txn) async {
final changedItems =
await isar.changedItems.get(managerId!) ?? ChangedItems(id: managerId);
final deletedCategory = DeletedCategory(categoryId: category.id);
changedItems.deletedCategories = changedItems.deletedCategories?.toList()
?..add(deletedCategory);
if (!txn) {
await isar.changedItems.put(changedItems);
} else {
await isar.writeTxn(() async {
await isar.changedItems.put(changedItems);
});
}
}
}
@riverpod
class Synching extends _$Synching {
@override
SyncPreference? build({required int? syncId}) {
return isar.syncPreferences.getSync(syncId!);
}
void login(SyncPreference syncPreference) {
isar.writeTxnSync(() {
isar.syncPreferences.putSync(syncPreference);
});
}
void logout() {
isar.writeTxnSync(() {
isar.syncPreferences.deleteSync(syncId!);
});
}
void setLastSync(int timestamp) {
isar.writeTxnSync(() {
isar.syncPreferences.putSync(
isar.syncPreferences.getSync(syncId!)!..lastSync = timestamp);
});
}
void setLastUpload(int timestamp) {
isar.writeTxnSync(() {
isar.syncPreferences.putSync(
isar.syncPreferences.getSync(syncId!)!..lastUpload = timestamp);
});
}
void setLastDownload(int timestamp) {
isar.writeTxnSync(() {
isar.syncPreferences.putSync(
isar.syncPreferences.getSync(syncId!)!..lastDownload = timestamp);
});
}
void setServer(String? server) {
isar.writeTxnSync(() {
isar.syncPreferences
.putSync(isar.syncPreferences.getSync(syncId!)!..server = server);
});
}
}
@riverpod
class SyncOnAppLaunchState extends _$SyncOnAppLaunchState {
@override
bool build() {
return isar.settings.getSync(227)!.syncOnAppLaunch ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..syncOnAppLaunch = value));
}
}
@riverpod
class SyncAfterReadingState extends _$SyncAfterReadingState {
@override
bool build() {
return isar.settings.getSync(227)!.syncAfterReading ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..syncAfterReading = value));
}
}

View file

@ -0,0 +1,647 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sync_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$addUpdatedChapterIndependentHash() =>
r'2a609f968ab03f617df4957fdd1ace6f013a3d2a';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [addUpdatedChapterIndependent].
@ProviderFor(addUpdatedChapterIndependent)
const addUpdatedChapterIndependentProvider =
AddUpdatedChapterIndependentFamily();
/// See also [addUpdatedChapterIndependent].
class AddUpdatedChapterIndependentFamily extends Family<void> {
/// See also [addUpdatedChapterIndependent].
const AddUpdatedChapterIndependentFamily();
/// See also [addUpdatedChapterIndependent].
AddUpdatedChapterIndependentProvider call(
Chapter chapter,
bool deleted,
bool txn,
) {
return AddUpdatedChapterIndependentProvider(
chapter,
deleted,
txn,
);
}
@override
AddUpdatedChapterIndependentProvider getProviderOverride(
covariant AddUpdatedChapterIndependentProvider provider,
) {
return call(
provider.chapter,
provider.deleted,
provider.txn,
);
}
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'addUpdatedChapterIndependentProvider';
}
/// See also [addUpdatedChapterIndependent].
class AddUpdatedChapterIndependentProvider extends AutoDisposeProvider<void> {
/// See also [addUpdatedChapterIndependent].
AddUpdatedChapterIndependentProvider(
Chapter chapter,
bool deleted,
bool txn,
) : this._internal(
(ref) => addUpdatedChapterIndependent(
ref as AddUpdatedChapterIndependentRef,
chapter,
deleted,
txn,
),
from: addUpdatedChapterIndependentProvider,
name: r'addUpdatedChapterIndependentProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$addUpdatedChapterIndependentHash,
dependencies: AddUpdatedChapterIndependentFamily._dependencies,
allTransitiveDependencies:
AddUpdatedChapterIndependentFamily._allTransitiveDependencies,
chapter: chapter,
deleted: deleted,
txn: txn,
);
AddUpdatedChapterIndependentProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.chapter,
required this.deleted,
required this.txn,
}) : super.internal();
final Chapter chapter;
final bool deleted;
final bool txn;
@override
Override overrideWith(
void Function(AddUpdatedChapterIndependentRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AddUpdatedChapterIndependentProvider._internal(
(ref) => create(ref as AddUpdatedChapterIndependentRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
chapter: chapter,
deleted: deleted,
txn: txn,
),
);
}
@override
AutoDisposeProviderElement<void> createElement() {
return _AddUpdatedChapterIndependentProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AddUpdatedChapterIndependentProvider &&
other.chapter == chapter &&
other.deleted == deleted &&
other.txn == txn;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, chapter.hashCode);
hash = _SystemHash.combine(hash, deleted.hashCode);
hash = _SystemHash.combine(hash, txn.hashCode);
return _SystemHash.finish(hash);
}
}
mixin AddUpdatedChapterIndependentRef on AutoDisposeProviderRef<void> {
/// The parameter `chapter` of this provider.
Chapter get chapter;
/// The parameter `deleted` of this provider.
bool get deleted;
/// The parameter `txn` of this provider.
bool get txn;
}
class _AddUpdatedChapterIndependentProviderElement
extends AutoDisposeProviderElement<void>
with AddUpdatedChapterIndependentRef {
_AddUpdatedChapterIndependentProviderElement(super.provider);
@override
Chapter get chapter =>
(origin as AddUpdatedChapterIndependentProvider).chapter;
@override
bool get deleted => (origin as AddUpdatedChapterIndependentProvider).deleted;
@override
bool get txn => (origin as AddUpdatedChapterIndependentProvider).txn;
}
String _$checkForSyncIndependentHash() =>
r'7f3820bbb551ad5a98145c0f05106bb104f2db26';
/// See also [checkForSyncIndependent].
@ProviderFor(checkForSyncIndependent)
const checkForSyncIndependentProvider = CheckForSyncIndependentFamily();
/// See also [checkForSyncIndependent].
class CheckForSyncIndependentFamily extends Family<void> {
/// See also [checkForSyncIndependent].
const CheckForSyncIndependentFamily();
/// See also [checkForSyncIndependent].
CheckForSyncIndependentProvider call(
bool silent,
) {
return CheckForSyncIndependentProvider(
silent,
);
}
@override
CheckForSyncIndependentProvider getProviderOverride(
covariant CheckForSyncIndependentProvider provider,
) {
return call(
provider.silent,
);
}
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'checkForSyncIndependentProvider';
}
/// See also [checkForSyncIndependent].
class CheckForSyncIndependentProvider extends AutoDisposeProvider<void> {
/// See also [checkForSyncIndependent].
CheckForSyncIndependentProvider(
bool silent,
) : this._internal(
(ref) => checkForSyncIndependent(
ref as CheckForSyncIndependentRef,
silent,
),
from: checkForSyncIndependentProvider,
name: r'checkForSyncIndependentProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$checkForSyncIndependentHash,
dependencies: CheckForSyncIndependentFamily._dependencies,
allTransitiveDependencies:
CheckForSyncIndependentFamily._allTransitiveDependencies,
silent: silent,
);
CheckForSyncIndependentProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.silent,
}) : super.internal();
final bool silent;
@override
Override overrideWith(
void Function(CheckForSyncIndependentRef provider) create,
) {
return ProviderOverride(
origin: this,
override: CheckForSyncIndependentProvider._internal(
(ref) => create(ref as CheckForSyncIndependentRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
silent: silent,
),
);
}
@override
AutoDisposeProviderElement<void> createElement() {
return _CheckForSyncIndependentProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is CheckForSyncIndependentProvider && other.silent == silent;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, silent.hashCode);
return _SystemHash.finish(hash);
}
}
mixin CheckForSyncIndependentRef on AutoDisposeProviderRef<void> {
/// The parameter `silent` of this provider.
bool get silent;
}
class _CheckForSyncIndependentProviderElement
extends AutoDisposeProviderElement<void> with CheckForSyncIndependentRef {
_CheckForSyncIndependentProviderElement(super.provider);
@override
bool get silent => (origin as CheckForSyncIndependentProvider).silent;
}
String _$changedItemsManagerHash() =>
r'a4f0363ab430ddb6c2a23fde6f5671ba8ec252cf';
abstract class _$ChangedItemsManager
extends BuildlessAutoDisposeNotifier<ChangedItems?> {
late final int? managerId;
ChangedItems? build({
required int? managerId,
});
}
/// See also [ChangedItemsManager].
@ProviderFor(ChangedItemsManager)
const changedItemsManagerProvider = ChangedItemsManagerFamily();
/// See also [ChangedItemsManager].
class ChangedItemsManagerFamily extends Family<ChangedItems?> {
/// See also [ChangedItemsManager].
const ChangedItemsManagerFamily();
/// See also [ChangedItemsManager].
ChangedItemsManagerProvider call({
required int? managerId,
}) {
return ChangedItemsManagerProvider(
managerId: managerId,
);
}
@override
ChangedItemsManagerProvider getProviderOverride(
covariant ChangedItemsManagerProvider provider,
) {
return call(
managerId: provider.managerId,
);
}
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'changedItemsManagerProvider';
}
/// See also [ChangedItemsManager].
class ChangedItemsManagerProvider extends AutoDisposeNotifierProviderImpl<
ChangedItemsManager, ChangedItems?> {
/// See also [ChangedItemsManager].
ChangedItemsManagerProvider({
required int? managerId,
}) : this._internal(
() => ChangedItemsManager()..managerId = managerId,
from: changedItemsManagerProvider,
name: r'changedItemsManagerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$changedItemsManagerHash,
dependencies: ChangedItemsManagerFamily._dependencies,
allTransitiveDependencies:
ChangedItemsManagerFamily._allTransitiveDependencies,
managerId: managerId,
);
ChangedItemsManagerProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.managerId,
}) : super.internal();
final int? managerId;
@override
ChangedItems? runNotifierBuild(
covariant ChangedItemsManager notifier,
) {
return notifier.build(
managerId: managerId,
);
}
@override
Override overrideWith(ChangedItemsManager Function() create) {
return ProviderOverride(
origin: this,
override: ChangedItemsManagerProvider._internal(
() => create()..managerId = managerId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
managerId: managerId,
),
);
}
@override
AutoDisposeNotifierProviderElement<ChangedItemsManager, ChangedItems?>
createElement() {
return _ChangedItemsManagerProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ChangedItemsManagerProvider && other.managerId == managerId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, managerId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin ChangedItemsManagerRef on AutoDisposeNotifierProviderRef<ChangedItems?> {
/// The parameter `managerId` of this provider.
int? get managerId;
}
class _ChangedItemsManagerProviderElement
extends AutoDisposeNotifierProviderElement<ChangedItemsManager,
ChangedItems?> with ChangedItemsManagerRef {
_ChangedItemsManagerProviderElement(super.provider);
@override
int? get managerId => (origin as ChangedItemsManagerProvider).managerId;
}
String _$synchingHash() => r'2ef7fd99da4292ed236252d2b727cff9a69f43a9';
abstract class _$Synching
extends BuildlessAutoDisposeNotifier<SyncPreference?> {
late final int? syncId;
SyncPreference? build({
required int? syncId,
});
}
/// See also [Synching].
@ProviderFor(Synching)
const synchingProvider = SynchingFamily();
/// See also [Synching].
class SynchingFamily extends Family<SyncPreference?> {
/// See also [Synching].
const SynchingFamily();
/// See also [Synching].
SynchingProvider call({
required int? syncId,
}) {
return SynchingProvider(
syncId: syncId,
);
}
@override
SynchingProvider getProviderOverride(
covariant SynchingProvider provider,
) {
return call(
syncId: provider.syncId,
);
}
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'synchingProvider';
}
/// See also [Synching].
class SynchingProvider
extends AutoDisposeNotifierProviderImpl<Synching, SyncPreference?> {
/// See also [Synching].
SynchingProvider({
required int? syncId,
}) : this._internal(
() => Synching()..syncId = syncId,
from: synchingProvider,
name: r'synchingProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$synchingHash,
dependencies: SynchingFamily._dependencies,
allTransitiveDependencies: SynchingFamily._allTransitiveDependencies,
syncId: syncId,
);
SynchingProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.syncId,
}) : super.internal();
final int? syncId;
@override
SyncPreference? runNotifierBuild(
covariant Synching notifier,
) {
return notifier.build(
syncId: syncId,
);
}
@override
Override overrideWith(Synching Function() create) {
return ProviderOverride(
origin: this,
override: SynchingProvider._internal(
() => create()..syncId = syncId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
syncId: syncId,
),
);
}
@override
AutoDisposeNotifierProviderElement<Synching, SyncPreference?>
createElement() {
return _SynchingProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SynchingProvider && other.syncId == syncId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, syncId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin SynchingRef on AutoDisposeNotifierProviderRef<SyncPreference?> {
/// The parameter `syncId` of this provider.
int? get syncId;
}
class _SynchingProviderElement
extends AutoDisposeNotifierProviderElement<Synching, SyncPreference?>
with SynchingRef {
_SynchingProviderElement(super.provider);
@override
int? get syncId => (origin as SynchingProvider).syncId;
}
String _$syncOnAppLaunchStateHash() =>
r'dc7f3243e38a748462628229066c8fc0653c908b';
/// See also [SyncOnAppLaunchState].
@ProviderFor(SyncOnAppLaunchState)
final syncOnAppLaunchStateProvider =
AutoDisposeNotifierProvider<SyncOnAppLaunchState, bool>.internal(
SyncOnAppLaunchState.new,
name: r'syncOnAppLaunchStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$syncOnAppLaunchStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SyncOnAppLaunchState = AutoDisposeNotifier<bool>;
String _$syncAfterReadingStateHash() =>
r'e507acd490b5aea7fc1a8fd7a369ec01f4c47192';
/// See also [SyncAfterReadingState].
@ProviderFor(SyncAfterReadingState)
final syncAfterReadingStateProvider =
AutoDisposeNotifierProvider<SyncAfterReadingState, bool>.internal(
SyncAfterReadingState.new,
name: r'syncAfterReadingStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$syncAfterReadingStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SyncAfterReadingState = AutoDisposeNotifier<bool>;
// 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

@ -0,0 +1,470 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/utils/date.dart';
import 'package:mangayomi/models/sync_preference.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/modules/more/settings/sync/widgets/sync_listile.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/services/sync_server.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
class SyncScreen extends ConsumerWidget {
const SyncScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final syncAfterReading = ref.watch(syncAfterReadingStateProvider);
final syncOnAppLaunch = ref.watch(syncOnAppLaunchStateProvider);
final l10n = l10nLocalizations(context)!;
return Scaffold(
appBar: AppBar(
title: Text(l10nLocalizations(context)!.syncing),
),
body: SingleChildScrollView(
child: StreamBuilder(
stream: isar.syncPreferences
.filter()
.syncIdIsNotNull()
.watch(fireImmediately: true),
builder: (context, snapshot) {
SyncPreference syncPreference = snapshot.data?.isNotEmpty ?? false
? snapshot.data?.first ?? SyncPreference()
: SyncPreference();
final bool isLogged =
syncPreference.authToken?.isNotEmpty ?? false;
return Column(
children: [
SwitchListTile(
value: syncAfterReading,
title: Text(context.l10n.syncAfterReading),
onChanged: !isLogged
? null
: (value) {
ref
.read(syncAfterReadingStateProvider.notifier)
.set(value);
}),
SwitchListTile(
value: syncOnAppLaunch,
title: Text(context.l10n.syncOnAppLaunch),
onChanged: !isLogged
? null
: (value) {
ref
.read(syncOnAppLaunchStateProvider.notifier)
.set(value);
}),
Padding(
padding: const EdgeInsets.only(
left: 15, right: 15, bottom: 10, top: 5),
child: Row(
children: [
Text(l10n.services,
style: TextStyle(
fontSize: 13, color: context.primaryColor)),
],
),
),
SyncListile(
onTap: () async {
_showDialogLogin(context, ref);
},
id: 1,
preference: syncPreference,
),
ListTile(
title: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Icon(
Icons.info_outline_rounded,
color: context.secondaryColor,
),
const SizedBox(width: 10),
Text(l10n.syncing_subtitle,
style: TextStyle(
fontSize: 11, color: context.secondaryColor))
],
),
),
),
ListTile(
title: Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
children: [
Icon(
Icons.sync,
color: context.secondaryColor,
),
const SizedBox(width: 10),
Column(children: [
const SizedBox(width: 20),
Text(
"${l10n.last_sync}: ${dateFormat((syncPreference.lastSync ?? 0).toString(), ref: ref, context: context)} ${dateFormatHour((syncPreference.lastSync ?? 0).toString(), context)}",
style: TextStyle(
fontSize: 11,
color: context.secondaryColor)),
const SizedBox(width: 20),
Text(
"${l10n.last_upload}: ${dateFormat((syncPreference.lastUpload ?? 0).toString(), ref: ref, context: context)} ${dateFormatHour((syncPreference.lastUpload ?? 0).toString(), context)}",
style: TextStyle(
fontSize: 11,
color: context.secondaryColor)),
const SizedBox(width: 20),
Text(
"${l10n.last_download}: ${dateFormat((syncPreference.lastDownload ?? 0).toString(), ref: ref, context: context)} ${dateFormatHour((syncPreference.lastDownload ?? 0).toString(), context)}",
style: TextStyle(
fontSize: 11,
color: context.secondaryColor)),
]),
],
),
),
),
Row(
children: [
const SizedBox(width: 20),
Column(
children: [
IconButton(
onPressed: !isLogged
? null
: () {
ref
.read(syncServerProvider(syncId: 1)
.notifier)
.checkForSync(false);
},
icon: Icon(
Icons.sync,
color: !isLogged ? context.secondaryColor : context.primaryColor,
)),
Text(l10n.sync_button_sync),
],
),
const SizedBox(width: 20),
Column(
children: [
IconButton(
onPressed: !isLogged
? null
: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(
l10n.sync_confirm_upload),
actions: [
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Colors
.transparent,
shadowColor: Colors
.transparent,
surfaceTintColor:
Colors
.transparent,
shape: RoundedRectangleBorder(
side: BorderSide(
color: context
.secondaryColor),
borderRadius:
BorderRadius
.circular(
20))),
onPressed: () {
Navigator.pop(
context);
},
child: Text(
l10n.cancel,
style: TextStyle(
color: context
.secondaryColor),
)),
const SizedBox(width: 15),
ElevatedButton(
style: ElevatedButton
.styleFrom(
backgroundColor:
Colors.red
.withOpacity(
0.7)),
onPressed: () {
ref
.read(
syncServerProvider(
syncId:
1)
.notifier)
.uploadToServer(
l10n);
Navigator.pop(
context);
},
child: Text(
l10n.dialog_confirm,
style: TextStyle(
color: context
.secondaryColor),
)),
],
)
],
);
});
},
icon: Icon(
Icons.cloud_upload_outlined,
color: !isLogged ? context.secondaryColor : context.primaryColor,
)),
Text(l10n.sync_button_upload),
],
),
const SizedBox(width: 20),
Column(
children: [
IconButton(
onPressed: !isLogged
? null
: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(
l10n.sync_confirm_download),
actions: [
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Colors
.transparent,
shadowColor: Colors
.transparent,
surfaceTintColor:
Colors
.transparent,
shape: RoundedRectangleBorder(
side: BorderSide(
color: context
.secondaryColor),
borderRadius:
BorderRadius
.circular(
20))),
onPressed: () {
Navigator.pop(
context);
},
child: Text(
l10n.cancel,
style: TextStyle(
color: context
.secondaryColor),
)),
const SizedBox(width: 15),
ElevatedButton(
style: ElevatedButton
.styleFrom(
backgroundColor:
Colors.red
.withOpacity(
0.7)),
onPressed: () {
ref
.read(
syncServerProvider(
syncId:
1)
.notifier)
.downloadFromServer(
l10n);
Navigator.pop(
context);
},
child: Text(
l10n.dialog_confirm,
style: TextStyle(
color: context
.secondaryColor),
)),
],
)
],
);
});
},
icon: Icon(
Icons.cloud_download_outlined,
color: !isLogged ? context.secondaryColor : context.primaryColor,
)),
Text(l10n.sync_button_download),
],
),
],
)
],
);
}),
),
);
}
}
void _showDialogLogin(BuildContext context, WidgetRef ref) {
final serverController = TextEditingController();
final emailController = TextEditingController();
final passwordController = TextEditingController();
String server = "";
String email = "";
String password = "";
String errorMessage = "";
bool isLoading = false;
bool obscureText = true;
final l10n = l10nLocalizations(context)!;
showDialog(
context: context,
builder: (context) => StatefulBuilder(builder: (context, setState) {
return AlertDialog(
title: Text(
l10n.login_into("SyncServer"),
style: const TextStyle(fontSize: 30),
),
content: SizedBox(
height: 400,
width: MediaQuery.of(context).size.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: serverController,
autofocus: true,
onChanged: (value) => setState(() {
server = value;
}),
decoration: InputDecoration(
hintText: l10n.sync_server,
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5)),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide()))),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: emailController,
autofocus: true,
onChanged: (value) => setState(() {
email = value;
}),
decoration: InputDecoration(
hintText: l10n.email_adress,
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5)),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide()))),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: TextFormField(
controller: passwordController,
obscureText: obscureText,
onChanged: (value) => setState(() {
password = value;
}),
decoration: InputDecoration(
hintText: l10n.password,
suffixIcon: IconButton(
onPressed: () => setState(() {
obscureText = !obscureText;
}),
icon: Icon(obscureText
? Icons.visibility_outlined
: Icons.visibility_off_outlined)),
filled: false,
contentPadding: const EdgeInsets.all(12),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(width: 0.4),
borderRadius: BorderRadius.circular(5)),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5)),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(5),
borderSide: const BorderSide()))),
),
const SizedBox(height: 10),
Text(errorMessage, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 30),
Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
child: SizedBox(
width: context.width(1),
height: 50,
child: ElevatedButton(
onPressed: isLoading
? null
: () async {
setState(() {
isLoading = true;
});
final res = await ref
.read(
syncServerProvider(syncId: 1).notifier)
.login(l10n, server, email, password);
if (!res.$1) {
setState(() {
isLoading = false;
errorMessage = res.$2;
});
} else {
if (context.mounted) {
Navigator.pop(context);
}
}
},
child: isLoading
? const CircularProgressIndicator()
: Text(l10n.login))),
)
],
),
),
);
}),
);
}

View file

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/models/sync_preference.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
class SyncListile extends ConsumerWidget {
final VoidCallback onTap;
final int id;
final SyncPreference preference;
final String? text;
const SyncListile(
{super.key,
required this.onTap,
required this.id,
required this.preference,
this.text});
@override
Widget build(BuildContext context, WidgetRef ref) {
final bool isLogged = preference.authToken?.isNotEmpty ?? false;
final l10n = l10nLocalizations(context)!;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
leading: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: const Color.fromRGBO(18, 25, 35, 1)),
width: 60,
height: 70,
child: const Icon(
Icons.dns_outlined,
size: 30,
color: Colors.grey,
),
),
trailing: (isLogged
? const Icon(
Icons.check,
size: 30,
color: Colors.green,
)
: null),
onTap: isLogged
? () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.log_out_from(l10n.sync_server)),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
surfaceTintColor: Colors.transparent,
shape: RoundedRectangleBorder(
side: BorderSide(
color: context.secondaryColor),
borderRadius:
BorderRadius.circular(20))),
onPressed: () {
Navigator.pop(context);
},
child: Text(
l10n.cancel,
style: TextStyle(
color: context.secondaryColor),
)),
const SizedBox(width: 15),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
Colors.red.withOpacity(0.7)),
onPressed: () {
ref
.read(synchingProvider(syncId: id)
.notifier)
.logout();
Navigator.pop(context);
},
child: Text(
l10n.log_out,
style: TextStyle(
color: context.secondaryColor),
)),
],
)
],
);
});
}
: onTap,
title: Text(
text ?? l10n.sync_server,
style: TextStyle(fontSize: text != null ? 13 : null),
),
),
);
}
}

View file

@ -4,12 +4,15 @@ import 'package:isar/isar.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/changed_items.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';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/models/sync_preference.dart';
import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:mangayomi/utils/extensions/string_extensions.dart';
@ -126,16 +129,19 @@ class StorageProvider {
final isar = Isar.openSync([
MangaSchema,
ChangedItemsSchema,
ChapterSchema,
CategorySchema,
FeedSchema,
HistorySchema,
DownloadSchema,
SourceSchema,
SettingsSchema,
TrackPreferenceSchema,
TrackSchema,
SyncPreferenceSchema,
SourcePreferenceSchema,
SourcePreferenceStringValueSchema
SourcePreferenceStringValueSchema,
], directory: dir!.path, name: "mangayomiDb", inspector: inspector!);
if (isar.settings.filter().idEqualTo(227).isEmptySync()) {

View file

@ -10,10 +10,12 @@ 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';
import 'package:mangayomi/modules/more/settings/player/player_screen.dart';
import 'package:mangayomi/modules/more/settings/sync/sync.dart';
import 'package:mangayomi/modules/more/settings/track/track.dart';
import 'package:mangayomi/modules/more/settings/track/manage_trackers/manage_trackers.dart';
import 'package:mangayomi/modules/more/settings/track/manage_trackers/tracking_detail.dart';
@ -114,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',
@ -327,6 +338,19 @@ class RouterNotifier extends ChangeNotifier {
);
},
),
GoRoute(
path: "/sync",
name: "sync",
builder: (context, state) {
return const SyncScreen();
},
pageBuilder: (context, state) {
return transitionPage(
key: state.pageKey,
child: const SyncScreen(),
);
},
),
GoRoute(
path: "/sourceFilter",
name: "sourceFilter",

View file

@ -0,0 +1,532 @@
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';
import 'package:mangayomi/models/category.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/modules/more/settings/sync/models/jwt.dart';
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/blend_level_state_provider.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/flex_scheme_color_state_provider.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'dart:convert';
import 'package:mangayomi/services/http/m_client.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
part 'sync_server.g.dart';
@riverpod
class SyncServer extends _$SyncServer {
final http = MClient.init(reqcopyWith: {'useDartHttpClient': true});
final String _loginUrl = '/login';
final String _checkUrl = '/check';
final String _syncUrl = '/sync';
final String _uploadUrl = '/upload/full';
final String _downloadUrl = '/download';
@override
void build({required int syncId}) {}
Future<(bool, String)> login(AppLocalizations l10n, String server,
String username, String password) async {
try {
var response = await http.post(
Uri.parse('$server$_loginUrl'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({'email': username, 'password': password}),
);
var jsonData = jsonDecode(response.body) as Map<String, dynamic>;
if (response.statusCode != 200) {
return (false, jsonData["error"] as String);
}
ref.read(synchingProvider(syncId: syncId).notifier).login(SyncPreference(
syncId: syncId,
email: username,
server: server,
authToken: jsonData["token"]));
botToast(l10n.sync_logged);
return (true, "");
} catch (e) {
return (false, e.toString());
}
}
Future<void> checkForSync(bool silent) async {
if (!silent) {
botToast("Checking for sync...", second: 2);
}
try {
final datas = _getData();
final accessToken = _getAccessToken();
final localHash = _getDataHash(datas);
var response = await http.get(
Uri.parse('${_getServer()}$_checkUrl'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $accessToken'
},
);
if (response.statusCode != 200) {
botToast("Check failed", second: 5);
return;
}
var jsonData = jsonDecode(response.body) as Map<String, dynamic>;
final remoteHash = jsonData["hash"];
if (localHash != remoteHash) {
syncToServer(silent);
} else if (!silent) {
botToast("Sync up to date", second: 2);
}
} catch (error) {
botToast(error.toString(), second: 5);
}
}
Future<void> syncToServer(bool silent) async {
if (!silent) {
botToast("Sync started...", second: 2);
}
try {
final datas = _getData();
final accessToken = _getAccessToken();
var response = await http.post(
Uri.parse('${_getServer()}$_syncUrl'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $accessToken'
},
body: jsonEncode(
{'backupData': datas, 'changedItems': _getChangedData()}),
);
if (response.statusCode != 200) {
botToast("Sync failed", second: 5);
return;
}
var jsonData = jsonDecode(response.body) as Map<String, dynamic>;
final decodedBackupData = jsonData["backupData"] is String
? jsonDecode(jsonData["backupData"])
: jsonData["backupData"];
_restoreMerge(decodedBackupData);
ref
.read(synchingProvider(syncId: syncId).notifier)
.setLastSync(DateTime.now().millisecondsSinceEpoch);
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.cleanChangedItems(true);
if (!silent) {
botToast("Sync finished", second: 2);
}
} catch (error) {
botToast(error.toString(), second: 5);
}
}
Future<void> uploadToServer(AppLocalizations l10n) async {
botToast(l10n.sync_uploading, second: 2);
try {
final datas = _getData();
final accessToken = _getAccessToken();
var response = await http.post(
Uri.parse('${_getServer()}$_uploadUrl'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $accessToken'
},
body: jsonEncode({'backupData': datas}),
);
if (response.statusCode != 200) {
botToast(l10n.sync_upload_failed, second: 5);
return;
}
ref
.read(synchingProvider(syncId: syncId).notifier)
.setLastUpload(DateTime.now().millisecondsSinceEpoch);
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.cleanChangedItems(true);
botToast(l10n.sync_upload_finished, second: 2);
} catch (error) {
botToast(error.toString(), second: 5);
}
}
Future<void> downloadFromServer(AppLocalizations l10n) async {
botToast(l10n.sync_downloading, second: 2);
try {
final accessToken = _getAccessToken();
var response = await http.get(
Uri.parse('${_getServer()}$_downloadUrl'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $accessToken'
},
);
if (response.statusCode != 200) {
botToast(l10n.sync_download_failed, second: 5);
return;
}
var jsonData = jsonDecode(response.body) as Map<String, dynamic>;
_restore(jsonData["backupData"] is String
? jsonDecode(jsonData["backupData"])
: jsonData["backupData"]);
ref
.read(synchingProvider(syncId: syncId).notifier)
.setLastDownload(DateTime.now().millisecondsSinceEpoch);
ref
.read(changedItemsManagerProvider(managerId: 1).notifier)
.cleanChangedItems(true);
botToast(l10n.sync_download_finished, second: 2);
} catch (error) {
botToast(error.toString(), second: 5);
}
}
String _getDataHash(Map<String, dynamic> data) {
Map<String, dynamic> datas = {};
datas["version"] = data["version"];
datas["manga"] = data["manga"];
datas["categories"] = data["categories"];
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();
}
Map<String, dynamic> _getChangedData() {
Map<String, dynamic> data = {};
final changedItems = isar.changedItems.getSync(1);
if (changedItems != null) {
data.addAll({
"deletedMangas":
changedItems.deletedMangas?.map((e) => e.toJson()).toList() ?? []
});
data.addAll({
"updatedChapters":
changedItems.updatedChapters?.map((e) => e.toJson()).toList() ?? []
});
data.addAll({
"deletedCategories":
changedItems.deletedCategories?.map((e) => e.toJson()).toList() ??
[]
});
}
return data;
}
Map<String, dynamic> _getData() {
Map<String, dynamic> datas = {};
datas.addAll({"version": "1"});
final mangas = isar.mangas
.filter()
.idIsNotNull()
.favoriteEqualTo(true)
.isLocalArchiveEqualTo(false)
.findAllSync()
.map((e) => e.toJson())
.toList();
datas.addAll({"manga": mangas});
final categorys = isar.categorys
.filter()
.idIsNotNull()
.findAllSync()
.map((e) => e.toJson())
.toList();
datas.addAll({"categories": categorys});
final chapters = isar.chapters
.filter()
.idIsNotNull()
.findAllSync()
.map((e) => e.toJson())
.toList();
datas.addAll({"chapters": chapters});
datas.addAll({"downloads": []});
final tracks = isar.tracks
.filter()
.idIsNotNull()
.findAllSync()
.map((e) => e.toJson())
.toList();
datas.addAll({"tracks": tracks});
datas.addAll({"trackPreferences": []});
final historys = isar.historys
.filter()
.idIsNotNull()
.findAllSync()
.map((e) => e.toJson())
.toList();
datas.addAll({"history": historys});
final settings = isar.settings
.filter()
.idIsNotNull()
.findAllSync()
.map((e) => e.toJson())
.toList();
datas.addAll({"settings": settings});
final sources = isar.sources
.filter()
.idIsNotNull()
.findAllSync()
.map((e) => e.toJson())
.toList();
datas.addAll({"extensions": sources});
final sourcePreferences = isar.sourcePreferences
.filter()
.idIsNotNull()
.keyIsNotNull()
.findAllSync()
.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;
}
void _restoreMerge(Map<String, dynamic> backup) {
if (backup['version'] == "1") {
try {
final manga =
(backup["manga"] as List?)?.map((e) => Manga.fromJson(e)).toList();
final chapters = (backup["chapters"] as List?)
?.map((e) => Chapter.fromJson(e))
.toList();
final categories = (backup["categories"] as List?)
?.map((e) => Category.fromJson(e))
.toList();
final track =
(backup["tracks"] as List?)?.map((e) => Track.fromJson(e)).toList();
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();
if (manga != null) {
isar.mangas.putAllSync(manga);
if (chapters != null) {
isar.chapters.clearSync();
for (var chapter in chapters) {
final manga = isar.mangas.getSync(chapter.mangaId!);
if (manga != null) {
isar.chapters.putSync(chapter..manga.value = manga);
chapter.manga.saveSync();
}
}
isar.historys.clearSync();
if (history != null) {
for (var element in history) {
final chapter = isar.chapters.getSync(element.chapterId!);
if (chapter != null) {
isar.historys.putSync(element..chapter.value = chapter);
element.chapter.saveSync();
}
}
}
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();
if (categories != null) {
isar.categorys.putAllSync(categories);
}
}
isar.tracks.clearSync();
if (track != null) {
isar.tracks.putAllSync(track);
}
ref.invalidate(themeModeStateProvider);
ref.invalidate(blendLevelStateProvider);
ref.invalidate(flexSchemeColorStateProvider);
ref.invalidate(pureBlackDarkModeStateProvider);
ref.invalidate(l10nLocaleStateProvider);
});
} catch (e) {
botToast(e.toString());
}
}
}
void _restore(Map<String, dynamic> backup) {
if (backup['version'] == "1") {
try {
final manga =
(backup["manga"] as List?)?.map((e) => Manga.fromJson(e)).toList();
final chapters = (backup["chapters"] as List?)
?.map((e) => Chapter.fromJson(e))
.toList();
final categories = (backup["categories"] as List?)
?.map((e) => Category.fromJson(e))
.toList();
final track =
(backup["tracks"] as List?)?.map((e) => Track.fromJson(e)).toList();
final history = (backup["history"] as List?)
?.map((e) => History.fromJson(e))
.toList();
final settings = (backup["settings"] as List?)
?.map((e) => Settings.fromJson(e))
.toList();
final extensions = (backup["extensions"] as List?)
?.map((e) => Source.fromJson(e))
.toList();
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();
if (manga != null) {
isar.mangas.putAllSync(manga);
if (chapters != null) {
isar.chapters.clearSync();
for (var chapter in chapters) {
final manga = isar.mangas.getSync(chapter.mangaId!);
if (manga != null) {
isar.chapters.putSync(chapter..manga.value = manga);
chapter.manga.saveSync();
}
}
isar.historys.clearSync();
if (history != null) {
for (var element in history) {
final chapter = isar.chapters.getSync(element.chapterId!);
if (chapter != null) {
isar.historys.putSync(element..chapter.value = chapter);
element.chapter.saveSync();
}
}
}
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();
if (categories != null) {
isar.categorys.putAllSync(categories);
}
}
isar.tracks.clearSync();
if (track != null) {
isar.tracks.putAllSync(track);
}
isar.sources.clearSync();
if (extensions != null) {
isar.sources.putAllSync(extensions);
}
isar.sourcePreferences.clearSync();
if (extensionsPref != null) {
isar.sourcePreferences.putAllSync(extensionsPref);
}
final syncAfterReading = isar.settings.getSync(227)!.syncAfterReading;
final syncOnAppLaunch = isar.settings.getSync(227)!.syncOnAppLaunch;
isar.settings.clearSync();
if (settings != null) {
isar.settings.putAllSync(settings);
}
isar.settings.putSync(
isar.settings.getSync(227)!..syncAfterReading = syncAfterReading);
isar.settings.putSync(
isar.settings.getSync(227)!..syncOnAppLaunch = syncOnAppLaunch);
ref.invalidate(themeModeStateProvider);
ref.invalidate(blendLevelStateProvider);
ref.invalidate(flexSchemeColorStateProvider);
ref.invalidate(pureBlackDarkModeStateProvider);
ref.invalidate(l10nLocaleStateProvider);
});
} catch (e) {
botToast(e.toString(), second: 5);
}
}
}
String _getAccessToken() {
final syncPrefs = ref.watch(synchingProvider(syncId: syncId));
if (syncPrefs == null || syncPrefs.authToken == null) {
return "";
}
var paddedPayload = syncPrefs.authToken!.split(".")[1];
if (paddedPayload.length % 4 > 0) {
paddedPayload += '=' * (4 - paddedPayload.length % 4);
}
final decodedJwt = jsonDecode(utf8.decode(base64Decode(paddedPayload)))
as Map<String, dynamic>;
final auth = JWToken.fromJson(decodedJwt);
final expiresIn = DateTime.fromMillisecondsSinceEpoch(auth.exp!);
if (DateTime.now().isAfter(expiresIn)) {
ref.read(synchingProvider(syncId: syncId).notifier).logout();
botToast("SyncServer Token expired");
throw Exception("Token expired");
}
return syncPrefs.authToken!;
}
String _getServer() {
final syncPrefs = ref.watch(synchingProvider(syncId: syncId));
return syncPrefs?.server ?? "";
}
}

View file

@ -0,0 +1,172 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sync_server.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$syncServerHash() => r'e019e8870184d25f7a2659e35f6c3969bc683b50';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$SyncServer extends BuildlessAutoDisposeNotifier<void> {
late final int syncId;
void build({
required int syncId,
});
}
/// See also [SyncServer].
@ProviderFor(SyncServer)
const syncServerProvider = SyncServerFamily();
/// See also [SyncServer].
class SyncServerFamily extends Family<void> {
/// See also [SyncServer].
const SyncServerFamily();
/// See also [SyncServer].
SyncServerProvider call({
required int syncId,
}) {
return SyncServerProvider(
syncId: syncId,
);
}
@override
SyncServerProvider getProviderOverride(
covariant SyncServerProvider provider,
) {
return call(
syncId: provider.syncId,
);
}
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'syncServerProvider';
}
/// See also [SyncServer].
class SyncServerProvider
extends AutoDisposeNotifierProviderImpl<SyncServer, void> {
/// See also [SyncServer].
SyncServerProvider({
required int syncId,
}) : this._internal(
() => SyncServer()..syncId = syncId,
from: syncServerProvider,
name: r'syncServerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$syncServerHash,
dependencies: SyncServerFamily._dependencies,
allTransitiveDependencies:
SyncServerFamily._allTransitiveDependencies,
syncId: syncId,
);
SyncServerProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.syncId,
}) : super.internal();
final int syncId;
@override
void runNotifierBuild(
covariant SyncServer notifier,
) {
return notifier.build(
syncId: syncId,
);
}
@override
Override overrideWith(SyncServer Function() create) {
return ProviderOverride(
origin: this,
override: SyncServerProvider._internal(
() => create()..syncId = syncId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
syncId: syncId,
),
);
}
@override
AutoDisposeNotifierProviderElement<SyncServer, void> createElement() {
return _SyncServerProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SyncServerProvider && other.syncId == syncId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, syncId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin SyncServerRef on AutoDisposeNotifierProviderRef<void> {
/// The parameter `syncId` of this provider.
int get syncId;
}
class _SyncServerProviderElement
extends AutoDisposeNotifierProviderElement<SyncServer, void>
with SyncServerRef {
_SyncServerProviderElement(super.provider);
@override
int get syncId => (origin as SyncServerProvider).syncId;
}
// 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

@ -10,7 +10,8 @@ String dateFormat(String? timestamp,
String? stringDate,
bool forHistoryValue = false,
bool useRelativeTimesTamps = true,
String dateFormat = ""}) {
String dateFormat = "",
bool showHOURorMINUTE = false}) {
final l10n = l10nLocalizations(context)!;
final locale = currentLocale(context);
final relativeTimestamps = ref.watch(relativeTimesTampsStateProvider);
@ -34,6 +35,22 @@ String dateFormat(String? timestamp,
dateFormat.isEmpty ? dateFrmt : dateFormat, locale.toLanguageTag());
if (date == today && useRelativeTimesTamps && relativeTimestamps != 0) {
if (showHOURorMINUTE) {
final difference = now.difference(dateTime);
if (difference.inMinutes < 60) {
return switch (difference.inMinutes) {
0 => l10n.now,
1 => l10n.n_minute_ago(difference.inMinutes),
_ => l10n.n_minutes_ago(difference.inMinutes),
};
} else if (difference.inHours < 24) {
return switch (difference.inHours) {
1 => l10n.n_hour_ago(difference.inHours),
_ => l10n.n_hours_ago(difference.inHours),
};
}
}
return l10n.today;
} else if (date == yesterday &&
useRelativeTimesTamps &&
@ -48,7 +65,11 @@ String dateFormat(String? timestamp,
date.isAfter(sixDaysAgo) ||
date.isAfter(aWeekAgo)) {
final difference = today.difference(date).inDays;
return difference != 7 ? l10n.n_days_ago(difference) : l10n.a_week_ago;
return switch (difference) {
1 => l10n.n_day_ago(difference),
!= 7 => l10n.n_days_ago(difference),
_ => l10n.a_week_ago,
};
}
}
return forHistoryValue