import 'dart:convert'; import 'package:flutter_riverpod/legacy.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/settings.dart'; class SettingsNotifier extends StateNotifier { SettingsNotifier() : super(const AppSettings()) { _load(); } Future _load() async { final prefs = await SharedPreferences.getInstance(); final themeJson = prefs.getString('themeProfile'); final profilesJson = prefs.getString('themeProfiles'); final loadedProfiles = profilesJson == null ? [ themeJson == null ? const ThemeProfile() : ThemeProfile.fromJson( Map.from(jsonDecode(themeJson) as Map), ), ] : (jsonDecode(profilesJson) as List) .map( (item) => ThemeProfile.fromJson( Map.from(item as Map), ), ) .toList(); final profiles = _uniqueProfiles(loadedProfiles); final appearanceJson = prefs.getString('appAppearanceProfiles'); final loadedAppearances = appearanceJson == null ? const [] : (jsonDecode(appearanceJson) as List) .map( (item) => AppAppearanceProfile.fromJson( Map.from(item as Map), ), ) .toList(); final appearances = _mergeAppearanceProfiles(loadedAppearances); final selectedAppearance = prefs.getString('selectedAppAppearanceProfileName') ?? 'Basic'; state = AppSettings( languageCode: prefs.getString('languageCode') ?? 'nl', homeDirectory: prefs.getString('homeDirectory'), exportDirectory: prefs.getString('exportDirectory'), themeProfiles: profiles.isEmpty ? const [ThemeProfile()] : profiles, selectedThemeProfileName: prefs.getString('selectedThemeProfileName') ?? profiles.first.name, appAppearanceProfiles: appearances, selectedAppAppearanceProfileName: appearances.any((profile) => profile.name == selectedAppearance) ? selectedAppearance : 'Basic', recentFiles: prefs.getStringList('recentFiles') ?? [], uiTextScale: (prefs.getDouble('uiTextScale') ?? 1.0).clamp(1.0, 2.0), ); } Future setUiTextScale(double scale) async { final clamped = scale.clamp(1.0, 2.0).toDouble(); state = state.copyWith(uiTextScale: clamped); final prefs = await SharedPreferences.getInstance(); await prefs.setDouble('uiTextScale', clamped); } Future addRecentFile(String path) async { final updated = [ path, ...state.recentFiles.where((f) => f != path), ].take(10).toList(); state = state.copyWith(recentFiles: updated); final prefs = await SharedPreferences.getInstance(); await prefs.setStringList('recentFiles', updated); } Future setLanguageCode(String code) async { state = state.copyWith(languageCode: code); final prefs = await SharedPreferences.getInstance(); await prefs.setString('languageCode', code); } Future setHomeDirectory(String? path) async { state = path == null ? state.copyWith(clearHomeDirectory: true) : state.copyWith(homeDirectory: path); final prefs = await SharedPreferences.getInstance(); if (path == null) { await prefs.remove('homeDirectory'); } else { await prefs.setString('homeDirectory', path); } } Future setExportDirectory(String? path) async { state = path == null ? state.copyWith(clearExportDirectory: true) : state.copyWith(exportDirectory: path); final prefs = await SharedPreferences.getInstance(); if (path == null) { await prefs.remove('exportDirectory'); } else { await prefs.setString('exportDirectory', path); } } /// Persist edits to the profile currently identified by [previousName], /// renaming it in place when the name changed. When no profile matches /// [previousName] (e.g. a freshly created one) the profile is added. The /// edited profile is selected afterwards. Future saveThemeProfile( ThemeProfile profile, { required String previousName, }) async { final profileName = _uniqueName(profile.name, exceptName: previousName); final renamed = profile.copyWith(name: profileName); final exists = state.themeProfiles.any((p) => p.name == previousName); final profiles = exists ? [ for (final p in state.themeProfiles) if (p.name == previousName) renamed else p, ] : [...state.themeProfiles, renamed]; state = state.copyWith( themeProfiles: profiles, selectedThemeProfileName: profileName, ); await _saveProfiles(); } Future selectThemeProfile(String name) async { state = state.copyWith(selectedThemeProfileName: name); final prefs = await SharedPreferences.getInstance(); await prefs.setString('selectedThemeProfileName', name); } /// Create a brand-new profile (optionally based on [base]), add it to the /// list, select it and persist. Returns the created profile (its name may /// have been made unique). Future createThemeProfile({ThemeProfile? base}) async { final source = base ?? state.themeProfile; final name = _uniqueName('Nieuw profiel'); final created = source.copyWith(name: name); state = state.copyWith( themeProfiles: [...state.themeProfiles, created], selectedThemeProfileName: name, ); await _saveProfiles(); return created; } Future deleteThemeProfile(String name) async { if (state.themeProfiles.length <= 1) return; final profiles = state.themeProfiles.where((p) => p.name != name).toList(); state = state.copyWith( themeProfiles: profiles, selectedThemeProfileName: profiles.first.name, ); await _saveProfiles(); } Future selectAppAppearanceProfile(String name) async { if (!state.appAppearanceProfiles.any((profile) => profile.name == name)) { return; } state = state.copyWith(selectedAppAppearanceProfileName: name); final prefs = await SharedPreferences.getInstance(); await prefs.setString('selectedAppAppearanceProfileName', name); } Future createAppAppearanceProfile({ AppAppearanceProfile? base, }) async { final source = base ?? state.appAppearanceProfile; final created = source.copyWith( name: _uniqueAppearanceName('Eigen thema'), isBuiltIn: false, ); state = state.copyWith( appAppearanceProfiles: [...state.appAppearanceProfiles, created], selectedAppAppearanceProfileName: created.name, ); await _saveAppearanceProfiles(); return created; } Future saveAppAppearanceProfile( AppAppearanceProfile profile, { required String previousName, }) async { final existing = state.appAppearanceProfiles.firstWhere( (item) => item.name == previousName, orElse: () => profile, ); if (existing.isBuiltIn) return; final name = _uniqueAppearanceName(profile.name, exceptName: previousName); final saved = profile.copyWith(name: name, isBuiltIn: false); final profiles = [ for (final item in state.appAppearanceProfiles) if (item.name == previousName) saved else item, ]; state = state.copyWith( appAppearanceProfiles: profiles, selectedAppAppearanceProfileName: name, ); await _saveAppearanceProfiles(); } Future deleteAppAppearanceProfile(String name) async { final profile = state.appAppearanceProfiles.firstWhere( (item) => item.name == name, orElse: () => AppAppearanceProfile.basic, ); if (profile.isBuiltIn) return; final profiles = state.appAppearanceProfiles .where((item) => item.name != name) .toList(); state = state.copyWith( appAppearanceProfiles: profiles, selectedAppAppearanceProfileName: 'Basic', ); await _saveAppearanceProfiles(); } Future _saveAppearanceProfiles() async { final prefs = await SharedPreferences.getInstance(); final customProfiles = state.appAppearanceProfiles .where((profile) => !profile.isBuiltIn) .map((profile) => profile.toJson()) .toList(); await prefs.setString('appAppearanceProfiles', jsonEncode(customProfiles)); await prefs.setString( 'selectedAppAppearanceProfileName', state.selectedAppAppearanceProfileName, ); } Future _saveProfiles() async { state = state.copyWith(themeProfiles: _uniqueProfiles(state.themeProfiles)); final prefs = await SharedPreferences.getInstance(); await prefs.setString( 'themeProfiles', jsonEncode(state.themeProfiles.map((p) => p.toJson()).toList()), ); await prefs.setString( 'selectedThemeProfileName', state.selectedThemeProfileName, ); await prefs.setString( 'themeProfile', jsonEncode(state.themeProfile.toJson()), ); } List _uniqueProfiles(List profiles) { final result = []; for (final profile in profiles) { result.add( profile.copyWith(name: _uniqueName(profile.name, profiles: result)), ); } return result.isEmpty ? const [ThemeProfile()] : result; } String _uniqueName( String rawName, { List? profiles, String? exceptName, }) { final existingProfiles = profiles ?? state.themeProfiles; final base = rawName.trim().isEmpty ? 'Stijlprofiel' : rawName.trim(); final used = existingProfiles .map((p) => p.name) .where((name) => name != exceptName) .toSet(); if (!used.contains(base)) return base; var index = 2; while (used.contains('$base $index')) { index++; } return '$base $index'; } List _mergeAppearanceProfiles( List loaded, ) { final result = [...AppAppearanceProfile.builtIns]; for (final profile in loaded.where((profile) => !profile.isBuiltIn)) { result.add( profile.copyWith( name: _uniqueAppearanceName(profile.name, profiles: result), isBuiltIn: false, ), ); } return result; } String _uniqueAppearanceName( String rawName, { List? profiles, String? exceptName, }) { final existingProfiles = profiles ?? state.appAppearanceProfiles; final base = rawName.trim().isEmpty ? 'Eigen thema' : rawName.trim(); final used = existingProfiles .map((profile) => profile.name) .where((name) => name != exceptName) .toSet(); if (!used.contains(base)) return base; var index = 2; while (used.contains('$base $index')) { index++; } return '$base $index'; } } final settingsProvider = StateNotifierProvider( (_) => SettingsNotifier(), );