What changed, and why it matters
This commit adds a dark theme option to the Skylight Wallet app. It lets users choose between light, dark, or system theme in settings, and adjusts QR code and status icon colors so they remain visible in dark mode. There is no security-relevant change here.
No security action needed. This is a routine UI/UX feature commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces a new ThemeModel backed by SharedPreferences, adds theme localization strings, wires light/dark/system theme mode into MaterialApp, and updates the receive screen QR code and status icon widgets to adapt colors based on Theme brightness. It also reorders some settings UI rows. No cryptographic, authentication, network, or data-handling logic is modified.
Changed components
lib/main.dartlib/models/theme_model.dartlib/screens/receive.dartlib/screens/settings.dartlib/services/shared_preferences_service.dartlib/widgets/status_icon.dartlib/l10n/app_en.arblib/l10n/app_localizations.dartlib/l10n/app_localizations_en.dartlib/l10n/app_localizations_pt.dartlib/l10n/app_pt.arbInspect captured patch +193 / −26
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index ecbac48..8e73a9f 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -79,6 +79,10 @@
"settingsAppLockUnableToAuthError": "Unable to authenticate.",
"settingsVerboseLoggingLabel": "Verbose Logging",
"settingsVerboseLoggingDescription": "Logs wallet operations to a text file in the app's data folder for debugging purposes.",
+ "settingsThemeLabel": "Theme",
+ "settingsThemeSystem": "System",
+ "settingsThemeLight": "Light",
+ "settingsThemeDark": "Dark",
"settingsLanguageLabel": "Language",
"settingsDisplayCurrencyLabel": "Display Currency",
"settingsLwsViewKeysLabel": "LWS View Keys",
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index d0e9118..2e97fd8 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -572,6 +572,30 @@ abstract class AppLocalizations {
/// **'Logs wallet operations to a text file in the app\'s data folder for debugging purposes.'**
String get settingsVerboseLoggingDescription;
+ /// No description provided for @settingsThemeLabel.
+ ///
+ /// In en, this message translates to:
+ /// **'Theme'**
+ String get settingsThemeLabel;
+
+ /// No description provided for @settingsThemeSystem.
+ ///
+ /// In en, this message translates to:
+ /// **'System'**
+ String get settingsThemeSystem;
+
+ /// No description provided for @settingsThemeLight.
+ ///
+ /// In en, this message translates to:
+ /// **'Light'**
+ String get settingsThemeLight;
+
+ /// No description provided for @settingsThemeDark.
+ ///
+ /// In en, this message translates to:
+ /// **'Dark'**
+ String get settingsThemeDark;
+
/// No description provided for @settingsLanguageLabel.
///
/// In en, this message translates to:
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 45e9526..ff0a3d3 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -258,6 +258,18 @@ class AppLocalizationsEn extends AppLocalizations {
String get settingsVerboseLoggingDescription =>
'Logs wallet operations to a text file in the app\'s data folder for debugging purposes.';
+ @override
+ String get settingsThemeLabel => 'Theme';
+
+ @override
+ String get settingsThemeSystem => 'System';
+
+ @override
+ String get settingsThemeLight => 'Light';
+
+ @override
+ String get settingsThemeDark => 'Dark';
+
@override
String get settingsLanguageLabel => 'Language';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index db4b2c7..96d34c2 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -258,6 +258,18 @@ class AppLocalizationsPt extends AppLocalizations {
String get settingsVerboseLoggingDescription =>
'Registra operações da carteira em um arquivo de texto na pasta de dados do app para fins de depuração.';
+ @override
+ String get settingsThemeLabel => 'Tema';
+
+ @override
+ String get settingsThemeSystem => 'Sistema';
+
+ @override
+ String get settingsThemeLight => 'Claro';
+
+ @override
+ String get settingsThemeDark => 'Escuro';
+
@override
String get settingsLanguageLabel => 'Idioma';
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index d788e0e..3b35b00 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -79,6 +79,10 @@
"settingsAppLockUnableToAuthError": "Não foi possível autenticar.",
"settingsVerboseLoggingLabel": "Logs Detalhados",
"settingsVerboseLoggingDescription": "Registra operações da carteira em um arquivo de texto na pasta de dados do app para fins de depuração.",
+ "settingsThemeLabel": "Tema",
+ "settingsThemeSystem": "Sistema",
+ "settingsThemeLight": "Claro",
+ "settingsThemeDark": "Escuro",
"settingsLanguageLabel": "Idioma",
"settingsDisplayCurrencyLabel": "Moeda Local",
"settingsLwsViewKeysLabel": "Chaves de Visualização do LWS",
diff --git a/lib/main.dart b/lib/main.dart
index 1a7e1c9..9357539 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -16,6 +16,7 @@ import 'package:skylight_wallet/screens/scan_qr.dart';
import 'package:skylight_wallet/screens/secret_keys.dart';
import 'package:skylight_wallet/services/tor_service.dart';
import 'package:skylight_wallet/models/language_model.dart';
+import 'package:skylight_wallet/models/theme_model.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/screens/settings.dart';
import 'package:skylight_wallet/models/wallet_model.dart';
@@ -60,18 +61,16 @@ class MyApp extends StatelessWidget {
providers: [
ChangeNotifierProvider(create: (context) => WalletModel()),
ChangeNotifierProvider(create: (context) => LanguageModel()),
+ ChangeNotifierProvider(create: (context) => ThemeModel()),
ChangeNotifierProvider(create: (context) => FiatRateModel()),
],
- child: Consumer<LanguageModel>(
- builder: (context, languageProvider, child) {
+ child: Consumer2<LanguageModel, ThemeModel>(
+ builder: (context, languageProvider, themeProvider, child) {
final wallet = Provider.of<WalletModel>(context, listen: false);
return FutureBuilder(
- // Since LanguageModel loads the user's language from
- // SharedPreferences, we need to wait until an instance is loaded.
- // We also need to wait until it is determined whether or not a
- // wallet already exists, so we know which screen to send the user
- // to initially.
+ // We need to check for wallet existence to determine the correct initial route,
+ // but we'll do this quickly without loading the wallet to avoid startup delay.
future: Future.wait([
SharedPreferences.getInstance(),
loadExistingWalletIfExists(wallet),
@@ -83,6 +82,10 @@ class MyApp extends StatelessWidget {
snapshot.data![0] as SharedPreferences;
final walletExists = snapshot.data![1] as bool;
+ final theme =
+ sharedPreferences.getString(SharedPreferencesKeys.theme) ??
+ 'system';
+
final appLockEnabled =
sharedPreferences.getBool(
SharedPreferencesKeys.appLockEnabled,
@@ -111,6 +114,17 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
+ darkTheme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: Colors.blue,
+ brightness: Brightness.dark,
+ ),
+ ),
+ themeMode: theme == 'dark'
+ ? ThemeMode.dark
+ : theme == 'light'
+ ? ThemeMode.light
+ : ThemeMode.system,
initialRoute: initialRoute,
locale: Locale.fromSubtags(
languageCode: languageProvider.language,
@@ -149,6 +163,13 @@ class MyApp extends StatelessWidget {
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
+ darkTheme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: Colors.blue,
+ brightness: Brightness.dark,
+ ),
+ ),
+ themeMode: ThemeMode.system,
builder: (context, child) => Scaffold(),
);
},
diff --git a/lib/models/theme_model.dart b/lib/models/theme_model.dart
new file mode 100644
index 0000000..2efc4d2
--- /dev/null
+++ b/lib/models/theme_model.dart
@@ -0,0 +1,48 @@
+import 'package:flutter/material.dart';
+import 'package:skylight_wallet/services/shared_preferences_service.dart';
+
+class ThemeModel with ChangeNotifier {
+ String _theme = 'system';
+
+ String get theme => _theme;
+
+ ThemeModel() {
+ _loadTheme();
+ }
+
+ Future<void> _loadTheme() async {
+ final String? preferencesTheme = await SharedPreferencesService.get<String>(
+ SharedPreferencesKeys.theme,
+ );
+
+ if (preferencesTheme != null) {
+ _theme = preferencesTheme;
+ }
+ notifyListeners();
+ }
+
+ void setTheme(String? newTheme) async {
+ if (newTheme == null) return;
+
+ _theme = newTheme;
+
+ await SharedPreferencesService.set<String>(
+ SharedPreferencesKeys.theme,
+ newTheme,
+ );
+
+ notifyListeners();
+ }
+
+ ThemeMode get themeMode {
+ switch (_theme) {
+ case 'light':
+ return ThemeMode.light;
+ case 'dark':
+ return ThemeMode.dark;
+ case 'system':
+ default:
+ return ThemeMode.system;
+ }
+ }
+}
diff --git a/lib/screens/receive.dart b/lib/screens/receive.dart
index 27868a9..cf69e08 100644
--- a/lib/screens/receive.dart
+++ b/lib/screens/receive.dart
@@ -84,6 +84,8 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
+ final brightness = Theme.of(context).brightness;
+ final isDarkTheme = brightness == Brightness.dark;
return Scaffold(
appBar: AppBar(title: Text(i18n.receiveTitle)),
@@ -94,7 +96,17 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
mainAxisAlignment: MainAxisAlignment.center,
spacing: 20,
children: [
- QrImageView(data: _address),
+ QrImageView(
+ data: _address,
+ eyeStyle: QrEyeStyle(
+ eyeShape: QrEyeShape.square,
+ color: isDarkTheme ? Colors.grey[300] : Colors.black,
+ ),
+ dataModuleStyle: QrDataModuleStyle(
+ dataModuleShape: QrDataModuleShape.square,
+ color: isDarkTheme ? Colors.grey[300] : Colors.black,
+ ),
+ ),
if (_showSubaddress)
Text(
i18n.receiveSubaddressWarn,
diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart
index 0f26e85..7232df1 100644
--- a/lib/screens/settings.dart
+++ b/lib/screens/settings.dart
@@ -7,6 +7,7 @@ import 'package:skylight_wallet/consts.dart';
import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/fiat_rate_model.dart';
import 'package:skylight_wallet/models/language_model.dart';
+import 'package:skylight_wallet/models/theme_model.dart';
import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/periodic_tasks.dart';
import 'package:skylight_wallet/services/notifications_service.dart';
@@ -287,6 +288,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
final language = context.watch<LanguageModel>();
+ final theme = context.watch<ThemeModel>();
return Scaffold(
bottomNavigationBar: WalletNavigationBar(selectedIndex: 1),
@@ -299,23 +301,27 @@ class _SettingsScreenState extends State<SettingsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(
- i18n.settingsNotifyNewTxsLabel,
- style: TextStyle(fontSize: 18),
- ),
- Switch(
- value: _newTxNotificationsEnabled,
- onChanged: _setTxNotificationsEnabled,
+ Text(i18n.settingsThemeLabel, style: TextStyle(fontSize: 18)),
+ DropdownButton<String>(
+ value: theme.theme,
+ onChanged: theme.setTheme,
+ items: [
+ DropdownMenuItem<String>(
+ value: 'system',
+ child: Text(i18n.settingsThemeSystem),
+ ),
+ DropdownMenuItem<String>(
+ value: 'light',
+ child: Text(i18n.settingsThemeLight),
+ ),
+ DropdownMenuItem<String>(
+ value: 'dark',
+ child: Text(i18n.settingsThemeDark),
+ ),
+ ],
),
],
),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(i18n.settingsAppLockLabel, style: TextStyle(fontSize: 18)),
- Switch(value: _appLockEnabled, onChanged: _setAppLockEnabled),
- ],
- ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -354,6 +360,26 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
],
),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(i18n.settingsAppLockLabel, style: TextStyle(fontSize: 18)),
+ Switch(value: _appLockEnabled, onChanged: _setAppLockEnabled),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ i18n.settingsNotifyNewTxsLabel,
+ style: TextStyle(fontSize: 18),
+ ),
+ Switch(
+ value: _newTxNotificationsEnabled,
+ onChanged: _setTxNotificationsEnabled,
+ ),
+ ],
+ ),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -367,7 +393,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
),
Text(
i18n.settingsVerboseLoggingDescription,
- style: TextStyle(fontSize: 13, color: Colors.grey[600]),
+ style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
diff --git a/lib/services/shared_preferences_service.dart b/lib/services/shared_preferences_service.dart
index b19b75c..4509a27 100644
--- a/lib/services/shared_preferences_service.dart
+++ b/lib/services/shared_preferences_service.dart
@@ -4,6 +4,7 @@ class SharedPreferencesKeys {
static const String language = 'language';
static const String fiatCurrency = 'fiatCurrency';
static const String fiatRate = 'fiatRate';
+ static const String theme = 'theme';
static const String notificationsEnabled = 'notificationsEnabled';
static const String appLockEnabled = 'appLockEnabled';
static const String verboseLoggingEnabled = 'verboseLoggingEnabled';
diff --git a/lib/widgets/status_icon.dart b/lib/widgets/status_icon.dart
index 2cedbe8..9e38445 100644
--- a/lib/widgets/status_icon.dart
+++ b/lib/widgets/status_icon.dart
@@ -3,13 +3,16 @@ import 'package:flutter/material.dart';
enum StatusIconStatus { loading, complete, fail }
class StatusIcon extends StatelessWidget {
+ const StatusIcon({super.key, required this.child, required this.status});
+
final Widget child;
final StatusIconStatus status;
- const StatusIcon({super.key, required this.child, required this.status});
-
@override
Widget build(BuildContext context) {
+ final brightness = Theme.of(context).brightness;
+ final isDarkTheme = brightness == Brightness.dark;
+
return SizedBox(
width: 22,
height: 22,
@@ -24,7 +27,7 @@ class StatusIcon extends StatelessWidget {
width: 12,
height: 12,
decoration: BoxDecoration(
- color: Colors.white,
+ color: isDarkTheme ? Colors.grey[900] : Colors.white,
shape: BoxShape.circle,
),
child: status == StatusIconStatus.complete
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.