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),
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||||
key: state.pageKey,
|
controller: ThemeController.auth,
|
||||||
loginChallenge: loginChallenge,
|
child: LoginScreen(
|
||||||
redirectUrl: redirectUrl,
|
key: state.pageKey,
|
||||||
|
loginChallenge: loginChallenge,
|
||||||
|
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(
|
||||||
key: state.pageKey,
|
controller: ThemeController.auth,
|
||||||
loginChallenge: loginChallenge,
|
child: LoginScreen(
|
||||||
redirectUrl: redirectUrl,
|
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(
|
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(
|
||||||
key: state.pageKey,
|
controller: ThemeController.auth,
|
||||||
verificationToken: token,
|
child: LoginScreen(
|
||||||
|
key: state.pageKey,
|
||||||
|
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(
|
||||||
errorId: params['id'],
|
controller: ThemeController.auth,
|
||||||
errorCode: params['error'],
|
child: ErrorScreen(
|
||||||
description: params['error_description'] ?? params['message'],
|
errorId: params['id'],
|
||||||
|
errorCode: params['error'],
|
||||||
|
description:
|
||||||
|
params['error_description'] ?? params['message'],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
builder: (context, state) => ErrorScreen(
|
builder: (context, state) => ScopedTheme(
|
||||||
errorCode: 'settings_disabled',
|
controller: ThemeController.auth,
|
||||||
description: tr('msg.userfront.settings.disabled'),
|
child: ErrorScreen(
|
||||||
|
errorCode: '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,25 +432,20 @@ class BaronSSOApp extends StatelessWidget {
|
|||||||
final locale =
|
final locale =
|
||||||
localization?.currentLocale ?? Locale(resolvePreferredLocaleCode());
|
localization?.currentLocale ?? Locale(resolvePreferredLocaleCode());
|
||||||
|
|
||||||
return ValueListenableBuilder<ThemeMode>(
|
return MaterialApp.router(
|
||||||
valueListenable: ThemeController.instance,
|
title: tr('ui.userfront.app_title'),
|
||||||
builder: (context, themeMode, _) {
|
localizationsDelegates: delegates,
|
||||||
return MaterialApp.router(
|
supportedLocales: supportedLocales,
|
||||||
title: tr('ui.userfront.app_title'),
|
locale: locale,
|
||||||
localizationsDelegates: delegates,
|
builder: (context, child) {
|
||||||
supportedLocales: supportedLocales,
|
return Stack(
|
||||||
locale: locale,
|
children: [if (child != null) child, const ToastViewport()],
|
||||||
builder: (context, child) {
|
|
||||||
return Stack(
|
|
||||||
children: [if (child != null) child, const ToastViewport()],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
theme: buildLightTheme(),
|
|
||||||
darkTheme: buildDarkTheme(),
|
|
||||||
themeMode: themeMode,
|
|
||||||
routerConfig: _router,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
theme: buildLightTheme(),
|
||||||
|
darkTheme: buildDarkTheme(),
|
||||||
|
themeMode: ThemeMode.light,
|
||||||
|
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