// SPDX-FileCopyrightText: 2019-Present Christian Kußowski // SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat // // SPDX-License-Identifier: AGPL-3.0-or-later import 'dart:async'; import 'package:collection/collection.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:tatchat/config/setting_keys.dart'; import 'package:tatchat/utils/account_config.dart'; import 'package:tatchat/utils/color_value.dart'; import 'package:material_ui/material_ui.dart'; import 'package:matrix/matrix.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; class ThemeBuilder extends StatefulWidget { final Widget Function( BuildContext context, ThemeMode themeMode, Color? primaryColor, ThemeController controller, ) builder; final String themeModeSettingsKey; final String primaryColorSettingsKey; const ThemeBuilder({ required this.builder, this.themeModeSettingsKey = 'theme_mode', this.primaryColorSettingsKey = 'primary_color', super.key, }); @override State createState() => ThemeController(); } class ThemeController extends State { SharedPreferences? _sharedPreferences; ThemeMode? _themeMode; Color? _primaryColor; Color? _localPrimaryColor; Color? _accountPrimaryColor; bool _softenPrimaryColor = false; bool _localSoftenPrimaryColor = false; bool? _accountSoftenPrimaryColor; Client? _accountClient; StreamSubscription? _accountClientSubscription; int _accountClientGeneration = 0; ThemeMode get themeMode => _themeMode ?? ThemeMode.system; Color? get primaryColor => _primaryColor; bool get softenPrimaryColor => _softenPrimaryColor; static ThemeController of(BuildContext context) => Provider.of(context, listen: false); void _updatePrimaryColor() { if (!mounted) return; final newColor = _accountPrimaryColor ?? _localPrimaryColor; if (newColor == _primaryColor) return; setState(() { _primaryColor = newColor; }); } void _updateSoftenPrimaryColor() { if (!mounted) return; final newValue = _accountSoftenPrimaryColor ?? _localSoftenPrimaryColor; if (newValue == _softenPrimaryColor) return; setState(() { _softenPrimaryColor = newValue; }); } Future _loadData(_) async { final preferences = _sharedPreferences ??= await SharedPreferences.getInstance(); final rawThemeMode = preferences.getString(widget.themeModeSettingsKey); final rawColor = preferences.getInt(widget.primaryColorSettingsKey); if (!mounted) return; setState(() { _themeMode = ThemeMode.values.singleWhereOrNull( (value) => value.name == rawThemeMode, ); _localPrimaryColor = rawColor == null ? null : Color(rawColor); _localSoftenPrimaryColor = AppSettings.softenThemeColor.value; }); _updatePrimaryColor(); _updateSoftenPrimaryColor(); } Future setThemeMode(ThemeMode newThemeMode) async { final preferences = _sharedPreferences ??= await SharedPreferences.getInstance(); await preferences.setString(widget.themeModeSettingsKey, newThemeMode.name); setState(() { _themeMode = newThemeMode; }); } Future setPrimaryColor(Color? newPrimaryColor) async { if (!mounted) return; setState(() { _localPrimaryColor = newPrimaryColor; _accountPrimaryColor = newPrimaryColor; _primaryColor = newPrimaryColor; }); final preferences = _sharedPreferences ??= await SharedPreferences.getInstance(); if (newPrimaryColor == null) { await preferences.remove(widget.primaryColorSettingsKey); } else { await preferences.setInt( widget.primaryColorSettingsKey, newPrimaryColor.hexValue, ); } } Future setSoftenPrimaryColor(bool soften) async { await AppSettings.softenThemeColor.setItem(soften); if (!mounted) return; setState(() { _localSoftenPrimaryColor = soften; _accountSoftenPrimaryColor = soften; _softenPrimaryColor = soften; }); final client = _accountClient; if (client == null) return; try { await client.setApplicationAccountSoftenThemeColor(soften); } catch (err) { Logs().e('TatuChat: failed to sync soften theme color', err); } } void setAccountClient(Client? client) { if (!mounted) return; final generation = ++_accountClientGeneration; _accountClientSubscription?.cancel(); _accountClientSubscription = null; _accountClient = client; _accountPrimaryColor = null; _accountSoftenPrimaryColor = null; _updatePrimaryColor(); _updateSoftenPrimaryColor(); if (client == null) return; unawaited(_loadAccountClientConfig(client, generation)); _accountClientSubscription = client.onSync.stream .where( (sync) => sync.accountData?.any( (accountData) => accountData.type == ApplicationAccountConfigExtension.accountDataKey, ) ?? false, ) .listen((_) => _applyAccountClientConfig(client, generation)); } Future _loadAccountClientConfig(Client client, int generation) async { final accountDataLoading = client.accountDataLoading; if (accountDataLoading != null) { try { await accountDataLoading; } catch (_) {} } if (!mounted || generation != _accountClientGeneration || !identical(_accountClient, client)) { return; } _applyAccountClientConfig(client, generation); } void _applyAccountClientConfig(Client client, int generation) { if (!mounted || generation != _accountClientGeneration || !identical(_accountClient, client)) { return; } final config = client.applicationAccountConfig; final primaryColor = config.primaryColor; _accountPrimaryColor = primaryColor == null ? null : Color(primaryColor); _accountSoftenPrimaryColor = config.softenThemeColor; _updatePrimaryColor(); _updateSoftenPrimaryColor(); } @override void initState() { WidgetsBinding.instance.addPostFrameCallback(_loadData); super.initState(); } @override Widget build(BuildContext context) { return Provider( create: (_) => this, child: DynamicColorBuilder( builder: (light, _) => widget.builder( context, themeMode, primaryColor ?? light?.primary, this, ), ), ); } @override void dispose() { _accountClientSubscription?.cancel(); super.dispose(); } }