- 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
227 lines
6.6 KiB
Dart
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();
|
|
}
|
|
}
|