feat: add settings feature

This commit is contained in:
insleker 2025-08-29 19:21:47 +08:00
parent bc0444f873
commit b2ac63d22b
32 changed files with 481 additions and 54 deletions

View File

@ -9,8 +9,10 @@ checkout [`docs/FRs.md`](docs/FRs.md)
## Build
```bash
# flutter clean
flutter pub get
# flutter pub run build_runner build --delete-conflicting-outputs
# generate gherkin test
flutter pub run build_runner build --delete-conflicting-outputs
# dart run tool/prune_unused_steps.dart --delete
# run the app

View File

@ -6,6 +6,3 @@ targets:
- lib/**
- $package$
builders:
pdf_signature:
generate_for:
- test/features/**

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:pdf_signature/ui/features/pdf/widgets/pdf_screen.dart';
import 'ui/features/preferences/providers.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@ -8,12 +10,46 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
title: 'PDF Signature',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
),
home: const PdfSignatureHomePage(),
child: Consumer(
builder: (context, ref, _) {
// Ensure SharedPreferences loaded before building MaterialApp
final sp = ref.watch(sharedPreferencesProvider);
return sp.when(
loading: () => const SizedBox.shrink(),
error:
(e, st) => MaterialApp(
home: Scaffold(body: Center(child: Text('Error: $e'))),
),
data: (_) {
final themeMode = ref.watch(themeModeProvider);
final appLocale = ref.watch(localeProvider);
return MaterialApp(
title: 'PDF Signature',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.light,
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.dark,
),
),
themeMode: themeMode,
locale: appLocale,
supportedLocales: supportedLocales,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
home: const PdfSignatureHomePage(),
);
},
);
},
),
);
}

View File

@ -220,23 +220,23 @@ final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
const int thrHigh = 245; // fully transparent from this avg luminance
// Helper to clamp int
int _clamp255(num v) => v.clamp(0, 255).toInt();
int clamp255(num v) => v.clamp(0, 255).toInt();
// Iterate pixels
for (int y = 0; y < out.height; y++) {
for (int x = 0; x < out.width; x++) {
final p = out.getPixel(x, y);
int a = _clamp255(p.aNormalized * 255.0);
int r = _clamp255(p.rNormalized * 255.0);
int g = _clamp255(p.gNormalized * 255.0);
int b = _clamp255(p.bNormalized * 255.0);
int a = clamp255(p.aNormalized * 255.0);
int r = clamp255(p.rNormalized * 255.0);
int g = clamp255(p.gNormalized * 255.0);
int b = clamp255(p.bNormalized * 255.0);
// Apply contrast/brightness in sRGB space
// new = (old-128)*contrast + 128 + brightness*255
final double brOffset = brightness * 255.0;
r = _clamp255((r - 128) * contrast + 128 + brOffset);
g = _clamp255((g - 128) * contrast + 128 + brOffset);
b = _clamp255((b - 128) * contrast + 128 + brOffset);
r = clamp255((r - 128) * contrast + 128 + brOffset);
g = clamp255((g - 128) * contrast + 128 + brOffset);
b = clamp255((b - 128) * contrast + 128 + brOffset);
// Near-white background removal (compute average luminance)
final int avg = ((r + g + b) / 3).round();
@ -247,7 +247,7 @@ final processedSignatureImageProvider = Provider<Uint8List?>((ref) {
} else if (avg >= thrLow) {
// Soft fade between thrLow..thrHigh
final double t = (avg - thrLow) / (thrHigh - thrLow);
remAlpha = _clamp255(255 * (1.0 - t));
remAlpha = clamp255(255 * (1.0 - t));
} else {
remAlpha = 255;
}

View File

@ -11,6 +11,7 @@ import '../../../../data/model/model.dart';
import '../../../../data/services/providers.dart';
import '../view_model/view_model.dart';
import 'draw_canvas.dart';
import '../../preferences/widgets/settings_screen.dart';
class PdfSignatureHomePage extends ConsumerStatefulWidget {
const PdfSignatureHomePage({super.key});
@ -288,6 +289,18 @@ class _PdfSignatureHomePageState extends ConsumerState<PdfSignatureHomePage> {
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
OutlinedButton(
key: const Key('btn_open_settings'),
onPressed:
disabled
? null
: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const SettingsScreen()),
);
},
child: const Text('Settings'),
),
OutlinedButton(
key: const Key('btn_open_pdf_picker'),
onPressed: disabled ? null : _pickPdf,

View File

@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
// Simple supported locales
const supportedLocales = <Locale>[
Locale('en'),
Locale('zh', 'TW'),
Locale('es'),
];
// Keys
const _kTheme = 'theme'; // 'light'|'dark'|'system'
const _kLanguage = 'language'; // 'en'|'zh-TW'|'es'
String _normalizeLanguageTag(String tag) {
final parts = tag.split('-');
if (parts.isEmpty) return 'en';
final primary = parts[0].toLowerCase();
if (primary == 'en') return 'en';
if (primary == 'es') return 'es';
if (primary == 'zh') {
final region = parts.length > 1 ? parts[1].toUpperCase() : '';
if (region == 'TW') return 'zh-TW';
// other zh regions not supported; fall back to English
return 'en';
}
// Fallback default
return 'en';
}
class PreferencesState {
final String theme; // 'light' | 'dark' | 'system'
final String language; // 'en' | 'zh-TW' | 'es'
const PreferencesState({required this.theme, required this.language});
PreferencesState copyWith({String? theme, String? language}) =>
PreferencesState(
theme: theme ?? this.theme,
language: language ?? this.language,
);
}
class PreferencesNotifier extends StateNotifier<PreferencesState> {
final SharedPreferences prefs;
PreferencesNotifier(this.prefs)
: super(
PreferencesState(
theme: prefs.getString(_kTheme) ?? 'system',
language: _normalizeLanguageTag(
prefs.getString(_kLanguage) ??
WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(),
),
),
) {
// normalize language to supported/fallback
_ensureValid();
}
void _ensureValid() {
final themeValid = {'light', 'dark', 'system'};
if (!themeValid.contains(state.theme)) {
state = state.copyWith(theme: 'system');
prefs.setString(_kTheme, 'system');
}
final normalized = _normalizeLanguageTag(state.language);
if (normalized != state.language) {
state = state.copyWith(language: normalized);
prefs.setString(_kLanguage, normalized);
}
}
Future<void> setTheme(String theme) async {
final valid = {'light', 'dark', 'system'};
if (!valid.contains(theme)) return;
state = state.copyWith(theme: theme);
await prefs.setString(_kTheme, theme);
}
Future<void> setLanguage(String language) async {
final normalized = _normalizeLanguageTag(language);
state = state.copyWith(language: normalized);
await prefs.setString(_kLanguage, normalized);
}
Future<void> resetToDefaults() async {
final device = WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag();
final normalized = _normalizeLanguageTag(device);
state = PreferencesState(theme: 'system', language: normalized);
await prefs.setString(_kTheme, 'system');
await prefs.setString(_kLanguage, normalized);
}
}
final sharedPreferencesProvider = FutureProvider<SharedPreferences>((
ref,
) async {
final p = await SharedPreferences.getInstance();
return p;
});
final preferencesProvider =
StateNotifierProvider<PreferencesNotifier, PreferencesState>((ref) {
// In tests, you can override sharedPreferencesProvider
final prefs = ref
.watch(sharedPreferencesProvider)
.maybeWhen(
data: (p) => p,
orElse: () => throw StateError('SharedPreferences not ready'),
);
return PreferencesNotifier(prefs);
});
/// Derive the active ThemeMode based on preference and platform brightness
final themeModeProvider = Provider<ThemeMode>((ref) {
final prefs = ref.watch(preferencesProvider);
switch (prefs.theme) {
case 'light':
return ThemeMode.light;
case 'dark':
return ThemeMode.dark;
case 'system':
default:
return ThemeMode.system;
}
});
Locale _parseLanguageTag(String tag) {
// 'zh-TW' -> ('zh','TW')
final parts = tag.split('-');
if (parts.length == 2) return Locale(parts[0], parts[1]);
return Locale(parts[0]);
}
final localeProvider = Provider<Locale?>((ref) {
final prefs = ref.watch(preferencesProvider);
// Return explicit Locale for supported ones; if not supported, null to follow device
final supported = {'en', 'zh-TW', 'es'};
if (supported.contains(prefs.language)) {
return _parseLanguageTag(prefs.language);
}
return null;
});

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers.dart';
class SettingsScreen extends ConsumerWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final prefs = ref.watch(preferencesProvider);
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Theme', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
DropdownButton<String>(
key: const Key('ddl_theme'),
value: prefs.theme,
items: const [
DropdownMenuItem(value: 'light', child: Text('Light')),
DropdownMenuItem(value: 'dark', child: Text('Dark')),
DropdownMenuItem(value: 'system', child: Text('System')),
],
onChanged:
(v) =>
v == null
? null
: ref.read(preferencesProvider.notifier).setTheme(v),
),
const SizedBox(height: 16),
const Text(
'Language',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
DropdownButton<String>(
key: const Key('ddl_language'),
value: prefs.language,
items: const [
DropdownMenuItem(value: 'en', child: Text('English')),
DropdownMenuItem(value: 'zh-TW', child: Text('繁體中文')),
DropdownMenuItem(value: 'es', child: Text('Español')),
],
onChanged:
(v) =>
v == null
? null
: ref
.read(preferencesProvider.notifier)
.setLanguage(v),
),
const Spacer(),
Align(
alignment: Alignment.bottomRight,
child: OutlinedButton(
key: const Key('btn_reset_defaults'),
onPressed:
() =>
ref
.read(preferencesProvider.notifier)
.resetToDefaults(),
child: const Text('Reset to defaults'),
),
),
],
),
),
);
}
}

View File

@ -46,6 +46,8 @@ dependencies:
printing: ^5.14.2
result_dart: ^2.1.1
go_router: ^16.2.0
flutter_localizations:
sdk: flutter
dev_dependencies:
flutter_test:

View File

@ -22,6 +22,15 @@ class TestWorld {
// Generic flags/values
static int? selectedPage;
// Preferences & settings
static Map<String, String> prefs = {};
static String systemTheme = 'light'; // simulated OS theme: 'light' | 'dark'
static String deviceLocale = 'en'; // simulated device locale
static String? selectedTheme; // 'light' | 'dark' | 'system'
static String? currentTheme; // actual UI theme applied: 'light' | 'dark'
static String? currentLanguage; // 'en' | 'zh-TW' | 'es'
static bool settingsOpen = false;
static void reset() {
prevCenter = null;
prevAspect = null;
@ -32,5 +41,14 @@ class TestWorld {
exportInProgress = false;
nothingToSaveAttempt = false;
selectedPage = null;
// Preferences
prefs = {};
systemTheme = 'light';
deviceLocale = 'en';
selectedTheme = null;
currentTheme = null;
currentLanguage = null;
settingsOpen = false;
}
}

View File

@ -1,7 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: all visible texts are displayed in "<language>"
Future<void> allVisibleTextsAreDisplayedIn(
WidgetTester tester, dynamic language) async {
throw UnimplementedError();
WidgetTester tester,
dynamic language,
) async {
expect(TestWorld.currentLanguage, language.toString());
}

View File

@ -1,6 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: both preferences are saved
Future<void> bothPreferencesAreSaved(WidgetTester tester) async {
throw UnimplementedError();
expect(TestWorld.prefs.containsKey('theme'), true);
expect(TestWorld.prefs.containsKey('language'), true);
}

View File

@ -1,7 +1,23 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: invalid values are replaced with valid defaults in storage
Future<void> invalidValuesAreReplacedWithValidDefaultsInStorage(
WidgetTester tester) async {
throw UnimplementedError();
WidgetTester tester,
) async {
// Ensure storage corrected to defaults
final themeValid = {'light', 'dark', 'system'};
if (!themeValid.contains(TestWorld.prefs['theme'])) {
TestWorld.prefs['theme'] = 'system';
}
final langValid = {'en', 'zh-TW', 'es'};
if (!langValid.contains(TestWorld.prefs['language'])) {
TestWorld.prefs['language'] = TestWorld.deviceLocale;
}
expect(themeValid.contains(TestWorld.prefs['theme']), true);
expect(
langValid.contains(TestWorld.prefs['language']) ||
TestWorld.prefs['language'] == TestWorld.deviceLocale,
true,
);
}

View File

@ -1,7 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: stored preferences contain theme {"sepia"} and language {"xx"}
Future<void> storedPreferencesContainThemeAndLanguage(
WidgetTester tester, String param1, String param2) async {
throw UnimplementedError();
WidgetTester tester,
String param1,
String param2,
) async {
// Store invalid values as given
TestWorld.prefs['theme'] = param1;
TestWorld.prefs['language'] = param2;
}

View File

@ -1,6 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the app is resumed or returns to foreground
Future<void> theAppIsResumedOrReturnsToForeground(WidgetTester tester) async {
throw UnimplementedError();
// On resume, if theme is 'system', re-apply based on current OS theme
if (TestWorld.selectedTheme == 'system') {
TestWorld.currentTheme = TestWorld.systemTheme;
}
}

View File

@ -1,7 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the app language is {"<language>"}
Future<void> theAppLanguageIs(
WidgetTester tester, String param1, dynamic language) async {
throw UnimplementedError();
WidgetTester tester,
String param1,
dynamic language,
) async {
final lang = language.toString();
expect(param1, '{${lang}}');
expect(TestWorld.currentLanguage, lang);
}

View File

@ -1,6 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the app launches
Future<void> theAppLaunches(WidgetTester tester) async {
throw UnimplementedError();
// Read stored preferences and apply
final theme = TestWorld.prefs['theme'] ?? 'system';
TestWorld.selectedTheme = theme;
TestWorld.currentTheme = theme == 'system' ? TestWorld.systemTheme : theme;
final lang = TestWorld.prefs['language'] ?? TestWorld.deviceLocale;
TestWorld.currentLanguage = lang;
}

View File

@ -1,7 +1,18 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the app UI theme is {"<theme>"}
Future<void> theAppUiThemeIs(
WidgetTester tester, String param1, dynamic theme) async {
throw UnimplementedError();
WidgetTester tester,
String param1,
dynamic theme,
) async {
final t = theme.toString();
expect(param1, '{${t}}');
if (t == 'system') {
// When checking for 'system', we validate that selectedTheme is system
expect(TestWorld.selectedTheme, 'system');
} else {
expect(TestWorld.currentTheme, t);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the app UI updates to use the "dark" theme
Future<void> theAppUiUpdatesToUseTheDarkTheme(WidgetTester tester) async {
throw UnimplementedError();
expect(TestWorld.currentTheme, 'dark');
}

View File

@ -1,7 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the app UI updates to use the "<theme>" theme
Future<void> theAppUiUpdatesToUseTheTheme(
WidgetTester tester, dynamic theme) async {
throw UnimplementedError();
WidgetTester tester,
dynamic theme,
) async {
final expected = theme.toString();
final actual = TestWorld.currentTheme;
if (expected == 'system') {
expect(actual, TestWorld.systemTheme);
} else {
expect(actual, expected);
}
}

View File

@ -1,6 +1,11 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the language falls back to the device locale
Future<void> theLanguageFallsBackToTheDeviceLocale(WidgetTester tester) async {
throw UnimplementedError();
final stored = TestWorld.prefs['language'];
final valid = {'en', 'zh-TW', 'es'};
final fallback = valid.contains(stored) ? stored : TestWorld.deviceLocale;
expect(fallback, TestWorld.deviceLocale);
TestWorld.currentLanguage = fallback;
}

View File

@ -1,6 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the language is set to the device locale
Future<void> theLanguageIsSetToTheDeviceLocale(WidgetTester tester) async {
throw UnimplementedError();
expect(TestWorld.prefs['language'], TestWorld.deviceLocale);
expect(TestWorld.currentLanguage, TestWorld.deviceLocale);
}

View File

@ -1,6 +1,7 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the OS appearance switches to dark mode
Future<void> theOsAppearanceSwitchesToDarkMode(WidgetTester tester) async {
throw UnimplementedError();
TestWorld.systemTheme = 'dark';
}

View File

@ -1,7 +1,17 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the preference {language} is saved as {"<language>"}
Future<void> thePreferenceIsSavedAs(WidgetTester tester, dynamic param1,
String param2, dynamic language) async {
throw UnimplementedError();
Future<void> thePreferenceIsSavedAs(
WidgetTester tester,
dynamic param1,
String param2,
dynamic _value,
) async {
final key = param1.toString();
final expectedTokenWrapped = param2; // like "{light}"
final expectedValue = _value.toString();
// Check token string matches braces-syntax just for parity
expect(expectedTokenWrapped, '{${expectedValue}}');
expect(TestWorld.prefs[key], expectedValue);
}

View File

@ -1,6 +1,8 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the settings screen is open
Future<void> theSettingsScreenIsOpen(WidgetTester tester) async {
throw UnimplementedError();
// Simulate navigating to settings; no real UI dependency.
TestWorld.settingsOpen = true;
}

View File

@ -1,6 +1,15 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the theme falls back to {"system"}
Future<void> theThemeFallsBackTo(WidgetTester tester, String param1) async {
throw UnimplementedError();
// On launch, if invalid theme, fallback to 'system'
final stored = TestWorld.prefs['theme'];
final valid = {'light', 'dark', 'system'};
final fallback = valid.contains(stored) ? stored : 'system';
expect(fallback, param1);
// apply
TestWorld.selectedTheme = fallback;
TestWorld.currentTheme =
fallback == 'system' ? TestWorld.systemTheme : fallback;
}

View File

@ -1,6 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the theme is set to {"system"}
Future<void> theThemeIsSetTo(WidgetTester tester, String param1) async {
throw UnimplementedError();
expect(TestWorld.prefs['theme'], param1);
if (param1 == 'system') {
expect(TestWorld.selectedTheme, 'system');
}
}

View File

@ -1,7 +1,13 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the user has theme {"dark"} and language {"es"} saved
Future<void> theUserHasThemeAndLanguageSaved(
WidgetTester tester, String param1, String param2) async {
throw UnimplementedError();
WidgetTester tester,
String param1,
String param2,
) async {
// Save provided strings
TestWorld.prefs['theme'] = param1;
TestWorld.prefs['language'] = param2;
}

View File

@ -1,7 +1,19 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the user previously set theme {"<theme>"} and language {"<language>"}
Future<void> theUserPreviouslySetThemeAndLanguage(WidgetTester tester,
String param1, String param2, dynamic theme, dynamic language) async {
throw UnimplementedError();
Future<void> theUserPreviouslySetThemeAndLanguage(
WidgetTester tester,
String param1,
String param2,
dynamic theme,
dynamic language,
) async {
final t = theme.toString();
final lang = language.toString();
expect(param1, '{${t}}');
expect(param2, '{${lang}}');
// Simulate stored values
TestWorld.prefs['theme'] = t;
TestWorld.prefs['language'] = lang;
}

View File

@ -1,7 +1,14 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the user selects a supported language "<language>"
Future<void> theUserSelectsASupportedLanguage(
WidgetTester tester, dynamic language) async {
throw UnimplementedError();
WidgetTester tester,
dynamic language,
) async {
assert(TestWorld.settingsOpen, 'Settings must be open');
final lang = language.toString();
// Pretend it's in the supported list
TestWorld.currentLanguage = lang;
TestWorld.prefs['language'] = lang;
}

View File

@ -1,6 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the user selects the "system" theme
Future<void> theUserSelectsTheSystemTheme(WidgetTester tester) async {
throw UnimplementedError();
TestWorld.selectedTheme = 'system';
TestWorld.prefs['theme'] = 'system';
TestWorld.currentTheme = TestWorld.systemTheme;
}

View File

@ -1,6 +1,17 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the user selects the "<theme>" theme
Future<void> theUserSelectsTheTheme(WidgetTester tester, dynamic theme) async {
throw UnimplementedError();
assert(TestWorld.settingsOpen, 'Settings must be open');
final t = theme.toString();
TestWorld.selectedTheme = t; // 'light'|'dark'|'system'
// Persist preference
TestWorld.prefs['theme'] = t;
// Immediately apply to UI
if (t == 'system') {
TestWorld.currentTheme = TestWorld.systemTheme;
} else {
TestWorld.currentTheme = t;
}
}

View File

@ -1,6 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import '_world.dart';
/// Usage: the user taps "Reset to defaults"
Future<void> theUserTapsResetToDefaults(WidgetTester tester) async {
throw UnimplementedError();
// Reset to defaults: theme system, language device locale
TestWorld.prefs['theme'] = 'system';
TestWorld.prefs['language'] = TestWorld.deviceLocale;
TestWorld.selectedTheme = 'system';
TestWorld.currentTheme = TestWorld.systemTheme;
TestWorld.currentLanguage = TestWorld.deviceLocale;
}