diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f712d80..ca3d84b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -3,6 +3,7 @@ "library": "Library", "updates": "Updates", "history": "History", + "feed": "Feed", "browse": "Browse", "more": "More", "open_random_entry": "Open random entry", @@ -39,6 +40,7 @@ "no_recent_updates": "No recent updates", "remove_everything": "Remove everything", "remove_everything_msg": "Are you sure? All history will be lost", + "remove_all_feed_msg": "Are you sure? The whole feed will be cleared", "ok": "OK", "cancel": "Cancel", "remove": "Remove", @@ -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}" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index baad466..c7988ad 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { @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); diff --git a/lib/models/changed_items.dart b/lib/models/changed_items.dart new file mode 100644 index 0000000..60555df --- /dev/null +++ b/lib/models/changed_items.dart @@ -0,0 +1,86 @@ +import 'package:isar/isar.dart'; +part 'changed_items.g.dart'; + +@collection +@Name("Changed Items") +class ChangedItems { + Id? id; + List? deletedMangas; + List? updatedChapters; + List? deletedCategories; + ChangedItems( + {this.id = Isar.autoIncrement, + this.deletedMangas = const [], + this.updatedChapters = const [], + this.deletedCategories = const []}); + + ChangedItems.fromJson(Map json) { + id = json['id']; + deletedMangas = json['deletedMangas']; + updatedChapters = json['updatedChapters']; + deletedCategories = json['deletedCategories']; + } + + Map toJson() => { + 'id': id, + 'deletedMangas': deletedMangas, + 'updatedChapters': updatedChapters, + 'deletedCategories': deletedCategories + }; +} + +@embedded +class DeletedManga { + int? mangaId; + DeletedManga({this.mangaId}); + DeletedManga.fromJson(Map json) { + mangaId = json['mangaId']; + } + + Map 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 json) { + chapterId = json['chapterId']; + mangaId = json['mangaId']; + isBookmarked = json['isBookmarked']; + isRead = json['isRead']; + lastPageRead = json['lastPageRead']; + deleted = json['deleted']; + } + + Map toJson() => { + 'chapterId': chapterId, + 'mangaId': mangaId, + 'isBookmarked': isBookmarked, + 'isRead': isRead, + 'lastPageRead': lastPageRead, + 'deleted': deleted + }; +} + +@embedded +class DeletedCategory { + int? categoryId; + DeletedCategory({this.categoryId}); + DeletedCategory.fromJson(Map json) { + categoryId = json['categoryId']; + } + + Map toJson() => {'categoryId': categoryId}; +} diff --git a/lib/models/changed_items.g.dart b/lib/models/changed_items.g.dart new file mode 100644 index 0000000..16a1754 --- /dev/null +++ b/lib/models/changed_items.g.dart @@ -0,0 +1,1555 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'changed_items.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 GetChangedItemsCollection on Isar { + IsarCollection get changedItems => this.collection(); +} + +const ChangedItemsSchema = CollectionSchema( + name: r'Changed Items', + id: 5738738771983667580, + properties: { + r'deletedCategories': PropertySchema( + id: 0, + name: r'deletedCategories', + type: IsarType.objectList, + target: r'DeletedCategory', + ), + r'deletedMangas': PropertySchema( + id: 1, + name: r'deletedMangas', + type: IsarType.objectList, + target: r'DeletedManga', + ), + r'updatedChapters': PropertySchema( + id: 2, + name: r'updatedChapters', + type: IsarType.objectList, + target: r'UpdatedChapter', + ) + }, + estimateSize: _changedItemsEstimateSize, + serialize: _changedItemsSerialize, + deserialize: _changedItemsDeserialize, + deserializeProp: _changedItemsDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: { + r'DeletedManga': DeletedMangaSchema, + r'UpdatedChapter': UpdatedChapterSchema, + r'DeletedCategory': DeletedCategorySchema + }, + getId: _changedItemsGetId, + getLinks: _changedItemsGetLinks, + attach: _changedItemsAttach, + version: '3.1.0+1', +); + +int _changedItemsEstimateSize( + ChangedItems object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final list = object.deletedCategories; + if (list != null) { + bytesCount += 3 + list.length * 3; + { + final offsets = allOffsets[DeletedCategory]!; + for (var i = 0; i < list.length; i++) { + final value = list[i]; + bytesCount += + DeletedCategorySchema.estimateSize(value, offsets, allOffsets); + } + } + } + } + { + final list = object.deletedMangas; + if (list != null) { + bytesCount += 3 + list.length * 3; + { + final offsets = allOffsets[DeletedManga]!; + for (var i = 0; i < list.length; i++) { + final value = list[i]; + bytesCount += + DeletedMangaSchema.estimateSize(value, offsets, allOffsets); + } + } + } + } + { + final list = object.updatedChapters; + if (list != null) { + bytesCount += 3 + list.length * 3; + { + final offsets = allOffsets[UpdatedChapter]!; + for (var i = 0; i < list.length; i++) { + final value = list[i]; + bytesCount += + UpdatedChapterSchema.estimateSize(value, offsets, allOffsets); + } + } + } + } + return bytesCount; +} + +void _changedItemsSerialize( + ChangedItems object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeObjectList( + offsets[0], + allOffsets, + DeletedCategorySchema.serialize, + object.deletedCategories, + ); + writer.writeObjectList( + offsets[1], + allOffsets, + DeletedMangaSchema.serialize, + object.deletedMangas, + ); + writer.writeObjectList( + offsets[2], + allOffsets, + UpdatedChapterSchema.serialize, + object.updatedChapters, + ); +} + +ChangedItems _changedItemsDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = ChangedItems( + deletedCategories: reader.readObjectList( + offsets[0], + DeletedCategorySchema.deserialize, + allOffsets, + DeletedCategory(), + ), + deletedMangas: reader.readObjectList( + offsets[1], + DeletedMangaSchema.deserialize, + allOffsets, + DeletedManga(), + ), + id: id, + updatedChapters: reader.readObjectList( + offsets[2], + UpdatedChapterSchema.deserialize, + allOffsets, + UpdatedChapter(), + ), + ); + return object; +} + +P _changedItemsDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readObjectList( + offset, + DeletedCategorySchema.deserialize, + allOffsets, + DeletedCategory(), + )) as P; + case 1: + return (reader.readObjectList( + offset, + DeletedMangaSchema.deserialize, + allOffsets, + DeletedManga(), + )) as P; + case 2: + return (reader.readObjectList( + offset, + UpdatedChapterSchema.deserialize, + allOffsets, + UpdatedChapter(), + )) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _changedItemsGetId(ChangedItems object) { + return object.id ?? Isar.autoIncrement; +} + +List> _changedItemsGetLinks(ChangedItems object) { + return []; +} + +void _changedItemsAttach( + IsarCollection col, Id id, ChangedItems object) { + object.id = id; +} + +extension ChangedItemsQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension ChangedItemsQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder 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 idGreaterThan( + Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder 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 ChangedItemsQueryFilter + on QueryBuilder { + QueryBuilder + deletedCategoriesIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'deletedCategories', + )); + }); + } + + QueryBuilder + deletedCategoriesIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'deletedCategories', + )); + }); + } + + QueryBuilder + deletedCategoriesLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedCategories', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + deletedCategoriesIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedCategories', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + deletedCategoriesIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedCategories', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + deletedCategoriesLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedCategories', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + deletedCategoriesLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedCategories', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + deletedCategoriesLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedCategories', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder + deletedMangasIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'deletedMangas', + )); + }); + } + + QueryBuilder + deletedMangasIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'deletedMangas', + )); + }); + } + + QueryBuilder + deletedMangasLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedMangas', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + deletedMangasIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedMangas', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + deletedMangasIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedMangas', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + deletedMangasLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedMangas', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + deletedMangasLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedMangas', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + deletedMangasLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'deletedMangas', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } + + QueryBuilder idIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'id', + )); + }); + } + + QueryBuilder + idIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'id', + )); + }); + } + + QueryBuilder idEqualTo( + Id? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idGreaterThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idLessThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder 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 + updatedChaptersIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'updatedChapters', + )); + }); + } + + QueryBuilder + updatedChaptersIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'updatedChapters', + )); + }); + } + + QueryBuilder + updatedChaptersLengthEqualTo(int length) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'updatedChapters', + length, + true, + length, + true, + ); + }); + } + + QueryBuilder + updatedChaptersIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'updatedChapters', + 0, + true, + 0, + true, + ); + }); + } + + QueryBuilder + updatedChaptersIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'updatedChapters', + 0, + false, + 999999, + true, + ); + }); + } + + QueryBuilder + updatedChaptersLengthLessThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'updatedChapters', + 0, + true, + length, + include, + ); + }); + } + + QueryBuilder + updatedChaptersLengthGreaterThan( + int length, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'updatedChapters', + length, + include, + 999999, + true, + ); + }); + } + + QueryBuilder + updatedChaptersLengthBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.listLength( + r'updatedChapters', + lower, + includeLower, + upper, + includeUpper, + ); + }); + } +} + +extension ChangedItemsQueryObject + on QueryBuilder { + QueryBuilder + deletedCategoriesElement(FilterQuery q) { + return QueryBuilder.apply(this, (query) { + return query.object(q, r'deletedCategories'); + }); + } + + QueryBuilder + deletedMangasElement(FilterQuery q) { + return QueryBuilder.apply(this, (query) { + return query.object(q, r'deletedMangas'); + }); + } + + QueryBuilder + updatedChaptersElement(FilterQuery q) { + return QueryBuilder.apply(this, (query) { + return query.object(q, r'updatedChapters'); + }); + } +} + +extension ChangedItemsQueryLinks + on QueryBuilder {} + +extension ChangedItemsQuerySortBy + on QueryBuilder {} + +extension ChangedItemsQuerySortThenBy + on QueryBuilder { + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } +} + +extension ChangedItemsQueryWhereDistinct + on QueryBuilder {} + +extension ChangedItemsQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder?, QQueryOperations> + deletedCategoriesProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'deletedCategories'); + }); + } + + QueryBuilder?, QQueryOperations> + deletedMangasProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'deletedMangas'); + }); + } + + QueryBuilder?, QQueryOperations> + updatedChaptersProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'updatedChapters'); + }); + } +} + +// ************************************************************************** +// IsarEmbeddedGenerator +// ************************************************************************** + +// 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 + +const DeletedMangaSchema = Schema( + name: r'DeletedManga', + id: -4943524126252993118, + properties: { + r'mangaId': PropertySchema( + id: 0, + name: r'mangaId', + type: IsarType.long, + ) + }, + estimateSize: _deletedMangaEstimateSize, + serialize: _deletedMangaSerialize, + deserialize: _deletedMangaDeserialize, + deserializeProp: _deletedMangaDeserializeProp, +); + +int _deletedMangaEstimateSize( + DeletedManga object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + return bytesCount; +} + +void _deletedMangaSerialize( + DeletedManga object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeLong(offsets[0], object.mangaId); +} + +DeletedManga _deletedMangaDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = DeletedManga( + mangaId: reader.readLongOrNull(offsets[0]), + ); + return object; +} + +P _deletedMangaDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readLongOrNull(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +extension DeletedMangaQueryFilter + on QueryBuilder { + QueryBuilder + mangaIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'mangaId', + )); + }); + } + + QueryBuilder + mangaIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'mangaId', + )); + }); + } + + QueryBuilder + mangaIdEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'mangaId', + value: value, + )); + }); + } + + QueryBuilder + mangaIdGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'mangaId', + value: value, + )); + }); + } + + QueryBuilder + mangaIdLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'mangaId', + value: value, + )); + }); + } + + QueryBuilder + 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 DeletedMangaQueryObject + on QueryBuilder {} + +// 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 + +const UpdatedChapterSchema = Schema( + name: r'UpdatedChapter', + id: -1728004238049586930, + properties: { + r'chapterId': PropertySchema( + id: 0, + name: r'chapterId', + type: IsarType.long, + ), + r'deleted': PropertySchema( + id: 1, + name: r'deleted', + type: IsarType.bool, + ), + r'isBookmarked': PropertySchema( + id: 2, + name: r'isBookmarked', + type: IsarType.bool, + ), + r'isRead': PropertySchema( + id: 3, + name: r'isRead', + type: IsarType.bool, + ), + r'lastPageRead': PropertySchema( + id: 4, + name: r'lastPageRead', + type: IsarType.string, + ), + r'mangaId': PropertySchema( + id: 5, + name: r'mangaId', + type: IsarType.long, + ) + }, + estimateSize: _updatedChapterEstimateSize, + serialize: _updatedChapterSerialize, + deserialize: _updatedChapterDeserialize, + deserializeProp: _updatedChapterDeserializeProp, +); + +int _updatedChapterEstimateSize( + UpdatedChapter object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.lastPageRead; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + return bytesCount; +} + +void _updatedChapterSerialize( + UpdatedChapter object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeLong(offsets[0], object.chapterId); + writer.writeBool(offsets[1], object.deleted); + writer.writeBool(offsets[2], object.isBookmarked); + writer.writeBool(offsets[3], object.isRead); + writer.writeString(offsets[4], object.lastPageRead); + writer.writeLong(offsets[5], object.mangaId); +} + +UpdatedChapter _updatedChapterDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = UpdatedChapter( + chapterId: reader.readLongOrNull(offsets[0]), + deleted: reader.readBoolOrNull(offsets[1]), + isBookmarked: reader.readBoolOrNull(offsets[2]), + isRead: reader.readBoolOrNull(offsets[3]), + lastPageRead: reader.readStringOrNull(offsets[4]), + mangaId: reader.readLongOrNull(offsets[5]), + ); + return object; +} + +P _updatedChapterDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readLongOrNull(offset)) as P; + case 1: + return (reader.readBoolOrNull(offset)) as P; + case 2: + return (reader.readBoolOrNull(offset)) as P; + case 3: + return (reader.readBoolOrNull(offset)) as P; + case 4: + return (reader.readStringOrNull(offset)) as P; + case 5: + return (reader.readLongOrNull(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +extension UpdatedChapterQueryFilter + on QueryBuilder { + QueryBuilder + chapterIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'chapterId', + )); + }); + } + + QueryBuilder + chapterIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'chapterId', + )); + }); + } + + QueryBuilder + chapterIdEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'chapterId', + value: value, + )); + }); + } + + QueryBuilder + chapterIdGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'chapterId', + value: value, + )); + }); + } + + QueryBuilder + chapterIdLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'chapterId', + value: value, + )); + }); + } + + QueryBuilder + chapterIdBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'chapterId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + deletedIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'deleted', + )); + }); + } + + QueryBuilder + deletedIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'deleted', + )); + }); + } + + QueryBuilder + deletedEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'deleted', + value: value, + )); + }); + } + + QueryBuilder + isBookmarkedIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'isBookmarked', + )); + }); + } + + QueryBuilder + isBookmarkedIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'isBookmarked', + )); + }); + } + + QueryBuilder + isBookmarkedEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isBookmarked', + value: value, + )); + }); + } + + QueryBuilder + isReadIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'isRead', + )); + }); + } + + QueryBuilder + isReadIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'isRead', + )); + }); + } + + QueryBuilder + isReadEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isRead', + value: value, + )); + }); + } + + QueryBuilder + lastPageReadIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'lastPageRead', + )); + }); + } + + QueryBuilder + lastPageReadIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'lastPageRead', + )); + }); + } + + QueryBuilder + lastPageReadEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lastPageRead', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + lastPageReadGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'lastPageRead', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + lastPageReadLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'lastPageRead', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + lastPageReadBetween( + 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'lastPageRead', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + lastPageReadStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'lastPageRead', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + lastPageReadEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'lastPageRead', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + lastPageReadContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'lastPageRead', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + lastPageReadMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'lastPageRead', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + lastPageReadIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lastPageRead', + value: '', + )); + }); + } + + QueryBuilder + lastPageReadIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'lastPageRead', + value: '', + )); + }); + } + + QueryBuilder + mangaIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'mangaId', + )); + }); + } + + QueryBuilder + mangaIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'mangaId', + )); + }); + } + + QueryBuilder + mangaIdEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'mangaId', + value: value, + )); + }); + } + + QueryBuilder + mangaIdGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'mangaId', + value: value, + )); + }); + } + + QueryBuilder + mangaIdLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'mangaId', + value: value, + )); + }); + } + + QueryBuilder + 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 UpdatedChapterQueryObject + on QueryBuilder {} + +// 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 + +const DeletedCategorySchema = Schema( + name: r'DeletedCategory', + id: -2357965502277606786, + properties: { + r'categoryId': PropertySchema( + id: 0, + name: r'categoryId', + type: IsarType.long, + ) + }, + estimateSize: _deletedCategoryEstimateSize, + serialize: _deletedCategorySerialize, + deserialize: _deletedCategoryDeserialize, + deserializeProp: _deletedCategoryDeserializeProp, +); + +int _deletedCategoryEstimateSize( + DeletedCategory object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + return bytesCount; +} + +void _deletedCategorySerialize( + DeletedCategory object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeLong(offsets[0], object.categoryId); +} + +DeletedCategory _deletedCategoryDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = DeletedCategory( + categoryId: reader.readLongOrNull(offsets[0]), + ); + return object; +} + +P _deletedCategoryDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readLongOrNull(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +extension DeletedCategoryQueryFilter + on QueryBuilder { + QueryBuilder + categoryIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'categoryId', + )); + }); + } + + QueryBuilder + categoryIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'categoryId', + )); + }); + } + + QueryBuilder + categoryIdEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'categoryId', + value: value, + )); + }); + } + + QueryBuilder + categoryIdGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'categoryId', + value: value, + )); + }); + } + + QueryBuilder + categoryIdLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'categoryId', + value: value, + )); + }); + } + + QueryBuilder + categoryIdBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'categoryId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } +} + +extension DeletedCategoryQueryObject + on QueryBuilder {} diff --git a/lib/models/feed.dart b/lib/models/feed.dart new file mode 100644 index 0000000..7ef4dca --- /dev/null +++ b/lib/models/feed.dart @@ -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(); + + String? date; + + Feed({ + this.id = Isar.autoIncrement, + required this.mangaId, + required this.chapterName, + required this.date, + }); + + Feed.fromJson(Map json) { + id = json['id']; + mangaId = json['mangaId']; + mangaId = json['chapterName']; + date = json['date']; + } + + Map toJson() => { + 'id': id, + 'mangaId': mangaId, + 'chapterName': chapterName, + 'date': date, + }; +} diff --git a/lib/models/feed.g.dart b/lib/models/feed.g.dart new file mode 100644 index 0000000..e8c8ce2 --- /dev/null +++ b/lib/models/feed.g.dart @@ -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 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 offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.chapterName; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.date; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + return bytesCount; +} + +void _feedSerialize( + Feed object, + IsarWriter writer, + List offsets, + Map> 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 offsets, + Map> 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

