Files
tatuchat/lib/widgets/theme_builder.dart
Welton Moura 9382a1f946 feat: usar a cor escolhida tal e qual e opcao de suavizar
- TatuPalette.scheme aceita softenPrimary e passa a usar a seed como primary, onPrimary por contraste e surfaceTint, em vez da paleta tonal do Material 3
- nova opcao AppSettings.softenThemeColor (default false, ou seja cor exacta)
- interruptor 'Suavizar cor do tema' em Configuracoes > Aparencia, com l10n en/pt
- a opcao e sincronizada via account data (soften_theme_color) e mantida localmente como fallback, tal como a cor do tema
- setApplicationAccountColor e updateApplicationAccountConfig preservam o novo campo
- um unico sync de account data actualiza cor e opcao
- testes de serializacao e round-trip para o novo campo
2026-09-25 13:46:00 -03:00

227 lines
6.6 KiB
Dart

// 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<ThemeBuilder> createState() => ThemeController();
}
class ThemeController extends State<ThemeBuilder> {
SharedPreferences? _sharedPreferences;
ThemeMode? _themeMode;
Color? _primaryColor;
Color? _localPrimaryColor;
Color? _accountPrimaryColor;
bool _softenPrimaryColor = false;
bool _localSoftenPrimaryColor = false;
bool? _accountSoftenPrimaryColor;
Client? _accountClient;
StreamSubscription<SyncUpdate>? _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<ThemeController>(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<void> _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<void> setThemeMode(ThemeMode newThemeMode) async {
final preferences = _sharedPreferences ??=
await SharedPreferences.getInstance();
await preferences.setString(widget.themeModeSettingsKey, newThemeMode.name);
setState(() {
_themeMode = newThemeMode;
});
}
Future<void> 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<void> 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<void> _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();
}
}