1
0
forked from baron/baron-sso

로그인 화면 플랫 UI 수정

This commit is contained in:
2026-04-08 17:45:51 +09:00
parent 332b657add
commit 1e53b66abb
6 changed files with 730 additions and 553 deletions

View File

@@ -2,10 +2,15 @@ import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ThemeController extends ValueNotifier<ThemeMode> {
ThemeController._() : super(ThemeMode.light);
ThemeController._(this.storageKey) : super(ThemeMode.light);
static const storageKey = 'userfront_theme';
static final ThemeController instance = ThemeController._();
static const appStorageKey = 'userfront_theme';
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;

View 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,
);
},
),
);
}
}

View File

@@ -1,8 +1,7 @@
import 'package:easy_localization/easy_localization.dart' hide tr;
import 'package:flutter/material.dart';
import 'package:userfront/i18n.dart';
import '../theme/theme_controller.dart';
import '../theme/theme_scope.dart';
class ThemeToggleButton extends StatelessWidget {
const ThemeToggleButton({super.key, this.compact = false});
@@ -11,10 +10,11 @@ class ThemeToggleButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
context.locale;
Localizations.localeOf(context);
final controller = ThemeScope.of(context);
return ValueListenableBuilder<ThemeMode>(
valueListenable: ThemeController.instance,
valueListenable: controller,
builder: (context, mode, _) {
final isLight = mode == ThemeMode.light;
final icon = isLight
@@ -28,13 +28,13 @@ class ThemeToggleButton extends StatelessWidget {
if (compact) {
return IconButton(
tooltip: tooltip,
onPressed: () => ThemeController.instance.toggle(),
onPressed: () => controller.toggle(),
icon: Icon(icon),
);
}
return OutlinedButton.icon(
onPressed: () => ThemeController.instance.toggle(),
onPressed: () => controller.toggle(),
icon: Icon(icon, size: 18),
label: Text(label),
);

View File

@@ -1389,6 +1389,73 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
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) {
return Scaffold(
@@ -1408,16 +1475,18 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
backgroundColor: colorScheme.surfaceContainerLowest,
body: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
return Theme(
data: localTheme,
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 400),
padding: const EdgeInsets.all(24),
child: Card(
child: Padding(
padding: const EdgeInsets.all(24),
constraints: const BoxConstraints(maxWidth: 480),
padding: const EdgeInsets.symmetric(
horizontal: 28,
vertical: 40,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -1425,20 +1494,22 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Text(
tr('ui.userfront.app_title'),
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 34,
fontWeight: FontWeight.w800,
letterSpacing: -0.7,
),
textAlign: TextAlign.center,
),
if (_drySendEnabled) ...[
const SizedBox(height: 16),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
horizontal: 14,
vertical: 12,
),
decoration: BoxDecoration(
color: const Color(0xFFFFF3CD),
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: const Color(0xFFFFC107),
),
@@ -1449,7 +1520,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Icons.warning_amber_rounded,
color: Color(0xFF8A6D3B),
),
const SizedBox(width: 8),
const SizedBox(width: 10),
Expanded(
child: Text(
tr('msg.userfront.login.dry_send'),
@@ -1463,32 +1534,41 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
),
],
const SizedBox(height: 40),
TabBar(
const SizedBox(height: 52),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 34),
child: TabBar(
controller: _tabController,
indicatorSize: TabBarIndicatorSize.label,
tabs: [
Tab(text: tr('ui.userfront.login.tabs.password')),
Tab(text: tr('ui.userfront.login.tabs.link')),
Tab(text: tr('ui.userfront.login.tabs.qr')),
],
),
const SizedBox(height: 24),
),
const SizedBox(height: 28),
SizedBox(
height: 350,
height: 360,
child: TabBarView(
controller: _tabController,
children: [
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(
children: [
TextField(
key: const ValueKey(
'password_login_id_input',
),
controller: _passwordLoginIdController,
controller:
_passwordLoginIdController,
decoration: InputDecoration(
labelText:
_loginIdLabel ??
@@ -1497,12 +1577,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
prefixIcon: const Icon(
Icons.person_outline,
size: 22,
),
),
onSubmitted: (_) =>
_handlePasswordLogin(),
),
const SizedBox(height: 16),
const SizedBox(height: 18),
TextField(
key: const ValueKey(
'password_login_password_input',
@@ -1516,13 +1597,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
prefixIcon: const Icon(
Icons.lock_outline,
size: 22,
),
),
onSubmitted: (_) =>
_handlePasswordLogin(),
),
if (_isPasswordCapsLockOn) ...[
const SizedBox(height: 8),
const SizedBox(height: 10),
Row(
children: [
const Icon(
@@ -1544,17 +1626,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
],
),
],
const SizedBox(height: 24),
const SizedBox(height: 28),
FilledButton(
key: const ValueKey(
'password_login_submit_button',
),
onPressed: _handlePasswordLogin,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(
50,
),
),
child: Text(
tr(
'ui.userfront.login.action.submit',
@@ -1564,9 +1641,16 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
],
),
),
),
),
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(
children: [
if (_linkPendingRef == null) ...[
@@ -1579,29 +1663,30 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
hintText: '',
prefixIcon: const Icon(
Icons.person_outline,
size: 22,
),
),
onSubmitted: (_) =>
_handleLinkLogin(),
),
const SizedBox(height: 24),
const SizedBox(height: 28),
FilledButton(
onPressed: _handleLinkLogin,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(
50,
),
),
child: Text(
tr('ui.userfront.login.link.send'),
tr(
'ui.userfront.login.link.send',
),
),
),
const SizedBox(height: 24),
Text(
tr('msg.userfront.login.link.helper'),
tr(
'msg.userfront.login.link.helper',
),
style: TextStyle(
color: mutedColor,
fontSize: 12,
height: 1.5,
),
textAlign: TextAlign.center,
),
@@ -1618,15 +1703,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
fontSize: 12,
),
),
const SizedBox(height: 12),
const SizedBox(height: 14),
FilledButton(
onPressed: () {
setState(_resetLinkLoginState);
},
style: FilledButton.styleFrom(
minimumSize:
const Size.fromHeight(45),
),
child: Text(
tr('ui.common.refresh'),
),
@@ -1639,10 +1720,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
style: TextStyle(
color: mutedColor,
fontSize: 12,
height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
const SizedBox(height: 14),
Row(
children: [
Expanded(
@@ -1661,11 +1743,12 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
hintStyle: TextStyle(
color: mutedColor,
),
counterText: '',
),
maxLength: 2,
),
),
const SizedBox(width: 8),
const SizedBox(width: 10),
Expanded(
flex: 4,
child: TextField(
@@ -1681,6 +1764,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
hintStyle: TextStyle(
color: mutedColor,
),
counterText: '',
suffixText:
_linkExpireSeconds > 0
? tr(
@@ -1698,7 +1782,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
],
),
const SizedBox(height: 12),
const SizedBox(height: 14),
FilledButton(
onPressed: () {
final prefix =
@@ -1719,19 +1803,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
);
return;
}
_verifyShortCode(prefix + digits);
_verifyShortCode(
prefix + digits,
);
},
style: FilledButton.styleFrom(
minimumSize:
const Size.fromHeight(45),
),
child: Text(
tr(
'ui.userfront.login.short_code.submit',
),
),
),
const SizedBox(height: 12),
const SizedBox(height: 14),
TextButton(
onPressed: () {
if (_linkResendSeconds > 0) {
@@ -1749,7 +1831,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
}
final loginId =
_lastLinkLoginId ??
_linkIdController.text.trim();
_linkIdController.text
.trim();
if (loginId.isEmpty) {
_showError(
tr(
@@ -1831,7 +1914,8 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
],
),
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
@@ -1842,25 +1926,17 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
Column(
children: [
Text(
tr(
'msg.userfront.login.qr_expired',
),
tr('msg.userfront.login.qr_expired'),
textAlign: TextAlign.center,
style: TextStyle(
color: mutedColor,
fontSize: 12,
),
),
const SizedBox(height: 12),
const SizedBox(height: 14),
FilledButton(
onPressed: _startQrFlow,
style: FilledButton.styleFrom(
minimumSize:
const Size.fromHeight(45),
),
child: Text(
tr('ui.common.refresh'),
),
child: Text(tr('ui.common.refresh')),
),
],
)
@@ -1870,13 +1946,14 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
border: Border.all(
color: colorScheme.outline,
),
borderRadius:
BorderRadius.circular(12),
borderRadius: BorderRadius.circular(
18,
),
),
child: QrImageView(
data: _qrImageBase64!,
@@ -1885,7 +1962,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
backgroundColor: Colors.white,
),
),
const SizedBox(height: 12),
const SizedBox(height: 14),
Text(
_qrRemainingSeconds > 0
? tr(
@@ -1902,7 +1979,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
textAlign: TextAlign.center,
style: TextStyle(
color: _qrRemainingSeconds > 30
? Colors.blue
? primaryColor
: Colors.red,
fontWeight: FontWeight.bold,
),
@@ -1916,23 +1993,20 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
style: TextStyle(
color: mutedColor,
fontSize: 12,
height: 1.5,
),
),
TextButton(
onPressed: _startQrFlow,
child: Text(
tr(
'ui.userfront.login.qr.refresh',
),
tr('ui.userfront.login.qr.refresh'),
),
),
],
)
else
Text(
tr(
'msg.userfront.login.qr.load_failed',
),
tr('msg.userfront.login.qr.load_failed'),
textAlign: TextAlign.center,
),
],
@@ -1940,12 +2014,11 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
],
),
),
const SizedBox(height: 16),
const SizedBox(height: 18),
Column(
children: [
TextButton(
onPressed: () =>
context.push('/forgot-password'),
onPressed: () => context.push('/forgot-password'),
child: Text(
tr('ui.userfront.login.forgot_password'),
),
@@ -1962,15 +2035,13 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
TextButton(
onPressed: () => context.push('/signup'),
child: Text(
tr('ui.userfront.login.signup'),
),
child: Text(tr('ui.userfront.login.signup')),
),
],
),
],
),
const SizedBox(height: 6),
const SizedBox(height: 12),
const Wrap(
alignment: WrapAlignment.center,
spacing: 10,
@@ -1983,7 +2054,6 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
),
),
),
),
);
},
),

