diff --git a/lib/models/changed.dart b/lib/models/changed.dart new file mode 100644 index 00000000..a414a358 --- /dev/null +++ b/lib/models/changed.dart @@ -0,0 +1,55 @@ +import 'package:isar/isar.dart'; +part 'changed.g.dart'; + +@collection +@Name("ChangedPart") +class ChangedPart { + Id? id; + @enumerated + late ActionType actionType; + int? isarId; + String data; + int clientDate; + + ChangedPart( + {this.id = Isar.autoIncrement, + required this.actionType, + this.isarId, + required this.data, + required this.clientDate}); + + Map toJson() => { + 'action': actionType.name, + 'isarId': isarId, + 'data': data, + 'clientDate': clientDate + }; +} + +enum ActionType { + addItem(name: "ADD_ITEM"), + removeItem(name: "REMOVE_ITEM"), + updateItem(name: "UPDATE_ITEM"), + addCategory(name: "ADD_CATEGORY"), + removeCategory(name: "REMOVE_CATEGORY"), + renameCategory(name: "RENAME_CATEGORY"), + addChapter(name: "ADD_CHAPTER"), + removeChapter(name: "REMOVE_CHAPTER"), + updateChapter(name: "UPDATE_CHAPTER"), + clearHistory(name: "CLEAR_HISTORY"), + addHistory(name: "ADD_HISTORY"), + removeHistory(name: "REMOVE_HISTORY"), + updateHistory(name: "UPDATE_HISTORY"), + clearUpdates(name: "CLEAR_UPDATES"), + addUpdate(name: "ADD_UPDATE"), + addExtension(name: "ADD_EXTENSION"), + removeExtension(name: "REMOVE_EXTENSION"), + updateExtension(name: "UPDATE_EXTENSION"), + addTrack(name: "ADD_TRACK"), + removeTrack(name: "REMOVE_TRACK"), + updateTrack(name: "UPDATE_TRACK"); + + final String name; + + const ActionType({required this.name}); +} diff --git a/lib/models/changed.g.dart b/lib/models/changed.g.dart new file mode 100644 index 00000000..90fb9473 --- /dev/null +++ b/lib/models/changed.g.dart @@ -0,0 +1,821 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'changed.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 GetChangedPartCollection on Isar { + IsarCollection get changedParts => this.collection(); +} + +const ChangedPartSchema = CollectionSchema( + name: r'ChangedPart', + id: 984304309479278230, + properties: { + r'actionType': PropertySchema( + id: 0, + name: r'actionType', + type: IsarType.byte, + enumMap: _ChangedPartactionTypeEnumValueMap, + ), + r'clientDate': PropertySchema( + id: 1, + name: r'clientDate', + type: IsarType.long, + ), + r'data': PropertySchema( + id: 2, + name: r'data', + type: IsarType.string, + ), + r'isarId': PropertySchema( + id: 3, + name: r'isarId', + type: IsarType.long, + ) + }, + estimateSize: _changedPartEstimateSize, + serialize: _changedPartSerialize, + deserialize: _changedPartDeserialize, + deserializeProp: _changedPartDeserializeProp, + idName: r'id', + indexes: {}, + links: {}, + embeddedSchemas: {}, + getId: _changedPartGetId, + getLinks: _changedPartGetLinks, + attach: _changedPartAttach, + version: '3.1.0+1', +); + +int _changedPartEstimateSize( + ChangedPart object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.data.length * 3; + return bytesCount; +} + +void _changedPartSerialize( + ChangedPart object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeByte(offsets[0], object.actionType.index); + writer.writeLong(offsets[1], object.clientDate); + writer.writeString(offsets[2], object.data); + writer.writeLong(offsets[3], object.isarId); +} + +ChangedPart _changedPartDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = ChangedPart( + actionType: + _ChangedPartactionTypeValueEnumMap[reader.readByteOrNull(offsets[0])] ?? + ActionType.addItem, + clientDate: reader.readLong(offsets[1]), + data: reader.readString(offsets[2]), + id: id, + isarId: reader.readLongOrNull(offsets[3]), + ); + return object; +} + +P _changedPartDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (_ChangedPartactionTypeValueEnumMap[ + reader.readByteOrNull(offset)] ?? + ActionType.addItem) as P; + case 1: + return (reader.readLong(offset)) as P; + case 2: + return (reader.readString(offset)) as P; + case 3: + return (reader.readLongOrNull(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +const _ChangedPartactionTypeEnumValueMap = { + 'addItem': 0, + 'removeItem': 1, + 'updateItem': 2, + 'addCategory': 3, + 'removeCategory': 4, + 'renameCategory': 5, + 'addChapter': 6, + 'removeChapter': 7, + 'updateChapter': 8, + 'clearHistory': 9, + 'addHistory': 10, + 'removeHistory': 11, + 'updateHistory': 12, + 'clearUpdates': 13, + 'addUpdate': 14, + 'addExtension': 15, + 'removeExtension': 16, + 'updateExtension': 17, + 'addTrack': 18, + 'removeTrack': 19, + 'updateTrack': 20, +}; +const _ChangedPartactionTypeValueEnumMap = { + 0: ActionType.addItem, + 1: ActionType.removeItem, + 2: ActionType.updateItem, + 3: ActionType.addCategory, + 4: ActionType.removeCategory, + 5: ActionType.renameCategory, + 6: ActionType.addChapter, + 7: ActionType.removeChapter, + 8: ActionType.updateChapter, + 9: ActionType.clearHistory, + 10: ActionType.addHistory, + 11: ActionType.removeHistory, + 12: ActionType.updateHistory, + 13: ActionType.clearUpdates, + 14: ActionType.addUpdate, + 15: ActionType.addExtension, + 16: ActionType.removeExtension, + 17: ActionType.updateExtension, + 18: ActionType.addTrack, + 19: ActionType.removeTrack, + 20: ActionType.updateTrack, +}; + +Id _changedPartGetId(ChangedPart object) { + return object.id ?? Isar.autoIncrement; +} + +List> _changedPartGetLinks(ChangedPart object) { + return []; +} + +void _changedPartAttach( + IsarCollection col, Id id, ChangedPart object) { + object.id = id; +} + +extension ChangedPartQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension ChangedPartQueryWhere + 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 ChangedPartQueryFilter + on QueryBuilder { + QueryBuilder + actionTypeEqualTo(ActionType value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'actionType', + value: value, + )); + }); + } + + QueryBuilder + actionTypeGreaterThan( + ActionType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'actionType', + value: value, + )); + }); + } + + QueryBuilder + actionTypeLessThan( + ActionType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'actionType', + value: value, + )); + }); + } + + QueryBuilder + actionTypeBetween( + ActionType lower, + ActionType upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'actionType', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + clientDateEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'clientDate', + value: value, + )); + }); + } + + QueryBuilder + clientDateGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'clientDate', + value: value, + )); + }); + } + + QueryBuilder + clientDateLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'clientDate', + value: value, + )); + }); + } + + QueryBuilder + clientDateBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'clientDate', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder dataEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'data', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dataGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'data', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dataLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'data', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dataBetween( + 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'data', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dataStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'data', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dataEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'data', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dataContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'data', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dataMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'data', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder dataIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'data', + value: '', + )); + }); + } + + QueryBuilder + dataIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'data', + 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 isarIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'isarId', + )); + }); + } + + QueryBuilder + isarIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'isarId', + )); + }); + } + + QueryBuilder isarIdEqualTo( + int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isarId', + value: value, + )); + }); + } + + QueryBuilder + isarIdGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'isarId', + value: value, + )); + }); + } + + QueryBuilder isarIdLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'isarId', + value: value, + )); + }); + } + + QueryBuilder isarIdBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'isarId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } +} + +extension ChangedPartQueryObject + on QueryBuilder {} + +extension ChangedPartQueryLinks + on QueryBuilder {} + +extension ChangedPartQuerySortBy + on QueryBuilder { + QueryBuilder sortByActionType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'actionType', Sort.asc); + }); + } + + QueryBuilder sortByActionTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'actionType', Sort.desc); + }); + } + + QueryBuilder sortByClientDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'clientDate', Sort.asc); + }); + } + + QueryBuilder sortByClientDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'clientDate', Sort.desc); + }); + } + + QueryBuilder sortByData() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'data', Sort.asc); + }); + } + + QueryBuilder sortByDataDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'data', Sort.desc); + }); + } + + QueryBuilder sortByIsarId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isarId', Sort.asc); + }); + } + + QueryBuilder sortByIsarIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isarId', Sort.desc); + }); + } +} + +extension ChangedPartQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByActionType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'actionType', Sort.asc); + }); + } + + QueryBuilder thenByActionTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'actionType', Sort.desc); + }); + } + + QueryBuilder thenByClientDate() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'clientDate', Sort.asc); + }); + } + + QueryBuilder thenByClientDateDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'clientDate', Sort.desc); + }); + } + + QueryBuilder thenByData() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'data', Sort.asc); + }); + } + + QueryBuilder thenByDataDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'data', 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 thenByIsarId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isarId', Sort.asc); + }); + } + + QueryBuilder thenByIsarIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isarId', Sort.desc); + }); + } +} + +extension ChangedPartQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByActionType() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'actionType'); + }); + } + + QueryBuilder distinctByClientDate() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'clientDate'); + }); + } + + QueryBuilder distinctByData( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'data', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByIsarId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isarId'); + }); + } +} + +extension ChangedPartQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder actionTypeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'actionType'); + }); + } + + QueryBuilder clientDateProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'clientDate'); + }); + } + + QueryBuilder dataProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'data'); + }); + } + + QueryBuilder isarIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isarId'); + }); + } +} diff --git a/lib/models/sync_preference.dart b/lib/models/sync_preference.dart index 30cb517c..a37619af 100644 --- a/lib/models/sync_preference.dart +++ b/lib/models/sync_preference.dart @@ -18,6 +18,8 @@ class SyncPreference { String? server; + bool syncOn = false; + SyncPreference({ this.syncId, this.email, @@ -26,6 +28,7 @@ class SyncPreference { this.lastUpload, this.lastDownload, this.server, + this.syncOn = false, }); SyncPreference.fromJson(Map json) { @@ -36,6 +39,7 @@ class SyncPreference { lastUpload = json['lastUpload']; lastDownload = json['lastDownload']; server = json['server']; + syncOn = json['syncOn'] ?? false; } Map toJson() => { @@ -45,6 +49,6 @@ class SyncPreference { 'lastSync': lastSync, 'lastUpload': lastUpload, 'lastDownload': lastDownload, - 'server': server + 'syncOn': syncOn }; } diff --git a/lib/models/sync_preference.g.dart b/lib/models/sync_preference.g.dart index c82f6bc5..739c2dae 100644 --- a/lib/models/sync_preference.g.dart +++ b/lib/models/sync_preference.g.dart @@ -46,6 +46,11 @@ const SyncPreferenceSchema = CollectionSchema( id: 5, name: r'server', type: IsarType.string, + ), + r'syncOn': PropertySchema( + id: 6, + name: r'syncOn', + type: IsarType.bool, ) }, estimateSize: _syncPreferenceEstimateSize, @@ -101,6 +106,7 @@ void _syncPreferenceSerialize( writer.writeLong(offsets[3], object.lastSync); writer.writeLong(offsets[4], object.lastUpload); writer.writeString(offsets[5], object.server); + writer.writeBool(offsets[6], object.syncOn); } SyncPreference _syncPreferenceDeserialize( @@ -117,6 +123,7 @@ SyncPreference _syncPreferenceDeserialize( lastUpload: reader.readLongOrNull(offsets[4]), server: reader.readStringOrNull(offsets[5]), syncId: id, + syncOn: reader.readBoolOrNull(offsets[6]) ?? false, ); return object; } @@ -140,6 +147,8 @@ P _syncPreferenceDeserializeProp

