make it possible to change the primary color
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-08-01 11:49:22 +02:00
parent c2607a99e3
commit 70ed3759d2
23 changed files with 270 additions and 50 deletions

View file

@ -89,15 +89,17 @@ class _AppState extends State<App> with WidgetsBindingObserver {
Locale('de', ''), Locale('de', ''),
]; ];
final settings = context.watch<SettingsChangeProvider>();
if (widget.storageError) { if (widget.storageError) {
return MaterialApp( return MaterialApp(
localizationsDelegates: localizationsDelegates, localizationsDelegates: localizationsDelegates,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales, supportedLocales: supportedLocales,
title: 'twonly', title: 'twonly',
theme: lightTheme, theme: getLightTheme(settings.primaryColor),
darkTheme: darkTheme, darkTheme: getDarkTheme(settings.primaryColor),
themeMode: context.read<SettingsChangeProvider>().themeMode, themeMode: settings.themeMode,
home: const CriticalErrorView(), home: const CriticalErrorView(),
); );
} }
@ -108,9 +110,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales, supportedLocales: supportedLocales,
title: 'twonly', title: 'twonly',
theme: lightTheme, theme: getLightTheme(settings.primaryColor),
darkTheme: darkTheme, darkTheme: getDarkTheme(settings.primaryColor),
themeMode: context.read<SettingsChangeProvider>().themeMode, themeMode: settings.themeMode,
home: const RecoveryView(), home: const RecoveryView(),
); );
} }
@ -121,9 +123,9 @@ class _AppState extends State<App> with WidgetsBindingObserver {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
supportedLocales: supportedLocales, supportedLocales: supportedLocales,
title: 'twonly', title: 'twonly',
theme: lightTheme, theme: getLightTheme(settings.primaryColor),
darkTheme: darkTheme, darkTheme: getDarkTheme(settings.primaryColor),
themeMode: context.read<SettingsChangeProvider>().themeMode, themeMode: settings.themeMode,
); );
}, },
); );

View file

@ -4405,6 +4405,30 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Brightness'** /// **'Brightness'**
String get brightness; String get brightness;
/// No description provided for @settingsAppearancePrimaryColor.
///
/// In en, this message translates to:
/// **'Primary Color'**
String get settingsAppearancePrimaryColor;
/// No description provided for @themeSystemDefault.
///
/// In en, this message translates to:
/// **'System default'**
String get themeSystemDefault;
/// No description provided for @themeLight.
///
/// In en, this message translates to:
/// **'Light'**
String get themeLight;
/// No description provided for @themeDark.
///
/// In en, this message translates to:
/// **'Dark'**
String get themeDark;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View file

@ -2553,4 +2553,16 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get brightness => 'Helligkeit'; String get brightness => 'Helligkeit';
@override
String get settingsAppearancePrimaryColor => 'Hauptfarbe';
@override
String get themeSystemDefault => 'Systemstandard';
@override
String get themeLight => 'Hell';
@override
String get themeDark => 'Dunkel';
} }

View file

@ -2526,4 +2526,16 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get brightness => 'Brightness'; String get brightness => 'Brightness';
@override
String get settingsAppearancePrimaryColor => 'Primary Color';
@override
String get themeSystemDefault => 'System default';
@override
String get themeLight => 'Light';
@override
String get themeDark => 'Dark';
} }

@ -1 +1 @@
Subproject commit 2f37f6cd8ebd07dbc113bab0d5816bd37fb4fca6 Subproject commit 5feb6f49653741ec37c49e57b3b6c7c178ed4427

View file

@ -57,6 +57,12 @@ class UserData {
@JsonKey(defaultValue: ThemeMode.system) @JsonKey(defaultValue: ThemeMode.system)
ThemeMode themeMode = ThemeMode.system; ThemeMode themeMode = ThemeMode.system;
int? primaryColorValue;
Color get primaryColor => primaryColorValue != null
? Color(primaryColorValue!)
: const Color(0xFF57CC99);
int? defaultShowTime; int? defaultShowTime;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)

View file