View File

@@ -26,6 +26,7 @@ import 'core/services/web_window.dart';
import 'core/notifiers/auth_notifier.dart';
import 'core/theme/app_theme.dart';
import 'core/theme/theme_controller.dart';
import 'core/theme/theme_scope.dart';
import 'core/i18n/locale_gate.dart';
import 'core/i18n/locale_registry.dart';
import 'core/i18n/locale_utils.dart';
@@ -108,7 +109,8 @@ void main() async {
// 0. Initialize Logger
LoggerService.init();
await ThemeController.instance.restore();
await ThemeController.app.restore();
await ThemeController.auth.restore();
// 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화
await _loadBundledFonts();
@@ -180,12 +182,18 @@ final _router = GoRouter(
GoRoute(
path: 'dashboard',
builder: (context, state) {
return const DashboardScreen();
return ScopedTheme(
controller: ThemeController.app,
child: const DashboardScreen(),
);
},
),
GoRoute(
path: 'profile',
builder: (context, state) => const ProfilePage(),
builder: (context, state) => ScopedTheme(
controller: ThemeController.app,
child: const ProfilePage(),
),
),
GoRoute(
path: 'signin',
@@ -195,10 +203,13 @@ final _router = GoRouter(
final redirectUrl =
state.uri.queryParameters['redirect_uri'] ??
state.uri.queryParameters['redirect_url'];
return LoginScreen(
return ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(
key: state.pageKey,
loginChallenge: loginChallenge,
redirectUrl: redirectUrl,
),
);
},
),
@@ -211,10 +222,13 @@ final _router = GoRouter(
final redirectUrl =
state.uri.queryParameters['redirect_uri'] ??
state.uri.queryParameters['redirect_url'];
return LoginScreen(
return ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(
key: state.pageKey,
loginChallenge: loginChallenge,
redirectUrl: redirectUrl,
),
);
},
),
@@ -230,88 +244,137 @@ final _router = GoRouter(
),
);
}
return ConsentScreen(consentChallenge: consentChallenge);
return ScopedTheme(
controller: ThemeController.auth,
child: ConsentScreen(consentChallenge: consentChallenge),
);
},
),
GoRoute(
path: 'signup',
builder: (context, state) => const SignupScreen(),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const SignupScreen(),
),
),
GoRoute(
path: 'registration',
builder: (context, state) => const SignupScreen(),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const SignupScreen(),
),
),
GoRoute(
path: 'verify',
builder: (context, state) => LoginScreen(key: state.pageKey),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(key: state.pageKey),
),
),
GoRoute(
path: 'verify/:token',
builder: (context, state) {
final token = state.pathParameters['token'];
return LoginScreen(
return ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(
key: state.pageKey,
verificationToken: token,
),
);
},
),
GoRoute(
path: 'verification',
builder: (context, state) => LoginScreen(key: state.pageKey),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(key: state.pageKey),
),
),
GoRoute(
path: 'l/:shortCode',
builder: (context, state) {
return LoginScreen(key: state.pageKey);
return ScopedTheme(
controller: ThemeController.auth,
child: LoginScreen(key: state.pageKey),
);
},
),
GoRoute(
path: 'forgot-password',
builder: (context, state) => const ForgotPasswordScreen(),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const ForgotPasswordScreen(),
),
),
GoRoute(
path: 'recovery',
builder: (context, state) => const ForgotPasswordScreen(),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const ForgotPasswordScreen(),
),
),
GoRoute(
path: 'reset-password',
builder: (context, state) => const ResetPasswordScreen(),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const ResetPasswordScreen(),
),
),
GoRoute(
path: 'error',
builder: (context, state) {
final params = state.uri.queryParameters;
return ErrorScreen(
return ScopedTheme(
controller: ThemeController.auth,
child: ErrorScreen(
errorId: params['id'],
errorCode: params['error'],
description: params['error_description'] ?? params['message'],
description:
params['error_description'] ?? params['message'],
),
);
},
),
GoRoute(
path: 'settings',
builder: (context, state) => ErrorScreen(
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: ErrorScreen(
errorCode: 'settings_disabled',
description: tr('msg.userfront.settings.disabled'),
),
),
),
GoRoute(
path: 'approve',
builder: (context, state) =>
ApproveQrScreen(pendingRef: state.uri.queryParameters['ref']),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: ApproveQrScreen(
pendingRef: state.uri.queryParameters['ref'],
),
),
),
GoRoute(
path: 'ql/:ref',
builder: (context, state) =>
ApproveQrScreen(pendingRef: state.pathParameters['ref']),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: ApproveQrScreen(pendingRef: state.pathParameters['ref']),
),
),
GoRoute(
path: 'scan',
builder: (context, state) => const QRScanScreen(),
builder: (context, state) => ScopedTheme(
controller: ThemeController.auth,
child: const QRScanScreen(),
),
),
GoRoute(
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 =
localization?.currentLocale ?? Locale(resolvePreferredLocaleCode());
return ValueListenableBuilder<ThemeMode>(
valueListenable: ThemeController.instance,
builder: (context, themeMode, _) {
return MaterialApp.router(
title: tr('ui.userfront.app_title'),
localizationsDelegates: delegates,
@@ -384,10 +444,8 @@ class BaronSSOApp extends StatelessWidget {
},
theme: buildLightTheme(),
darkTheme: buildDarkTheme(),
themeMode: themeMode,
themeMode: ThemeMode.light,
routerConfig: _router,
);
},
);
}
}

View File

@@ -8,25 +8,25 @@ void main() {
setUp(() async {
SharedPreferences.setMockInitialValues({});
await ThemeController.instance.setThemeMode(ThemeMode.light);
await ThemeController.app.setThemeMode(ThemeMode.light);
});
test('저장된 dark 값을 복원한다', () async {
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 {
await ThemeController.instance.restore();
await ThemeController.instance.toggle();
await ThemeController.app.restore();
await ThemeController.app.toggle();
final prefs = await SharedPreferences.getInstance();
expect(ThemeController.instance.value, ThemeMode.dark);
expect(prefs.getString(ThemeController.storageKey), 'dark');
expect(ThemeController.app.value, ThemeMode.dark);
expect(prefs.getString(ThemeController.appStorageKey), 'dark');
});
}