import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/init.dart'; import 'package:venera/utils/io.dart'; class Appdata with Init { Appdata._create(); final Settings settings = Settings._create(); var searchHistory = []; bool _isSavingData = false; Future saveData([bool sync = true]) async { while (_isSavingData) { await Future.delayed(const Duration(milliseconds: 20)); } _isSavingData = true; try { var data = jsonEncode(toJson()); var file = File(FilePath.join(App.dataPath, 'appdata.json')); await file.writeAsString(data); } finally { _isSavingData = false; } if (sync) { DataSync().uploadData(); } } void addSearchHistory(String keyword) { if (searchHistory.contains(keyword)) { searchHistory.remove(keyword); } searchHistory.insert(0, keyword); if (searchHistory.length > 50) { searchHistory.removeLast(); } saveData(); } void removeSearchHistory(String keyword) { searchHistory.remove(keyword); saveData(); } void clearSearchHistory() { searchHistory.clear(); saveData(); } Map toJson() { return { 'settings': settings._data, 'searchHistory': searchHistory, }; } /// Following fields are related to device-specific data and should not be synced. static const _disableSync = [ "proxy", "authorizationRequired", "customImageProcessing", "webdav", ]; /// Sync data from another device void syncData(Map data) { if (data['settings'] is Map) { var settings = data['settings'] as Map; for (var key in settings.keys) { if (!_disableSync.contains(key)) { this.settings[key] = settings[key]; } } } searchHistory = List.from(data['searchHistory'] ?? []); saveData(); } var implicitData = {}; void writeImplicitData() async { while (_isSavingData) { await Future.delayed(const Duration(milliseconds: 20)); } _isSavingData = true; try { var file = File(FilePath.join(App.dataPath, 'implicitData.json')); await file.writeAsString(jsonEncode(implicitData)); } finally { _isSavingData = false; } } @override Future doInit() async { var dataPath = (await getApplicationSupportDirectory()).path; var file = File(FilePath.join( dataPath, 'appdata.json', )); if (!await file.exists()) { return; } try { var json = jsonDecode(await file.readAsString()); for (var key in (json['settings'] as Map).keys) { if (json['settings'][key] != null) { settings[key] = json['settings'][key]; } } searchHistory = List.from(json['searchHistory']); } catch(e) { Log.error("Appdata", "Failed to load appdata", e); Log.info("Appdata", "Resetting appdata"); file.deleteIgnoreError(); } try { var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); if (await implicitDataFile.exists()) { implicitData = jsonDecode(await implicitDataFile.readAsString()); } } catch (e) { Log.error("Appdata", "Failed to load implicit data", e); Log.info("Appdata", "Resetting implicit data"); var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); implicitDataFile.deleteIgnoreError(); } } } final appdata = Appdata._create(); class Settings with ChangeNotifier { Settings._create(); final _data = { 'comicDisplayMode': 'detailed', // detailed, brief 'comicTileScale': 1.00, // 0.75-1.25 'color': 'system', // red, pink, purple, green, orange, blue 'theme_mode': 'system', // light, dark, system 'newFavoriteAddTo': 'end', // start, end 'moveFavoriteAfterRead': 'none', // none, end, start 'proxy': 'system', // direct, system, proxy string 'explore_pages': [], 'categories': [], 'favorites': [], 'searchSources': null, 'showFavoriteStatusOnTile': true, 'showHistoryStatusOnTile': false, 'blockedWords': [], 'defaultSearchTarget': null, 'autoPageTurningInterval': 5, // in seconds 'readerMode': 'galleryLeftToRight', // values of [ReaderMode] 'readerScreenPicNumberForLandscape': 1, // 1 - 5 'readerScreenPicNumberForPortrait': 1, // 1 - 5 'enableTapToTurnPages': true, 'reverseTapToTurnPages': false, 'enablePageAnimation': true, 'language': 'system', // system, zh-CN, zh-TW, en-US 'cacheSize': 2048, // in MB 'downloadThreads': 5, 'enableLongPressToZoom': true, 'longPressZoomPosition': "press", // press, center 'checkUpdateOnStart': false, 'limitImageWidth': true, 'webdav': [], // empty means not configured 'dataVersion': 0, 'quickFavorite': null, 'enableTurnPageByVolumeKey': true, 'enableClockAndBatteryInfoInReader': true, 'quickCollectImage': 'No', // No, DoubleTap, Swipe 'authorizationRequired': false, 'onClickFavorite': 'viewDetail', // viewDetail, read 'enableDnsOverrides': false, 'dnsOverrides': {}, 'enableCustomImageProcessing': false, 'customImageProcessing': defaultCustomImageProcessing, 'sni': true, 'autoAddLanguageFilter': 'none', // none, chinese, english, japanese 'comicSourceListUrl': '', 'preloadImageCount': 4, 'followUpdatesFolder': null, 'initialPage': '0', 'comicListDisplayMode': 'paging', // paging, continuous 'showPageNumberInReader': true, 'showSingleImageOnFirstPage': false, 'enableDoubleTapToZoom': true, 'reverseChapterOrder': false, 'showSystemStatusBar': false, }; operator [](String key) { return _data[key]; } operator []=(String key, dynamic value) { _data[key] = value; if (key != "dataVersion") { notifyListeners(); } } @override String toString() { return _data.toString(); } } const defaultCustomImageProcessing = ''' /** * Process an image * @param image {ArrayBuffer} - The image to process * @param cid {string} - The comic ID * @param eid {string} - The episode ID * @param page {number} - The page number * @param sourceKey {string} - The source key * @returns {Promise | {image: Promise, onCancel: () => void}} - The processed image */ function processImage(image, cid, eid, page, sourceKey) { let futureImage = new Promise((resolve, reject) => { resolve(image); }); return futureImage; } ''';