forked from baron/baron-sso
로그인 화면 플랫 UI 수정
This commit is contained in:
@@ -2,10 +2,15 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class ThemeController extends ValueNotifier<ThemeMode> {
|
class ThemeController extends ValueNotifier<ThemeMode> {
|
||||||
ThemeController._() : super(ThemeMode.light);
|
ThemeController._(this.storageKey) : super(ThemeMode.light);
|
||||||
|
|
||||||
static const storageKey = 'userfront_theme';
|
static const appStorageKey = 'userfront_theme';
|
||||||
static final ThemeController instance = ThemeController._();
|
static const authStorageKey = 'userfront_auth_theme';
|
||||||
|
static final ThemeController app = ThemeController._(appStorageKey);
|
||||||
|
static final ThemeController auth = ThemeController._(authStorageKey);
|
||||||
|
static final ThemeController instance = app;
|
||||||
|
|
||||||
|
final String storageKey;
|
||||||
|
|
||||||
bool get isDark => value == ThemeMode.dark;
|
bool get isDark => value == ThemeMode.dark;
|
||||||
|
|
||||||
|
|||||||
44
userfront/lib/core/theme/theme_scope.dart
Normal file
44
userfront/lib/core/theme/theme_scope.dart
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'app_theme.dart';
|
||||||
|
import 'theme_controller.dart';
|
||||||
|
|
||||||
|
class ThemeScope extends InheritedWidget {
|
||||||
|
const ThemeScope({super.key, required this.controller, required Widget child})
|
||||||
|
: super(child: child);
|
||||||
|
|
||||||
|
final ThemeController controller;
|
||||||
|
|
||||||
|
static ThemeController of(BuildContext context) {
|
||||||
|
final scope = context.dependOnInheritedWidgetOfExactType<ThemeScope>();
|
||||||
|
return scope?.controller ?? ThemeController.app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(ThemeScope oldWidget) {
|
||||||
|
return oldWidget.controller != controller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScopedTheme extends StatelessWidget {
|
||||||
|
const ScopedTheme({super.key, required this.controller, required this.child});
|
||||||
|
|
||||||
|
final ThemeController controller;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ThemeScope(
|
||||||
|
controller: controller,
|
||||||
|
child: ValueListenableBuilder<ThemeMode>(
|
||||||
|
valueListenable: controller,
|
||||||
|
builder: (context, mode, _) {
|
||||||
|
return Theme(
|
||||||
|
data: mode == ThemeMode.dark ? buildDarkTheme() : buildLightTheme(),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart' hide tr;
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:userfront/i18n.dart';
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
import '../theme/theme_controller.dart';
|
import '../theme/theme_scope.dart';
|
||||||
|
|
||||||
class ThemeToggleButton extends StatelessWidget {
|
class ThemeToggleButton extends StatelessWidget {
|
||||||
const ThemeToggleButton({super.key, this.compact = false});
|
const ThemeToggleButton({super.key, this.compact = false});
|
||||||
@@ -11,10 +10,11 @@ class ThemeToggleButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
context.locale;
|
Localizations.localeOf(context);
|
||||||
|
final controller = ThemeScope.of(context);
|
||||||
|
|
||||||
return ValueListenableBuilder<ThemeMode>(
|
return ValueListenableBuilder<ThemeMode>(
|
||||||
valueListenable: ThemeController.instance,
|
valueListenable: controller,
|
||||||
builder: (context, mode, _) {
|
builder: (context, mode, _) {
|
||||||
final isLight = mode == ThemeMode.light;
|
final isLight = mode == ThemeMode.light;
|
||||||
final icon = isLight
|
final icon = isLight
|
||||||
@@ -28,13 +28,13 @@ class ThemeToggleButton extends StatelessWidget {
|
|||||||
if (compact) {
|
if (compact) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
tooltip: tooltip,
|
tooltip: tooltip,
|
||||||
onPressed: () => ThemeController.instance.toggle(),
|
onPressed: () => controller.toggle(),
|
||||||
icon: Icon(icon),
|
icon: Icon(icon),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return OutlinedButton.icon(
|
return OutlinedButton.icon(
|
||||||
onPressed: () => ThemeController.instance.toggle(),
|
onPressed: () => controller.toggle(),
|
||||||
icon: Icon(icon, size: 18),
|
icon: Icon(icon, size: 18),
|
||||||
label: Text(label),
|
label: Text(label),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1389,6 +1389,73 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final colorScheme = theme.colorScheme;
|
final colorScheme = theme.colorScheme;
|
||||||
final mutedColor = colorScheme.onSurfaceVariant;
|
final mutedColor = colorScheme.onSurfaceVariant;
|
||||||
|
final inputForegroundColor = colorScheme.brightness == Brightness.dark
|
||||||
|
? const Color(0xFFE2E8F0)
|
||||||
|
: const Color(0xFF334155);
|
||||||
|
final primaryColor = colorScheme.brightness == Brightness.dark
|
||||||
|
? const Color(0xFF93C5FD)
|
||||||
|
: const Color(0xFF1E3A8A);
|
||||||
|
final onPrimaryColor = colorScheme.brightness == Brightness.dark
|
||||||
|
? const Color(0xFF0F172A)
|
||||||
|
: Colors.white;
|
||||||
|
final inputDecorationTheme = theme.inputDecorationTheme.copyWith(
|
||||||
|
filled: false,
|
||||||
|
fillColor: Colors.transparent,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 18, vertical: 18),
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: colorScheme.outline),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: colorScheme.outline),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide(color: primaryColor, width: 1.6),
|
||||||
|
),
|
||||||
|
labelStyle: TextStyle(color: inputForegroundColor),
|
||||||
|
floatingLabelStyle: TextStyle(color: primaryColor),
|
||||||
|
hintStyle: TextStyle(color: inputForegroundColor),
|
||||||
|
prefixIconColor: inputForegroundColor,
|
||||||
|
);
|
||||||
|
final localTheme = theme.copyWith(
|
||||||
|
inputDecorationTheme: inputDecorationTheme,
|
||||||
|
tabBarTheme: theme.tabBarTheme.copyWith(
|
||||||
|
dividerColor: colorScheme.outlineVariant,
|
||||||
|
indicatorColor: primaryColor,
|
||||||
|
labelColor: colorScheme.onSurface,
|
||||||
|
unselectedLabelColor: mutedColor,
|
||||||
|
labelStyle: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
unselectedLabelStyle: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
backgroundColor: primaryColor,
|
||||||
|
foregroundColor: onPrimaryColor,
|
||||||
|
textStyle: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
textButtonTheme: TextButtonThemeData(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: primaryColor,
|
||||||
|
textStyle: theme.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (_verificationOnly && _verificationApproved) {
|
if (_verificationOnly && _verificationApproved) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -1408,16 +1475,18 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
body: LayoutBuilder(
|
body: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
return SingleChildScrollView(
|
return Theme(
|
||||||
|
data: localTheme,
|
||||||
|
child: SingleChildScrollView(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 400),
|
constraints: const BoxConstraints(maxWidth: 480),
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.symmetric(
|
||||||
child: Card(
|
horizontal: 28,
|
||||||
child: Padding(
|
vertical: 40,
|
||||||
padding: const EdgeInsets.all(24),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
@@ -1425,20 +1494,22 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
Text(
|
Text(
|
||||||
tr('ui.userfront.app_title'),
|
tr('ui.userfront.app_title'),
|
||||||
style: theme.textTheme.headlineMedium?.copyWith(
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontSize: 34,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: -0.7,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
if (_drySendEnabled) ...[
|
if (_drySendEnabled) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 20),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 12,
|
horizontal: 14,
|
||||||
vertical: 10,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFFFF3CD),
|
color: const Color(0xFFFFF3CD),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: const Color(0xFFFFC107),
|
color: const Color(0xFFFFC107),
|
||||||
),
|
),
|
||||||
@@ -1449,7 +1520,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
Icons.warning_amber_rounded,
|
Icons.warning_amber_rounded,
|
||||||
color: Color(0xFF8A6D3B),
|
color: Color(0xFF8A6D3B),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('msg.userfront.login.dry_send'),
|
tr('msg.userfront.login.dry_send'),
|
||||||
@@ -1463,32 +1534,41 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 52),
|
||||||
|
Padding(
|
||||||
TabBar(
|
padding: const EdgeInsets.symmetric(horizontal: 34),
|
||||||
|
child: TabBar(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
|
indicatorSize: TabBarIndicatorSize.label,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(text: tr('ui.userfront.login.tabs.password')),
|
Tab(text: tr('ui.userfront.login.tabs.password')),
|
||||||
Tab(text: tr('ui.userfront.login.tabs.link')),
|
Tab(text: tr('ui.userfront.login.tabs.link')),
|
||||||
Tab(text: tr('ui.userfront.login.tabs.qr')),
|
Tab(text: tr('ui.userfront.login.tabs.qr')),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
),
|
||||||
|
const SizedBox(height: 28),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 350,
|
height: 360,
|
||||||
child: TabBarView(
|
child: TabBarView(
|
||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 20),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 356,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TextField(
|
TextField(
|
||||||
key: const ValueKey(
|
key: const ValueKey(
|
||||||
'password_login_id_input',
|
'password_login_id_input',
|
||||||
),
|
),
|
||||||
controller: _passwordLoginIdController,
|
controller:
|
||||||
|
_passwordLoginIdController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText:
|
labelText:
|
||||||
_loginIdLabel ??
|
_loginIdLabel ??
|
||||||
@@ -1497,12 +1577,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
),
|
),
|
||||||
prefixIcon: const Icon(
|
prefixIcon: const Icon(
|
||||||
Icons.person_outline,
|
Icons.person_outline,
|
||||||
|
size: 22,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onSubmitted: (_) =>
|
onSubmitted: (_) =>
|
||||||
_handlePasswordLogin(),
|
_handlePasswordLogin(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 18),
|
||||||
TextField(
|
TextField(
|
||||||
key: const ValueKey(
|
key: const ValueKey(
|
||||||
'password_login_password_input',
|
'password_login_password_input',
|
||||||
@@ -1516,13 +1597,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
),
|
),
|
||||||
prefixIcon: const Icon(
|
prefixIcon: const Icon(
|
||||||
Icons.lock_outline,
|
Icons.lock_outline,
|
||||||
|
size: 22,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onSubmitted: (_) =>
|
onSubmitted: (_) =>
|
||||||
_handlePasswordLogin(),
|
_handlePasswordLogin(),
|
||||||
),
|
),
|
||||||
if (_isPasswordCapsLockOn) ...[
|
if (_isPasswordCapsLockOn) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
@@ -1544,17 +1626,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 28),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
key: const ValueKey(
|
key: const ValueKey(
|
||||||
'password_login_submit_button',
|
'password_login_submit_button',
|
||||||
),
|
),
|
||||||
onPressed: _handlePasswordLogin,
|
onPressed: _handlePasswordLogin,
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
minimumSize: const Size.fromHeight(
|
|
||||||
50,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr(
|
||||||
'ui.userfront.login.action.submit',
|
'ui.userfront.login.action.submit',
|
||||||
@@ -1564,9 +1641,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
padding: const EdgeInsets.only(top: 20),
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 356,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (_linkPendingRef == null) ...[
|
if (_linkPendingRef == null) ...[
|
||||||
@@ -1579,29 +1663,30 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
hintText: '',
|
hintText: '',
|
||||||
prefixIcon: const Icon(
|
prefixIcon: const Icon(
|
||||||
Icons.person_outline,
|
Icons.person_outline,
|
||||||
|
size: 22,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onSubmitted: (_) =>
|
onSubmitted: (_) =>
|
||||||
_handleLinkLogin(),
|
_handleLinkLogin(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 28),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _handleLinkLogin,
|
onPressed: _handleLinkLogin,
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
minimumSize: const Size.fromHeight(
|
|
||||||
50,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('ui.userfront.login.link.send'),
|
tr(
|
||||||
|
'ui.userfront.login.link.send',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
tr('msg.userfront.login.link.helper'),
|
tr(
|
||||||
|
'msg.userfront.login.link.helper',
|
||||||
|
),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: mutedColor,
|
color: mutedColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
height: 1.5,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@@ -1618,15 +1703,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 14),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(_resetLinkLoginState);
|
setState(_resetLinkLoginState);
|
||||||
},
|
},
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
minimumSize:
|
|
||||||
const Size.fromHeight(45),
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('ui.common.refresh'),
|
tr('ui.common.refresh'),
|
||||||
),
|
),
|
||||||
@@ -1639,10 +1720,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: mutedColor,
|
color: mutedColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
height: 1.5,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 14),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -1661,11 +1743,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: mutedColor,
|
color: mutedColor,
|
||||||
),
|
),
|
||||||
|
counterText: '',
|
||||||
),
|
),
|
||||||
maxLength: 2,
|
maxLength: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 4,
|
flex: 4,
|
||||||
child: TextField(
|
child: TextField(
|
||||||
@@ -1681,6 +1764,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: mutedColor,
|
color: mutedColor,
|
||||||
),
|
),
|
||||||
|
counterText: '',
|
||||||
suffixText:
|
suffixText:
|
||||||
_linkExpireSeconds > 0
|
_linkExpireSeconds > 0
|
||||||
? tr(
|
? tr(
|
||||||
@@ -1698,7 +1782,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 14),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final prefix =
|
final prefix =
|
||||||
@@ -1719,19 +1803,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_verifyShortCode(prefix + digits);
|
_verifyShortCode(
|
||||||
|
prefix + digits,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
style: FilledButton.styleFrom(
|
|
||||||
minimumSize:
|
|
||||||
const Size.fromHeight(45),
|
|
||||||
),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr(
|
||||||
'ui.userfront.login.short_code.submit',
|
'ui.userfront.login.short_code.submit',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 14),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_linkResendSeconds > 0) {
|
if (_linkResendSeconds > 0) {
|
||||||
@@ -1749,7 +1831,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
}
|
}
|
||||||
final loginId =
|
final loginId =
|
||||||
_lastLinkLoginId ??
|
_lastLinkLoginId ??
|
||||||
_linkIdController.text.trim();
|
_linkIdController.text
|
||||||
|
.trim();
|
||||||
if (loginId.isEmpty) {
|
if (loginId.isEmpty) {
|
||||||
_showError(
|
_showError(
|
||||||
tr(
|
tr(
|
||||||
@@ -1831,7 +1914,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Column(
|
Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
@@ -1842,25 +1926,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.login.qr_expired'),
|
||||||
'msg.userfront.login.qr_expired',
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: mutedColor,
|
color: mutedColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 14),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: _startQrFlow,
|
onPressed: _startQrFlow,
|
||||||
style: FilledButton.styleFrom(
|
child: Text(tr('ui.common.refresh')),
|
||||||
minimumSize:
|
|
||||||
const Size.fromHeight(45),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
tr('ui.common.refresh'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -1870,13 +1946,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
CrossAxisAlignment.center,
|
CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(18),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: colorScheme.outline,
|
color: colorScheme.outline,
|
||||||
),
|
),
|
||||||
borderRadius:
|
borderRadius: BorderRadius.circular(
|
||||||
BorderRadius.circular(12),
|
18,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: QrImageView(
|
child: QrImageView(
|
||||||
data: _qrImageBase64!,
|
data: _qrImageBase64!,
|
||||||
@@ -1885,7 +1962,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 14),
|
||||||
Text(
|
Text(
|
||||||
_qrRemainingSeconds > 0
|
_qrRemainingSeconds > 0
|
||||||
? tr(
|
? tr(
|
||||||
@@ -1902,7 +1979,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: _qrRemainingSeconds > 30
|
color: _qrRemainingSeconds > 30
|
||||||
? Colors.blue
|
? primaryColor
|
||||||
: Colors.red,
|
: Colors.red,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -1916,23 +1993,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: mutedColor,
|
color: mutedColor,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: _startQrFlow,
|
onPressed: _startQrFlow,
|
||||||
child: Text(
|
child: Text(
|
||||||
tr(
|
tr('ui.userfront.login.qr.refresh'),
|
||||||
'ui.userfront.login.qr.refresh',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
Text(
|
Text(
|
||||||
tr(
|
tr('msg.userfront.login.qr.load_failed'),
|
||||||
'msg.userfront.login.qr.load_failed',
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1940,12 +2014,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 18),
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () =>
|
onPressed: () => context.push('/forgot-password'),
|
||||||
context.push('/forgot-password'),
|
|
||||||
child: Text(
|
child: Text(
|
||||||
tr('ui.userfront.login.forgot_password'),
|
tr('ui.userfront.login.forgot_password'),
|
||||||
),
|
),
|
||||||
@@ -1962,15 +2035,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => context.push('/signup'),
|
onPressed: () => context.push('/signup'),
|
||||||
child: Text(
|
child: Text(tr('ui.userfront.login.signup')),
|
||||||
tr('ui.userfront.login.signup'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 12),
|
||||||
const Wrap(
|
const Wrap(
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
spacing: 10,
|
spacing: 10,
|
||||||
@@ -1983,7 +2054,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import 'core/services/web_window.dart';
|
|||||||
import 'core/notifiers/auth_notifier.dart';
|
import 'core/notifiers/auth_notifier.dart';
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
import 'core/theme/theme_controller.dart';
|
import 'core/theme/theme_controller.dart';
|
||||||
|
import 'core/theme/theme_scope.dart';
|
||||||
import 'core/i18n/locale_gate.dart';
|
import 'core/i18n/locale_gate.dart';
|
||||||
import 'core/i18n/locale_registry.dart';
|
import 'core/i18n/locale_registry.dart';
|
||||||
import 'core/i18n/locale_utils.dart';
|
import 'core/i18n/locale_utils.dart';
|
||||||
@@ -108,7 +109,8 @@ void main() async {
|
|||||||
|
|
||||||
// 0. Initialize Logger
|
// 0. Initialize Logger
|
||||||
LoggerService.init();
|
LoggerService.init();
|
||||||
await ThemeController.instance.restore();
|
await ThemeController.app.restore();
|
||||||
|
await ThemeController.auth.restore();
|
||||||
|
|
||||||
// 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화
|
// 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화
|
||||||
await _loadBundledFonts();
|
await _loadBundledFonts();
|
||||||
@@ -180,12 +182,18 @@ final _router = GoRouter(
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return const DashboardScreen();
|
return ScopedTheme(
|
||||||
|
controller: ThemeController.app,
|
||||||
|
child: const DashboardScreen(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'profile',
|
path: 'profile',
|
||||||
builder: (context, state) => const ProfilePage(),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.app,
|
||||||
|
child: const ProfilePage(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'signin',
|
path: 'signin',
|
||||||
@@ -195,10 +203,13 @@ final _router = GoRouter(
|
|||||||
final redirectUrl =
|
final redirectUrl =
|
||||||
state.uri.queryParameters['redirect_uri'] ??
|
state.uri.queryParameters['redirect_uri'] ??
|
||||||
state.uri.queryParameters['redirect_url'];
|
state.uri.queryParameters['redirect_url'];
|
||||||
return LoginScreen(
|
return ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: LoginScreen(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
loginChallenge: loginChallenge,
|
loginChallenge: loginChallenge,
|
||||||
redirectUrl: redirectUrl,
|
redirectUrl: redirectUrl,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -211,10 +222,13 @@ final _router = GoRouter(
|
|||||||
final redirectUrl =
|
final redirectUrl =
|
||||||
state.uri.queryParameters['redirect_uri'] ??
|
state.uri.queryParameters['redirect_uri'] ??
|
||||||
state.uri.queryParameters['redirect_url'];
|
state.uri.queryParameters['redirect_url'];
|
||||||
return LoginScreen(
|
return ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: LoginScreen(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
loginChallenge: loginChallenge,
|
loginChallenge: loginChallenge,
|
||||||
redirectUrl: redirectUrl,
|
redirectUrl: redirectUrl,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -230,88 +244,137 @@ final _router = GoRouter(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return ConsentScreen(consentChallenge: consentChallenge);
|
return ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: ConsentScreen(consentChallenge: consentChallenge),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'signup',
|
path: 'signup',
|
||||||
builder: (context, state) => const SignupScreen(),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: const SignupScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'registration',
|
path: 'registration',
|
||||||
builder: (context, state) => const SignupScreen(),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: const SignupScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'verify',
|
path: 'verify',
|
||||||
builder: (context, state) => LoginScreen(key: state.pageKey),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: LoginScreen(key: state.pageKey),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'verify/:token',
|
path: 'verify/:token',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final token = state.pathParameters['token'];
|
final token = state.pathParameters['token'];
|
||||||
return LoginScreen(
|
return ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: LoginScreen(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
verificationToken: token,
|
verificationToken: token,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'verification',
|
path: 'verification',
|
||||||
builder: (context, state) => LoginScreen(key: state.pageKey),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: LoginScreen(key: state.pageKey),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'l/:shortCode',
|
path: 'l/:shortCode',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return LoginScreen(key: state.pageKey);
|
return ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: LoginScreen(key: state.pageKey),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'forgot-password',
|
path: 'forgot-password',
|
||||||
builder: (context, state) => const ForgotPasswordScreen(),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: const ForgotPasswordScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'recovery',
|
path: 'recovery',
|
||||||
builder: (context, state) => const ForgotPasswordScreen(),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: const ForgotPasswordScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'reset-password',
|
path: 'reset-password',
|
||||||
builder: (context, state) => const ResetPasswordScreen(),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: const ResetPasswordScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'error',
|
path: 'error',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final params = state.uri.queryParameters;
|
final params = state.uri.queryParameters;
|
||||||
return ErrorScreen(
|
return ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: ErrorScreen(
|
||||||
errorId: params['id'],
|
errorId: params['id'],
|
||||||
errorCode: params['error'],
|
errorCode: params['error'],
|
||||||
description: params['error_description'] ?? params['message'],
|
description:
|
||||||
|
params['error_description'] ?? params['message'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
builder: (context, state) => ErrorScreen(
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: ErrorScreen(
|
||||||
errorCode: 'settings_disabled',
|
errorCode: 'settings_disabled',
|
||||||
description: tr('msg.userfront.settings.disabled'),
|
description: tr('msg.userfront.settings.disabled'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'approve',
|
path: 'approve',
|
||||||
builder: (context, state) =>
|
builder: (context, state) => ScopedTheme(
|
||||||
ApproveQrScreen(pendingRef: state.uri.queryParameters['ref']),
|
controller: ThemeController.auth,
|
||||||
|
child: ApproveQrScreen(
|
||||||
|
pendingRef: state.uri.queryParameters['ref'],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'ql/:ref',
|
path: 'ql/:ref',
|
||||||
builder: (context, state) =>
|
builder: (context, state) => ScopedTheme(
|
||||||
ApproveQrScreen(pendingRef: state.pathParameters['ref']),
|
controller: ThemeController.auth,
|
||||||
|
child: ApproveQrScreen(pendingRef: state.pathParameters['ref']),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'scan',
|
path: 'scan',
|
||||||
builder: (context, state) => const QRScanScreen(),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.auth,
|
||||||
|
child: const QRScanScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'admin/users',
|
path: 'admin/users',
|
||||||
builder: (context, state) => const UserManagementScreen(),
|
builder: (context, state) => ScopedTheme(
|
||||||
|
controller: ThemeController.app,
|
||||||
|
child: const UserManagementScreen(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -369,9 +432,6 @@ class BaronSSOApp extends StatelessWidget {
|
|||||||
final locale =
|
final locale =
|
||||||
localization?.currentLocale ?? Locale(resolvePreferredLocaleCode());
|
localization?.currentLocale ?? Locale(resolvePreferredLocaleCode());
|
||||||
|
|
||||||
return ValueListenableBuilder<ThemeMode>(
|
|
||||||
valueListenable: ThemeController.instance,
|
|
||||||
builder: (context, themeMode, _) {
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
title: tr('ui.userfront.app_title'),
|
title: tr('ui.userfront.app_title'),
|
||||||
localizationsDelegates: delegates,
|
localizationsDelegates: delegates,
|
||||||
@@ -384,10 +444,8 @@ class BaronSSOApp extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
theme: buildLightTheme(),
|
theme: buildLightTheme(),
|
||||||
darkTheme: buildDarkTheme(),
|
darkTheme: buildDarkTheme(),
|
||||||
themeMode: themeMode,
|
themeMode: ThemeMode.light,
|
||||||
routerConfig: _router,
|
routerConfig: _router,
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,25 +8,25 @@ void main() {
|
|||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
SharedPreferences.setMockInitialValues({});
|
SharedPreferences.setMockInitialValues({});
|
||||||
await ThemeController.instance.setThemeMode(ThemeMode.light);
|
await ThemeController.app.setThemeMode(ThemeMode.light);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('저장된 dark 값을 복원한다', () async {
|
test('저장된 dark 값을 복원한다', () async {
|
||||||
SharedPreferences.setMockInitialValues({
|
SharedPreferences.setMockInitialValues({
|
||||||
ThemeController.storageKey: 'dark',
|
ThemeController.appStorageKey: 'dark',
|
||||||
});
|
});
|
||||||
|
|
||||||
await ThemeController.instance.restore();
|
await ThemeController.app.restore();
|
||||||
|
|
||||||
expect(ThemeController.instance.value, ThemeMode.dark);
|
expect(ThemeController.app.value, ThemeMode.dark);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('toggle 결과를 저장한다', () async {
|
test('toggle 결과를 저장한다', () async {
|
||||||
await ThemeController.instance.restore();
|
await ThemeController.app.restore();
|
||||||
await ThemeController.instance.toggle();
|
await ThemeController.app.toggle();
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
expect(ThemeController.instance.value, ThemeMode.dark);
|
expect(ThemeController.app.value, ThemeMode.dark);
|
||||||
expect(prefs.getString(ThemeController.storageKey), 'dark');
|
expect(prefs.getString(ThemeController.appStorageKey), 'dark');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user