@ -33,6 +33,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..themeMode = ..themeMode =
$enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system ThemeMode.system
..primaryColorValue = (json['primaryColorValue'] as num?)?.toInt()
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..requestedAudioPermission = ..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false json['requestedAudioPermission'] as bool? ?? false
@ -137,6 +138,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'lastPlanBallance': instance.lastPlanBallance, 'lastPlanBallance': instance.lastPlanBallance,
'additionalUserInvites': instance.additionalUserInvites, 'additionalUserInvites': instance.additionalUserInvites,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'primaryColorValue': instance.primaryColorValue,
'defaultShowTime': instance.defaultShowTime, 'defaultShowTime': instance.defaultShowTime,
'requestedAudioPermission': instance.requestedAudioPermission, 'requestedAudioPermission': instance.requestedAudioPermission,
'enableDatabaseLogging': instance.enableDatabaseLogging, 'enableDatabaseLogging': instance.enableDatabaseLogging,

View file

@ -2,18 +2,23 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/visual/themes/light.dart';
class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin { class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
late ThemeMode _themeMode; late ThemeMode _themeMode;
late Color _primaryColor;
ThemeMode get themeMode => _themeMode; ThemeMode get themeMode => _themeMode;
Color get primaryColor => _primaryColor;
void loadSettings() { void loadSettings() {
if (userService.isUserCreated) { if (userService.isUserCreated) {
_themeMode = userService.currentUser.themeMode; _themeMode = userService.currentUser.themeMode;
_primaryColor = userService.currentUser.primaryColor;
notifyListeners(); notifyListeners();
} else { } else {
_themeMode = ThemeMode.system; _themeMode = ThemeMode.system;
_primaryColor = defaultPrimaryColor;
} }
} }
@ -28,4 +33,16 @@ class SettingsChangeProvider with ChangeNotifier, DiagnosticableTreeMixin {
await UserService.update((u) => u.themeMode = newThemeMode); await UserService.update((u) => u.themeMode = newThemeMode);
} }
Future<void> updatePrimaryColor(Color newColor) async {
if (newColor.toARGB32() == _primaryColor.toARGB32()) return;
_primaryColor = newColor;
notifyListeners();
await UserService.update(
(u) => u.primaryColorValue = newColor.toARGB32(),
);
}
} }

View file

@ -5,7 +5,6 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/notification_badge.comp.dart'; import 'package:twonly/src/visual/components/notification_badge.comp.dart';
import 'package:twonly/src/visual/themes/light.dart';
class ContactRequestBadgeComp extends StatelessWidget { class ContactRequestBadgeComp extends StatelessWidget {
const ContactRequestBadgeComp({super.key}); const ContactRequestBadgeComp({super.key});
@ -26,8 +25,8 @@ class ContactRequestBadgeComp extends StatelessWidget {
child: Container( child: Container(
width: 40, width: 40,
height: 40, height: 40,
decoration: const BoxDecoration( decoration: BoxDecoration(
color: primaryColor, color: context.color.primary,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),

View file

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/visual/themes/light.dart'; import 'package:twonly/src/utils/misc.dart';
class SelectableThumbnailComp extends StatelessWidget { class SelectableThumbnailComp extends StatelessWidget {
const SelectableThumbnailComp({ const SelectableThumbnailComp({
@ -19,7 +19,7 @@ class SelectableThumbnailComp extends StatelessWidget {
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut, curve: Curves.easeInOut,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.transparent, color: isSelected ? context.color.primary : Colors.transparent,
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
color: Colors.black12, color: Colors.black12,
@ -49,7 +49,7 @@ class SelectableThumbnailComp extends StatelessWidget {
child: Container( child: Container(
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.black38, color: isSelected ? context.color.primary : Colors.black38,
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: Theme.of(context).brightness == Brightness.dark color: Theme.of(context).brightness == Brightness.dark

View file

@ -9,7 +9,6 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/profile_qr_code.comp.dart'; import 'package:twonly/src/visual/components/profile_qr_code.comp.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/elements/svg_icon.element.dart'; import 'package:twonly/src/visual/elements/svg_icon.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
const colorVerificationBadgeYellow = Color.fromARGB(255, 0, 182, 238); const colorVerificationBadgeYellow = Color.fromARGB(255, 0, 182, 238);
@ -90,7 +89,7 @@ class VerificationBadgeInfo extends StatelessWidget {
context, context,
icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40), icon: const SvgIcon(assetPath: SvgIcons.verifiedGreen, size: 40),
description: context.lang.verificationBadgeGreenDesc, description: context.lang.verificationBadgeGreenDesc,
boldTextColor: primaryColor, boldTextColor: context.color.primary,
onTap: () => context.push( onTap: () => context.push(
Routes.cameraQRScanner, Routes.cameraQRScanner,
extra: { extra: {

View file

@ -86,7 +86,7 @@ class VerificationSuccessAnimationState
'<svg viewBox="0 0 640 640"><path d="$_path2" fill="white"/></svg>'; '<svg viewBox="0 0 640 640"><path d="$_path2" fill="white"/></svg>';
static const _grey = Color(0xFF8E9AAF); static const _grey = Color(0xFF8E9AAF);
static const Color _green = primaryColor; static const Color _green = defaultPrimaryColor;
@override @override
void initState() { void initState() {

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart'; import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
enum MyButtonVariant { enum MyButtonVariant {
primary, primary,
@ -49,7 +48,7 @@ class _MyButtonState extends State<MyButton> {
switch (widget.variant) { switch (widget.variant) {
case MyButtonVariant.primary: case MyButtonVariant.primary:
buttonStyle = FilledButton.styleFrom( buttonStyle = FilledButton.styleFrom(
backgroundColor: primaryColor, backgroundColor: context.color.primary,
foregroundColor: Colors.black87, foregroundColor: Colors.black87,
disabledBackgroundColor: disabledBgColor, disabledBackgroundColor: disabledBgColor,
disabledForegroundColor: disabledFgColor, disabledForegroundColor: disabledFgColor,
@ -96,7 +95,7 @@ class _MyButtonState extends State<MyButton> {
); );
case MyButtonVariant.primaryMiddle: case MyButtonVariant.primaryMiddle:
buttonStyle = FilledButton.styleFrom( buttonStyle = FilledButton.styleFrom(
backgroundColor: primaryColor, backgroundColor: context.color.primary,
foregroundColor: Colors.black87, foregroundColor: Colors.black87,
disabledBackgroundColor: disabledBgColor, disabledBackgroundColor: disabledBgColor,
disabledForegroundColor: disabledFgColor, disabledForegroundColor: disabledFgColor,
@ -115,7 +114,7 @@ class _MyButtonState extends State<MyButton> {
); );
case MyButtonVariant.primaryDense: case MyButtonVariant.primaryDense:
buttonStyle = FilledButton.styleFrom( buttonStyle = FilledButton.styleFrom(
backgroundColor: primaryColor, backgroundColor: context.color.primary,
foregroundColor: Colors.black87, foregroundColor: Colors.black87,
disabledBackgroundColor: disabledBgColor, disabledBackgroundColor: disabledBgColor,
disabledForegroundColor: disabledFgColor, disabledForegroundColor: disabledFgColor,

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart'; import 'package:twonly/src/visual/elements/reactive_tap_feedback.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
enum MyIconButtonVariant { enum MyIconButtonVariant {
primary, primary,
@ -43,7 +42,7 @@ class _MyIconButtonState extends State<MyIconButton> {
late final Color fgColor; late final Color fgColor;
if (widget.variant == MyIconButtonVariant.primary) { if (widget.variant == MyIconButtonVariant.primary) {
bgColor = primaryColor; bgColor = context.color.primary;
fgColor = Colors.black87; fgColor = Colors.black87;
} else { } else {
bgColor = isDark ? Colors.grey[800]! : Colors.grey[200]!; bgColor = isDark ? Colors.grey[800]! : Colors.grey[200]!;

View file

@ -1,11 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/visual/themes/light.dart';
final ThemeData darkTheme = () { ThemeData getDarkTheme([Color primary = defaultPrimaryColor]) {
final base = ThemeData.dark().copyWith( final base = ThemeData.dark().copyWith(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark, brightness: Brightness.dark,
seedColor: const Color(0xFF57CC99), seedColor: primary,
primary: primary,
surface: const Color.fromARGB(255, 20, 18, 23), surface: const Color.fromARGB(255, 20, 18, 23),
surfaceContainer: const Color.fromARGB(255, 45, 41, 54), surfaceContainer: const Color.fromARGB(255, 45, 41, 54),
surfaceContainerLow: const Color.fromARGB(255, 38, 34, 45), surfaceContainerLow: const Color.fromARGB(255, 38, 34, 45),
@ -21,4 +23,6 @@ final ThemeData darkTheme = () {
fontFamilyFallback: Platform.isAndroid ? const ['NotoColorEmoji'] : null, fontFamilyFallback: Platform.isAndroid ? const ['NotoColorEmoji'] : null,
), ),
); );
}(); }
final ThemeData darkTheme = getDarkTheme();

View file

@ -2,12 +2,13 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
const primaryColor = Color(0xFF57CC99); const defaultPrimaryColor = Color(0xFF57CC99);
final ThemeData lightTheme = () { ThemeData getLightTheme([Color primary = defaultPrimaryColor]) {
final base = ThemeData( final base = ThemeData(
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
seedColor: primaryColor, seedColor: primary,
primary: primary,
), ),
inputDecorationTheme: const InputDecorationTheme( inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(), border: OutlineInputBorder(),
@ -19,10 +20,12 @@ final ThemeData lightTheme = () {
fontFamilyFallback: Platform.isAndroid ? const ['NotoColorEmoji'] : null, fontFamilyFallback: Platform.isAndroid ? const ['NotoColorEmoji'] : null,
), ),
); );
}(); }
final ThemeData lightTheme = getLightTheme();
final ButtonStyle primaryColorButtonStyle = FilledButton.styleFrom( final ButtonStyle primaryColorButtonStyle = FilledButton.styleFrom(
backgroundColor: primaryColor, backgroundColor: defaultPrimaryColor,
foregroundColor: Colors.black87, foregroundColor: Colors.black87,
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(

View file

@ -15,7 +15,6 @@ import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/avatar_icon.comp.dart'; import 'package:twonly/src/visual/components/avatar_icon.comp.dart';
import 'package:twonly/src/visual/components/connection_status.comp.dart'; import 'package:twonly/src/visual/components/connection_status.comp.dart';
import 'package:twonly/src/visual/components/notification_badge.comp.dart'; import 'package:twonly/src/visual/components/notification_badge.comp.dart';
import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/empty_chat_list.comp.dart';
import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/group_list_item.comp.dart';
import 'package:twonly/src/visual/views/chats/chat_list_components/news_btn.comp.dart'; import 'package:twonly/src/visual/views/chats/chat_list_components/news_btn.comp.dart';
@ -196,8 +195,8 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie
child: Container( child: Container(
width: 40, width: 40,
height: 40, height: 40,
decoration: const BoxDecoration( decoration: BoxDecoration(
color: primaryColor, color: context.color.primary,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),
@ -329,7 +328,7 @@ class _ChatListViewState extends State<ChatListView> with AutomaticKeepAliveClie
FloatingActionButton( FloatingActionButton(
heroTag: 'new_chat_fab', heroTag: 'new_chat_fab',
elevation: 2, elevation: 2,
backgroundColor: primaryColor, backgroundColor: context.color.primary,
foregroundColor: Colors.black87, foregroundColor: Colors.black87,
onPressed: () => context.push(Routes.chatsStartNewChat), onPressed: () => context.push(Routes.chatsStartNewChat),
child: const FaIcon( child: const FaIcon(

View file

@ -5,7 +5,6 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/notification_badge.comp.dart'; import 'package:twonly/src/visual/components/notification_badge.comp.dart';
import 'package:twonly/src/visual/themes/light.dart';
class NewsIconButtonComp extends StatelessWidget { class NewsIconButtonComp extends StatelessWidget {
const NewsIconButtonComp({super.key}); const NewsIconButtonComp({super.key});
@ -23,7 +22,7 @@ class NewsIconButtonComp extends StatelessWidget {
builder: (context, count, child) { builder: (context, count, child) {
return NotificationBadgeComp( return NotificationBadgeComp(
count: count.toString(), count: count.toString(),
backgroundColor: primaryColor, backgroundColor: context.color.primary,
textColor: Colors.black87, textColor: Colors.black87,
child: IconButton( child: IconButton(
onPressed: () => context.push(Routes.settingsHelpNews), onPressed: () => context.push(Routes.settingsHelpNews),

View file

@ -8,7 +8,6 @@ import 'package:twonly/src/visual/components/avatar_icon.comp.dart'
show AvatarIcon; show AvatarIcon;
import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/elements/my_input.element.dart'; import 'package:twonly/src/visual/elements/my_input.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart'; import 'package:twonly/src/visual/views/onboarding/setup/components/next_button.comp.dart';
class ProfileSetupPage extends StatefulWidget { class ProfileSetupPage extends StatefulWidget {
@ -67,7 +66,7 @@ class _ProfileSetupPageState extends State<ProfileSetupPage> {
foregroundDecoration: BoxDecoration( foregroundDecoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: primaryColor, color: context.color.primary,
width: 4, width: 4,
), ),
), ),

View file

@ -6,6 +6,7 @@ import 'package:twonly/locator.dart';
import 'package:twonly/src/providers/settings.provider.dart'; import 'package:twonly/src/providers/settings.provider.dart';
import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/components/custom_color_picker_dialog.comp.dart';
import 'package:twonly/src/visual/elements/radio_button.element.dart'; import 'package:twonly/src/visual/elements/radio_button.element.dart';
class AppearanceView extends StatefulWidget { class AppearanceView extends StatefulWidget {
@ -36,7 +37,7 @@ class _AppearanceViewState extends State<AppearanceView> {
RadioButton<ThemeMode>( RadioButton<ThemeMode>(
value: ThemeMode.system, value: ThemeMode.system,
groupValue: selectedValue, groupValue: selectedValue,
label: 'System default', label: context.lang.themeSystemDefault,
onChanged: (value) { onChanged: (value) {
selectedValue = value; selectedValue = value;
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -45,7 +46,7 @@ class _AppearanceViewState extends State<AppearanceView> {
RadioButton<ThemeMode>( RadioButton<ThemeMode>(
value: ThemeMode.light, value: ThemeMode.light,
groupValue: selectedValue, groupValue: selectedValue,
label: 'Light', label: context.lang.themeLight,
onChanged: (value) { onChanged: (value) {
selectedValue = value; selectedValue = value;
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -54,7 +55,7 @@ class _AppearanceViewState extends State<AppearanceView> {
RadioButton<ThemeMode>( RadioButton<ThemeMode>(
value: ThemeMode.dark, value: ThemeMode.dark,
groupValue: selectedValue, groupValue: selectedValue,
label: 'Dark', label: context.lang.themeDark,
onChanged: (value) { onChanged: (value) {
selectedValue = value; selectedValue = value;
Navigator.of(context).pop(); Navigator.of(context).pop();
@ -72,6 +73,124 @@ class _AppearanceViewState extends State<AppearanceView> {
} }
} }
Future<void> _showSelectPrimaryColor(BuildContext context) async {
const presetColors = <Color>[
Color(0xFF57CC99), // Original Twonly Green
Color(0xFF3A76F0), // Signal Blue
Color(0xFF9D4EDD), // Purple
Color(0xFFFF5964), // Coral Red
Color(0xFFFF9F1C), // Amber Orange
];
await showModalBottomSheet<void>(
context: context,
showDragHandle: true,
builder: (context) {
final activeColor =
context.watch<SettingsChangeProvider>().primaryColor;
return Padding(
padding: const EdgeInsets.fromLTRB(24, 8, 24, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.lang.settingsAppearancePrimaryColor,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: presetColors.map((color) {
final isSelected =
activeColor.toARGB32() == color.toARGB32();
return GestureDetector(
onTap: () async {
await context
.read<SettingsChangeProvider>()
.updatePrimaryColor(color);
},
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: color.withValues(alpha: 0.4),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
border: Border.all(
color:
isSelected ? Colors.white : Colors.transparent,
width: 3,
),
),
child: isSelected
? const Icon(
Icons.check,
color: Colors.white,
size: 24,
)
: null,
),
);
}).toList(),
),
const SizedBox(height: 24),
ListTile(
contentPadding: EdgeInsets.zero,
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const SweepGradient(
colors: [
Colors.red,
Colors.yellow,
Colors.green,
Colors.cyan,
Colors.blue,
Color(0xFFFF00FF),
Colors.red,
],
),
border: Border.all(color: Colors.white24, width: 2),
),
),
title: Text(context.lang.customColor),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
final pickedValue = await showDialog<int>(
context: context,
builder: (context) => CustomColorPickerDialog(
initialColor: activeColor,
),
);
if (pickedValue != null && context.mounted) {
final pickedColor = Color(pickedValue);
await context
.read<SettingsChangeProvider>()
.updatePrimaryColor(pickedColor);
if (context.mounted) {
Navigator.of(context).pop();
}
}
},
),
],
),
);
},
);
}
Future<void> toggleShowNewsIcon() async { Future<void> toggleShowNewsIcon() async {
await UserService.update((u) { await UserService.update((u) {
u.showNewsShortcut = !u.showNewsShortcut; u.showNewsShortcut = !u.showNewsShortcut;
@ -90,9 +209,22 @@ class _AppearanceViewState extends State<AppearanceView> {
}); });
} }
String _themeModeLabel(BuildContext context, ThemeMode mode) {
switch (mode) {
case ThemeMode.system:
return context.lang.themeSystemDefault;
case ThemeMode.light:
return context.lang.themeLight;
case ThemeMode.dark:
return context.lang.themeDark;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final selectedTheme = context.watch<SettingsChangeProvider>().themeMode; final selectedTheme = context.watch<SettingsChangeProvider>().themeMode;
final primaryColor = context.watch<SettingsChangeProvider>().primaryColor;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(context.lang.settingsAppearance), title: Text(context.lang.settingsAppearance),
@ -105,13 +237,28 @@ class _AppearanceViewState extends State<AppearanceView> {
ListTile( ListTile(
title: Text(context.lang.settingsAppearanceTheme), title: Text(context.lang.settingsAppearanceTheme),
subtitle: Text( subtitle: Text(
selectedTheme.name, _themeModeLabel(context, selectedTheme),
style: const TextStyle(color: Colors.grey), style: const TextStyle(color: Colors.grey),
), ),
onTap: () async { onTap: () async {
await _showSelectThemeMode(context); await _showSelectThemeMode(context);
}, },
), ),
ListTile(
title: Text(context.lang.settingsAppearancePrimaryColor),
trailing: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: primaryColor,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 1.5),
),
),
onTap: () async {
await _showSelectPrimaryColor(context);
},
),
ListTile( ListTile(
title: Text(context.lang.hideNewsIcon), title: Text(context.lang.hideNewsIcon),
onTap: toggleShowNewsIcon, onTap: toggleShowNewsIcon,

View file

@ -3,7 +3,6 @@ import 'package:flutter/services.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart'; import 'package:twonly/src/services/passwordless_recovery.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/my_input.element.dart'; import 'package:twonly/src/visual/elements/my_input.element.dart';
import 'package:twonly/src/visual/themes/light.dart';
class _FactorOption { class _FactorOption {
const _FactorOption({ const _FactorOption({
@ -113,7 +112,7 @@ class SecondFactorPicker extends StatelessWidget {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.transparent, color: isSelected ? context.color.primary : Colors.transparent,
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
child: Column( child: Column(

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/themes/light.dart';
class ThresholdPicker extends StatelessWidget { class ThresholdPicker extends StatelessWidget {
const ThresholdPicker({ const ThresholdPicker({
@ -103,7 +102,7 @@ class ThresholdPicker extends StatelessWidget {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? primaryColor : Colors.transparent, color: isSelected ? context.color.primary : Colors.transparent,
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
child: Center( child: Center(

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none' publish_to: 'none'
version: 0.4.3+164 version: 0.4.3+165
environment: environment:
sdk: ^3.11.0 sdk: ^3.11.0