( return (reader.readLongOrNull(offset)) as P; case 5: return (reader.readStringOrNull(offset)) as P; + case 6: + return (reader.readBoolOrNull(offset) ?? false) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -996,6 +1005,16 @@ extension SyncPreferenceQueryFilter )); }); } + + QueryBuilder + syncOnEqualTo(bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'syncOn', + value: value, + )); + }); + } } extension SyncPreferenceQueryObject @@ -1084,6 +1103,19 @@ extension SyncPreferenceQuerySortBy return query.addSortBy(r'server', Sort.desc); }); } + + QueryBuilder sortBySyncOn() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncOn', Sort.asc); + }); + } + + QueryBuilder + sortBySyncOnDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncOn', Sort.desc); + }); + } } extension SyncPreferenceQuerySortThenBy @@ -1179,6 +1211,19 @@ extension SyncPreferenceQuerySortThenBy return query.addSortBy(r'syncId', Sort.desc); }); } + + QueryBuilder thenBySyncOn() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncOn', Sort.asc); + }); + } + + QueryBuilder + thenBySyncOnDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'syncOn', Sort.desc); + }); + } } extension SyncPreferenceQueryWhereDistinct @@ -1223,6 +1268,12 @@ extension SyncPreferenceQueryWhereDistinct return query.addDistinctBy(r'server', caseSensitive: caseSensitive); }); } + + QueryBuilder distinctBySyncOn() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'syncOn'); + }); + } } extension SyncPreferenceQueryProperty @@ -1268,4 +1319,10 @@ extension SyncPreferenceQueryProperty return query.addPropertyName(r'server'); }); } + + QueryBuilder syncOnProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'syncOn'); + }); + } } diff --git a/lib/modules/anime/providers/anime_player_controller_provider.dart b/lib/modules/anime/providers/anime_player_controller_provider.dart index 7b2f7cbc..37d59f34 100644 --- a/lib/modules/anime/providers/anime_player_controller_provider.dart +++ b/lib/modules/anime/providers/anime_player_controller_provider.dart @@ -1,5 +1,6 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; @@ -7,6 +8,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'; @@ -121,6 +123,8 @@ class AnimeStreamController extends _$AnimeStreamController { Manga? anime = episode.manga.value; anime!.lastRead = DateTime.now().millisecondsSinceEpoch; isar.mangas.putSync(anime); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateItem, anime.id, anime.toJson(), false); }); History? history; @@ -146,6 +150,13 @@ class AnimeStreamController extends _$AnimeStreamController { isar.writeTxnSync(() { isar.historys.putSync(history!); history.chapter.saveSync(); + if (empty) { + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.addHistory, null, history.toJson(), false); + } else { + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateHistory, history.id, history.toJson(), false); + } }); } @@ -166,6 +177,8 @@ class AnimeStreamController extends _$AnimeStreamController { ep.isRead = isWatch; ep.lastPageRead = (duration.inMilliseconds).toString(); isar.chapters.putSync(ep); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, ep.id, ep.toJson(), false); }); if (isWatch) { episode.updateTrackChapterRead(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 b5baf06d..1ad09712 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'57ebd35f033d51fd213763173c26cd887f5c42d7'; + r'e0217071ae7b908a12bbba2dcdc4a6da8828e1c5'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/browse/global_search/global_search_screen.dart b/lib/modules/browse/global_search/global_search_screen.dart index 4f7fa3cc..56aa1ae0 100644 --- a/lib/modules/browse/global_search/global_search_screen.dart +++ b/lib/modules/browse/global_search/global_search_screen.dart @@ -233,6 +233,7 @@ class _MangaGlobalImageCardState extends ConsumerState return GestureDetector( onTap: () async { pushToMangaReaderDetail( + ref: ref, context: context, getManga: getMangaDetail, lang: widget.source.lang!, diff --git a/lib/modules/history/history_screen.dart b/lib/modules/history/history_screen.dart index 265822a3..beb51690 100644 --- a/lib/modules/history/history_screen.dart +++ b/lib/modules/history/history_screen.dart @@ -6,11 +6,13 @@ import 'package:grouped_list/sliver_grouped_list.dart'; import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; 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/reader/providers/reader_state_provider.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'; @@ -424,6 +426,17 @@ class _HistoryTabState extends ConsumerState { .delete(manga .id!); }); + await ref + .read(synchingProvider( + syncId: + 1) + .notifier) + .addChangedPartAsync( + ActionType + .removeItem, + manga.id, + "{}", + true); if (context .mounted) { Navigator.pop( diff --git a/lib/modules/library/library_screen.dart b/lib/modules/library/library_screen.dart index 15402aea..6c820e71 100644 --- a/lib/modules/library/library_screen.dart +++ b/lib/modules/library/library_screen.dart @@ -11,6 +11,7 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/category.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; import 'package:mangayomi/models/history.dart'; @@ -21,6 +22,7 @@ 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'; @@ -1030,6 +1032,15 @@ class _LibraryScreenState extends ConsumerState manga!.categories = categoryIds; isar.mangas.putSync(manga); + ref + .read(synchingProvider( + syncId: 1) + .notifier) + .addChangedPart( + ActionType.updateItem, + manga.id, + manga.toJson(), + false); } }); ref @@ -1041,6 +1052,7 @@ class _LibraryScreenState extends ConsumerState isLongPressedMangaStateProvider .notifier) .update(false); + if (mounted) { Navigator.pop(context); } @@ -1164,9 +1176,19 @@ class _LibraryScreenState extends ConsumerState isar.chapters.deleteSync(chapter.id!); } isar.mangas.deleteSync(manga.id!); + ref + .read(synchingProvider(syncId: 1) + .notifier) + .addChangedPart(ActionType.removeItem, + manga.id, "{}", false); } else { manga.favorite = false; isar.mangas.putSync(manga); + ref + .read(synchingProvider(syncId: 1) + .notifier) + .addChangedPart(ActionType.updateItem, + manga.id, manga.toJson(), false); } } }); @@ -1804,6 +1826,7 @@ class _LibraryScreenState extends ConsumerState manga.whenData((value) { var randomManga = (value..shuffle()).first; pushToMangaReaderDetail( + ref: ref, archiveId: randomManga.isLocalArchive ?? false ? randomManga.id : null, diff --git a/lib/modules/library/providers/add_torrent.dart b/lib/modules/library/providers/add_torrent.dart index 68499e26..03443182 100644 --- a/lib/modules/library/providers/add_torrent.dart +++ b/lib/modules/library/providers/add_torrent.dart @@ -1,7 +1,9 @@ import 'package:file_picker/file_picker.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/services/torrent_server.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -45,6 +47,11 @@ Future addTorrentFromUrlOrFromFile(Ref ref, Manga? mManga, isLocalArchive: true, artist: '', ); + + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.addItem, null, manga.toJson(), true); + if (url != null) { manga.customCoverImage = null; isar.writeTxnSync(() { @@ -53,6 +60,8 @@ Future addTorrentFromUrlOrFromFile(Ref ref, Manga? mManga, ..manga.value = manga; isar.chapters.putSync(chapters); chapters.manga.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.addChapter, null, chapters.toJson(), false); }); } else { for (var file in result!.files.reversed.toList()) { @@ -69,6 +78,8 @@ Future addTorrentFromUrlOrFromFile(Ref ref, Manga? mManga, ..manga.value = manga; isar.chapters.putSync(chapters); chapters.manga.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.addChapter, null, chapters.toJson(), false); }); } } diff --git a/lib/modules/library/providers/add_torrent.g.dart b/lib/modules/library/providers/add_torrent.g.dart index 47af35c6..2421c444 100644 --- a/lib/modules/library/providers/add_torrent.g.dart +++ b/lib/modules/library/providers/add_torrent.g.dart @@ -7,7 +7,7 @@ part of 'add_torrent.dart'; // ************************************************************************** String _$addTorrentFromUrlOrFromFileHash() => - r'11cc239bb8b517326f9a005b0c89dd5eb1127099'; + r'd12f901b675ecbf4a29c496cf99da17f219745f7'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/library/providers/library_state_provider.dart b/lib/modules/library/providers/library_state_provider.dart index 68ff5fbe..509ad074 100644 --- a/lib/modules/library/providers/library_state_provider.dart +++ b/lib/modules/library/providers/library_state_provider.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; 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'; @@ -788,6 +790,8 @@ class MangasSetIsReadState extends _$MangasSetIsReadState { chapter.lastPageRead = "1"; isar.chapters.putSync(chapter..manga.value = manga); chapter.manga.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, chapter.id, chapter.toJson(), false); } }); } @@ -812,6 +816,8 @@ class MangasSetUnReadState extends _$MangasSetUnReadState { chapter.isRead = false; isar.chapters.putSync(chapter..manga.value = manga); chapter.manga.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, chapter.id, chapter.toJson(), false); } }); } diff --git a/lib/modules/library/providers/library_state_provider.g.dart b/lib/modules/library/providers/library_state_provider.g.dart index 44ade022..5d25d51e 100644 --- a/lib/modules/library/providers/library_state_provider.g.dart +++ b/lib/modules/library/providers/library_state_provider.g.dart @@ -2551,7 +2551,7 @@ final isLongPressedMangaStateProvider = typedef _$IsLongPressedMangaState = AutoDisposeNotifier; String _$mangasSetIsReadStateHash() => - r'8f86296f588a48747de625e0471048978ee9bdeb'; + r'b599664aed8cc00d35a683fa6660bf79b66c555d'; abstract class _$MangasSetIsReadState extends BuildlessAutoDisposeNotifier { @@ -2698,7 +2698,7 @@ class _MangasSetIsReadStateProviderElement } String _$mangasSetUnReadStateHash() => - r'3413e731b2fd8476a4032d3e47b943ca12f25090'; + r'03906113f5e5878909a5a6399ead997eaa2c1204'; abstract class _$MangasSetUnReadState extends BuildlessAutoDisposeNotifier { diff --git a/lib/modules/library/providers/local_archive.dart b/lib/modules/library/providers/local_archive.dart index fa6b87fa..99939763 100644 --- a/lib/modules/library/providers/local_archive.dart +++ b/lib/modules/library/providers/local_archive.dart @@ -1,10 +1,12 @@ import 'dart:typed_data'; import 'package:file_picker/file_picker.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/modules/manga/archive_reader/models/models.dart'; import 'package:mangayomi/modules/manga/archive_reader/providers/archive_reader_providers.dart'; +import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; part 'local_archive.g.dart'; @@ -38,6 +40,11 @@ Future importArchivesFromFile(Ref ref, Manga? mManga, isLocalArchive: true, artist: '', ); + + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.addItem, null, manga.toJson(), true); + for (var file in result.files.reversed.toList()) { (String, LocalExtensionType, Uint8List, String)? data = itemType == ItemType.manga @@ -58,6 +65,8 @@ Future importArchivesFromFile(Ref ref, Manga? mManga, ..manga.value = manga; isar.chapters.putSync(chapters); chapters.manga.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.addChapter, null, chapters.toJson(), false); }); } } diff --git a/lib/modules/library/providers/local_archive.g.dart b/lib/modules/library/providers/local_archive.g.dart index 842e1996..f4929f23 100644 --- a/lib/modules/library/providers/local_archive.g.dart +++ b/lib/modules/library/providers/local_archive.g.dart @@ -7,7 +7,7 @@ part of 'local_archive.dart'; // ************************************************************************** String _$importArchivesFromFileHash() => - r'8e6e592c927ad080e93d54dac1144ef8637a7e52'; + r'49cd5455a5ff601e4b7b3fccd2fd5f6463c35fb3'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/library/widgets/library_gridview_widget.dart b/lib/modules/library/widgets/library_gridview_widget.dart index 195d3adf..96d94662 100644 --- a/lib/modules/library/widgets/library_gridview_widget.dart +++ b/lib/modules/library/widgets/library_gridview_widget.dart @@ -91,6 +91,7 @@ class _LibraryGridViewWidgetState extends State { ref.read(mangasListStateProvider.notifier).update(entry); } else { await pushToMangaReaderDetail( + ref: ref, archiveId: isLocalArchive ? entry.id : null, context: context, lang: entry.lang!, diff --git a/lib/modules/library/widgets/library_listview_widget.dart b/lib/modules/library/widgets/library_listview_widget.dart index a1f1102e..8b84b9f2 100644 --- a/lib/modules/library/widgets/library_listview_widget.dart +++ b/lib/modules/library/widgets/library_listview_widget.dart @@ -53,6 +53,7 @@ class LibraryListViewWidget extends StatelessWidget { ref.read(mangasListStateProvider.notifier).update(entry); } else { await pushToMangaReaderDetail( + ref: ref, archiveId: isLocalArchive ? entry.id : null, context: context, lang: entry.lang!, diff --git a/lib/modules/manga/detail/manga_detail_view.dart b/lib/modules/manga/detail/manga_detail_view.dart index 8a30bcb2..6c40f209 100644 --- a/lib/modules/manga/detail/manga_detail_view.dart +++ b/lib/modules/manga/detail/manga_detail_view.dart @@ -9,6 +9,7 @@ import 'package:go_router/go_router.dart'; import 'package:isar/isar.dart'; import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; import 'package:mangayomi/models/manga.dart'; @@ -22,6 +23,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'; @@ -729,6 +731,11 @@ class _MangaDetailViewState extends ConsumerState isar.chapters.putSync( chapter..manga.value = widget.manga); chapter.manga.saveSync(); + ref + .read( + synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.updateChapter, + chapter.id, chapter.toJson(), false); } }); ref @@ -772,6 +779,11 @@ class _MangaDetailViewState extends ConsumerState if (chapter.isRead!) { chapter.updateTrackChapterRead(ref); } + ref + .read( + synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.updateChapter, + chapter.id, chapter.toJson(), false); } }); ref @@ -814,6 +826,14 @@ class _MangaDetailViewState extends ConsumerState isar.chapters.putSync(chapters[i] ..manga.value = widget.manga); chapters[i].manga.saveSync(); + ref + .read(synchingProvider(syncId: 1) + .notifier) + .addChangedPart( + ActionType.updateChapter, + chapters[i].id, + chapters[i].toJson(), + false); } } ref @@ -1666,6 +1686,15 @@ class _MangaDetailViewState extends ConsumerState ..customCoverImage = null ..customCoverFromTracker = trackSearch.coverUrl); + ref + .read(synchingProvider( + syncId: 1) + .notifier) + .addChangedPart( + ActionType.updateItem, + widget.manga!.id, + widget.manga!.toJson(), + false); }); if (context.mounted) { Navigator.pop(context); @@ -1791,6 +1820,15 @@ class _MangaDetailViewState extends ConsumerState isar.mangas.putSync(manga ..customCoverImage = null ..customCoverFromTracker = null); + ref + .read( + synchingProvider(syncId: 1) + .notifier) + .addChangedPart( + ActionType.updateItem, + manga.id, + manga.toJson(), + false); }); Navigator.pop(context); } else if (value == 1) { @@ -1814,6 +1852,15 @@ class _MangaDetailViewState extends ConsumerState isar.mangas.putSync(manga ..customCoverImage = customCoverImage); + ref + .read(synchingProvider( + syncId: 1) + .notifier) + .addChangedPart( + ActionType.updateItem, + manga.id, + manga.toJson(), + false); }); botToast( context.l10n.cover_updated, @@ -1920,6 +1967,10 @@ class _MangaDetailViewState extends ConsumerState manga.description = description.text; manga.name = name.text; isar.mangas.putSync(manga); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.updateItem, manga.id, + manga.toJson(), false); }); Navigator.pop(context); }, diff --git a/lib/modules/manga/detail/manga_details_view.dart b/lib/modules/manga/detail/manga_details_view.dart index 43cc87ce..81fa9661 100644 --- a/lib/modules/manga/detail/manga_details_view.dart +++ b/lib/modules/manga/detail/manga_details_view.dart @@ -4,10 +4,12 @@ import 'package:go_router/go_router.dart'; import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/category.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/modules/manga/detail/widgets/custom_floating_action_btn.dart'; import 'package:mangayomi/models/manga.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'; import 'package:mangayomi/utils/constant.dart'; @@ -197,6 +199,10 @@ class _MangaDetailsViewState extends ConsumerState { model.favorite = false; model.dateAdded = 0; isar.mangas.putSync(model); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.updateItem, model.id, + model.toJson(), false); }); }, child: Column( @@ -236,6 +242,14 @@ class _MangaDetailsViewState extends ConsumerState { model.favorite = true; model.dateAdded = DateTime.now().millisecondsSinceEpoch; isar.mangas.putSync(model); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart( + ActionType.addItem, null, model.toJson(), false); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.updateItem, model.id, + model.toJson(), false); }); } }, @@ -355,6 +369,16 @@ class _MangaDetailsViewState extends ConsumerState { model.dateAdded = DateTime.now().millisecondsSinceEpoch; isar.mangas.putSync(model); + ref + .read( + synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.addItem, + model.id, model.toJson(), false); + ref + .read( + synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.updateItem, + model.id, model.toJson(), false); }); if (mounted) { Navigator.pop(context); diff --git a/lib/modules/manga/detail/providers/state_providers.dart b/lib/modules/manga/detail/providers/state_providers.dart index bee226b2..ca27005b 100644 --- a/lib/modules/manga/detail/providers/state_providers.dart +++ b/lib/modules/manga/detail/providers/state_providers.dart @@ -1,10 +1,12 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; 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'; @@ -311,6 +313,8 @@ class ChapterSetIsBookmarkState extends _$ChapterSetIsBookmarkState { chapter.isBookmarked = !chapter.isBookmarked!; isar.chapters.putSync(chapter..manga.value = manga); chapter.manga.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, chapter.id, chapter.toJson(), false); } }); ref.read(isLongPressedStateProvider.notifier).update(false); @@ -330,6 +334,8 @@ class ChapterSetIsReadState extends _$ChapterSetIsReadState { chapter.isRead = !chapter.isRead!; isar.chapters.putSync(chapter..manga.value = manga); chapter.manga.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, chapter.id, chapter.toJson(), false); } }); ref.read(isLongPressedStateProvider.notifier).update(false); diff --git a/lib/modules/manga/detail/providers/state_providers.g.dart b/lib/modules/manga/detail/providers/state_providers.g.dart index 198cee93..b831d018 100644 --- a/lib/modules/manga/detail/providers/state_providers.g.dart +++ b/lib/modules/manga/detail/providers/state_providers.g.dart @@ -816,7 +816,7 @@ class _ChapterFilterResultStateProviderElement } String _$chapterSetIsBookmarkStateHash() => - r'113131bb13e50566390ee3e34aa2f08820a8870c'; + r'9b4359e87f6083323cc49d20bedde0ce0f61d9b3'; abstract class _$ChapterSetIsBookmarkState extends BuildlessAutoDisposeNotifier { @@ -963,7 +963,7 @@ class _ChapterSetIsBookmarkStateProviderElement } String _$chapterSetIsReadStateHash() => - r'c319f81ec30565ad81a28cb0a8ce7fddcb47cd77'; + r'9cfd45df3f359a43140c023a584b52f8c81cbace'; 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 80d630d1..03091882 100644 --- a/lib/modules/manga/detail/providers/update_manga_detail_providers.dart +++ b/lib/modules/manga/detail/providers/update_manga_detail_providers.dart @@ -1,9 +1,11 @@ import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/update.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'; @@ -56,6 +58,9 @@ Future updateMangaDetail(Ref ref, } isar.writeTxnSync(() { isar.mangas.putSync(manga); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.updateItem, manga.id, manga.toJson(), false); manga.lastUpdate = DateTime.now().millisecondsSinceEpoch; List chapters = []; @@ -81,6 +86,8 @@ Future updateMangaDetail(Ref ref, for (var chap in chapters.reversed.toList()) { isar.chapters.putSync(chap); chap.manga.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.addChapter, chap.id, chap.toJson(), false); if (manga.chapters.isNotEmpty) { final update = Update( mangaId: mangaId, @@ -89,6 +96,8 @@ Future updateMangaDetail(Ref ref, ..chapter.value = chap; isar.updates.putSync(update); update.chapter.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.addUpdate, update.id, update.toJson(), false); } } } @@ -103,6 +112,10 @@ Future updateMangaDetail(Ref ref, oldChap.scanlator = newChap.scanlator; isar.chapters.putSync(oldChap); oldChap.manga.saveSync(); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateItem, manga.id, manga.toJson(), false); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, oldChap.id, oldChap.toJson(), false); } } }); 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 2ffd0112..7c2e6eea 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'a86fe8fea46e411203182287c970cd80cc9a1a0c'; +String _$updateMangaDetailHash() => r'ebd820d3e9d1900c464aebfbf711f43f6619e586'; /// 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 a56f03ef..7a8a6138 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -2,6 +2,7 @@ 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/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; import 'package:mangayomi/models/history.dart'; @@ -11,6 +12,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'; @@ -159,6 +161,8 @@ class ReaderController extends _$ReaderController { Manga? manga = chapter.manga.value; manga!.lastRead = DateTime.now().millisecondsSinceEpoch; isar.mangas.putSync(manga); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateItem, manga.id, manga.toJson(), false); }); History? history; @@ -184,6 +188,13 @@ class ReaderController extends _$ReaderController { isar.writeTxnSync(() { isar.historys.putSync(history!); history.chapter.saveSync(); + if (empty) { + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.addHistory, null, history.toJson(), false); + } else { + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateHistory, history.id, history.toJson(), false); + } }); } @@ -194,6 +205,8 @@ class ReaderController extends _$ReaderController { isar.writeTxnSync(() { chap.isBookmarked = !isBookmarked; isar.chapters.putSync(chap); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, chapter.id, chapter.toJson(), false); }); } @@ -331,6 +344,8 @@ class ReaderController extends _$ReaderController { chap.isRead = isRead; chap.lastPageRead = isRead ? '1' : (newIndex + 1).toString(); isar.chapters.putSync(chap); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, chapter.id, chapter.toJson(), false); }); if (isRead) { chapter.updateTrackChapterRead(ref); 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 18900f96..9f9d3e87 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.g.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.g.dart @@ -171,7 +171,7 @@ class _CurrentIndexProviderElement Chapter get chapter => (origin as CurrentIndexProvider).chapter; } -String _$readerControllerHash() => r'471617bf6fa730d837c8e0d9504bde17cfb635a8'; +String _$readerControllerHash() => r'58256638f87a8c24ee8081260685692b6e819fc3'; abstract class _$ReaderController extends BuildlessAutoDisposeNotifier { late final Chapter chapter; diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 878caa16..1e812547 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -11,6 +11,7 @@ import 'package:go_router/go_router.dart'; import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/changed.dart' as changed; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/page.dart'; import 'package:mangayomi/models/settings.dart'; @@ -22,6 +23,7 @@ import 'package:mangayomi/modules/manga/reader/double_columm_view_center.dart'; import 'package:mangayomi/modules/manga/reader/providers/color_filter_provider.dart'; import 'package:mangayomi/modules/manga/reader/widgets/color_filter_widget.dart'; import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; +import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/providers/storage_provider.dart'; @@ -335,6 +337,16 @@ class _MangaChapterPageGalleryState isar.mangas.putSync(manga ..customCoverImage = imageBytes); + ref + .read(synchingProvider( + syncId: 1) + .notifier) + .addChangedPart( + changed.ActionType + .updateItem, + manga.id, + manga.toJson(), + false); }); if (mounted) { Navigator.pop(context, "ok"); 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 6d7c50a1..97b79039 100644 --- a/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart +++ b/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.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'; @@ -153,17 +155,23 @@ class _ChapterListTileState extends State { ) ], ), - trailing: IconButton( - onPressed: () { - setState(() { - isBookmarked = !isBookmarked; - }); - isar.writeTxnSync(() => { - isar.chapters.putSync(chapter..isBookmarked = isBookmarked), - }); - }, - icon: Icon(isBookmarked ? Icons.bookmark : Icons.bookmark_outline, - color: context.primaryColor), + trailing: Consumer( + builder: (context, ref, child) => IconButton( + onPressed: () { + setState(() { + isBookmarked = !isBookmarked; + }); + isar.writeTxnSync(() => { + isar.chapters.putSync(chapter..isBookmarked = isBookmarked), + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.updateChapter, chapter.id, + chapter.toJson(), false), + }); + }, + icon: Icon(isBookmarked ? Icons.bookmark : Icons.bookmark_outline, + color: context.primaryColor), + ), ), ), ); diff --git a/lib/modules/more/categories/categories_screen.dart b/lib/modules/more/categories/categories_screen.dart index dc0e3c46..ddcd8b9e 100644 --- a/lib/modules/more/categories/categories_screen.dart +++ b/lib/modules/more/categories/categories_screen.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/category.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/manga.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/reader/providers/reader_state_provider.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'; @@ -206,6 +208,19 @@ class _CategoriesTabState extends ConsumerState { index] .id!); }); + await ref + .read(synchingProvider( + syncId: + 1) + .notifier) + .addChangedPartAsync( + ActionType + .removeCategory, + _entries[ + index] + .id, + "{}", + true); if (context .mounted) { Navigator.pop( @@ -291,12 +306,23 @@ class _CategoriesTabState extends ConsumerState { isExist ? null : () async { + final category = Category( + forItemType: widget.itemType, + name: controller.text, + ); await isar.writeTxn(() async { - await isar.categorys.put(Category( - forItemType: widget.itemType, - name: controller.text, - )); + await isar.categorys + .put(category); }); + await ref + .read( + synchingProvider(syncId: 1) + .notifier) + .addChangedPartAsync( + ActionType.addCategory, + category.id, + category.toJson(), + true); if (context.mounted) { Navigator.pop(context); } @@ -375,18 +401,27 @@ class _CategoriesTabState extends ConsumerState { width: 15, ), TextButton( - onPressed: - controller.text.isEmpty || isExist || isSameName - ? null - : () async { - await isar.writeTxn(() async { - category.name = controller.text; - await isar.categorys.put(category); - }); - if (context.mounted) { - Navigator.pop(context); - } - }, + onPressed: controller.text.isEmpty || + isExist || + isSameName + ? null + : () async { + await isar.writeTxn(() async { + category.name = controller.text; + await isar.categorys.put(category); + }); + await ref + .read( + synchingProvider(syncId: 1).notifier) + .addChangedPartAsync( + ActionType.renameCategory, + category.id, + category.toJson(), + true); + if (context.mounted) { + Navigator.pop(context); + } + }, child: Text( l10n.ok, style: TextStyle( diff --git a/lib/modules/more/data_and_storage/providers/restore.dart b/lib/modules/more/data_and_storage/providers/restore.dart index 87e30139..ff29e217 100644 --- a/lib/modules/more/data_and_storage/providers/restore.dart +++ b/lib/modules/more/data_and_storage/providers/restore.dart @@ -20,6 +20,7 @@ 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/browse/providers/browse_state_provider.dart'; import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -54,7 +55,7 @@ void doRestore(Ref ref, {required String path, required BuildContext context}) { } @riverpod -void restoreBackup(Ref ref, Map backup) { +void restoreBackup(Ref ref, Map backup, {bool full = true}) { final version = backup['version']; if (["1", "2"].any((e) => e == version)) { try { @@ -106,13 +107,15 @@ void restoreBackup(Ref ref, Map backup) { } } - isar.downloads.clearSync(); - if (downloads != null) { - for (var download in downloads) { - final chapter = isar.chapters.getSync(download.id!); - if (chapter != null) { - isar.downloads.putSync(download..chapter.value = chapter); - download.chapter.saveSync(); + if (full) { + isar.downloads.clearSync(); + if (downloads != null) { + for (var download in downloads) { + final chapter = isar.chapters.getSync(download.id!); + if (chapter != null) { + isar.downloads.putSync(download..chapter.value = chapter); + download.chapter.saveSync(); + } } } } @@ -157,31 +160,42 @@ void restoreBackup(Ref ref, Map backup) { isar.tracks.putAllSync(track); } - isar.trackPreferences.clearSync(); - if (trackPreferences != null) { - isar.trackPreferences.putAllSync(trackPreferences); + if (full) { + isar.trackPreferences.clearSync(); + if (trackPreferences != null) { + isar.trackPreferences.putAllSync(trackPreferences); + } } - isar.sources.clearSync(); - if (extensions != null) { - isar.sources.putAllSync(extensions); + if (full) { + isar.sources.clearSync(); + if (extensions != null) { + isar.sources.putAllSync(extensions); + } } - isar.sourcePreferences.clearSync(); - if (sourcesPrefs != null) { - isar.sourcePreferences.putAllSync(sourcesPrefs); + if (full) { + isar.sourcePreferences.clearSync(); + if (sourcesPrefs != null) { + isar.sourcePreferences.putAllSync(sourcesPrefs); + } + isar.settings.clearSync(); + if (settings != null) { + isar.settings.putAllSync(settings); + } } - isar.settings.clearSync(); - if (settings != null) { - isar.settings.putAllSync(settings); + if (full) { + ref.invalidate(themeModeStateProvider); + ref.invalidate(blendLevelStateProvider); + ref.invalidate(flexSchemeColorStateProvider); + ref.invalidate(pureBlackDarkModeStateProvider); + ref.invalidate(l10nLocaleStateProvider); + ref.invalidate(navigationOrderStateProvider); + ref.invalidate(hideItemsStateProvider); + ref.invalidate(extensionsRepoStateProvider(ItemType.manga)); + ref.invalidate(extensionsRepoStateProvider(ItemType.anime)); + ref.invalidate(extensionsRepoStateProvider(ItemType.novel)); } - ref.invalidate(themeModeStateProvider); - ref.invalidate(blendLevelStateProvider); - ref.invalidate(flexSchemeColorStateProvider); - ref.invalidate(pureBlackDarkModeStateProvider); - ref.invalidate(l10nLocaleStateProvider); - ref.invalidate(navigationOrderStateProvider); - ref.invalidate(hideItemsStateProvider); }); } catch (e) { rethrow; @@ -203,7 +217,7 @@ ItemType _convertToItemType(Map backup) { ItemType _convertToItemTypeCategory(Map backup) { final forManga = backup['forManga']; return forManga == null - ? ItemType.values[backup['itemType'] ?? 0] + ? ItemType.values[backup['forItemType'] ?? 0] : forManga ? ItemType.manga : ItemType.anime; diff --git a/lib/modules/more/data_and_storage/providers/restore.g.dart b/lib/modules/more/data_and_storage/providers/restore.g.dart index e833da14..af592576 100644 --- a/lib/modules/more/data_and_storage/providers/restore.g.dart +++ b/lib/modules/more/data_and_storage/providers/restore.g.dart @@ -173,7 +173,7 @@ class _DoRestoreProviderElement extends AutoDisposeProviderElement BuildContext get context => (origin as DoRestoreProvider).context; } -String _$restoreBackupHash() => r'24405b9be28204324e47d6c1db34495d55a491d2'; +String _$restoreBackupHash() => r'2645a4e3f29e1e5b65acff8d66a6f634a8773acf'; /// See also [restoreBackup]. @ProviderFor(restoreBackup) @@ -186,10 +186,12 @@ class RestoreBackupFamily extends Family { /// See also [restoreBackup]. RestoreBackupProvider call( - Map backup, - ) { + Map backup, { + bool full = true, + }) { return RestoreBackupProvider( backup, + full: full, ); } @@ -199,6 +201,7 @@ class RestoreBackupFamily extends Family { ) { return call( provider.backup, + full: provider.full, ); } @@ -221,11 +224,13 @@ class RestoreBackupFamily extends Family { class RestoreBackupProvider extends AutoDisposeProvider { /// See also [restoreBackup]. RestoreBackupProvider( - Map backup, - ) : this._internal( + Map backup, { + bool full = true, + }) : this._internal( (ref) => restoreBackup( ref as RestoreBackupRef, backup, + full: full, ), from: restoreBackupProvider, name: r'restoreBackupProvider', @@ -237,6 +242,7 @@ class RestoreBackupProvider extends AutoDisposeProvider { allTransitiveDependencies: RestoreBackupFamily._allTransitiveDependencies, backup: backup, + full: full, ); RestoreBackupProvider._internal( @@ -247,9 +253,11 @@ class RestoreBackupProvider extends AutoDisposeProvider { required super.debugGetCreateSourceHash, required super.from, required this.backup, + required this.full, }) : super.internal(); final Map backup; + final bool full; @override Override overrideWith( @@ -265,6 +273,7 @@ class RestoreBackupProvider extends AutoDisposeProvider { allTransitiveDependencies: null, debugGetCreateSourceHash: null, backup: backup, + full: full, ), ); } @@ -276,13 +285,16 @@ class RestoreBackupProvider extends AutoDisposeProvider { @override bool operator ==(Object other) { - return other is RestoreBackupProvider && other.backup == backup; + return other is RestoreBackupProvider && + other.backup == backup && + other.full == full; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, backup.hashCode); + hash = _SystemHash.combine(hash, full.hashCode); return _SystemHash.finish(hash); } @@ -293,6 +305,9 @@ class RestoreBackupProvider extends AutoDisposeProvider { mixin RestoreBackupRef on AutoDisposeProviderRef { /// The parameter `backup` of this provider. Map get backup; + + /// The parameter `full` of this provider. + bool get full; } class _RestoreBackupProviderElement extends AutoDisposeProviderElement @@ -301,6 +316,8 @@ class _RestoreBackupProviderElement extends AutoDisposeProviderElement @override Map get backup => (origin as RestoreBackupProvider).backup; + @override + bool get full => (origin as RestoreBackupProvider).full; } // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/modules/more/settings/browse/providers/browse_state_provider.g.dart b/lib/modules/more/settings/browse/providers/browse_state_provider.g.dart index 135069ec..af6fe625 100644 --- a/lib/modules/more/settings/browse/providers/browse_state_provider.g.dart +++ b/lib/modules/more/settings/browse/providers/browse_state_provider.g.dart @@ -6,7 +6,7 @@ part of 'browse_state_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$getRepoInfosHash() => r'd5d5eca9fd23accd515bf51b470edb99a5d58733'; +String _$getRepoInfosHash() => r'250bc0082ac2841114d6f1815303955b8798240e'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/more/settings/sync/providers/sync_providers.dart b/lib/modules/more/settings/sync/providers/sync_providers.dart index 07de85b6..4975d560 100644 --- a/lib/modules/more/settings/sync/providers/sync_providers.dart +++ b/lib/modules/more/settings/sync/providers/sync_providers.dart @@ -1,4 +1,8 @@ +import 'dart:convert'; + +import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/sync_preference.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'sync_providers.g.dart'; @@ -6,8 +10,8 @@ part 'sync_providers.g.dart'; @riverpod class Synching extends _$Synching { @override - SyncPreference? build({required int? syncId}) { - return isar.syncPreferences.getSync(syncId!); + SyncPreference build({required int? syncId}) { + return isar.syncPreferences.getSync(syncId!) ?? SyncPreference(syncId: 1); } void login(SyncPreference syncPreference) { @@ -18,28 +22,152 @@ class Synching extends _$Synching { void logout() { isar.writeTxnSync(() { - isar.syncPreferences.deleteSync(syncId!); + isar.syncPreferences.putSync(state..authToken = null); }); } void setLastUpload(int timestamp) { isar.writeTxnSync(() { - isar.syncPreferences.putSync( - isar.syncPreferences.getSync(syncId!)!..lastUpload = timestamp); + isar.syncPreferences.putSync(state..lastUpload = timestamp); }); } void setLastDownload(int timestamp) { isar.writeTxnSync(() { - isar.syncPreferences.putSync( - isar.syncPreferences.getSync(syncId!)!..lastDownload = timestamp); + isar.syncPreferences.putSync(state..lastDownload = timestamp); + }); + } + + void setLastSync(int timestamp) { + isar.writeTxnSync(() { + isar.syncPreferences.putSync(state..lastSync = timestamp); }); } void setServer(String? server) { isar.writeTxnSync(() { - isar.syncPreferences - .putSync(isar.syncPreferences.getSync(syncId!)!..server = server); + isar.syncPreferences.putSync(state..server = server); + }); + } + + void setSyncOn(bool value) { + isar.writeTxnSync(() { + isar.syncPreferences.putSync(state..syncOn = value); + }); + } + + List getAllChangedParts() { + return isar.changedParts.filter().idIsNotNull().findAllSync(); + } + + List getChangedParts(List actionTypes) { + var query = isar.changedParts + .filter() + .idIsNotNull() + .and() + .actionTypeEqualTo(actionTypes.first); + for (final at in actionTypes.skip(1)) { + query = query.or().actionTypeEqualTo(at); + } + return query.findAllSync(); + } + + void addChangedPart( + ActionType action, int? isarId, Object data, bool writeTxn) { + if (!state.syncOn) { + return; + } + final changedPart = isar.changedParts + .filter() + .actionTypeEqualTo(action) + .isarIdEqualTo(isarId) + .findFirstSync(); + if (writeTxn) { + isar.writeTxnSync(() { + if (changedPart != null) { + isar.changedParts.putSync(changedPart + ..data = jsonEncode(data) + ..clientDate = DateTime.now().millisecondsSinceEpoch); + } else { + isar.changedParts.putSync(ChangedPart( + actionType: action, + isarId: isarId, + data: jsonEncode(data), + clientDate: DateTime.now().millisecondsSinceEpoch)); + } + }); + } else { + if (changedPart != null) { + isar.changedParts.putSync(changedPart + ..data = jsonEncode(data) + ..clientDate = DateTime.now().millisecondsSinceEpoch); + } else { + isar.changedParts.putSync(ChangedPart( + actionType: action, + isarId: isarId, + data: jsonEncode(data), + clientDate: DateTime.now().millisecondsSinceEpoch)); + } + } + } + + Future addChangedPartAsync( + ActionType action, int? isarId, Object data, bool writeTxn) async { + if (!state.syncOn) { + return; + } + final changedPart = isar.changedParts + .filter() + .actionTypeEqualTo(action) + .isarIdEqualTo(isarId) + .findFirstSync(); + if (writeTxn) { + await isar.writeTxn(() async { + if (changedPart != null) { + await isar.changedParts.put(changedPart + ..data = jsonEncode(data) + ..clientDate = DateTime.now().millisecondsSinceEpoch); + } else { + await isar.changedParts.put(ChangedPart( + actionType: action, + isarId: isarId, + data: jsonEncode(data), + clientDate: DateTime.now().millisecondsSinceEpoch)); + } + }); + } else { + if (changedPart != null) { + await isar.changedParts.put(changedPart + ..data = jsonEncode(data) + ..clientDate = DateTime.now().millisecondsSinceEpoch); + } else { + await isar.changedParts.put(ChangedPart( + actionType: action, + isarId: isarId, + data: jsonEncode(data), + clientDate: DateTime.now().millisecondsSinceEpoch)); + } + } + } + + void clearChangedParts(List actions) { + var temp = isar.changedParts + .filter() + .idIsNotNull() + .and() + .actionTypeEqualTo(actions.first); + for (ActionType action in actions.skip(1)) { + temp = temp.or().actionTypeEqualTo(action); + } + final changedParts = temp.findAllSync().map((cp) => cp.id as Id).toList(); + isar.writeTxnSync(() { + isar.changedParts.deleteAllSync(changedParts); + }); + } + + void clearAllChangedParts() { + isar.writeTxnSync(() { + isar.changedParts.clearSync(); }); } } diff --git a/lib/modules/more/settings/sync/providers/sync_providers.g.dart b/lib/modules/more/settings/sync/providers/sync_providers.g.dart index 910c3bc8..fe3a2161 100644 --- a/lib/modules/more/settings/sync/providers/sync_providers.g.dart +++ b/lib/modules/more/settings/sync/providers/sync_providers.g.dart @@ -6,7 +6,7 @@ part of 'sync_providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$synchingHash() => r'3ab44d9e753f2d4b51fd10af6c98ffac78cbf201'; +String _$synchingHash() => r'45ac5bd29f880dfc7ac1fcdf12f49d751325279b'; /// Copied from Dart SDK class _SystemHash { @@ -29,11 +29,10 @@ class _SystemHash { } } -abstract class _$Synching - extends BuildlessAutoDisposeNotifier { +abstract class _$Synching extends BuildlessAutoDisposeNotifier { late final int? syncId; - SyncPreference? build({ + SyncPreference build({ required int? syncId, }); } @@ -43,7 +42,7 @@ abstract class _$Synching const synchingProvider = SynchingFamily(); /// See also [Synching]. -class SynchingFamily extends Family { +class SynchingFamily extends Family { /// See also [Synching]. const SynchingFamily(); @@ -82,7 +81,7 @@ class SynchingFamily extends Family { /// See also [Synching]. class SynchingProvider - extends AutoDisposeNotifierProviderImpl { + extends AutoDisposeNotifierProviderImpl { /// See also [Synching]. SynchingProvider({ required int? syncId, @@ -112,7 +111,7 @@ class SynchingProvider final int? syncId; @override - SyncPreference? runNotifierBuild( + SyncPreference runNotifierBuild( covariant Synching notifier, ) { return notifier.build( @@ -137,8 +136,7 @@ class SynchingProvider } @override - AutoDisposeNotifierProviderElement - createElement() { + AutoDisposeNotifierProviderElement createElement() { return _SynchingProviderElement(this); } @@ -158,13 +156,13 @@ class SynchingProvider @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -mixin SynchingRef on AutoDisposeNotifierProviderRef { +mixin SynchingRef on AutoDisposeNotifierProviderRef { /// The parameter `syncId` of this provider. int? get syncId; } class _SynchingProviderElement - extends AutoDisposeNotifierProviderElement + extends AutoDisposeNotifierProviderElement with SynchingRef { _SynchingProviderElement(super.provider); diff --git a/lib/modules/more/settings/sync/sync.dart b/lib/modules/more/settings/sync/sync.dart index 70dfb142..9205b8a4 100644 --- a/lib/modules/more/settings/sync/sync.dart +++ b/lib/modules/more/settings/sync/sync.dart @@ -2,6 +2,8 @@ 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/models/changed.dart'; +import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/utils/date.dart'; import 'package:mangayomi/models/sync_preference.dart'; import 'package:mangayomi/modules/more/settings/sync/widgets/sync_listile.dart'; @@ -14,6 +16,8 @@ class SyncScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final syncProvider = ref.watch(synchingProvider(syncId: 1)); + final changedParts = ref.watch(synchingProvider(syncId: 1).notifier); final l10n = l10nLocalizations(context)!; return Scaffold( appBar: AppBar( @@ -33,6 +37,16 @@ class SyncScreen extends ConsumerWidget { syncPreference.authToken?.isNotEmpty ?? false; return Column( children: [ + SwitchListTile( + value: syncProvider.syncOn, + title: Text(context.l10n.sync_on), + onChanged: !isLogged + ? null + : (value) { + ref + .read(SynchingProvider(syncId: 1).notifier) + .setSyncOn(value); + }), Padding( padding: const EdgeInsets.only( left: 15, right: 15, bottom: 10, top: 5), @@ -62,6 +76,7 @@ class SyncScreen extends ConsumerWidget { ), const SizedBox(width: 10), Text(l10n.syncing_subtitle, + softWrap: true, style: TextStyle( fontSize: 11, color: context.secondaryColor)) ], @@ -80,6 +95,12 @@ class SyncScreen extends ConsumerWidget { 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( @@ -98,6 +119,27 @@ class SyncScreen extends ConsumerWidget { ), Row( children: [ + const SizedBox(width: 20), + Column( + children: [ + IconButton( + onPressed: !isLogged + ? null + : () { + ref + .read(syncServerProvider(syncId: 1) + .notifier) + .startSync(l10n); + }, + icon: Icon( + Icons.sync, + color: !isLogged + ? context.secondaryColor + : context.primaryColor, + )), + Text(l10n.sync_button_sync), + ], + ), const SizedBox(width: 20), Column( children: [ @@ -187,6 +229,94 @@ class SyncScreen extends ConsumerWidget { ], ), const SizedBox(width: 20), + Column( + children: [ + IconButton( + onPressed: !isLogged + ? null + : () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + l10n.sync_confirm_snapshot), + 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 + .withValues( + alpha: + 0.7)), + onPressed: () { + ref + .read( + syncServerProvider( + syncId: + 1) + .notifier) + .createSnapshot( + l10n); + Navigator.pop( + context); + }, + child: Text( + l10n.dialog_confirm, + style: TextStyle( + color: context + .secondaryColor), + )), + ], + ) + ], + ); + }); + }, + icon: Icon( + Icons.save_as, + color: !isLogged + ? context.secondaryColor + : context.primaryColor, + )), + Text(l10n.sync_button_snapshot), + ], + ), + const SizedBox(width: 20), Column( children: [ IconButton( @@ -249,7 +379,9 @@ class SyncScreen extends ConsumerWidget { 1) .notifier) .downloadFromServer( - l10n); + l10n, + false, + true); Navigator.pop( context); }, @@ -275,7 +407,76 @@ class SyncScreen extends ConsumerWidget { ], ), ], - ) + ), + const SizedBox(height: 30), + buildChangedItemWidget( + l10n.sync_pending_manga, + changedParts.getChangedParts([ + ActionType.addItem, + ActionType.removeItem, + ActionType.updateItem + ])), + const SizedBox(height: 10), + buildChangedItemWidget( + l10n.sync_pending_chapter, + changedParts.getChangedParts([ + ActionType.addChapter, + ActionType.removeChapter, + ActionType.updateChapter + ])), + const SizedBox(height: 10), + buildChangedItemWidget( + l10n.sync_pending_category, + changedParts.getChangedParts([ + ActionType.addCategory, + ActionType.removeCategory, + ActionType.renameCategory + ])), + const SizedBox(height: 10), + buildChangedItemWidget( + l10n.sync_pending_history, + changedParts.getChangedParts([ + ActionType.addHistory, + ActionType.clearHistory, + ActionType.removeHistory + ])), + const SizedBox(height: 10), + buildChangedItemWidget( + l10n.sync_pending_update, + changedParts.getChangedParts( + [ActionType.addUpdate, ActionType.clearUpdates])), + const SizedBox(height: 10), + buildChangedItemWidget( + l10n.sync_pending_extension, + changedParts.getChangedParts([ + ActionType.addExtension, + ActionType.removeExtension, + ActionType.updateExtension + ])), + const SizedBox(height: 10), + buildChangedItemWidget( + l10n.sync_pending_track, + changedParts.getChangedParts([ + ActionType.addTrack, + ActionType.removeTrack, + ActionType.updateTrack + ])), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.only( + left: 15, right: 15, bottom: 10, top: 5), + child: Row( + children: [ + ElevatedButton( + onPressed: () { + ref + .read(syncServerProvider(syncId: 1).notifier) + .getSnapshots(l10n); + }, + child: Text("Browse / Download older backups")), + ], + ), + ), ], ); }), @@ -284,6 +485,32 @@ class SyncScreen extends ConsumerWidget { } } +Widget buildChangedItemWidget(String text, List changedParts) { + return Padding( + padding: const EdgeInsets.only(left: 25, right: 25, bottom: 10, top: 5), + child: Row( + children: [ + Text( + "$text: ${changedParts.length}", + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + background: Paint() + ..color = changedParts.isEmpty + ? Color.fromARGB(125, 78, 182, 92) + : Color.fromARGB(123, 245, 233, 132) + ..strokeWidth = 20 + ..strokeJoin = StrokeJoin.round + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke, + color: Colors.white, + ), + ), + ], + ), + ); +} + void _showDialogLogin(BuildContext context, WidgetRef ref) { final serverController = TextEditingController(); final emailController = TextEditingController(); diff --git a/lib/modules/more/settings/track/providers/track_providers.dart b/lib/modules/more/settings/track/providers/track_providers.dart index f3f60e52..d48e8b67 100644 --- a/lib/modules/more/settings/track/providers/track_providers.dart +++ b/lib/modules/more/settings/track/providers/track_providers.dart @@ -1,9 +1,11 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/track.dart'; import 'package:mangayomi/models/track_preference.dart'; +import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'track_providers.g.dart'; @@ -38,13 +40,28 @@ class Tracks extends _$Tracks { } } - isar.writeTxnSync(() => isar.tracks.putSync(track - ..syncId = syncId - ..itemType = itemType)); + isar.writeTxnSync(() { + isar.tracks.putSync(track + ..syncId = syncId + ..itemType = itemType); + if (tra.isEmpty) { + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.addTrack, null, track.toJson(), false); + } else { + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateTrack, track.id, track.toJson(), false); + } + }); } void deleteTrackManga(Track track) { - isar.writeTxnSync(() => isar.tracks.deleteSync(track.id!)); + isar.writeTxnSync(() { + isar.tracks.deleteSync(track.id!); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.removeTrack, track.id, "{}", false); + }); } } diff --git a/lib/modules/more/settings/track/providers/track_providers.g.dart b/lib/modules/more/settings/track/providers/track_providers.g.dart index 7dd1a10c..e411502f 100644 --- a/lib/modules/more/settings/track/providers/track_providers.g.dart +++ b/lib/modules/more/settings/track/providers/track_providers.g.dart @@ -6,7 +6,7 @@ part of 'track_providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$tracksHash() => r'a6e052c4102bbe640a397c37887dd618a4512c49'; +String _$tracksHash() => r'de3a19fc6542e0f610d154978fbd0272259142fc'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/novel/novel_reader_controller_provider.dart b/lib/modules/novel/novel_reader_controller_provider.dart index 513b01a9..53697217 100644 --- a/lib/modules/novel/novel_reader_controller_provider.dart +++ b/lib/modules/novel/novel_reader_controller_provider.dart @@ -1,10 +1,12 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; 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/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'novel_reader_controller_provider.g.dart'; @@ -33,6 +35,8 @@ class NovelReaderController extends _$NovelReaderController { Manga? manga = chapter.manga.value; manga!.lastRead = DateTime.now().millisecondsSinceEpoch; isar.mangas.putSync(manga); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateItem, manga.id, manga.toJson(), false); }); History? history; @@ -58,6 +62,13 @@ class NovelReaderController extends _$NovelReaderController { isar.writeTxnSync(() { isar.historys.putSync(history!); history.chapter.saveSync(); + if (empty) { + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.addHistory, null, history.toJson(), false); + } else { + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateHistory, history.id, history.toJson(), false); + } }); } @@ -71,6 +82,8 @@ class NovelReaderController extends _$NovelReaderController { ch.lastPageRead = (maxOffset != 0 ? newOffset / maxOffset : 0).toString(); isar.chapters.putSync(ch); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, chapter.id, chapter.toJson(), false); }); } } @@ -82,6 +95,8 @@ class NovelReaderController extends _$NovelReaderController { isar.writeTxnSync(() { chap.isBookmarked = !isBookmarked; isar.chapters.putSync(chap); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateChapter, chapter.id, chapter.toJson(), false); }); } diff --git a/lib/modules/novel/novel_reader_controller_provider.g.dart b/lib/modules/novel/novel_reader_controller_provider.g.dart index 0d0d48b8..473395e5 100644 --- a/lib/modules/novel/novel_reader_controller_provider.g.dart +++ b/lib/modules/novel/novel_reader_controller_provider.g.dart @@ -7,7 +7,7 @@ part of 'novel_reader_controller_provider.dart'; // ************************************************************************** String _$novelReaderControllerHash() => - r'2eec885b858de8195e31a2d0b70feb56c1dc4268'; + r'f05612ee0d25a5e5592f4e931b4078d992079f37'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/widgets/manga_image_card_widget.dart b/lib/modules/widgets/manga_image_card_widget.dart index 90d8dc1e..a5905962 100644 --- a/lib/modules/widgets/manga_image_card_widget.dart +++ b/lib/modules/widgets/manga_image_card_widget.dart @@ -5,10 +5,12 @@ import 'package:go_router/go_router.dart'; import 'package:isar/isar.dart'; import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/modules/manga/detail/manga_detail_main.dart'; +import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart'; import 'package:mangayomi/router/router.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; @@ -64,6 +66,7 @@ class MangaImageCardWidget extends ConsumerWidget { cacheMaxAge: const Duration(days: 7)), onTap: () { pushToMangaReaderDetail( + ref: ref, context: context, getManga: getMangaDetail!, lang: source.lang!, @@ -72,6 +75,7 @@ class MangaImageCardWidget extends ConsumerWidget { }, onLongPress: () { pushToMangaReaderDetail( + ref: ref, context: context, getManga: getMangaDetail!, lang: source.lang!, @@ -81,6 +85,7 @@ class MangaImageCardWidget extends ConsumerWidget { }, onSecondaryTap: () { pushToMangaReaderDetail( + ref: ref, context: context, getManga: getMangaDetail!, lang: source.lang!, @@ -162,6 +167,7 @@ class MangaImageCardListTileWidget extends ConsumerWidget { child: InkWell( onTap: () { pushToMangaReaderDetail( + ref: ref, context: context, getManga: getMangaDetail!, lang: source.lang!, @@ -170,6 +176,7 @@ class MangaImageCardListTileWidget extends ConsumerWidget { }, onLongPress: () { pushToMangaReaderDetail( + ref: ref, context: context, getManga: getMangaDetail!, lang: source.lang!, @@ -179,6 +186,7 @@ class MangaImageCardListTileWidget extends ConsumerWidget { }, onSecondaryTap: () { pushToMangaReaderDetail( + ref: ref, context: context, getManga: getMangaDetail!, lang: source.lang!, @@ -247,6 +255,7 @@ class MangaImageCardListTileWidget extends ConsumerWidget { Future pushToMangaReaderDetail( {MManga? getManga, + required WidgetRef ref, required String lang, required BuildContext context, required String source, @@ -280,6 +289,9 @@ Future pushToMangaReaderDetail( if (empty) { isar.writeTxnSync(() { isar.mangas.putSync(manga); + ref + .read(synchingProvider(syncId: 1).notifier) + .addChangedPart(ActionType.addItem, null, manga.toJson(), false); }); } @@ -346,6 +358,8 @@ Future pushToMangaReaderDetail( final getManga = isar.mangas.filter().idEqualTo(mangaId).findFirstSync()!; isar.writeTxnSync(() { isar.mangas.putSync(getManga..favorite = !getManga.favorite!); + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateItem, getManga.id, getManga.toJson(), false); }); } } diff --git a/lib/providers/storage_provider.dart b/lib/providers/storage_provider.dart index 92670895..f577727e 100644 --- a/lib/providers/storage_provider.dart +++ b/lib/providers/storage_provider.dart @@ -4,6 +4,7 @@ import 'package:isar/isar.dart'; import 'package:mangayomi/eval/model/source_preference.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/category.dart'; +import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; import 'package:mangayomi/models/update.dart'; @@ -146,6 +147,7 @@ class StorageProvider { final isar = Isar.openSync([ MangaSchema, + ChangedPartSchema, ChapterSchema, CategorySchema, UpdateSchema, diff --git a/lib/services/aniskip.g.dart b/lib/services/aniskip.g.dart index b2e1defb..c05db920 100644 --- a/lib/services/aniskip.g.dart +++ b/lib/services/aniskip.g.dart @@ -6,7 +6,7 @@ part of 'aniskip.dart'; // RiverpodGenerator // ************************************************************************** -String _$aniSkipHash() => r'887869b54e2e151633efd46da83bde845e14f421'; +String _$aniSkipHash() => r'2e5d19b025a2207ff64da7bf7908450ea9e5ff8c'; /// See also [AniSkip]. @ProviderFor(AniSkip) diff --git a/lib/services/sync_server.dart b/lib/services/sync_server.dart index 0d77c947..ff0642ed 100644 --- a/lib/services/sync_server.dart +++ b/lib/services/sync_server.dart @@ -1,21 +1,22 @@ +import 'package:crypto/crypto.dart'; import 'package:isar/isar.dart'; import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/eval/model/source_preference.dart'; import 'package:mangayomi/main.dart'; -import 'package:mangayomi/models/update.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/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/update.dart'; import 'package:mangayomi/modules/more/data_and_storage/providers/restore.dart'; import 'package:mangayomi/modules/more/settings/sync/models/jwt.dart'; import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; -import 'dart:convert'; import 'package:mangayomi/services/http/m_client.dart'; +import 'dart:convert'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; part 'sync_server.g.dart'; @@ -26,6 +27,9 @@ class SyncServer extends _$SyncServer { final String _loginUrl = '/login'; final String _uploadUrl = '/upload/full'; final String _downloadUrl = '/download'; + final String _snapshotUrl = '/snapshot'; + final String _checkUrl = '/check'; + final String _syncUrl = '/sync'; @override void build({required int syncId}) {} @@ -59,6 +63,168 @@ class SyncServer extends _$SyncServer { } } + Future startSync(AppLocalizations l10n) async { + botToast(l10n.sync_checking, second: 2); + try { + final changedParts = ref + .read(synchingProvider(syncId: syncId).notifier) + .getAllChangedParts(); + + if (changedParts.isNotEmpty) { + final accessToken = _getAccessToken(); + + var response = await http.post( + Uri.parse('${_getServer()}$_syncUrl'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $accessToken' + }, + body: jsonEncode({'changedParts': changedParts}), + ); + if (response.statusCode != 200) { + botToast(l10n.sync_upload_failed, second: 5); + return; + } + var jsonData = jsonDecode(response.body) as Map; + final localHash = _getDataHash(_getData()); + final remoteHash = jsonData["hash"]; + if (localHash != remoteHash) { + await downloadFromServer(l10n, true, false); + } + } else { + await forceCheck(l10n, true); + } + ref + .read(synchingProvider(syncId: syncId).notifier) + .setLastSync(DateTime.now().millisecondsSinceEpoch); + ref + .read(synchingProvider(syncId: syncId).notifier) + .clearAllChangedParts(); + ref.invalidate(synchingProvider(syncId: syncId)); + botToast(l10n.sync_download_finished, second: 2); + } catch (error) { + botToast(error.toString(), second: 5); + } + } + + Future forceCheck(AppLocalizations l10n, bool silent) async { + if (!silent) { + botToast(l10n.sync_checking, second: 2); + } + try { + final accessToken = _getAccessToken(); + final localHash = _getDataHash(_getData()); + var response = await http.get( + Uri.parse('${_getServer()}$_checkUrl'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $accessToken' + }, + ); + if (response.statusCode != 200) { + botToast(l10n.sync_download_failed, second: 2); + return; + } + var jsonData = jsonDecode(response.body) as Map; + final remoteHash = jsonData["hash"]; + if (localHash != remoteHash) { + await downloadFromServer(l10n, silent, false); + } else if (!silent) { + botToast("Sync up to date", second: 2); + } + } catch (error) { + botToast(error.toString(), second: 5); + } + } + + Future getSnapshots(AppLocalizations l10n) async { + try { + final accessToken = _getAccessToken(); + + var response = await http.get( + Uri.parse('${_getServer()}$_snapshotUrl'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $accessToken' + }, + ); + if (response.statusCode != 200) { + botToast(l10n.server_error, second: 5); + return List.empty(); + } + var snapshots = jsonDecode(response.body) as List; + for (final snapshot in snapshots) { + print( + "${snapshot["id"]} - ${DateTime.parse(snapshot["dbCreatedAt"]).millisecondsSinceEpoch}"); + } + return snapshots; + } catch (error) { + botToast(error.toString(), second: 5); + } + return List.empty(); + } + + Future downloadSnapshot( + AppLocalizations l10n, String snapshotId) async { + botToast(l10n.sync_downloading, second: 2); + try { + final accessToken = _getAccessToken(); + + var response = await http.get( + Uri.parse('${_getServer()}$_snapshotUrl/$snapshotId'), + 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"], + true); + ref + .read(synchingProvider(syncId: syncId).notifier) + .setLastDownload(DateTime.now().millisecondsSinceEpoch); + ref + .read(synchingProvider(syncId: syncId).notifier) + .clearAllChangedParts(); + ref.invalidate(synchingProvider(syncId: syncId)); + botToast(l10n.sync_download_finished, second: 2); + } catch (error) { + botToast(error.toString(), second: 5); + } + } + + Future createSnapshot(AppLocalizations l10n) async { + botToast(l10n.sync_snapshot_creating, second: 2); + try { + final accessToken = _getAccessToken(); + + var response = await http.post( + Uri.parse('${_getServer()}$_snapshotUrl'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $accessToken' + }, + ); + if (response.statusCode == 400) { + botToast(l10n.sync_snapshot_no_data, second: 5); + return; + } else if (response.statusCode != 200) { + botToast(l10n.server_error, second: 5); + return; + } + botToast(l10n.sync_snapshot_created, second: 2); + } catch (error) { + botToast(error.toString(), second: 5); + } + } + Future uploadToServer(AppLocalizations l10n) async { botToast(l10n.sync_uploading, second: 2); try { @@ -80,14 +246,21 @@ class SyncServer extends _$SyncServer { ref .read(synchingProvider(syncId: syncId).notifier) .setLastUpload(DateTime.now().millisecondsSinceEpoch); + ref + .read(synchingProvider(syncId: syncId).notifier) + .clearAllChangedParts(); + ref.invalidate(synchingProvider(syncId: syncId)); 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); + Future downloadFromServer( + AppLocalizations l10n, bool silent, bool full) async { + if (!silent) { + botToast(l10n.sync_downloading, second: 2); + } try { final accessToken = _getAccessToken(); @@ -103,18 +276,39 @@ class SyncServer extends _$SyncServer { return; } var jsonData = jsonDecode(response.body) as Map; - _restore(jsonData["backupData"] is String - ? jsonDecode(jsonData["backupData"]) - : jsonData["backupData"]); + _restore( + jsonData["backupData"] is String + ? jsonDecode(jsonData["backupData"]) + : jsonData["backupData"], + full); ref .read(synchingProvider(syncId: syncId).notifier) .setLastDownload(DateTime.now().millisecondsSinceEpoch); - botToast(l10n.sync_download_finished, second: 2); + ref + .read(synchingProvider(syncId: syncId).notifier) + .clearAllChangedParts(); + ref.invalidate(synchingProvider(syncId: syncId)); + if (!silent) { + 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["updates"] = data["updates"]; + var encodedJson = jsonEncode(datas); + return sha256.convert(utf8.encode(encodedJson)).toString(); + } + Map _getData() { Map datas = {}; datas.addAll({"version": "2"}); @@ -189,13 +383,13 @@ class SyncServer extends _$SyncServer { return datas; } - void _restore(Map backup) { - ref.read(restoreBackupProvider(backup)); + void _restore(Map backup, bool full) { + ref.read(restoreBackupProvider(backup, full: full)); } String _getAccessToken() { final syncPrefs = ref.watch(synchingProvider(syncId: syncId)); - if (syncPrefs == null || syncPrefs.authToken == null) { + if (syncPrefs.authToken == null) { return ""; } var paddedPayload = syncPrefs.authToken!.split(".")[1]; @@ -216,6 +410,6 @@ class SyncServer extends _$SyncServer { String _getServer() { final syncPrefs = ref.watch(synchingProvider(syncId: syncId)); - return syncPrefs?.server ?? ""; + return syncPrefs.server ?? ""; } } diff --git a/lib/services/sync_server.g.dart b/lib/services/sync_server.g.dart index cadd097e..d54ac41a 100644 --- a/lib/services/sync_server.g.dart +++ b/lib/services/sync_server.g.dart @@ -6,7 +6,7 @@ part of 'sync_server.dart'; // RiverpodGenerator // ************************************************************************** -String _$syncServerHash() => r'd752de5e7238487e6176bb3ec69d2bd98c0509ad'; +String _$syncServerHash() => r'f9e68540eb702f3e3498fc642454897921a17f42'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/services/trackers/anilist.g.dart b/lib/services/trackers/anilist.g.dart index 558da2c4..834afd64 100644 --- a/lib/services/trackers/anilist.g.dart +++ b/lib/services/trackers/anilist.g.dart @@ -6,7 +6,7 @@ part of 'anilist.dart'; // RiverpodGenerator // ************************************************************************** -String _$anilistHash() => r'70e8cd537270a9054a1ef72de117fc7ad5545218'; +String _$anilistHash() => r'ddd07acc8d28d2aa95c942566109e9393ca9e5ed'; /// Copied from Dart SDK class _SystemHash {