testing first version of sync feature

This commit is contained in:
Schnitzel5 2024-09-04 14:37:36 +02:00
parent 28b3f8b0a5
commit 82a18101fe
23 changed files with 3055 additions and 20 deletions

View file

@ -180,6 +180,28 @@
"one_tracker": "1 tracker",
"n_tracker": "{n} trackers",
"tracking": "Tracking",
"syncing": "Sync",
"sync_logged": "Login successful",
"syncing_subtitle": "Sync your progress across multiple devices via a self-hosted \nserver. Make sure to upload first if this is your first time \nsyncing or download before using (auto) sync on this device!",
"last_sync": "Last sync at: ",
"last_upload": "Last upload at: ",
"last_download": "Last download at: ",
"sync_server": "Sync Server Address",
"sync_login_invalid_creds": "Invalid email or password",
"sync_checking": "Checking for sync...",
"sync_uploading": "Upload started...",
"sync_downloading": "Download started...",
"sync_upload_finished": "Upload finished",
"sync_download_finished": "Download finished",
"sync_up_to_date": "Sync up to date",
"sync_upload_failed": "Upload failed",
"sync_download_failed": "Download failed",
"sync_button_sync": "Sync progress",
"sync_button_upload": "Full upload",
"sync_button_download": "Full download",
"sync_confirm_upload": "A full upload will completely replace the remote data with your current one!",
"sync_confirm_download": "A full download will completely replace your current data with the remote one!",
"dialog_confirm": "Confirm",
"description": "Description",
"episode_progress": "Progress: {n}",
"n_episodes": "{n} episodes",
@ -274,6 +296,8 @@
"default_skip_intro_length": "Default Skip intro length",
"default_playback_speed_length": "Default Playback speed length",
"updateProgressAfterReading": "Update progress after reading",
"syncAfterReading": "Sync after reading or watching",
"syncOnAppLaunch": "Sync when opening the app",
"no_sources_installed": "No sources installed!",
"show_extensions": "Show extensions",
"default_skip_forward_skip_length": "Default skip forward skip length",

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -168,6 +168,7 @@ class AnimeStreamController extends _$AnimeStreamController {
});
if (isWatch) {
episode.updateTrackChapterRead(ref);
episode.syncProgressAfterChapterRead(ref);
}
}
}

View file

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

View file

