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';
|
||||
|
||||
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;
|
||||
|
||||
|
||||
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: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),
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user