import 'dart:convert'; import 'package:isar/isar.dart'; import '../../core/database/app_database.dart'; import '../../core/database/database_service.dart'; import '../../core/errors/failure.dart'; import '../../core/utils/result.dart'; import '../models/content_provider_config.dart'; import '../models/pawchive_account.dart'; import '../models/provider_diagnostics.dart'; import '../repositories/provider_repository.dart'; class AppSettings { const AppSettings({ required this.enabledProviderIds, required this.nsfwEnabled, required this.cacheTtlHours, required this.cacheMaxItems, required this.providerPriority, required this.themeMode, required this.languageCode, required this.appSeedColor, required this.hiddenTabs, required this.allowExperimentalUpdates, required this.desktopColumns, required this.mobileColumns, required this.blurExplicitContent, required this.allowDownloads, required this.autoDownloadFavorites, required this.selectedFeedProviderIds, required this.showPostBadges, required this.defaultTopPeriodFilter, required this.blacklistedTags, required this.whitelistedTags, required this.smartBlacklistRules, required this.hideViewedPosts, required this.mediaQualityMode, required this.motionRefreshMode, required this.autoBatterySaver60Hz, required this.videoPlayerMuted, this.videoPlayerVolume = 100.0, required this.videoPlayerHalfVolume, required this.videoPlayerLoop, required this.videoPlayerCover, required this.videoPlaybackPositions, required this.hiddenPostKeys, required this.diagnosticLogLines, required this.lastFeedTags, required this.lastFeedProviderIds, required this.lastFeedTopPeriod, required this.lastFeedScrollOffset, this.favoriteArtists = const [], this.pawchiveAccounts = const [], this.pawchiveBidirectionalSync = false, this.skippedUpdateVersion, this.lastUpdateCheckAt, this.defaultRatingFilter, this.lastFeedRating, this.amoledMode = false, this.useDynamicColor = false, this.gridMode = 'masonry', this.searchPresets = const [], this.downloadPathTemplate = '{Artist}/{ID}', this.searchHistoryLimit = 500, this.tagCacheLimit = 5000, this.lastActiveLocation = '/', }); final String lastActiveLocation; final List favoriteArtists; final List pawchiveAccounts; final bool pawchiveBidirectionalSync; final List enabledProviderIds; final bool nsfwEnabled; final int cacheTtlHours; final int cacheMaxItems; final Map providerPriority; final String themeMode; final String languageCode; final int appSeedColor; final List hiddenTabs; final bool allowExperimentalUpdates; final int desktopColumns; final int mobileColumns; final bool blurExplicitContent; final bool allowDownloads; final bool autoDownloadFavorites; final List selectedFeedProviderIds; final bool showPostBadges; final String defaultTopPeriodFilter; final List blacklistedTags; final List whitelistedTags; final List smartBlacklistRules; final bool hideViewedPosts; final String mediaQualityMode; final String motionRefreshMode; final bool autoBatterySaver60Hz; final bool videoPlayerMuted; final double videoPlayerVolume; final bool videoPlayerHalfVolume; final bool videoPlayerLoop; final bool videoPlayerCover; final Map videoPlaybackPositions; final List hiddenPostKeys; final List diagnosticLogLines; final List lastFeedTags; final List lastFeedProviderIds; final String lastFeedTopPeriod; final double lastFeedScrollOffset; final String? skippedUpdateVersion; final String? lastUpdateCheckAt; final String? defaultRatingFilter; final String? lastFeedRating; final bool amoledMode; final bool useDynamicColor; final String gridMode; final List searchPresets; final String downloadPathTemplate; final int searchHistoryLimit; final int tagCacheLimit; static const defaults = AppSettings( enabledProviderIds: ['gelbooru', 'rule34', 'realbooru'], nsfwEnabled: true, cacheTtlHours: 24, cacheMaxItems: 2000, providerPriority: {'gelbooru': 0, 'rule34': 1, 'realbooru': 2}, favoriteArtists: [], pawchiveAccounts: [], pawchiveBidirectionalSync: false, themeMode: 'dark', languageCode: 'ru', appSeedColor: 0xFFE84D8A, hiddenTabs: [], allowExperimentalUpdates: false, desktopColumns: 5, mobileColumns: 2, blurExplicitContent: true, allowDownloads: true, autoDownloadFavorites: true, selectedFeedProviderIds: [], showPostBadges: true, defaultTopPeriodFilter: 'none', blacklistedTags: [], whitelistedTags: [], smartBlacklistRules: [], hideViewedPosts: false, mediaQualityMode: 'auto', motionRefreshMode: 'auto', autoBatterySaver60Hz: true, videoPlayerMuted: false, videoPlayerVolume: 100.0, videoPlayerHalfVolume: false, videoPlayerLoop: false, videoPlayerCover: false, videoPlaybackPositions: {}, hiddenPostKeys: [], diagnosticLogLines: [], lastFeedTags: [], lastFeedProviderIds: [], lastFeedTopPeriod: 'none', lastFeedScrollOffset: 0, skippedUpdateVersion: null, lastUpdateCheckAt: null, defaultRatingFilter: null, lastFeedRating: null, searchHistoryLimit: 500, tagCacheLimit: 5000, lastActiveLocation: '/', ); AppSettings copyWith({ List? enabledProviderIds, bool? nsfwEnabled, int? cacheTtlHours, int? cacheMaxItems, Map? providerPriority, String? themeMode, String? languageCode, int? appSeedColor, List? hiddenTabs, bool? allowExperimentalUpdates, int? desktopColumns, int? mobileColumns, bool? blurExplicitContent, bool? allowDownloads, bool? autoDownloadFavorites, List? selectedFeedProviderIds, bool? showPostBadges, String? defaultTopPeriodFilter, List? blacklistedTags, List? whitelistedTags, List? smartBlacklistRules, bool? hideViewedPosts, String? mediaQualityMode, String? motionRefreshMode, bool? autoBatterySaver60Hz, bool? videoPlayerMuted, double? videoPlayerVolume, bool? videoPlayerHalfVolume, bool? videoPlayerLoop, bool? videoPlayerCover, Map? videoPlaybackPositions, List? hiddenPostKeys, List? diagnosticLogLines, List? lastFeedTags, List? lastFeedProviderIds, String? lastFeedTopPeriod, double? lastFeedScrollOffset, String? skippedUpdateVersion, String? lastUpdateCheckAt, String? defaultRatingFilter, String? lastFeedRating, bool clearLastFeedRating = false, List? favoriteArtists, List? pawchiveAccounts, bool? pawchiveBidirectionalSync, bool? amoledMode, bool? useDynamicColor, String? gridMode, List? searchPresets, String? downloadPathTemplate, int? searchHistoryLimit, int? tagCacheLimit, String? lastActiveLocation, }) { return AppSettings( lastActiveLocation: lastActiveLocation ?? this.lastActiveLocation, favoriteArtists: favoriteArtists ?? this.favoriteArtists, pawchiveAccounts: pawchiveAccounts ?? this.pawchiveAccounts, pawchiveBidirectionalSync: pawchiveBidirectionalSync ?? this.pawchiveBidirectionalSync, enabledProviderIds: enabledProviderIds ?? this.enabledProviderIds, nsfwEnabled: nsfwEnabled ?? this.nsfwEnabled, cacheTtlHours: cacheTtlHours ?? this.cacheTtlHours, cacheMaxItems: cacheMaxItems ?? this.cacheMaxItems, providerPriority: providerPriority ?? this.providerPriority, themeMode: themeMode ?? this.themeMode, languageCode: languageCode ?? this.languageCode, appSeedColor: appSeedColor ?? this.appSeedColor, hiddenTabs: hiddenTabs ?? this.hiddenTabs, allowExperimentalUpdates: allowExperimentalUpdates ?? this.allowExperimentalUpdates, desktopColumns: desktopColumns ?? this.desktopColumns, mobileColumns: mobileColumns ?? this.mobileColumns, blurExplicitContent: blurExplicitContent ?? this.blurExplicitContent, allowDownloads: allowDownloads ?? this.allowDownloads, autoDownloadFavorites: autoDownloadFavorites ?? this.autoDownloadFavorites, selectedFeedProviderIds: selectedFeedProviderIds ?? this.selectedFeedProviderIds, showPostBadges: showPostBadges ?? this.showPostBadges, defaultTopPeriodFilter: defaultTopPeriodFilter ?? this.defaultTopPeriodFilter, blacklistedTags: blacklistedTags ?? this.blacklistedTags, whitelistedTags: whitelistedTags ?? this.whitelistedTags, smartBlacklistRules: smartBlacklistRules ?? this.smartBlacklistRules, hideViewedPosts: hideViewedPosts ?? this.hideViewedPosts, mediaQualityMode: mediaQualityMode ?? this.mediaQualityMode, motionRefreshMode: motionRefreshMode ?? this.motionRefreshMode, autoBatterySaver60Hz: autoBatterySaver60Hz ?? this.autoBatterySaver60Hz, videoPlayerMuted: videoPlayerMuted ?? this.videoPlayerMuted, videoPlayerVolume: videoPlayerVolume ?? this.videoPlayerVolume, videoPlayerHalfVolume: videoPlayerHalfVolume ?? this.videoPlayerHalfVolume, videoPlayerLoop: videoPlayerLoop ?? this.videoPlayerLoop, videoPlayerCover: videoPlayerCover ?? this.videoPlayerCover, videoPlaybackPositions: videoPlaybackPositions ?? this.videoPlaybackPositions, hiddenPostKeys: hiddenPostKeys ?? this.hiddenPostKeys, diagnosticLogLines: diagnosticLogLines ?? this.diagnosticLogLines, lastFeedTags: lastFeedTags ?? this.lastFeedTags, lastFeedProviderIds: lastFeedProviderIds ?? this.lastFeedProviderIds, lastFeedTopPeriod: lastFeedTopPeriod ?? this.lastFeedTopPeriod, lastFeedScrollOffset: lastFeedScrollOffset ?? this.lastFeedScrollOffset, skippedUpdateVersion: skippedUpdateVersion ?? this.skippedUpdateVersion, lastUpdateCheckAt: lastUpdateCheckAt ?? this.lastUpdateCheckAt, defaultRatingFilter: defaultRatingFilter ?? this.defaultRatingFilter, lastFeedRating: clearLastFeedRating ? null : lastFeedRating ?? this.lastFeedRating, amoledMode: amoledMode ?? this.amoledMode, useDynamicColor: useDynamicColor ?? this.useDynamicColor, gridMode: gridMode ?? this.gridMode, searchPresets: searchPresets ?? this.searchPresets, downloadPathTemplate: downloadPathTemplate ?? this.downloadPathTemplate, searchHistoryLimit: searchHistoryLimit ?? this.searchHistoryLimit, tagCacheLimit: tagCacheLimit ?? this.tagCacheLimit, ); } Map toJson() => { 'favoriteArtists': favoriteArtists, 'pawchiveAccounts': pawchiveAccounts, 'pawchiveBidirectionalSync': pawchiveBidirectionalSync, 'enabledProviderIds': enabledProviderIds, 'nsfwEnabled': nsfwEnabled, 'cacheTtlHours': cacheTtlHours, 'cacheMaxItems': cacheMaxItems, 'providerPriority': providerPriority, 'themeMode': themeMode, 'languageCode': languageCode, 'appSeedColor': appSeedColor, 'hiddenTabs': hiddenTabs, 'allowExperimentalUpdates': allowExperimentalUpdates, 'desktopColumns': desktopColumns, 'mobileColumns': mobileColumns, 'blurExplicitContent': blurExplicitContent, 'allowDownloads': allowDownloads, 'autoDownloadFavorites': autoDownloadFavorites, 'selectedFeedProviderIds': selectedFeedProviderIds, 'showPostBadges': showPostBadges, 'defaultTopPeriodFilter': defaultTopPeriodFilter, 'blacklistedTags': blacklistedTags, 'whitelistedTags': whitelistedTags, 'smartBlacklistRules': smartBlacklistRules, 'hideViewedPosts': hideViewedPosts, 'mediaQualityMode': mediaQualityMode, 'motionRefreshMode': motionRefreshMode, 'autoBatterySaver60Hz': autoBatterySaver60Hz, 'videoPlayerMuted': videoPlayerMuted, 'videoPlayerVolume': videoPlayerVolume, 'videoPlayerHalfVolume': videoPlayerHalfVolume, 'videoPlayerLoop': videoPlayerLoop, 'videoPlayerCover': videoPlayerCover, 'videoPlaybackPositions': videoPlaybackPositions, 'hiddenPostKeys': hiddenPostKeys, 'diagnosticLogLines': diagnosticLogLines, 'lastFeedTags': lastFeedTags, 'lastFeedProviderIds': lastFeedProviderIds, 'lastFeedTopPeriod': lastFeedTopPeriod, 'lastFeedScrollOffset': lastFeedScrollOffset, 'skippedUpdateVersion': skippedUpdateVersion, 'lastUpdateCheckAt': lastUpdateCheckAt, 'defaultRatingFilter': defaultRatingFilter, 'lastFeedRating': lastFeedRating, 'amoledMode': amoledMode, 'useDynamicColor': useDynamicColor, 'gridMode': gridMode, 'searchPresets': searchPresets, 'downloadPathTemplate': downloadPathTemplate, 'searchHistoryLimit': searchHistoryLimit, 'tagCacheLimit': tagCacheLimit, 'lastActiveLocation': lastActiveLocation, }; factory AppSettings.fromJson(Map json) => AppSettings( enabledProviderIds: List.from( (json['enabledProviderIds'] as List?) ?? defaults.enabledProviderIds, ), nsfwEnabled: (json['nsfwEnabled'] as bool?) ?? defaults.nsfwEnabled, cacheTtlHours: (json['cacheTtlHours'] as num?)?.toInt() ?? defaults.cacheTtlHours, cacheMaxItems: (json['cacheMaxItems'] as num?)?.toInt() ?? defaults.cacheMaxItems, providerPriority: Map.from( (json['providerPriority'] as Map?) ?? defaults.providerPriority, ), themeMode: (json['themeMode'] as String?) ?? defaults.themeMode, languageCode: (json['languageCode'] as String?) ?? defaults.languageCode, appSeedColor: (json['appSeedColor'] as num?)?.toInt() ?? defaults.appSeedColor, hiddenTabs: List.from( (json['hiddenTabs'] as List?) ?? defaults.hiddenTabs), allowExperimentalUpdates: (json['allowExperimentalUpdates'] as bool?) ?? defaults.allowExperimentalUpdates, desktopColumns: (json['desktopColumns'] as num?)?.toInt() ?? defaults.desktopColumns, mobileColumns: (json['mobileColumns'] as num?)?.toInt() ?? defaults.mobileColumns, blurExplicitContent: (json['blurExplicitContent'] as bool?) ?? defaults.blurExplicitContent, allowDownloads: (json['allowDownloads'] as bool?) ?? defaults.allowDownloads, autoDownloadFavorites: (json['autoDownloadFavorites'] as bool?) ?? defaults.autoDownloadFavorites, selectedFeedProviderIds: List.from( (json['selectedFeedProviderIds'] as List?) ?? defaults.selectedFeedProviderIds, ), showPostBadges: (json['showPostBadges'] as bool?) ?? defaults.showPostBadges, defaultTopPeriodFilter: (json['defaultTopPeriodFilter'] as String?) ?? defaults.defaultTopPeriodFilter, blacklistedTags: List.from( (json['blacklistedTags'] as List?) ?? defaults.blacklistedTags, ), whitelistedTags: List.from( (json['whitelistedTags'] as List?) ?? defaults.whitelistedTags, ), smartBlacklistRules: List.from( (json['smartBlacklistRules'] as List?) ?? (json['blacklistedTags'] as List?) ?? defaults.smartBlacklistRules, ), hideViewedPosts: (json['hideViewedPosts'] as bool?) ?? defaults.hideViewedPosts, mediaQualityMode: (json['mediaQualityMode'] as String?) ?? defaults.mediaQualityMode, motionRefreshMode: (json['motionRefreshMode'] as String?) ?? defaults.motionRefreshMode, autoBatterySaver60Hz: (json['autoBatterySaver60Hz'] as bool?) ?? defaults.autoBatterySaver60Hz, videoPlayerMuted: (json['videoPlayerMuted'] as bool?) ?? defaults.videoPlayerMuted, videoPlayerVolume: (json['videoPlayerVolume'] as num?)?.toDouble() ?? defaults.videoPlayerVolume, videoPlayerHalfVolume: (json['videoPlayerHalfVolume'] as bool?) ?? defaults.videoPlayerHalfVolume, videoPlayerLoop: (json['videoPlayerLoop'] as bool?) ?? defaults.videoPlayerLoop, videoPlayerCover: (json['videoPlayerCover'] as bool?) ?? defaults.videoPlayerCover, videoPlaybackPositions: Map.from( (json['videoPlaybackPositions'] as Map?) ?? defaults.videoPlaybackPositions, ), hiddenPostKeys: List.from( (json['hiddenPostKeys'] as List?) ?? defaults.hiddenPostKeys, ), diagnosticLogLines: List.from( (json['diagnosticLogLines'] as List?) ?? defaults.diagnosticLogLines, ), lastFeedTags: List.from( (json['lastFeedTags'] as List?) ?? defaults.lastFeedTags, ), lastFeedProviderIds: List.from( (json['lastFeedProviderIds'] as List?) ?? (json['selectedFeedProviderIds'] as List?) ?? defaults.lastFeedProviderIds, ), lastFeedTopPeriod: (json['lastFeedTopPeriod'] as String?) ?? (json['defaultTopPeriodFilter'] as String?) ?? defaults.lastFeedTopPeriod, lastFeedScrollOffset: (json['lastFeedScrollOffset'] as num?)?.toDouble() ?? defaults.lastFeedScrollOffset, favoriteArtists: List.from( (json['favoriteArtists'] as List?) ?? defaults.favoriteArtists, ), pawchiveAccounts: List.from( (json['pawchiveAccounts'] as List?) ?? defaults.pawchiveAccounts, ), pawchiveBidirectionalSync: (json['pawchiveBidirectionalSync'] as bool?) ?? defaults.pawchiveBidirectionalSync, skippedUpdateVersion: json['skippedUpdateVersion'] as String?, lastUpdateCheckAt: json['lastUpdateCheckAt'] as String?, defaultRatingFilter: json['defaultRatingFilter'] as String?, lastFeedRating: json['lastFeedRating'] as String?, amoledMode: (json['amoledMode'] as bool?) ?? false, useDynamicColor: (json['useDynamicColor'] as bool?) ?? false, gridMode: (json['gridMode'] as String?) ?? 'masonry', searchPresets: List.from( (json['searchPresets'] as List?) ?? const [], ), downloadPathTemplate: (json['downloadPathTemplate'] as String?) ?? '{Artist}/{ID}', searchHistoryLimit: (json['searchHistoryLimit'] as num?)?.toInt() ?? defaults.searchHistoryLimit, tagCacheLimit: (json['tagCacheLimit'] as num?)?.toInt() ?? defaults.tagCacheLimit, lastActiveLocation: (json['lastActiveLocation'] as String?) ?? defaults.lastActiveLocation, ); List get parsedPawchiveAccounts { final list = []; for (final raw in pawchiveAccounts) { try { final decoded = jsonDecode(raw); if (decoded is Map) { list.add(PawchiveAccount.fromJson(decoded)); } } catch (_) {} } return list; } PawchiveAccount? get activePawchiveAccount { final accounts = parsedPawchiveAccounts; if (accounts.isEmpty) return null; return accounts.firstWhere((a) => a.isActive, orElse: () => accounts.first); } } class SettingsService { SettingsService( this._databaseService, { ProviderRepository? providerRepository, }) : _providerRepository = providerRepository; final DatabaseService _databaseService; final ProviderRepository? _providerRepository; static const _settingsKey = 'app_settings'; Future> getSettings() { return _databaseService.safeRead((isar) async { final entity = await isar.appSettingEntitys .filter() .keyEqualTo(_settingsKey) .findFirst(); if (entity == null) return AppSettings.defaults; return AppSettings.fromJson( jsonDecode(entity.jsonValue) as Map); }); } Future> updateSettings(AppSettings settings) { return _databaseService.safeWrite((isar) async { await isar.appSettingEntitys.put( AppSettingEntity() ..key = _settingsKey ..jsonValue = jsonEncode(settings.toJson()) ..updatedAt = DateTime.now(), ); }); } Future> saveEnabledProviders(List providerIds) async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; final updateResult = await updateSettings( settings.copyWith(enabledProviderIds: providerIds)); if (updateResult is Error) return updateResult; final repository = _providerRepository; if (repository == null) return updateResult; final providersResult = await repository.getProviders(); if (providersResult is Error>) { return Error(providersResult.failure); } final enabled = providerIds.toSet(); for (final provider in (providersResult as Success>).data) { await repository.saveProvider( provider.copyWith( enabled: enabled.contains(provider.id), updatedAt: DateTime.now(), ), ); } return updateResult; } Future> saveNsfwFilter(bool enabled) async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; return updateSettings(settings.copyWith(nsfwEnabled: enabled)); } Future> saveLastActiveLocation(String location) async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; if (settings.lastActiveLocation == location) return const Success(null); return updateSettings(settings.copyWith(lastActiveLocation: location)); } Future> saveCacheSettings({ required int ttlHours, required int maxItems, }) async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; return updateSettings( settings.copyWith(cacheTtlHours: ttlHours, cacheMaxItems: maxItems), ); } Future> hidePostKey(String cacheKey) async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; final hidden = {...settings.hiddenPostKeys, cacheKey}.toList() ..sort(); return updateSettings(settings.copyWith(hiddenPostKeys: hidden)); } Future> unhidePostKey(String cacheKey) async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; return updateSettings( settings.copyWith( hiddenPostKeys: settings.hiddenPostKeys.where((key) => key != cacheKey).toList(), ), ); } Future> clearHiddenPosts() async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; return updateSettings(settings.copyWith(hiddenPostKeys: const [])); } Future> saveVideoPlaybackPosition( String cacheKey, int milliseconds, { int maxEntries = 300, }) async { if (cacheKey.isEmpty || milliseconds < 1000) return const Success(null); final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; final existing = settings.videoPlaybackPositions[cacheKey] ?? 0; if ((existing - milliseconds).abs() < 1000) return const Success(null); final positions = { ...settings.videoPlaybackPositions, cacheKey: milliseconds, }; if (positions.length > maxEntries) { final removeCount = positions.length - maxEntries; for (final key in positions.keys.take(removeCount).toList()) { positions.remove(key); } } return updateSettings(settings.copyWith(videoPlaybackPositions: positions)); } Future> setVideoPlayerVolume(double volume) async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; final clamped = volume.clamp(0.0, 100.0); if ((settings.videoPlayerVolume - clamped).abs() < 0.01) { return const Success(null); } return updateSettings(settings.copyWith(videoPlayerVolume: clamped)); } Future> appendDiagnosticLog(String message) async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; final line = '${DateTime.now().toIso8601String()} $message'; final lines = [...settings.diagnosticLogLines, line]; return updateSettings( settings.copyWith( diagnosticLogLines: lines.length > 200 ? lines.sublist(lines.length - 200) : lines, ), ); } Future> clearDiagnosticLogs() async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; return updateSettings(settings.copyWith(diagnosticLogLines: const [])); } Future> buildDiagnosticsReport({ required String appVersion, required int buildNumber, }) async { final settingsResult = await getSettings(); if (settingsResult is Error) { return Error(settingsResult.failure); } final settings = (settingsResult as Success).data; final providers = []; final diagnostics = []; final repository = _providerRepository; if (repository != null) { final providersResult = await repository.getProviders(enabledOnly: false); if (providersResult is Success>) { providers.addAll(providersResult.data); } final diagnosticsResult = await repository.getDiagnostics(); if (diagnosticsResult is Success>) { for (final item in diagnosticsResult.data) { diagnostics.add( '${item.providerId}: ${item.lastResultCount} posts, ' 'lastSearchAt=${item.lastSearchAt}, ' 'error=${item.lastErrorMessage ?? 'none'}', ); } } } final enabledProviders = providers.where((provider) => provider.enabled).map((p) => p.id); return Success( const JsonEncoder.withIndent(' ').convert({ 'app': { 'name': 'Prisma', 'version': appVersion, 'build': buildNumber, 'generatedAt': DateTime.now().toIso8601String(), }, 'settingsSummary': { 'themeMode': settings.themeMode, 'languageCode': settings.languageCode, 'appSeedColor': settings.appSeedColor, 'hiddenTabs': settings.hiddenTabs, 'allowExperimentalUpdates': settings.allowExperimentalUpdates, 'nsfwEnabled': settings.nsfwEnabled, 'blurExplicitContent': settings.blurExplicitContent, 'mediaQualityMode': settings.mediaQualityMode, 'motionRefreshMode': settings.motionRefreshMode, 'autoBatterySaver60Hz': settings.autoBatterySaver60Hz, 'cacheTtlHours': settings.cacheTtlHours, 'cacheMaxItems': settings.cacheMaxItems, 'hiddenPosts': settings.hiddenPostKeys.length, 'blacklistRules': settings.smartBlacklistRules.length, 'whitelistTags': settings.whitelistedTags.length, 'hideViewedPosts': settings.hideViewedPosts, }, 'providers': { 'enabled': enabledProviders.toList(), 'all': providers .map((provider) => { 'id': provider.id, 'name': provider.name, 'apiType': provider.apiType, 'enabled': provider.enabled, 'priority': provider.priority, 'baseUrl': provider.baseUrl, }) .toList(), }, 'recentLogs': settings.diagnosticLogLines.take(60).toList(), 'providerDiagnostics': diagnostics, }), ); } Future> exportSettingsToJson() async { final result = await getSettings(); if (result is Error) return Error(result.failure); final settings = (result as Success).data; final providers = []; final repository = _providerRepository; if (repository != null) { final providersResult = await repository.getProviders(enabledOnly: false); if (providersResult is Success>) { providers.addAll(providersResult.data); } } return Success( const JsonEncoder.withIndent(' ').convert({ 'schemaVersion': 2, 'settings': settings.toJson(), 'filters': { 'blacklistedTags': settings.blacklistedTags, 'whitelistedTags': settings.whitelistedTags, 'smartBlacklistRules': settings.smartBlacklistRules, 'hiddenPostKeys': settings.hiddenPostKeys, }, 'providers': providers.map((provider) => provider.toJson()).toList(), }), ); } Future> importSettingsFromJson(String json) async { try { final decoded = jsonDecode(json) as Map; final settingsJson = (decoded['settings'] as Map?)?.cast() ?? decoded; final filtersJson = (decoded['filters'] as Map?)?.cast() ?? const {}; for (final key in [ 'blacklistedTags', 'whitelistedTags', 'smartBlacklistRules', 'hiddenPostKeys', ]) { settingsJson.putIfAbsent(key, () => filtersJson[key]); } final settings = AppSettings.fromJson(settingsJson); final result = await updateSettings(settings); if (result is Error) return result; final repository = _providerRepository; final providerItems = decoded['providers']; if (repository != null && providerItems is List) { for (final item in providerItems.whereType()) { await repository.saveProvider( ContentProviderConfig.fromJson( Map.from(item), ).copyWith(updatedAt: DateTime.now()), ); } } await saveEnabledProviders(settings.enabledProviderIds); return result; } catch (error) { return Error( Failure( code: 'settings_import', message: 'Invalid settings JSON', details: error, ), ); } } }