forked from baron/baron-sso
다크 모드 전역 상태와 테마 기반 추가
This commit is contained in:
148
userfront/lib/core/theme/app_theme.dart
Normal file
148
userfront/lib/core/theme/app_theme.dart
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
ThemeData buildLightTheme() {
|
||||||
|
final scheme =
|
||||||
|
ColorScheme.fromSeed(
|
||||||
|
seedColor: const Color(0xFF1A1F2C),
|
||||||
|
brightness: Brightness.light,
|
||||||
|
).copyWith(
|
||||||
|
surface: Colors.white,
|
||||||
|
surfaceContainerLowest: const Color(0xFFF7F8FA),
|
||||||
|
surfaceContainerLow: const Color(0xFFF3F4F6),
|
||||||
|
surfaceContainerHighest: const Color(0xFFE5E7EB),
|
||||||
|
outline: const Color(0xFFD1D5DB),
|
||||||
|
outlineVariant: const Color(0xFFE5E7EB),
|
||||||
|
primary: const Color(0xFF1A1F2C),
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
onSurface: const Color(0xFF111827),
|
||||||
|
onSurfaceVariant: const Color(0xFF6B7280),
|
||||||
|
);
|
||||||
|
return _buildTheme(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData buildDarkTheme() {
|
||||||
|
final scheme =
|
||||||
|
ColorScheme.fromSeed(
|
||||||
|
seedColor: const Color(0xFF7DD3FC),
|
||||||
|
brightness: Brightness.dark,
|
||||||
|
).copyWith(
|
||||||
|
surface: const Color(0xFF0F172A),
|
||||||
|
surfaceContainerLowest: const Color(0xFF020617),
|
||||||
|
surfaceContainerLow: const Color(0xFF111827),
|
||||||
|
surfaceContainerHighest: const Color(0xFF1F2937),
|
||||||
|
outline: const Color(0xFF334155),
|
||||||
|
outlineVariant: const Color(0xFF1E293B),
|
||||||
|
primary: const Color(0xFFBAE6FD),
|
||||||
|
onPrimary: const Color(0xFF082F49),
|
||||||
|
onSurface: const Color(0xFFF8FAFC),
|
||||||
|
onSurfaceVariant: const Color(0xFF94A3B8),
|
||||||
|
);
|
||||||
|
return _buildTheme(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeData _buildTheme(ColorScheme colorScheme) {
|
||||||
|
final isDark = colorScheme.brightness == Brightness.dark;
|
||||||
|
final base = ThemeData(
|
||||||
|
useMaterial3: true,
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
fontFamily: 'NotoSansKR',
|
||||||
|
);
|
||||||
|
|
||||||
|
return base.copyWith(
|
||||||
|
scaffoldBackgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
|
pageTransitionsTheme: const PageTransitionsTheme(
|
||||||
|
builders: {
|
||||||
|
TargetPlatform.android: NoTransitionsBuilder(),
|
||||||
|
TargetPlatform.iOS: NoTransitionsBuilder(),
|
||||||
|
TargetPlatform.linux: NoTransitionsBuilder(),
|
||||||
|
TargetPlatform.macOS: NoTransitionsBuilder(),
|
||||||
|
TargetPlatform.windows: NoTransitionsBuilder(),
|
||||||
|
TargetPlatform.fuchsia: NoTransitionsBuilder(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
appBarTheme: AppBarTheme(
|
||||||
|
elevation: 0,
|
||||||
|
centerTitle: false,
|
||||||
|
backgroundColor: colorScheme.surface,
|
||||||
|
foregroundColor: colorScheme.onSurface,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
cardTheme: CardThemeData(
|
||||||
|
color: colorScheme.surface,
|
||||||
|
elevation: 0,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: BorderSide(color: colorScheme.outlineVariant),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
dividerTheme: DividerThemeData(
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
thickness: 1,
|
||||||
|
),
|
||||||
|
drawerTheme: DrawerThemeData(
|
||||||
|
backgroundColor: colorScheme.surface,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
dialogTheme: DialogThemeData(
|
||||||
|
backgroundColor: colorScheme.surface,
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||||
|
),
|
||||||
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
|
filled: true,
|
||||||
|
fillColor: isDark ? colorScheme.surfaceContainerLow : colorScheme.surface,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(color: colorScheme.outline),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(color: colorScheme.outline),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
borderSide: BorderSide(color: colorScheme.primary, width: 1.4),
|
||||||
|
),
|
||||||
|
labelStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
|
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
|
prefixIconColor: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
filledButtonTheme: FilledButtonThemeData(
|
||||||
|
style: FilledButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(50),
|
||||||
|
backgroundColor: colorScheme.primary,
|
||||||
|
foregroundColor: colorScheme.onPrimary,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: colorScheme.onSurface,
|
||||||
|
side: BorderSide(color: colorScheme.outline),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
tabBarTheme: TabBarThemeData(
|
||||||
|
dividerColor: colorScheme.outlineVariant,
|
||||||
|
labelColor: colorScheme.onSurface,
|
||||||
|
unselectedLabelColor: colorScheme.onSurfaceVariant,
|
||||||
|
indicatorColor: colorScheme.primary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoTransitionsBuilder extends PageTransitionsBuilder {
|
||||||
|
const NoTransitionsBuilder();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions<T>(
|
||||||
|
PageRoute<T> route,
|
||||||
|
BuildContext context,
|
||||||
|
Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation,
|
||||||
|
Widget child,
|
||||||
|
) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
userfront/lib/core/theme/theme_controller.dart
Normal file
32
userfront/lib/core/theme/theme_controller.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class ThemeController extends ValueNotifier<ThemeMode> {
|
||||||
|
ThemeController._() : super(ThemeMode.light);
|
||||||
|
|
||||||
|
static const storageKey = 'userfront_theme';
|
||||||
|
static final ThemeController instance = ThemeController._();
|
||||||
|
|
||||||
|
bool get isDark => value == ThemeMode.dark;
|
||||||
|
|
||||||
|
Future<void> restore() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final stored = prefs.getString(storageKey);
|
||||||
|
value = stored == 'dark' ? ThemeMode.dark : ThemeMode.light;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setThemeMode(ThemeMode mode) async {
|
||||||
|
if (value != mode) {
|
||||||
|
value = mode;
|
||||||
|
}
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(
|
||||||
|
storageKey,
|
||||||
|
mode == ThemeMode.dark ? 'dark' : 'light',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggle() {
|
||||||
|
return setThemeMode(isDark ? ThemeMode.light : ThemeMode.dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
userfront/lib/core/widgets/theme_toggle_button.dart
Normal file
44
userfront/lib/core/widgets/theme_toggle_button.dart
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart' hide tr;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:userfront/i18n.dart';
|
||||||
|
|
||||||
|
import '../theme/theme_controller.dart';
|
||||||
|
|
||||||
|
class ThemeToggleButton extends StatelessWidget {
|
||||||
|
const ThemeToggleButton({super.key, this.compact = false});
|
||||||
|
|
||||||
|
final bool compact;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
context.locale;
|
||||||
|
|
||||||
|
return ValueListenableBuilder<ThemeMode>(
|
||||||
|
valueListenable: ThemeController.instance,
|
||||||
|
builder: (context, mode, _) {
|
||||||
|
final isLight = mode == ThemeMode.light;
|
||||||
|
final icon = isLight
|
||||||
|
? Icons.light_mode_outlined
|
||||||
|
: Icons.dark_mode_outlined;
|
||||||
|
final label = isLight
|
||||||
|
? tr('ui.common.theme_light', fallback: 'Light')
|
||||||
|
: tr('ui.common.theme_dark', fallback: 'Dark');
|
||||||
|
final tooltip = tr('ui.common.theme_toggle', fallback: '테마 전환');
|
||||||
|
|
||||||
|
if (compact) {
|
||||||
|
return IconButton(
|
||||||
|
tooltip: tooltip,
|
||||||
|
onPressed: () => ThemeController.instance.toggle(),
|
||||||
|
icon: Icon(icon),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OutlinedButton.icon(
|
||||||
|
onPressed: () => ThemeController.instance.toggle(),
|
||||||
|
icon: Icon(icon, size: 18),
|
||||||
|
label: Text(label),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ import 'core/services/logger_service.dart';
|
|||||||
import 'core/services/null_check_recovery.dart';
|
import 'core/services/null_check_recovery.dart';
|
||||||
import 'core/services/web_window.dart';
|
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/theme_controller.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';
|
||||||
@@ -106,6 +108,7 @@ void main() async {
|
|||||||
|
|
||||||
// 0. Initialize Logger
|
// 0. Initialize Logger
|
||||||
LoggerService.init();
|
LoggerService.init();
|
||||||
|
await ThemeController.instance.restore();
|
||||||
|
|
||||||
// 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화
|
// 폰트를 먼저 로딩해서 렌더링 깨짐(FOIT/FOUT) 최소화
|
||||||
await _loadBundledFonts();
|
await _loadBundledFonts();
|
||||||
@@ -366,50 +369,25 @@ class BaronSSOApp extends StatelessWidget {
|
|||||||
final locale =
|
final locale =
|
||||||
localization?.currentLocale ?? Locale(resolvePreferredLocaleCode());
|
localization?.currentLocale ?? Locale(resolvePreferredLocaleCode());
|
||||||
|
|
||||||
return MaterialApp.router(
|
return ValueListenableBuilder<ThemeMode>(
|
||||||
title: tr('ui.userfront.app_title'),
|
valueListenable: ThemeController.instance,
|
||||||
localizationsDelegates: delegates,
|
builder: (context, themeMode, _) {
|
||||||
supportedLocales: supportedLocales,
|
return MaterialApp.router(
|
||||||
locale: locale,
|
title: tr('ui.userfront.app_title'),
|
||||||
builder: (context, child) {
|
localizationsDelegates: delegates,
|
||||||
return Stack(
|
supportedLocales: supportedLocales,
|
||||||
children: [if (child != null) child, const ToastViewport()],
|
locale: locale,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Stack(
|
||||||
|
children: [if (child != null) child, const ToastViewport()],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
theme: buildLightTheme(),
|
||||||
|
darkTheme: buildDarkTheme(),
|
||||||
|
themeMode: themeMode,
|
||||||
|
routerConfig: _router,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
theme: ThemeData(
|
|
||||||
colorScheme: ColorScheme.fromSeed(
|
|
||||||
seedColor: const Color(0xFF1A1F2C), // Dark Navy/Black base
|
|
||||||
brightness: Brightness.light,
|
|
||||||
),
|
|
||||||
useMaterial3: true,
|
|
||||||
fontFamily: 'NotoSansKR',
|
|
||||||
pageTransitionsTheme: const PageTransitionsTheme(
|
|
||||||
builders: {
|
|
||||||
TargetPlatform.android: NoTransitionsBuilder(),
|
|
||||||
TargetPlatform.iOS: NoTransitionsBuilder(),
|
|
||||||
TargetPlatform.linux: NoTransitionsBuilder(),
|
|
||||||
TargetPlatform.macOS: NoTransitionsBuilder(),
|
|
||||||
TargetPlatform.windows: NoTransitionsBuilder(),
|
|
||||||
TargetPlatform.fuchsia: NoTransitionsBuilder(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
routerConfig: _router,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoTransitionsBuilder extends PageTransitionsBuilder {
|
|
||||||
const NoTransitionsBuilder();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildTransitions<T>(
|
|
||||||
PageRoute<T> route,
|
|
||||||
BuildContext context,
|
|
||||||
Animation<double> animation,
|
|
||||||
Animation<double> secondaryAnimation,
|
|
||||||
Widget child,
|
|
||||||
) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.2.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ dependencies:
|
|||||||
easy_localization: ^3.0.7
|
easy_localization: ^3.0.7
|
||||||
toml: ^0.15.0
|
toml: ^0.15.0
|
||||||
web: ^1.1.0
|
web: ^1.1.0
|
||||||
|
shared_preferences: ^2.5.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user