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 '../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.desktopColumns, required this.mobileColumns, required this.blurExplicitContent, required this.allowDownloads, 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.lastFeedTags, required this.lastFeedProviderIds, required this.lastFeedTopPeriod, required this.lastFeedScrollOffset, this.skippedUpdateVersion, this.lastUpdateCheckAt, this.defaultRatingFilter, this.lastFeedRating, }); final List enabledProviderIds; final bool nsfwEnabled; final int cacheTtlHours; final int cacheMaxItems; final Map providerPriority; final String themeMode; final int desktopColumns; final int mobileColumns; final bool blurExplicitContent; final bool allowDownloads; 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 List lastFeedTags; final List lastFeedProviderIds; final String lastFeedTopPeriod; final double lastFeedScrollOffset; final String? skippedUpdateVersion; final String? lastUpdateCheckAt; final String? defaultRatingFilter; final String? lastFeedRating; static const defaults = AppSettings( enabledProviderIds: ['gelbooru', 'rule34', 'safebooru'], nsfwEnabled: true, cacheTtlHours: 24, cacheMaxItems: 2000, providerPriority: {'gelbooru': 0, 'rule34': 1, 'safebooru': 2}, themeMode: 'dark', desktopColumns: 5, mobileColumns: 2, blurExplicitContent: true, allowDownloads: true, selectedFeedProviderIds: [], showPostBadges: true, defaultTopPeriodFilter: 'none', blacklistedTags: [], whitelistedTags: [], smartBlacklistRules: [], hideViewedPosts: false, mediaQualityMode: 'auto', lastFeedTags: [], lastFeedProviderIds: [], lastFeedTopPeriod: 'none', lastFeedScrollOffset: 0, skippedUpdateVersion: null, lastUpdateCheckAt: null, defaultRatingFilter: null, lastFeedRating: null, ); AppSettings copyWith({ List? enabledProviderIds, bool? nsfwEnabled, int? cacheTtlHours, int? cacheMaxItems, Map? providerPriority, String? themeMode, int? desktopColumns, int? mobileColumns, bool? blurExplicitContent, bool? allowDownloads, List? selectedFeedProviderIds, bool? showPostBadges, String? defaultTopPeriodFilter, List? blacklistedTags, List? whitelistedTags, List? smartBlacklistRules, bool? hideViewedPosts, String? mediaQualityMode, List? lastFeedTags, List? lastFeedProviderIds, String? lastFeedTopPeriod, double? lastFeedScrollOffset, String? skippedUpdateVersion, String? lastUpdateCheckAt, String? defaultRatingFilter, String? lastFeedRating, bool clearLastFeedRating = false, }) { return AppSettings( enabledProviderIds: enabledProviderIds ?? this.enabledProviderIds, nsfwEnabled: nsfwEnabled ?? this.nsfwEnabled, cacheTtlHours: cacheTtlHours ?? this.cacheTtlHours, cacheMaxItems: cacheMaxItems ?? this.cacheMaxItems, providerPriority: providerPriority ?? this.providerPriority, themeMode: themeMode ?? this.themeMode, desktopColumns: desktopColumns ?? this.desktopColumns, mobileColumns: mobileColumns ?? this.mobileColumns, blurExplicitContent: blurExplicitContent ?? this.blurExplicitContent, allowDownloads: allowDownloads ?? this.allowDownloads, 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, 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, ); } Map toJson() => { 'enabledProviderIds': enabledProviderIds, 'nsfwEnabled': nsfwEnabled, 'cacheTtlHours': cacheTtlHours, 'cacheMaxItems': cacheMaxItems, 'providerPriority': providerPriority, 'themeMode': themeMode, 'desktopColumns': desktopColumns, 'mobileColumns': mobileColumns, 'blurExplicitContent': blurExplicitContent, 'allowDownloads': allowDownloads, 'selectedFeedProviderIds': selectedFeedProviderIds, 'showPostBadges': showPostBadges, 'defaultTopPeriodFilter': defaultTopPeriodFilter, 'blacklistedTags': blacklistedTags, 'whitelistedTags': whitelistedTags, 'smartBlacklistRules': smartBlacklistRules, 'hideViewedPosts': hideViewedPosts, 'mediaQualityMode': mediaQualityMode, 'lastFeedTags': lastFeedTags, 'lastFeedProviderIds': lastFeedProviderIds, 'lastFeedTopPeriod': lastFeedTopPeriod, 'lastFeedScrollOffset': lastFeedScrollOffset, 'skippedUpdateVersion': skippedUpdateVersion, 'lastUpdateCheckAt': lastUpdateCheckAt, 'defaultRatingFilter': defaultRatingFilter, 'lastFeedRating': lastFeedRating, }; 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, 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, 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, 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, skippedUpdateVersion: json['skippedUpdateVersion'] as String?, lastUpdateCheckAt: json['lastUpdateCheckAt'] as String?, defaultRatingFilter: json['defaultRatingFilter'] as String?, lastFeedRating: json['lastFeedRating'] as String?, ); } 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> 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> 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, }, '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', ]) { 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, ), ); } } }