@ -646,6 +646,7 @@ class MangasSetIsReadState extends _$MangasSetIsReadState {
final chapters = manga.chapters;
if (chapters.isNotEmpty) {
chapters.last.updateTrackChapterRead(ref);
chapters.last.syncProgressAfterChapterRead(ref);
isar.writeTxnSync(() {
for (var chapter in chapters) {
chapter.isRead = true;

View file

@ -2520,7 +2520,7 @@ final isLongPressedMangaStateProvider =
typedef _$IsLongPressedMangaState = AutoDisposeNotifier<bool>;
String _$mangasSetIsReadStateHash() =>
r'8f86296f588a48747de625e0471048978ee9bdeb';
r'dce8293fa6b8338791c76ddad07efa4339ca8628';
abstract class _$MangasSetIsReadState
extends BuildlessAutoDisposeNotifier<void> {

View file

@ -753,6 +753,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
chapter.manga.saveSync();
if (chapter.isRead!) {
chapter.updateTrackChapterRead(ref);
chapter.syncProgressAfterChapterRead(ref);
}
}
});
@ -786,6 +787,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
onPressed: () {
int index = chapters.indexOf(chap.first);
chapters[index + 1].updateTrackChapterRead(ref);
chapters[index + 1].syncProgressAfterChapterRead(ref);
isar.writeTxnSync(() {
for (var i = index + 1;
i < chapters.length;

View file

@ -6,7 +6,7 @@ part of 'download_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$downloadChapterHash() => r'eca8ccbe5f93f07c3471af81355fe9b3a8ec11e8';
String _$downloadChapterHash() => r'6bf91d6683ebaacb5b822d5e4e7926e44362a98b';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -11,7 +11,9 @@ 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/services/sync_server.dart';
import 'package:mangayomi/utils/chapter_recognition.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'reader_controller_provider.g.dart';
@ -396,6 +398,13 @@ extension ChapterExtensions on Chapter {
}
}
}
void syncProgressAfterChapterRead(dynamic ref) {
if (!(ref is WidgetRef || ref is AutoDisposeNotifierProviderRef)) return;
final syncAfterReading = ref.watch(syncAfterReadingStateProvider);
if (!syncAfterReading) return;
ref.read(syncServerProvider(syncId: 1).notifier).syncToServer(ref, true);
}
}
extension MangaExtensions on Manga {

View file

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

View file

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

View file

@ -0,0 +1,79 @@
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/sync_preference.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'sync_providers.g.dart';
@riverpod
class Synching extends _$Synching {
@override
SyncPreference? build({required int? syncId}) {
return isar.syncPreferences.getSync(syncId!);
}
void login(SyncPreference syncPreference) {
isar.writeTxnSync(() {
isar.syncPreferences.putSync(syncPreference);
});
}
void logout() {
isar.writeTxnSync(() {
isar.syncPreferences.deleteSync(syncId!);
});
}
void setLastSync(int timestamp) {
isar.writeTxnSync(() {
isar.syncPreferences.putSync(isar.syncPreferences.getSync(syncId!)!..lastSync = timestamp);
});
}
void setLastUpload(int timestamp) {
isar.writeTxnSync(() {
isar.syncPreferences.putSync(isar.syncPreferences.getSync(syncId!)!..lastUpload = timestamp);
});
}
void setLastDownload(int timestamp) {
isar.writeTxnSync(() {
isar.syncPreferences.putSync(isar.syncPreferences.getSync(syncId!)!..lastDownload = timestamp);
});
}
void setServer(String? server) {
isar.writeTxnSync(() {
isar.syncPreferences.putSync(isar.syncPreferences.getSync(syncId!)!..server = server);
});
}
}
@riverpod
class SyncOnAppLaunchState extends _$SyncOnAppLaunchState {
@override
bool build() {
return isar.settings.getSync(227)!.syncOnAppLaunch ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..syncOnAppLaunch = value));
}
}
@riverpod
class SyncAfterReadingState extends _$SyncAfterReadingState {
@override
bool build() {
return isar.settings.getSync(227)!.syncAfterReading ?? false;
}
void set(bool value) {
final settings = isar.settings.getSync(227);
state = value;
isar.writeTxnSync(
() => isar.settings.putSync(settings!..syncAfterReading = value));
}
}

View file

@ -0,0 +1,208 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sync_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$synchingHash() => r'2ef7fd99da4292ed236252d2b727cff9a69f43a9';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$Synching
extends BuildlessAutoDisposeNotifier<SyncPreference?> {
late final int? syncId;
SyncPreference? build({
required int? syncId,
});
}
/// See also [Synching].
@ProviderFor(Synching)
const synchingProvider = SynchingFamily();
/// See also [Synching].
class SynchingFamily extends Family<SyncPreference?> {
/// See also [Synching].
const SynchingFamily();
/// See also [Synching].
SynchingProvider call({
required int? syncId,
}) {
return SynchingProvider(
syncId: syncId,
);
}
@override
SynchingProvider getProviderOverride(
covariant SynchingProvider provider,
) {
return call(
syncId: provider.syncId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'synchingProvider';
}
/// See also [Synching].
class SynchingProvider
extends AutoDisposeNotifierProviderImpl<Synching, SyncPreference?> {
/// See also [Synching].
SynchingProvider({
required int? syncId,
}) : this._internal(
() => Synching()..syncId = syncId,
from: synchingProvider,
name: r'synchingProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$synchingHash,
dependencies: SynchingFamily._dependencies,
allTransitiveDependencies: SynchingFamily._allTransitiveDependencies,
syncId: syncId,
);
SynchingProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.syncId,
}) : super.internal();
final int? syncId;
@override
SyncPreference? runNotifierBuild(
covariant Synching notifier,
) {
return notifier.build(
syncId: syncId,
);
}
@override
Override overrideWith(Synching Function() create) {
return ProviderOverride(
origin: this,
override: SynchingProvider._internal(
() => create()..syncId = syncId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
syncId: syncId,
),
);
}
@override
AutoDisposeNotifierProviderElement<Synching, SyncPreference?>
createElement() {
return _SynchingProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SynchingProvider && other.syncId == syncId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, syncId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin SynchingRef on AutoDisposeNotifierProviderRef<SyncPreference?> {
/// The parameter `syncId` of this provider.
int? get syncId;
}
class _SynchingProviderElement
extends AutoDisposeNotifierProviderElement<Synching, SyncPreference?>
with SynchingRef {
_SynchingProviderElement(super.provider);
@override
int? get syncId => (origin as SynchingProvider).syncId;
}
String _$syncOnAppLaunchStateHash() =>
r'dc7f3243e38a748462628229066c8fc0653c908b';
/// See also [SyncOnAppLaunchState].
@ProviderFor(SyncOnAppLaunchState)
final syncOnAppLaunchStateProvider =
AutoDisposeNotifierProvider<SyncOnAppLaunchState, bool>.internal(
SyncOnAppLaunchState.new,
name: r'syncOnAppLaunchStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$syncOnAppLaunchStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SyncOnAppLaunchState = AutoDisposeNotifier<bool>;
String _$syncAfterReadingStateHash() =>
r'e507acd490b5aea7fc1a8fd7a369ec01f4c47192';
/// See also [SyncAfterReadingState].
@ProviderFor(SyncAfterReadingState)
final syncAfterReadingStateProvider =
AutoDisposeNotifierProvider<SyncAfterReadingState, bool>.internal(
SyncAfterReadingState.new,
name: r'syncAfterReadingStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$syncAfterReadingStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SyncAfterReadingState = AutoDisposeNotifier<bool>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/source.dart';
import 'package:mangayomi/models/sync_preference.dart';
import 'package:mangayomi/models/track.dart';
import 'package:mangayomi/models/track_preference.dart';
import 'package:path_provider/path_provider.dart';
@ -136,6 +137,7 @@ class StorageProvider {
SettingsSchema,
TrackPreferenceSchema,
TrackSchema,
SyncPreferenceSchema,
SourcePreferenceSchema,
SourcePreferenceStringValueSchema
], directory: dir!.path, name: "mangayomiDb", inspector: inspector!);

View file

@ -14,6 +14,7 @@ import 'package:mangayomi/modules/more/backup_and_restore/backup_and_restore.dar
import 'package:mangayomi/modules/more/categories/categories_screen.dart';
import 'package:mangayomi/modules/more/settings/downloads/downloads_screen.dart';
import 'package:mangayomi/modules/more/settings/player/player_screen.dart';
import 'package:mangayomi/modules/more/settings/sync/sync.dart';
import 'package:mangayomi/modules/more/settings/track/track.dart';
import 'package:mangayomi/modules/more/settings/track/manage_trackers/manage_trackers.dart';
import 'package:mangayomi/modules/more/settings/track/manage_trackers/tracking_detail.dart';
@ -327,6 +328,19 @@ class RouterNotifier extends ChangeNotifier {
);
},
),
GoRoute(
path: "/sync",
name: "sync",
builder: (context, state) {
return const SyncScreen();
},
pageBuilder: (context, state) {
return transitionPage(
key: state.pageKey,
child: const SyncScreen(),
);
},
),
GoRoute(
path: "/sourceFilter",
name: "sourceFilter",

View file

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

View file

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