( + IsarReader reader, + int propertyId, + int offset, + Map> 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> _feedGetLinks(Feed object) { + return [object.chapter]; +} + +void _feedAttach(IsarCollection col, Id id, Feed object) { + object.id = id; + object.chapter.attach(col, col.isar.collection(), r'chapter', id); +} + +extension FeedQueryWhereSort on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension FeedQueryWhere on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder 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 idGreaterThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder 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 { + QueryBuilder chapterNameIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'chapterName', + )); + }); + } + + QueryBuilder chapterNameIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'chapterName', + )); + }); + } + + QueryBuilder chapterNameEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'chapterName', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'chapterName', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'chapterName', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder chapterNameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'chapterName', + value: '', + )); + }); + } + + QueryBuilder chapterNameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'chapterName', + value: '', + )); + }); + } + + QueryBuilder dateIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'date', + )); + }); + } + + QueryBuilder dateIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'date', + )); + }); + } + + QueryBuilder dateEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'date', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder 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 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 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 dateStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'date', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dateEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'date', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dateContains(String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'date', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dateMatches(String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'date', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dateIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'date', + value: '', + )); + }); + } + + QueryBuilder dateIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'date', + value: '', + )); + }); + } + + QueryBuilder idIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'id', + )); + }); + } + + QueryBuilder idIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'id', + )); + }); + } + + QueryBuilder idEqualTo(Id? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idGreaterThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idLessThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder 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 mangaIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'mangaId', + )); + }); + } + + QueryBuilder mangaIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'mangaId', + )); + }); + } + + QueryBuilder mangaIdEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'mangaId', + value: value, + )); + }); + } + + QueryBuilder mangaIdGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'mangaId', + value: value, + )); + }); + } + + QueryBuilder mangaIdLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'mangaId', + value: value, + )); + }); + } + + QueryBuilder 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 {} + +extension FeedQueryLinks on QueryBuilder { + QueryBuilder chapter( + FilterQuery q) { + return QueryBuilder.apply(this, (query) { + return query.link(q, r'chapter'); + }); + } + + QueryBuilder chapterIsNull() { + return QueryBuilder.apply(this, (query) { + return query.linkLength(r'chapter', 0, true, 0, true); + }); + } +} + +extension FeedQuerySortBy on QueryBuilder { + QueryBuilder sortByChapterName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'chapterName', Sort.asc); + }); + } + + QueryBuilder sortByChapterNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'chapterName', Sort.desc); + }); + } + + QueryBuilder sortByDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'date', Sort.asc); + }); + } + + QueryBuilder sortByDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'date', Sort.desc); + }); + } + + QueryBuilder sortByMangaId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mangaId', Sort.asc); + }); + } + + QueryBuilder sortByMangaIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mangaId', Sort.desc); + }); + } +} + +extension FeedQuerySortThenBy on QueryBuilder { + QueryBuilder thenByChapterName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'chapterName', Sort.asc); + }); + } + + QueryBuilder thenByChapterNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'chapterName', Sort.desc); + }); + } + + QueryBuilder thenByDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'date', Sort.asc); + }); + } + + QueryBuilder thenByDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'date', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByMangaId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mangaId', Sort.asc); + }); + } + + QueryBuilder thenByMangaIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mangaId', Sort.desc); + }); + } +} + +extension FeedQueryWhereDistinct on QueryBuilder { + QueryBuilder distinctByChapterName( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'chapterName', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByDate( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'date', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByMangaId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'mangaId'); + }); + } +} + +extension FeedQueryProperty on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder chapterNameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'chapterName'); + }); + } + + QueryBuilder dateProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'date'); + }); + } + + QueryBuilder mangaIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'mangaId'); + }); + } +} diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 19e6cca..5713a54 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -137,6 +137,10 @@ class Settings { List? 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(); + 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, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index d1305e6..23ea71a 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -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( @@ -1375,6 +1389,10 @@ P _settingsDeserializeProp

( 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 + syncAfterReadingIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'syncAfterReading', + )); + }); + } + + QueryBuilder + syncAfterReadingIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'syncAfterReading', + )); + }); + } + + QueryBuilder + syncAfterReadingEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'syncAfterReading', + value: value, + )); + }); + } + + QueryBuilder + syncOnAppLaunchIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'syncOnAppLaunch', + )); + }); + } + + QueryBuilder + syncOnAppLaunchIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'syncOnAppLaunch', + )); + }); + } + + QueryBuilder + syncOnAppLaunchEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'syncOnAppLaunch', + value: value, + )); + }); + } + QueryBuilder themeIsDarkIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( @@ -8017,6 +8091,30 @@ extension SettingsQuerySortBy on QueryBuilder { }); } + QueryBuilder sortBySyncAfterReading() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncAfterReading', Sort.asc); + }); + } + + QueryBuilder sortBySyncAfterReadingDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncAfterReading', Sort.desc); + }); + } + + QueryBuilder sortBySyncOnAppLaunch() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncOnAppLaunch', Sort.asc); + }); + } + + QueryBuilder sortBySyncOnAppLaunchDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncOnAppLaunch', Sort.desc); + }); + } + QueryBuilder sortByThemeIsDark() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'themeIsDark', Sort.asc); @@ -8917,6 +9015,30 @@ extension SettingsQuerySortThenBy }); } + QueryBuilder thenBySyncAfterReading() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncAfterReading', Sort.asc); + }); + } + + QueryBuilder thenBySyncAfterReadingDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncAfterReading', Sort.desc); + }); + } + + QueryBuilder thenBySyncOnAppLaunch() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncOnAppLaunch', Sort.asc); + }); + } + + QueryBuilder thenBySyncOnAppLaunchDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncOnAppLaunch', Sort.desc); + }); + } + QueryBuilder thenByThemeIsDark() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'themeIsDark', Sort.asc); @@ -9404,6 +9526,18 @@ extension SettingsQueryWhereDistinct }); } + QueryBuilder distinctBySyncAfterReading() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'syncAfterReading'); + }); + } + + QueryBuilder distinctBySyncOnAppLaunch() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'syncOnAppLaunch'); + }); + } + QueryBuilder distinctByThemeIsDark() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'themeIsDark'); @@ -9980,6 +10114,18 @@ extension SettingsQueryProperty }); } + QueryBuilder syncAfterReadingProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'syncAfterReading'); + }); + } + + QueryBuilder syncOnAppLaunchProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'syncOnAppLaunch'); + }); + } + QueryBuilder themeIsDarkProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'themeIsDark'); diff --git a/lib/models/sync_preference.dart b/lib/models/sync_preference.dart new file mode 100644 index 0000000..30cb517 --- /dev/null +++ b/lib/models/sync_preference.dart @@ -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 json) { + syncId = json['syncId']; + email = json['email']; + authToken = json['authToken']; + lastSync = json['lastSync']; + lastUpload = json['lastUpload']; + lastDownload = json['lastDownload']; + server = json['server']; + } + + Map toJson() => { + 'syncId': syncId, + 'email': email, + 'authToken': authToken, + 'lastSync': lastSync, + 'lastUpload': lastUpload, + 'lastDownload': lastDownload, + 'server': server + }; +} diff --git a/lib/models/sync_preference.g.dart b/lib/models/sync_preference.g.dart new file mode 100644 index 0000000..c82f6bc --- /dev/null +++ b/lib/models/sync_preference.g.dart @@ -0,0 +1,1271 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sync_preference.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 GetSyncPreferenceCollection on Isar { + IsarCollection get syncPreferences => this.collection(); +} + +const SyncPreferenceSchema = CollectionSchema( + name: r'Sync Preference', + id: 2788277548653279925, + properties: { + r'authToken': PropertySchema( + id: 0, + name: r'authToken', + type: IsarType.string, + ), + r'email': PropertySchema( + id: 1, + name: r'email', + type: IsarType.string, + ), + r'lastDownload': PropertySchema( + id: 2, + name: r'lastDownload', + type: IsarType.long, + ), + r'lastSync': PropertySchema( + id: 3, + name: r'lastSync', + type: IsarType.long, + ), + r'lastUpload': PropertySchema( + id: 4, + name: r'lastUpload', + type: IsarType.long, + ), + r'server': PropertySchema( + id: 5, + name: r'server', + type: IsarType.string, + ) + }, + estimateSize: _syncPreferenceEstimateSize, + serialize: _syncPreferenceSerialize, + deserialize: _syncPreferenceDeserialize, + deserializeProp: _syncPreferenceDeserializeProp, + idName: r'syncId', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _syncPreferenceGetId, + getLinks: _syncPreferenceGetLinks, + attach: _syncPreferenceAttach, + version: '3.1.0+1', +); + +int _syncPreferenceEstimateSize( + SyncPreference object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.authToken; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.email; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.server; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + return bytesCount; +} + +void _syncPreferenceSerialize( + SyncPreference object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.authToken); + writer.writeString(offsets[1], object.email); + writer.writeLong(offsets[2], object.lastDownload); + writer.writeLong(offsets[3], object.lastSync); + writer.writeLong(offsets[4], object.lastUpload); + writer.writeString(offsets[5], object.server); +} + +SyncPreference _syncPreferenceDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = SyncPreference( + authToken: reader.readStringOrNull(offsets[0]), + email: reader.readStringOrNull(offsets[1]), + lastDownload: reader.readLongOrNull(offsets[2]), + lastSync: reader.readLongOrNull(offsets[3]), + lastUpload: reader.readLongOrNull(offsets[4]), + server: reader.readStringOrNull(offsets[5]), + syncId: id, + ); + return object; +} + +P _syncPreferenceDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> 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; + case 3: + return (reader.readLongOrNull(offset)) as P; + case 4: + return (reader.readLongOrNull(offset)) as P; + case 5: + return (reader.readStringOrNull(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _syncPreferenceGetId(SyncPreference object) { + return object.syncId ?? Isar.autoIncrement; +} + +List> _syncPreferenceGetLinks(SyncPreference object) { + return []; +} + +void _syncPreferenceAttach( + IsarCollection col, Id id, SyncPreference object) { + object.syncId = id; +} + +extension SyncPreferenceQueryWhereSort + on QueryBuilder { + QueryBuilder anySyncId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension SyncPreferenceQueryWhere + on QueryBuilder { + QueryBuilder syncIdEqualTo( + Id syncId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: syncId, + upper: syncId, + )); + }); + } + + QueryBuilder + syncIdNotEqualTo(Id syncId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: syncId, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: syncId, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: syncId, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: syncId, includeUpper: false), + ); + } + }); + } + + QueryBuilder + syncIdGreaterThan(Id syncId, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: syncId, includeLower: include), + ); + }); + } + + QueryBuilder + syncIdLessThan(Id syncId, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: syncId, includeUpper: include), + ); + }); + } + + QueryBuilder syncIdBetween( + Id lowerSyncId, + Id upperSyncId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerSyncId, + includeLower: includeLower, + upper: upperSyncId, + includeUpper: includeUpper, + )); + }); + } +} + +extension SyncPreferenceQueryFilter + on QueryBuilder { + QueryBuilder + authTokenIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'authToken', + )); + }); + } + + QueryBuilder + authTokenIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'authToken', + )); + }); + } + + QueryBuilder + authTokenEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'authToken', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + authTokenGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'authToken', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + authTokenLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'authToken', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + authTokenBetween( + 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'authToken', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + authTokenStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'authToken', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + authTokenEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'authToken', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + authTokenContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'authToken', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + authTokenMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'authToken', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + authTokenIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'authToken', + value: '', + )); + }); + } + + QueryBuilder + authTokenIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'authToken', + value: '', + )); + }); + } + + QueryBuilder + emailIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'email', + )); + }); + } + + QueryBuilder + emailIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'email', + )); + }); + } + + QueryBuilder + emailEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailBetween( + 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'email', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'email', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'email', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + emailIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'email', + value: '', + )); + }); + } + + QueryBuilder + emailIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'email', + value: '', + )); + }); + } + + QueryBuilder + lastDownloadIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'lastDownload', + )); + }); + } + + QueryBuilder + lastDownloadIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'lastDownload', + )); + }); + } + + QueryBuilder + lastDownloadEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lastDownload', + value: value, + )); + }); + } + + QueryBuilder + lastDownloadGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'lastDownload', + value: value, + )); + }); + } + + QueryBuilder + lastDownloadLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'lastDownload', + value: value, + )); + }); + } + + QueryBuilder + lastDownloadBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'lastDownload', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + lastSyncIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'lastSync', + )); + }); + } + + QueryBuilder + lastSyncIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'lastSync', + )); + }); + } + + QueryBuilder + lastSyncEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lastSync', + value: value, + )); + }); + } + + QueryBuilder + lastSyncGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'lastSync', + value: value, + )); + }); + } + + QueryBuilder + lastSyncLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'lastSync', + value: value, + )); + }); + } + + QueryBuilder + lastSyncBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'lastSync', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + lastUploadIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'lastUpload', + )); + }); + } + + QueryBuilder + lastUploadIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'lastUpload', + )); + }); + } + + QueryBuilder + lastUploadEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'lastUpload', + value: value, + )); + }); + } + + QueryBuilder + lastUploadGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'lastUpload', + value: value, + )); + }); + } + + QueryBuilder + lastUploadLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'lastUpload', + value: value, + )); + }); + } + + QueryBuilder + lastUploadBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'lastUpload', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + serverIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'server', + )); + }); + } + + QueryBuilder + serverIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'server', + )); + }); + } + + QueryBuilder + serverEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'server', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'server', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'server', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverBetween( + 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'server', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'server', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'server', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'server', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'server', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + serverIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'server', + value: '', + )); + }); + } + + QueryBuilder + serverIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'server', + value: '', + )); + }); + } + + QueryBuilder + syncIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'syncId', + )); + }); + } + + QueryBuilder + syncIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'syncId', + )); + }); + } + + QueryBuilder + syncIdEqualTo(Id? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'syncId', + value: value, + )); + }); + } + + QueryBuilder + syncIdGreaterThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'syncId', + value: value, + )); + }); + } + + QueryBuilder + syncIdLessThan( + Id? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'syncId', + value: value, + )); + }); + } + + QueryBuilder + syncIdBetween( + Id? lower, + Id? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'syncId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } +} + +extension SyncPreferenceQueryObject + on QueryBuilder {} + +extension SyncPreferenceQueryLinks + on QueryBuilder {} + +extension SyncPreferenceQuerySortBy + on QueryBuilder { + QueryBuilder sortByAuthToken() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'authToken', Sort.asc); + }); + } + + QueryBuilder + sortByAuthTokenDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'authToken', Sort.desc); + }); + } + + QueryBuilder sortByEmail() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'email', Sort.asc); + }); + } + + QueryBuilder sortByEmailDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'email', Sort.desc); + }); + } + + QueryBuilder + sortByLastDownload() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastDownload', Sort.asc); + }); + } + + QueryBuilder + sortByLastDownloadDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastDownload', Sort.desc); + }); + } + + QueryBuilder sortByLastSync() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastSync', Sort.asc); + }); + } + + QueryBuilder + sortByLastSyncDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastSync', Sort.desc); + }); + } + + QueryBuilder + sortByLastUpload() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastUpload', Sort.asc); + }); + } + + QueryBuilder + sortByLastUploadDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastUpload', Sort.desc); + }); + } + + QueryBuilder sortByServer() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'server', Sort.asc); + }); + } + + QueryBuilder + sortByServerDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'server', Sort.desc); + }); + } +} + +extension SyncPreferenceQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByAuthToken() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'authToken', Sort.asc); + }); + } + + QueryBuilder + thenByAuthTokenDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'authToken', Sort.desc); + }); + } + + QueryBuilder thenByEmail() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'email', Sort.asc); + }); + } + + QueryBuilder thenByEmailDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'email', Sort.desc); + }); + } + + QueryBuilder + thenByLastDownload() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastDownload', Sort.asc); + }); + } + + QueryBuilder + thenByLastDownloadDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastDownload', Sort.desc); + }); + } + + QueryBuilder thenByLastSync() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastSync', Sort.asc); + }); + } + + QueryBuilder + thenByLastSyncDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastSync', Sort.desc); + }); + } + + QueryBuilder + thenByLastUpload() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastUpload', Sort.asc); + }); + } + + QueryBuilder + thenByLastUploadDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'lastUpload', Sort.desc); + }); + } + + QueryBuilder thenByServer() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'server', Sort.asc); + }); + } + + QueryBuilder + thenByServerDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'server', Sort.desc); + }); + } + + QueryBuilder thenBySyncId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncId', Sort.asc); + }); + } + + QueryBuilder + thenBySyncIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncId', Sort.desc); + }); + } +} + +extension SyncPreferenceQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByAuthToken( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'authToken', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByEmail( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'email', caseSensitive: caseSensitive); + }); + } + + QueryBuilder + distinctByLastDownload() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'lastDownload'); + }); + } + + QueryBuilder distinctByLastSync() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'lastSync'); + }); + } + + QueryBuilder + distinctByLastUpload() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'lastUpload'); + }); + } + + QueryBuilder distinctByServer( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'server', caseSensitive: caseSensitive); + }); + } +} + +extension SyncPreferenceQueryProperty + on QueryBuilder { + QueryBuilder syncIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'syncId'); + }); + } + + QueryBuilder authTokenProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'authToken'); + }); + } + + QueryBuilder emailProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'email'); + }); + } + + QueryBuilder lastDownloadProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'lastDownload'); + }); + } + + QueryBuilder lastSyncProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'lastSync'); + }); + } + + QueryBuilder lastUploadProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'lastUpload'); + }); + } + + QueryBuilder serverProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'server'); + }); + } +} diff --git a/lib/modules/anime/providers/anime_player_controller_provider.dart b/lib/modules/anime/providers/anime_player_controller_provider.dart index 059366a..179d05a 100644 --- a/lib/modules/anime/providers/anime_player_controller_provider.dart +++ b/lib/modules/anime/providers/anime_player_controller_provider.dart @@ -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); } } } diff --git a/lib/modules/anime/providers/anime_player_controller_provider.g.dart b/lib/modules/anime/providers/anime_player_controller_provider.g.dart index 94eb07c..e177b2e 100644 --- a/lib/modules/anime/providers/anime_player_controller_provider.g.dart +++ b/lib/modules/anime/providers/anime_player_controller_provider.g.dart @@ -7,7 +7,7 @@ part of 'anime_player_controller_provider.dart'; // ************************************************************************** String _$animeStreamControllerHash() => - r'24639a8644ea9820458658807035e4cffb1b1644'; + r'afa475dbb6f73d33b2495dd6d4502f3df1ab931d'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/feed/feed_screen.dart b/lib/modules/feed/feed_screen.dart new file mode 100644 index 0000000..361384a --- /dev/null +++ b/lib/modules/feed/feed_screen.dart @@ -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 createState() => _FeedScreenState(); +} + +class _FeedScreenState extends ConsumerState + with TickerProviderStateMixin { + late TabController _tabBarController; + + @override + void initState() { + _tabBarController = TabController(length: 2, vsync: this); + _tabBarController.animateTo(0); + _tabBarController.addListener(() { + setState(() { + _textEditingController.clear(); + _isSearch = false; + }); + }); + super.initState(); + } + + final _textEditingController = TextEditingController(); + bool _isSearch = false; + List entriesData = []; + @override + Widget build(BuildContext context) { + final l10n = l10nLocalizations(context)!; + return DefaultTabController( + animationDuration: Duration.zero, + length: 2, + child: Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.transparent, + title: _isSearch + ? null + : Text( + l10n.feed, + style: TextStyle(color: Theme.of(context).hintColor), + ), + actions: [ + _isSearch + ? SeachFormTextField( + onChanged: (value) { + setState(() {}); + }, + onSuffixPressed: () { + _textEditingController.clear(); + setState(() {}); + }, + onPressed: () { + setState(() { + _isSearch = false; + }); + _textEditingController.clear(); + }, + controller: _textEditingController, + ) + : IconButton( + splashRadius: 20, + onPressed: () { + setState(() { + _isSearch = true; + }); + }, + icon: + Icon(Icons.search, color: Theme.of(context).hintColor)), + IconButton( + splashRadius: 20, + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + l10n.remove_everything, + ), + content: Text(l10n.remove_all_feed_msg), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(l10n.cancel)), + const SizedBox( + width: 15, + ), + TextButton( + onPressed: () { + List feeds = isar.feeds + .filter() + .idIsNotNull() + .chapter((q) => q.manga((q) => + q.isMangaEqualTo( + _tabBarController.index == + 0))) + .findAllSync() + .toList(); + isar.writeTxnSync(() { + for (var feed in feeds) { + isar.feeds.deleteSync(feed.id!); + } + }); + if (mounted) { + Navigator.pop(context); + } + }, + child: Text(l10n.ok)), + ], + ) + ], + ); + }); + }, + icon: Icon(Icons.delete_sweep_outlined, + color: Theme.of(context).hintColor)), + ], + bottom: TabBar( + indicatorSize: TabBarIndicatorSize.tab, + controller: _tabBarController, + tabs: [ + Tab(text: l10n.manga), + Tab(text: l10n.anime), + ], + ), + ), + body: Padding( + padding: const EdgeInsets.only(top: 10), + child: TabBarView(controller: _tabBarController, children: [ + FeedTab( + isManga: true, + query: _textEditingController.text, + ), + FeedTab( + isManga: false, + query: _textEditingController.text, + ) + ]), + ), + ), + ); + } +} + +class FeedTab extends ConsumerStatefulWidget { + final String query; + final bool isManga; + const FeedTab({required this.isManga, required this.query, super.key}); + + @override + ConsumerState createState() => _FeedTabState(); +} + +class _FeedTabState extends ConsumerState { + @override + Widget build(BuildContext context) { + final l10n = l10nLocalizations(context)!; + final feed = ref.watch(getAllFeedStreamProvider(isManga: widget.isManga)); + return Scaffold( + body: feed.when( + data: (data) { + final entries = data + .where((element) => widget.query.isNotEmpty + ? element.chapter.value!.manga.value!.name! + .toLowerCase() + .contains(widget.query.toLowerCase()) + : true) + .toList(); + 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( + 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(); + }, + )); + } +} diff --git a/lib/modules/feed/widgets/feed_chapter_list_tile_widget.dart b/lib/modules/feed/widgets/feed_chapter_list_tile_widget.dart new file mode 100644 index 0000000..fec2ea6 --- /dev/null +++ b/lib/modules/feed/widgets/feed_chapter_list_tile_widget.dart @@ -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) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/history/history_screen.dart b/lib/modules/history/history_screen.dart index 7145af3..20fc747 100644 --- a/lib/modules/history/history_screen.dart +++ b/lib/modules/history/history_screen.dart @@ -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 { ), 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 { .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) { diff --git a/lib/modules/history/providers/isar_providers.dart b/lib/modules/history/providers/isar_providers.dart index b950e0a..a08b3b9 100644 --- a/lib/modules/history/providers/isar_providers.dart +++ b/lib/modules/history/providers/isar_providers.dart @@ -1,6 +1,7 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/feed.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -16,3 +17,14 @@ Stream> getAllHistoryStream(GetAllHistoryStreamRef ref, .chapter((q) => q.manga((q) => q.isMangaEqualTo(isManga))) .watch(fireImmediately: true); } + +@riverpod +Stream> getAllFeedStream(GetAllFeedStreamRef ref, + {required bool isManga}) async* { + yield* isar.feeds + .filter() + .idIsNotNull() + .and() + .chapter((q) => q.manga((q) => q.isMangaEqualTo(isManga))) + .watch(fireImmediately: true); +} diff --git a/lib/modules/history/providers/isar_providers.g.dart b/lib/modules/history/providers/isar_providers.g.dart index 6518fcb..f370b20 100644 --- a/lib/modules/history/providers/isar_providers.g.dart +++ b/lib/modules/history/providers/isar_providers.g.dart @@ -157,5 +157,134 @@ class _GetAllHistoryStreamProviderElement @override bool get isManga => (origin as GetAllHistoryStreamProvider).isManga; } + +String _$getAllFeedStreamHash() => r'3d60bca5377bf6fc2aee36e7bec5b319b2377add'; + +/// See also [getAllFeedStream]. +@ProviderFor(getAllFeedStream) +const getAllFeedStreamProvider = GetAllFeedStreamFamily(); + +/// See also [getAllFeedStream]. +class GetAllFeedStreamFamily extends Family>> { + /// See also [getAllFeedStream]. + const GetAllFeedStreamFamily(); + + /// See also [getAllFeedStream]. + GetAllFeedStreamProvider call({ + required bool isManga, + }) { + return GetAllFeedStreamProvider( + isManga: isManga, + ); + } + + @override + GetAllFeedStreamProvider getProviderOverride( + covariant GetAllFeedStreamProvider provider, + ) { + return call( + isManga: provider.isManga, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'getAllFeedStreamProvider'; +} + +/// See also [getAllFeedStream]. +class GetAllFeedStreamProvider extends AutoDisposeStreamProvider> { + /// See also [getAllFeedStream]. + GetAllFeedStreamProvider({ + required bool isManga, + }) : this._internal( + (ref) => getAllFeedStream( + ref as GetAllFeedStreamRef, + isManga: isManga, + ), + from: getAllFeedStreamProvider, + name: r'getAllFeedStreamProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$getAllFeedStreamHash, + dependencies: GetAllFeedStreamFamily._dependencies, + allTransitiveDependencies: + GetAllFeedStreamFamily._allTransitiveDependencies, + isManga: isManga, + ); + + GetAllFeedStreamProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.isManga, + }) : super.internal(); + + final bool isManga; + + @override + Override overrideWith( + Stream> Function(GetAllFeedStreamRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: GetAllFeedStreamProvider._internal( + (ref) => create(ref as GetAllFeedStreamRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + isManga: isManga, + ), + ); + } + + @override + AutoDisposeStreamProviderElement> createElement() { + return _GetAllFeedStreamProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is GetAllFeedStreamProvider && other.isManga == isManga; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, isManga.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin GetAllFeedStreamRef on AutoDisposeStreamProviderRef> { + /// The parameter `isManga` of this provider. + bool get isManga; +} + +class _GetAllFeedStreamProviderElement + extends AutoDisposeStreamProviderElement> + with GetAllFeedStreamRef { + _GetAllFeedStreamProviderElement(super.provider); + + @override + bool get isManga => (origin as GetAllFeedStreamProvider).isManga; +} // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/modules/library/library_screen.dart b/lib/modules/library/library_screen.dart index ab14591..89985dd 100644 --- a/lib/modules/library/library_screen.dart +++ b/lib/modules/library/library_screen.dart @@ -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 } 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 }); } else if (value == 2) { _importLocal(context, widget.isManga); - } else if (value == 3 && !widget.isManga){ + } else if (value == 3 && !widget.isManga) { addTorrent(context); } }), diff --git a/lib/modules/library/providers/library_state_provider.dart b/lib/modules/library/providers/library_state_provider.dart index 06ef506..d662e9e 100644 --- a/lib/modules/library/providers/library_state_provider.dart +++ b/lib/modules/library/providers/library_state_provider.dart @@ -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(); } diff --git a/lib/modules/library/providers/library_state_provider.g.dart b/lib/modules/library/providers/library_state_provider.g.dart index 15834da..60f06fc 100644 --- a/lib/modules/library/providers/library_state_provider.g.dart +++ b/lib/modules/library/providers/library_state_provider.g.dart @@ -2520,7 +2520,7 @@ final isLongPressedMangaStateProvider = typedef _$IsLongPressedMangaState = AutoDisposeNotifier; String _$mangasSetIsReadStateHash() => - r'8f86296f588a48747de625e0471048978ee9bdeb'; + r'926659caebb85b15952f66f437f773a4b660bb78'; abstract class _$MangasSetIsReadState extends BuildlessAutoDisposeNotifier { @@ -2665,7 +2665,7 @@ class _MangasSetIsReadStateProviderElement } String _$mangasSetUnReadStateHash() => - r'3413e731b2fd8476a4032d3e47b943ca12f25090'; + r'7b2f4c579f9cb392830ed4d70aff9ccc3e7952a0'; abstract class _$MangasSetUnReadState extends BuildlessAutoDisposeNotifier { diff --git a/lib/modules/main_view/main_screen.dart b/lib/modules/main_view/main_screen.dart index 2fe7bb5..7f86bce 100644 --- a/lib/modules/main_view/main_screen.dart +++ b/lib/modules/main_view/main_screen.dart @@ -5,6 +5,7 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/feed.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/modules/widgets/loading_icon.dart'; import 'package:mangayomi/services/fetch_anime_sources.dart'; @@ -45,8 +46,9 @@ class MainScreen extends ConsumerWidget { '/MangaLibrary' => 0, '/AnimeLibrary' => 1, '/history' => 2, - '/browse' => 3, - _ => 4, + '/feed' => 3, + '/browse' => 4, + _ => 5, }; final incognitoMode = ref.watch(incognitoModeStateProvider); @@ -96,6 +98,7 @@ class MainScreen extends ConsumerWidget { != '/MangaLibrary' && != '/AnimeLibrary' && != '/history' && + != '/feed' && != '/browse' && != '/more' => 0, @@ -141,6 +144,36 @@ class MainScreen extends ConsumerWidget { padding: const EdgeInsets.only(top: 5), child: Text(l10n.history))), + NavigationRailDestination( + selectedIcon: Stack( + children: [ + const Icon(Icons.rss_feed), + Positioned( + right: 0, + top: 0, + child: _feedTotalNumbers( + ref, false)) + ], + ), + icon: Stack( + children: [ + const Icon( + Icons.rss_feed_outlined), + Positioned( + right: 0, + top: 0, + child: _feedTotalNumbers( + ref, false)) + ], + ), + label: Padding( + padding: + const EdgeInsets.only(top: 5), + child: Stack( + children: [ + Text(l10n.feed), + ], + ))), NavigationRailDestination( selectedIcon: const Icon(Icons.explore), @@ -169,8 +202,10 @@ class MainScreen extends ConsumerWidget { } else if (newIndex == 2) { route.go('/history'); } else if (newIndex == 3) { - route.go('/browse'); + route.go('/feed'); } else if (newIndex == 4) { + route.go('/browse'); + } else if (newIndex == 5) { route.go('/more'); } }, @@ -199,6 +234,7 @@ class MainScreen extends ConsumerWidget { != '/MangaLibrary' && != '/AnimeLibrary' && != '/history' && + != '/feed' && != '/browse' && != '/more' => 0, @@ -231,6 +267,18 @@ class MainScreen extends ConsumerWidget { selectedIcon: const Icon(Icons.history), icon: const Icon(Icons.history_outlined), label: l10n.history), + Stack( + children: [ + NavigationDestination( + selectedIcon: const Icon(Icons.rss_feed), + icon: const Icon(Icons.rss_feed_outlined), + label: l10n.feed), + Positioned( + right: 14, + top: 3, + child: _feedTotalNumbers(ref, true)), + ], + ), Stack( children: [ NavigationDestination( @@ -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(); + }); +} diff --git a/lib/modules/manga/detail/manga_detail_view.dart b/lib/modules/manga/detail/manga_detail_view.dart index ebd0cf6..b69d11d 100644 --- a/lib/modules/manga/detail/manga_detail_view.dart +++ b/lib/modules/manga/detail/manga_detail_view.dart @@ -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 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 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 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 .read(chaptersListStateProvider.notifier) .clear(); }); + chapters[index + 1] + .syncProgressAfterChapterRead(ref); }, child: Stack( children: [ diff --git a/lib/modules/manga/detail/providers/state_providers.dart b/lib/modules/manga/detail/providers/state_providers.dart index b1ee292..28db4dd 100644 --- a/lib/modules/manga/detail/providers/state_providers.dart +++ b/lib/modules/manga/detail/providers/state_providers.dart @@ -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(); } diff --git a/lib/modules/manga/detail/providers/state_providers.g.dart b/lib/modules/manga/detail/providers/state_providers.g.dart index d16adb6..cbfc3b8 100644 --- a/lib/modules/manga/detail/providers/state_providers.g.dart +++ b/lib/modules/manga/detail/providers/state_providers.g.dart @@ -806,7 +806,7 @@ class _ChapterFilterResultStateProviderElement } String _$chapterSetIsBookmarkStateHash() => - r'113131bb13e50566390ee3e34aa2f08820a8870c'; + r'48d4f203ba51616e9d1142e0dd482d3ae065a4f4'; abstract class _$ChapterSetIsBookmarkState extends BuildlessAutoDisposeNotifier { @@ -951,7 +951,7 @@ class _ChapterSetIsBookmarkStateProviderElement } String _$chapterSetIsReadStateHash() => - r'c319f81ec30565ad81a28cb0a8ce7fddcb47cd77'; + r'1e219dd68898fc30b6cb64d294377776516775d4'; abstract class _$ChapterSetIsReadState extends BuildlessAutoDisposeNotifier { diff --git a/lib/modules/manga/detail/providers/update_manga_detail_providers.dart b/lib/modules/manga/detail/providers/update_manga_detail_providers.dart index 179db99..1c93e11 100644 --- a/lib/modules/manga/detail/providers/update_manga_detail_providers.dart +++ b/lib/modules/manga/detail/providers/update_manga_detail_providers.dart @@ -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 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 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(); } diff --git a/lib/modules/manga/detail/providers/update_manga_detail_providers.g.dart b/lib/modules/manga/detail/providers/update_manga_detail_providers.g.dart index b01df12..c314898 100644 --- a/lib/modules/manga/detail/providers/update_manga_detail_providers.g.dart +++ b/lib/modules/manga/detail/providers/update_manga_detail_providers.g.dart @@ -6,7 +6,7 @@ part of 'update_manga_detail_providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$updateMangaDetailHash() => r'7733551fd578f5d093c4ca11427015a3f3af415a'; +String _$updateMangaDetailHash() => r'c21ac4f7725b5ac4403902bac07a3b5462488bbd'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.dart b/lib/modules/manga/reader/providers/reader_controller_provider.dart index 509471b..9e16b6b 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -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 { diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.g.dart b/lib/modules/manga/reader/providers/reader_controller_provider.g.dart index f79bde3..394e9fb 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.g.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.g.dart @@ -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 { late final Chapter chapter; diff --git a/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart b/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart index f3a1cfa..c711444 100644 --- a/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart +++ b/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart @@ -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 { 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), diff --git a/lib/modules/more/backup_and_restore/providers/restore.dart b/lib/modules/more/backup_and_restore/providers/restore.dart index ba74807..8aab257 100644 --- a/lib/modules/more/backup_and_restore/providers/restore.dart +++ b/lib/modules/more/backup_and_restore/providers/restore.dart @@ -2,12 +2,14 @@ import 'dart:convert'; import 'package:archive/archive_io.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; +import 'package:isar/isar.dart'; import 'package:mangayomi/eval/dart/model/m_bridge.dart'; import 'package:mangayomi/eval/dart/model/source_preference.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/category.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; +import 'package:mangayomi/models/feed.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; @@ -59,6 +61,8 @@ void doRestore(DoRestoreRef ref, final extensionsPref = (backup["extensions_preferences"] as List?) ?.map((e) => SourcePreference.fromJson(e)) .toList(); + final feeds = + (backup["feeds"] as List?)?.map((e) => Feed.fromJson(e)).toList(); isar.writeTxnSync(() { isar.mangas.clearSync(); @@ -95,6 +99,23 @@ void doRestore(DoRestoreRef ref, } } } + + isar.feeds.clearSync(); + if (feeds != null) { + final tempChapters = + isar.chapters.filter().idIsNotNull().findAllSync().toList(); + for (var feed in feeds) { + final matchingChapter = tempChapters + .where((chapter) => + chapter.mangaId == feed.mangaId && + chapter.name == feed.chapterName) + .firstOrNull; + if (matchingChapter != null) { + isar.feeds.putSync(feed..chapter.value = matchingChapter); + feed.chapter.saveSync(); + } + } + } } isar.categorys.clearSync(); diff --git a/lib/modules/more/backup_and_restore/providers/restore.g.dart b/lib/modules/more/backup_and_restore/providers/restore.g.dart index 2789683..d14af08 100644 --- a/lib/modules/more/backup_and_restore/providers/restore.g.dart +++ b/lib/modules/more/backup_and_restore/providers/restore.g.dart @@ -6,7 +6,7 @@ part of 'restore.dart'; // RiverpodGenerator // ************************************************************************** -String _$doRestoreHash() => r'3c88ad8ba80c245a4b511961111f7ab79c0d330f'; +String _$doRestoreHash() => r'823b26bade20d89ae7b7b56a7eb7c25020795b45'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/more/categories/categories_screen.dart b/lib/modules/more/categories/categories_screen.dart index 5136f5e..4e209c7 100644 --- a/lib/modules/more/categories/categories_screen.dart +++ b/lib/modules/more/categories/categories_screen.dart @@ -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 { 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); } diff --git a/lib/modules/more/settings/settings_screen.dart b/lib/modules/more/settings/settings_screen.dart index fbf4a00..4bf95c0 100644 --- a/lib/modules/more/settings/settings_screen.dart +++ b/lib/modules/more/settings/settings_screen.dart @@ -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, diff --git a/lib/modules/more/settings/sync/models/jwt.dart b/lib/modules/more/settings/sync/models/jwt.dart new file mode 100644 index 0000000..c6e0c97 --- /dev/null +++ b/lib/modules/more/settings/sync/models/jwt.dart @@ -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 json) { + email = json['email']; + uuid = json['uuid']; + iat = (json['iat'] as int) * 1000; + exp = (json['exp'] as int) * 1000; + } + + Map toJson() { + final Map data = {}; + data['uuid'] = uuid; + data['email'] = email; + data['iat'] = iat; + data['exp'] = exp; + return data; + } +} diff --git a/lib/modules/more/settings/sync/providers/sync_providers.dart b/lib/modules/more/settings/sync/providers/sync_providers.dart new file mode 100644 index 0000000..8f95a07 --- /dev/null +++ b/lib/modules/more/settings/sync/providers/sync_providers.dart @@ -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)); + } +} diff --git a/lib/modules/more/settings/sync/providers/sync_providers.g.dart b/lib/modules/more/settings/sync/providers/sync_providers.g.dart new file mode 100644 index 0000000..af01e24 --- /dev/null +++ b/lib/modules/more/settings/sync/providers/sync_providers.g.dart @@ -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 { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'addUpdatedChapterIndependentProvider'; +} + +/// See also [addUpdatedChapterIndependent]. +class AddUpdatedChapterIndependentProvider extends AutoDisposeProvider { + /// 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 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 { + /// 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 + 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 { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'checkForSyncIndependentProvider'; +} + +/// See also [checkForSyncIndependent]. +class CheckForSyncIndependentProvider extends AutoDisposeProvider { + /// 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 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 { + /// The parameter `silent` of this provider. + bool get silent; +} + +class _CheckForSyncIndependentProviderElement + extends AutoDisposeProviderElement with CheckForSyncIndependentRef { + _CheckForSyncIndependentProviderElement(super.provider); + + @override + bool get silent => (origin as CheckForSyncIndependentProvider).silent; +} + +String _$changedItemsManagerHash() => + r'a4f0363ab430ddb6c2a23fde6f5671ba8ec252cf'; + +abstract class _$ChangedItemsManager + extends BuildlessAutoDisposeNotifier { + late final int? managerId; + + ChangedItems? build({ + required int? managerId, + }); +} + +/// See also [ChangedItemsManager]. +@ProviderFor(ChangedItemsManager) +const changedItemsManagerProvider = ChangedItemsManagerFamily(); + +/// See also [ChangedItemsManager]. +class ChangedItemsManagerFamily extends Family { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? 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 + 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 { + /// The parameter `managerId` of this provider. + int? get managerId; +} + +class _ChangedItemsManagerProviderElement + extends AutoDisposeNotifierProviderElement with ChangedItemsManagerRef { + _ChangedItemsManagerProviderElement(super.provider); + + @override + int? get managerId => (origin as ChangedItemsManagerProvider).managerId; +} + +String _$synchingHash() => r'2ef7fd99da4292ed236252d2b727cff9a69f43a9'; + +abstract class _$Synching + extends BuildlessAutoDisposeNotifier { + late final int? syncId; + + SyncPreference? build({ + required int? syncId, + }); +} + +/// See also [Synching]. +@ProviderFor(Synching) +const synchingProvider = SynchingFamily(); + +/// See also [Synching]. +class SynchingFamily extends Family { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'synchingProvider'; +} + +/// See also [Synching]. +class SynchingProvider + extends AutoDisposeNotifierProviderImpl { + /// 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 + 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 { + /// The parameter `syncId` of this provider. + int? get syncId; +} + +class _SynchingProviderElement + extends AutoDisposeNotifierProviderElement + 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.internal( + SyncOnAppLaunchState.new, + name: r'syncOnAppLaunchStateProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$syncOnAppLaunchStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SyncOnAppLaunchState = AutoDisposeNotifier; +String _$syncAfterReadingStateHash() => + r'e507acd490b5aea7fc1a8fd7a369ec01f4c47192'; + +/// See also [SyncAfterReadingState]. +@ProviderFor(SyncAfterReadingState) +final syncAfterReadingStateProvider = + AutoDisposeNotifierProvider.internal( + SyncAfterReadingState.new, + name: r'syncAfterReadingStateProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$syncAfterReadingStateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SyncAfterReadingState = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/modules/more/settings/sync/sync.dart b/lib/modules/more/settings/sync/sync.dart new file mode 100644 index 0000000..0d8e501 --- /dev/null +++ b/lib/modules/more/settings/sync/sync.dart @@ -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))), + ) + ], + ), + ), + ); + }), + ); +} diff --git a/lib/modules/more/settings/sync/widgets/sync_listile.dart b/lib/modules/more/settings/sync/widgets/sync_listile.dart new file mode 100644 index 0000000..74418ed --- /dev/null +++ b/lib/modules/more/settings/sync/widgets/sync_listile.dart @@ -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), + ), + ), + ); + } +} diff --git a/lib/providers/storage_provider.dart b/lib/providers/storage_provider.dart index faf34c2..7260a06 100644 --- a/lib/providers/storage_provider.dart +++ b/lib/providers/storage_provider.dart @@ -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()) { diff --git a/lib/router/router.dart b/lib/router/router.dart index 48aed1c..d056590 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -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", diff --git a/lib/services/sync_server.dart b/lib/services/sync_server.dart new file mode 100644 index 0000000..7a4c48a --- /dev/null +++ b/lib/services/sync_server.dart @@ -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; + 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 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; + 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 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; + 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 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 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; + _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 data) { + Map 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 _getChangedData() { + Map 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 _getData() { + Map 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 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 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; + 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 ?? ""; + } +} diff --git a/lib/services/sync_server.g.dart b/lib/services/sync_server.g.dart new file mode 100644 index 0000000..87b7a54 --- /dev/null +++ b/lib/services/sync_server.g.dart @@ -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 { + late final int syncId; + + void build({ + required int syncId, + }); +} + +/// See also [SyncServer]. +@ProviderFor(SyncServer) +const syncServerProvider = SyncServerFamily(); + +/// See also [SyncServer]. +class SyncServerFamily extends Family { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'syncServerProvider'; +} + +/// See also [SyncServer]. +class SyncServerProvider + extends AutoDisposeNotifierProviderImpl { + /// 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 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 { + /// The parameter `syncId` of this provider. + int get syncId; +} + +class _SyncServerProviderElement + extends AutoDisposeNotifierProviderElement + 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 diff --git a/lib/utils/date.dart b/lib/utils/date.dart index bd36253..fd8a70e 100644 --- a/lib/utils/date.dart +++ b/lib/utils/date.dart @@ -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