mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 21:35:32 +00:00
616 lines
19 KiB
Dart
616 lines
19 KiB
Dart
import 'package:flutter_qjs/quickjs/ffi.dart';
|
|
import 'package:isar_community/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/history.dart';
|
|
import 'package:mangayomi/models/manga.dart';
|
|
import 'package:mangayomi/models/settings.dart';
|
|
import 'package:mangayomi/models/track.dart';
|
|
import 'package:mangayomi/models/update.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/modules/more/settings/browse/providers/browse_state_provider.dart';
|
|
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
|
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
|
import 'package:mangayomi/services/http/m_client.dart';
|
|
import 'dart:convert';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:mangayomi/l10n/generated/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 _syncMangaUrl = '/sync/manga';
|
|
final String _syncHistoryUrl = '/sync/histories';
|
|
final String _syncUpdateUrl = '/sync/updates';
|
|
final String _syncSettingsUrl = '/sync/settings';
|
|
|
|
@override
|
|
void build({required int syncId}) {}
|
|
|
|
Future<(bool, String)> login(
|
|
AppLocalizations l10n,
|
|
String server,
|
|
String username,
|
|
String password,
|
|
) async {
|
|
server = server.isNotEmpty && server[server.length - 1] == '/'
|
|
? server.substring(0, server.length - 1)
|
|
: server;
|
|
try {
|
|
var response = await http.post(
|
|
Uri.parse('$server$_loginUrl'),
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: jsonEncode({'email': username, 'password': password}),
|
|
);
|
|
var cookieHeader = response.headers["set-cookie"];
|
|
var startIdx = cookieHeader?.indexOf("id=") ?? -1;
|
|
var endIdx = cookieHeader?.indexOf(";", startIdx) ?? -1;
|
|
if (startIdx == -1 || endIdx == -1) {
|
|
return (false, "Auth failed");
|
|
}
|
|
final authToken = cookieHeader!.substring(startIdx + 3, endIdx);
|
|
ref
|
|
.read(synchingProvider(syncId: syncId).notifier)
|
|
.login(server, username, authToken);
|
|
botToast(l10n.sync_logged);
|
|
return (true, "");
|
|
} catch (e) {
|
|
return (false, e.toString());
|
|
}
|
|
}
|
|
|
|
Future<void> startSync(
|
|
AppLocalizations l10n,
|
|
bool silent, {
|
|
bool upload = false,
|
|
bool download = false,
|
|
}) async {
|
|
if (!silent) {
|
|
botToast(l10n.sync_starting, second: 500);
|
|
}
|
|
try {
|
|
final syncPreference = ref.read(synchingProvider(syncId: syncId));
|
|
final syncNotifier = ref.read(synchingProvider(syncId: syncId).notifier);
|
|
|
|
final resultManga = await _syncManga(
|
|
l10n,
|
|
syncNotifier,
|
|
download: download,
|
|
upload: upload,
|
|
);
|
|
if (!resultManga) {
|
|
botToast(l10n.sync_failed, second: 5);
|
|
return;
|
|
}
|
|
if (syncPreference.syncHistories) {
|
|
final resultHistory = await _syncHistory(
|
|
l10n,
|
|
syncNotifier,
|
|
download: download,
|
|
upload: upload,
|
|
);
|
|
if (!resultHistory) {
|
|
botToast(l10n.sync_failed, second: 5);
|
|
return;
|
|
}
|
|
}
|
|
if (syncPreference.syncUpdates) {
|
|
final resultUpdate = await _syncUpdate(
|
|
l10n,
|
|
syncNotifier,
|
|
download: download,
|
|
upload: upload,
|
|
);
|
|
if (!resultUpdate) {
|
|
botToast(l10n.sync_failed, second: 5);
|
|
return;
|
|
}
|
|
}
|
|
if (syncPreference.syncSettings) {
|
|
final resultSettings = await _syncSettings(
|
|
l10n,
|
|
download: download,
|
|
upload: upload,
|
|
);
|
|
if (!resultSettings) {
|
|
botToast(l10n.sync_failed, second: 5);
|
|
return;
|
|
}
|
|
}
|
|
|
|
ref.invalidate(synchingProvider(syncId: syncId));
|
|
if (!silent) {
|
|
botToast(l10n.sync_finished, second: 2);
|
|
}
|
|
} catch (error) {
|
|
botToast(error.toString(), second: 5);
|
|
}
|
|
}
|
|
|
|
Future<bool> _syncManga(
|
|
AppLocalizations l10n,
|
|
Synching syncNotifier, {
|
|
bool upload = false,
|
|
bool download = false,
|
|
}) async {
|
|
final mangaData = _getMangaData(upload: upload, download: download);
|
|
final accessToken = _getAccessToken();
|
|
var response = await http.post(
|
|
Uri.parse('${_getServer()}$_syncMangaUrl'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Cookie': 'id=$accessToken',
|
|
},
|
|
body: mangaData,
|
|
);
|
|
if (response.statusCode != 200) {
|
|
botToast(l10n.sync_failed, second: 5);
|
|
return false;
|
|
}
|
|
|
|
if (!upload) {
|
|
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
|
|
await _upsertCategories(jsonData, syncNotifier);
|
|
await _upsertManga(jsonData, syncNotifier);
|
|
await _upsertChapters(jsonData, syncNotifier);
|
|
await _upsertTracks(jsonData, syncNotifier);
|
|
} else {
|
|
await syncNotifier.clearChangedParts([
|
|
ActionType.removeCategory,
|
|
ActionType.removeItem,
|
|
ActionType.removeChapter,
|
|
ActionType.removeTrack,
|
|
], true);
|
|
}
|
|
|
|
syncNotifier.setLastSyncManga(DateTime.now().millisecondsSinceEpoch);
|
|
|
|
return true;
|
|
}
|
|
|
|
Future<bool> _syncHistory(
|
|
AppLocalizations l10n,
|
|
Synching syncNotifier, {
|
|
bool upload = false,
|
|
bool download = false,
|
|
}) async {
|
|
final historyData = _getHistoryData(upload: upload, download: download);
|
|
final accessToken = _getAccessToken();
|
|
var response = await http.post(
|
|
Uri.parse('${_getServer()}$_syncHistoryUrl'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Cookie': 'id=$accessToken',
|
|
},
|
|
body: historyData,
|
|
);
|
|
if (response.statusCode != 200) {
|
|
botToast(l10n.sync_failed, second: 5);
|
|
return false;
|
|
}
|
|
|
|
if (!upload) {
|
|
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
|
|
await _upsertHistories(jsonData, syncNotifier);
|
|
} else {
|
|
await syncNotifier.clearChangedParts([ActionType.removeHistory], true);
|
|
}
|
|
|
|
syncNotifier.setLastSyncHistory(DateTime.now().millisecondsSinceEpoch);
|
|
|
|
return true;
|
|
}
|
|
|
|
Future<bool> _syncUpdate(
|
|
AppLocalizations l10n,
|
|
Synching syncNotifier, {
|
|
bool upload = false,
|
|
bool download = false,
|
|
}) async {
|
|
final updateData = _getUpdateData(upload: upload, download: download);
|
|
final accessToken = _getAccessToken();
|
|
var response = await http.post(
|
|
Uri.parse('${_getServer()}$_syncUpdateUrl'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Cookie': 'id=$accessToken',
|
|
},
|
|
body: updateData,
|
|
);
|
|
if (response.statusCode != 200) {
|
|
botToast(l10n.sync_failed, second: 5);
|
|
return false;
|
|
}
|
|
|
|
if (!upload) {
|
|
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
|
|
await _upsertUpdates(jsonData, syncNotifier);
|
|
} else {
|
|
await syncNotifier.clearChangedParts([ActionType.removeUpdate], true);
|
|
}
|
|
|
|
syncNotifier.setLastSyncUpdate(DateTime.now().millisecondsSinceEpoch);
|
|
|
|
return true;
|
|
}
|
|
|
|
Future<bool> _syncSettings(
|
|
AppLocalizations l10n, {
|
|
bool upload = false,
|
|
bool download = false,
|
|
}) async {
|
|
final settingsData = _getSettingsData(download: download);
|
|
final accessToken = _getAccessToken();
|
|
var response = await http.post(
|
|
Uri.parse('${_getServer()}$_syncSettingsUrl'),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Cookie': 'id=$accessToken',
|
|
},
|
|
body: settingsData,
|
|
);
|
|
if (response.statusCode != 200) {
|
|
botToast(l10n.sync_failed, second: 5);
|
|
return false;
|
|
}
|
|
|
|
if (!upload) {
|
|
final jsonData = jsonDecode(response.body) as Map<String, dynamic>;
|
|
await _upsertSettings(jsonData);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Future<void> _upsertCategories(
|
|
Map<String, dynamic> jsonData,
|
|
Synching syncNotifier,
|
|
) async {
|
|
final categories =
|
|
(jsonData["categories"] as List?)
|
|
?.map((e) => Category.fromJson(e))
|
|
.toList() ??
|
|
[];
|
|
await isar.writeTxn(() async {
|
|
for (var category
|
|
in await isar.categorys.filter().idIsNotNull().findAll()) {
|
|
final temp = categories.firstWhereOrNull((e) => e.id == category.id);
|
|
if (temp != null) {
|
|
if ((category.updatedAt ?? 0) < (temp.updatedAt ?? 1)) {
|
|
await isar.categorys.put(temp);
|
|
}
|
|
categories.remove(temp);
|
|
} else {
|
|
await isar.categorys.delete(category.id!);
|
|
}
|
|
}
|
|
for (var category in categories) {
|
|
await isar.categorys.put(category);
|
|
}
|
|
await syncNotifier.clearChangedParts([ActionType.removeCategory], false);
|
|
});
|
|
}
|
|
|
|
Future<void> _upsertManga(
|
|
Map<String, dynamic> jsonData,
|
|
Synching syncNotifier,
|
|
) async {
|
|
final mangas =
|
|
(jsonData["manga"] as List?)?.map((e) => Manga.fromJson(e)).toList() ??
|
|
[];
|
|
await isar.writeTxn(() async {
|
|
for (var manga in await isar.mangas.filter().idIsNotNull().findAll()) {
|
|
final temp = mangas.firstWhereOrNull((e) => e.id == manga.id);
|
|
if (temp != null) {
|
|
if ((manga.updatedAt ?? 0) < (temp.updatedAt ?? 1)) {
|
|
await isar.mangas.put(temp);
|
|
}
|
|
mangas.remove(temp);
|
|
} else {
|
|
await isar.mangas.delete(manga.id!);
|
|
}
|
|
}
|
|
for (var manga in mangas) {
|
|
await isar.mangas.put(manga);
|
|
}
|
|
await syncNotifier.clearChangedParts([ActionType.removeItem], false);
|
|
});
|
|
}
|
|
|
|
Future<void> _upsertChapters(
|
|
Map<String, dynamic> jsonData,
|
|
Synching syncNotifier,
|
|
) async {
|
|
final chapters =
|
|
(jsonData["chapters"] as List?)
|
|
?.map((e) => Chapter.fromJson(e))
|
|
.toList() ??
|
|
[];
|
|
await isar.writeTxn(() async {
|
|
for (var chapter
|
|
in await isar.chapters.filter().idIsNotNull().findAll()) {
|
|
final temp = chapters.firstWhereOrNull((e) => e.id == chapter.id);
|
|
if (temp != null) {
|
|
final manga = await isar.mangas.get(temp.mangaId!);
|
|
if (manga != null &&
|
|
(chapter.updatedAt ?? 0) < (temp.updatedAt ?? 1)) {
|
|
await isar.chapters.put(temp..manga.value = manga);
|
|
await temp.manga.save();
|
|
}
|
|
chapters.remove(temp);
|
|
} else {
|
|
await isar.chapters.delete(chapter.id!);
|
|
}
|
|
}
|
|
for (var chapter in chapters) {
|
|
final manga = await isar.mangas.get(chapter.mangaId!);
|
|
if (manga != null) {
|
|
await isar.chapters.put(chapter..manga.value = manga);
|
|
await chapter.manga.save();
|
|
}
|
|
}
|
|
await syncNotifier.clearChangedParts([ActionType.removeChapter], false);
|
|
});
|
|
}
|
|
|
|
Future<void> _upsertTracks(
|
|
Map<String, dynamic> jsonData,
|
|
Synching syncNotifier,
|
|
) async {
|
|
final tracks =
|
|
(jsonData["tracks"] as List?)?.map((e) => Track.fromJson(e)).toList() ??
|
|
[];
|
|
await isar.writeTxn(() async {
|
|
for (var track in await isar.tracks.filter().idIsNotNull().findAll()) {
|
|
final temp = tracks.firstWhereOrNull((e) => e.id == track.id);
|
|
if (temp != null) {
|
|
if ((track.updatedAt ?? 0) < (temp.updatedAt ?? 1)) {
|
|
await isar.tracks.put(temp);
|
|
}
|
|
tracks.remove(temp);
|
|
} else {
|
|
await isar.tracks.delete(track.id!);
|
|
}
|
|
}
|
|
for (var track in tracks) {
|
|
await isar.tracks.put(track);
|
|
}
|
|
await syncNotifier.clearChangedParts([ActionType.removeTrack], false);
|
|
});
|
|
}
|
|
|
|
Future<void> _upsertHistories(
|
|
Map<String, dynamic> jsonData,
|
|
Synching syncNotifier,
|
|
) async {
|
|
final histories =
|
|
(jsonData["histories"] as List?)
|
|
?.map((e) => History.fromJson(e))
|
|
.toList() ??
|
|
[];
|
|
await isar.writeTxn(() async {
|
|
for (var history
|
|
in await isar.historys.filter().idIsNotNull().findAll()) {
|
|
final temp = histories.firstWhereOrNull((e) => e.id == history.id);
|
|
if (temp != null) {
|
|
final chapter = await isar.chapters.get(temp.chapterId!);
|
|
if (chapter != null &&
|
|
(history.updatedAt ?? 0) < (temp.updatedAt ?? 1)) {
|
|
await isar.historys.put(temp..chapter.value = chapter);
|
|
await temp.chapter.save();
|
|
}
|
|
histories.remove(temp);
|
|
} else {
|
|
await isar.historys.delete(history.id!);
|
|
}
|
|
}
|
|
for (var history in histories) {
|
|
final chapter = await isar.chapters.get(history.chapterId!);
|
|
if (chapter != null) {
|
|
await isar.historys.put(history..chapter.value = chapter);
|
|
await history.chapter.save();
|
|
}
|
|
}
|
|
await syncNotifier.clearChangedParts([ActionType.removeHistory], false);
|
|
});
|
|
}
|
|
|
|
Future<void> _upsertUpdates(
|
|
Map<String, dynamic> jsonData,
|
|
Synching syncNotifier,
|
|
) async {
|
|
final updates =
|
|
(jsonData["updates"] as List?)
|
|
?.map((e) => Update.fromJson(e))
|
|
.toList() ??
|
|
[];
|
|
await isar.writeTxn(() async {
|
|
for (var update in await isar.updates.filter().idIsNotNull().findAll()) {
|
|
final temp = updates.firstWhereOrNull((e) => e.id == update.id);
|
|
if (temp != null) {
|
|
final chapter = await isar.chapters
|
|
.filter()
|
|
.mangaIdEqualTo(temp.mangaId)
|
|
.nameEqualTo(temp.chapterName)
|
|
.findFirst();
|
|
if (chapter != null &&
|
|
(update.updatedAt ?? 0) < (temp.updatedAt ?? 1)) {
|
|
await isar.updates.put(temp..chapter.value = chapter);
|
|
await temp.chapter.save();
|
|
}
|
|
updates.remove(temp);
|
|
} else {
|
|
await isar.updates.delete(update.id!);
|
|
}
|
|
}
|
|
for (var update in updates) {
|
|
final chapter = await isar.chapters
|
|
.filter()
|
|
.mangaIdEqualTo(update.mangaId)
|
|
.nameEqualTo(update.chapterName)
|
|
.findFirst();
|
|
if (chapter != null) {
|
|
await isar.updates.put(update..chapter.value = chapter);
|
|
await update.chapter.save();
|
|
}
|
|
}
|
|
await syncNotifier.clearChangedParts([ActionType.removeUpdate], false);
|
|
});
|
|
}
|
|
|
|
Future<void> _upsertSettings(Map<String, dynamic> jsonData) async {
|
|
final oldSettings = isar.settings.getSync(227)!;
|
|
final settings = Settings.fromJson(jsonData["settings"]);
|
|
await isar.writeTxn(() async {
|
|
await isar.settings.put(settings..cookiesList = oldSettings.cookiesList);
|
|
ref.invalidate(followSystemThemeStateProvider);
|
|
ref.invalidate(themeModeStateProvider);
|
|
ref.invalidate(blendLevelStateProvider);
|
|
ref.invalidate(flexSchemeColorStateProvider);
|
|
ref.invalidate(pureBlackDarkModeStateProvider);
|
|
ref.invalidate(l10nLocaleStateProvider);
|
|
ref.invalidate(extensionsRepoStateProvider(ItemType.manga));
|
|
ref.invalidate(extensionsRepoStateProvider(ItemType.anime));
|
|
ref.invalidate(extensionsRepoStateProvider(ItemType.novel));
|
|
});
|
|
}
|
|
|
|
String _getMangaData({bool upload = false, bool download = false}) {
|
|
Map<String, dynamic> data = {};
|
|
data["categories"] = download ? [] : _getCategories();
|
|
data["deleted_categories"] = download
|
|
? []
|
|
: _getDeletedObjects(ActionType.removeCategory);
|
|
data["manga"] = download ? [] : _getManga();
|
|
data["deleted_manga"] = download
|
|
? []
|
|
: _getDeletedObjects(ActionType.removeItem);
|
|
data["chapters"] = download ? [] : _getChapters();
|
|
data["deleted_chapters"] = download
|
|
? []
|
|
: _getDeletedObjects(ActionType.removeChapter);
|
|
data["tracks"] = download ? [] : _getTracks();
|
|
data["deleted_tracks"] = download
|
|
? []
|
|
: _getDeletedObjects(ActionType.removeTrack);
|
|
if (upload) {
|
|
data["resetAll"] = true;
|
|
}
|
|
return jsonEncode(data);
|
|
}
|
|
|
|
String _getHistoryData({bool upload = false, bool download = false}) {
|
|
Map<String, dynamic> data = {};
|
|
data["histories"] = download ? [] : _getHistories();
|
|
data["deleted_histories"] = download
|
|
? []
|
|
: _getDeletedObjects(ActionType.removeHistory);
|
|
if (upload) {
|
|
data["resetAll"] = true;
|
|
}
|
|
return jsonEncode(data);
|
|
}
|
|
|
|
String _getUpdateData({bool upload = false, bool download = false}) {
|
|
Map<String, dynamic> data = {};
|
|
data["updates"] = download ? [] : _getUpdates();
|
|
data["deleted_updates"] = download
|
|
? []
|
|
: _getDeletedObjects(ActionType.removeUpdate);
|
|
if (upload) {
|
|
data["resetAll"] = true;
|
|
}
|
|
return jsonEncode(data);
|
|
}
|
|
|
|
String _getSettingsData({bool download = false}) {
|
|
Map<String, dynamic> data = {};
|
|
if (!download) {
|
|
data["settings"] = isar.settings.getSync(227)!
|
|
..updatedAt ??= DateTime.now().millisecondsSinceEpoch
|
|
..cookiesList = [];
|
|
}
|
|
return jsonEncode(data);
|
|
}
|
|
|
|
List<int> _getDeletedObjects(ActionType actionType) {
|
|
return ref
|
|
.read(synchingProvider(syncId: syncId).notifier)
|
|
.getChangedParts([actionType])
|
|
.map((e) => e.isarId)
|
|
.nonNulls
|
|
.toList();
|
|
}
|
|
|
|
List<Map<String, dynamic>> _getManga() {
|
|
return isar.mangas
|
|
.filter()
|
|
.idIsNotNull()
|
|
.findAllSync()
|
|
.map((e) => (e..customCoverImage = null).toJson())
|
|
.toList();
|
|
}
|
|
|
|
List<Map<String, dynamic>> _getCategories() {
|
|
return isar.categorys
|
|
.filter()
|
|
.idIsNotNull()
|
|
.findAllSync()
|
|
.map((e) => e.toJson())
|
|
.toList();
|
|
}
|
|
|
|
List<Map<String, dynamic>> _getChapters() {
|
|
return isar.chapters
|
|
.filter()
|
|
.idIsNotNull()
|
|
.findAllSync()
|
|
.map((e) => e.toJson())
|
|
.toList();
|
|
}
|
|
|
|
List<Map<String, dynamic>> _getTracks() {
|
|
return isar.tracks
|
|
.filter()
|
|
.idIsNotNull()
|
|
.findAllSync()
|
|
.map((e) => e.toJson())
|
|
.toList();
|
|
}
|
|
|
|
List<Map<String, dynamic>> _getHistories() {
|
|
return isar.historys
|
|
.filter()
|
|
.idIsNotNull()
|
|
.findAllSync()
|
|
.map((e) => e.toJson())
|
|
.toList();
|
|
}
|
|
|
|
List<Map<String, dynamic>> _getUpdates() {
|
|
return isar.updates
|
|
.filter()
|
|
.idIsNotNull()
|
|
.findAllSync()
|
|
.map((e) => e.toJson())
|
|
.toList();
|
|
}
|
|
|
|
String _getAccessToken() {
|
|
final syncPrefs = ref.watch(synchingProvider(syncId: syncId));
|
|
return syncPrefs.authToken ?? "";
|
|
}
|
|
|
|
String _getServer() {
|
|
final syncPrefs = ref.watch(synchingProvider(syncId: syncId));
|
|
return syncPrefs.server ?? "";
|
|
}
|
|
}
|