feat: add settings feature
This commit is contained in:
parent
bc0444f873
commit
b2ac63d22b
|
@ -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
|
||||
|
|
|
@ -6,6 +6,3 @@ targets:
|
|||
- lib/**
|
||||
- $package$
|
||||
builders:
|
||||
pdf_signature:
|
||||
generate_for:
|
||||
- test/features/**
|
||||
|
|
48
lib/app.dart
48
lib/app.dart
|
@ -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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
});
|
|
@ -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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue