Major UI/UX and architecture improvements

- Implemented new navigation system with NavigationProvider and route management
- Added adaptive theme system with ThemeProvider for better theme handling
- Introduced glassmorphism design elements (app bars, scaffolds, cards)
- Added advanced animations (spring animations, page transitions, staggered lists)
- Implemented performance optimizations (memory manager, lazy loading)
- Refactored Analysis screen into modular components
- Added floating navigation bar with haptic feedback
- Improved subscription cards with swipe actions
- Enhanced skeleton loading with better animations
- Added cached network image support
- Improved overall app architecture and code organization

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
JiWoong Sul
2025-07-10 18:36:57 +09:00
parent 8619e96739
commit 4731288622
55 changed files with 8219 additions and 2149 deletions

View File

@@ -0,0 +1,379 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart';
import 'app_theme.dart';
/// 적응형 테마 관리 클래스
class AdaptiveTheme {
/// 라이트 테마
static ThemeData get lightTheme => AppTheme.lightTheme;
/// 다크 테마
static ThemeData get darkTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.dark(
primary: AppColors.primaryColor,
onPrimary: Colors.white,
secondary: AppColors.secondaryColor,
tertiary: AppColors.infoColor,
error: AppColors.dangerColor,
background: const Color(0xFF121212),
surface: const Color(0xFF1E1E1E),
),
scaffoldBackgroundColor: const Color(0xFF121212),
cardTheme: CardTheme(
color: const Color(0xFF1E1E1E),
elevation: 2,
shadowColor: Colors.black.withValues(alpha: 0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.white.withValues(alpha: 0.1), width: 0.5),
),
clipBehavior: Clip.antiAlias,
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
),
appBarTheme: AppBarTheme(
backgroundColor: const Color(0xFF1E1E1E),
foregroundColor: Colors.white,
elevation: 0,
centerTitle: false,
titleTextStyle: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
),
iconTheme: IconThemeData(
color: Colors.white.withValues(alpha: 0.9),
size: 24,
),
),
textTheme: TextTheme(
headlineLarge: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
height: 1.2,
),
headlineMedium: const TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.w700,
letterSpacing: -0.5,
height: 1.2,
),
headlineSmall: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.w600,
letterSpacing: -0.25,
height: 1.3,
),
titleLarge: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.w600,
letterSpacing: -0.2,
height: 1.4,
),
titleMedium: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
letterSpacing: -0.1,
height: 1.4,
),
titleSmall: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
letterSpacing: 0,
height: 1.4,
),
bodyLarge: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.1,
height: 1.5,
),
bodyMedium: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.1,
height: 1.5,
),
bodySmall: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.2,
height: 1.5,
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFF2A2A2A),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.white.withValues(alpha: 0.1), width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: AppColors.primaryColor, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: AppColors.dangerColor, width: 1),
),
labelStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 14,
fontWeight: FontWeight.w500,
),
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryColor,
foregroundColor: Colors.white,
minimumSize: const Size(0, 48),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
),
dividerTheme: DividerThemeData(
color: Colors.white.withValues(alpha: 0.1),
thickness: 1,
space: 16,
),
);
}
/// OLED 최적화 다크 테마
static ThemeData get oledTheme {
return darkTheme.copyWith(
scaffoldBackgroundColor: Colors.black,
colorScheme: darkTheme.colorScheme.copyWith(
background: Colors.black,
surface: const Color(0xFF0A0A0A),
),
cardTheme: darkTheme.cardTheme.copyWith(
color: const Color(0xFF0A0A0A),
),
appBarTheme: darkTheme.appBarTheme.copyWith(
backgroundColor: Colors.black,
),
inputDecorationTheme: darkTheme.inputDecorationTheme.copyWith(
fillColor: const Color(0xFF0A0A0A),
),
);
}
/// 고대비 테마
static ThemeData get highContrastTheme {
return ThemeData(
useMaterial3: true,
brightness: Brightness.light,
colorScheme: const ColorScheme.highContrastLight(
primary: Colors.black,
secondary: Colors.black87,
tertiary: Colors.black54,
error: Colors.red,
background: Colors.white,
surface: Colors.white,
),
textTheme: const TextTheme(
headlineLarge: TextStyle(
color: Colors.black,
fontSize: 32,
fontWeight: FontWeight.w900,
),
headlineMedium: TextStyle(
color: Colors.black,
fontSize: 28,
fontWeight: FontWeight.w900,
),
headlineSmall: TextStyle(
color: Colors.black,
fontSize: 24,
fontWeight: FontWeight.w800,
),
bodyLarge: TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
),
bodyMedium: TextStyle(
color: Colors.black87,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
cardTheme: CardTheme(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: const BorderSide(color: Colors.black, width: 2),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
side: const BorderSide(color: Colors.black, width: 2),
textStyle: const TextStyle(
fontWeight: FontWeight.w700,
),
),
),
);
}
/// 시스템 테마에 따른 상태바 스타일 적용
static void applySystemUIOverlay(BuildContext context) {
final brightness = Theme.of(context).brightness;
final isOled = Theme.of(context).scaffoldBackgroundColor == Colors.black;
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
statusBarBrightness: brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: isOled
? Colors.black
: (brightness == Brightness.dark
? const Color(0xFF121212)
: Colors.white),
systemNavigationBarIconBrightness: brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
));
}
/// 접근성 설정에 따른 테마 조정
static ThemeData getAccessibleTheme(
ThemeData baseTheme, {
required bool largeText,
required bool reduceMotion,
required bool highContrast,
}) {
if (highContrast) {
return highContrastTheme;
}
ThemeData theme = baseTheme;
if (largeText) {
theme = theme.copyWith(
textTheme: theme.textTheme.apply(
fontSizeFactor: 1.2,
),
);
}
if (reduceMotion) {
theme = theme.copyWith(
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(),
},
),
);
}
return theme;
}
}
/// 테마 모드 열거형
enum AppThemeMode {
light,
dark,
oled,
system,
}
/// 테마 설정 클래스
class ThemeSettings {
final AppThemeMode mode;
final bool useSystemColors;
final bool largeText;
final bool reduceMotion;
final bool highContrast;
const ThemeSettings({
this.mode = AppThemeMode.system,
this.useSystemColors = false,
this.largeText = false,
this.reduceMotion = false,
this.highContrast = false,
});
ThemeSettings copyWith({
AppThemeMode? mode,
bool? useSystemColors,
bool? largeText,
bool? reduceMotion,
bool? highContrast,
}) {
return ThemeSettings(
mode: mode ?? this.mode,
useSystemColors: useSystemColors ?? this.useSystemColors,
largeText: largeText ?? this.largeText,
reduceMotion: reduceMotion ?? this.reduceMotion,
highContrast: highContrast ?? this.highContrast,
);
}
Map<String, dynamic> toJson() => {
'mode': mode.name,
'useSystemColors': useSystemColors,
'largeText': largeText,
'reduceMotion': reduceMotion,
'highContrast': highContrast,
};
factory ThemeSettings.fromJson(Map<String, dynamic> json) {
return ThemeSettings(
mode: AppThemeMode.values.firstWhere(
(mode) => mode.name == json['mode'],
orElse: () => AppThemeMode.system,
),
useSystemColors: json['useSystemColors'] ?? false,
largeText: json['largeText'] ?? false,
reduceMotion: json['reduceMotion'] ?? false,
highContrast: json['highContrast'] ?? false,
);
}
}

View File

@@ -46,4 +46,49 @@ class AppColors {
Color(0xFFF43F5E),
Color(0xFFE11D48)
];
// Glassmorphism 효과를 위한 색상
static const glassSurface = Color(0x0FFFFFFF); // 매우 연한 흰색 (6% opacity)
static const glassBackground = Color(0x1AFFFFFF); // 연한 흰색 (10% opacity)
static const glassCard = Color(0x33FFFFFF); // 반투명 흰색 (20% opacity)
static const glassBorder = Color(0x4DFFFFFF); // 반투명 테두리 (30% opacity)
static const glassOverlay = Color(0x0D000000); // 연한 검정 오버레이 (5% opacity)
// 다크 모드용 Glassmorphism 색상
static const glassSurfaceDark = Color(0x0F000000); // 매우 연한 검정 (6% opacity)
static const glassBackgroundDark = Color(0x1A000000); // 연한 검정 (10% opacity)
static const glassCardDark = Color(0x33000000); // 반투명 검정 (20% opacity)
static const glassBorderDark = Color(0x4D000000); // 반투명 검정 테두리 (30% opacity)
// 백드롭 블러 효과를 위한 그라디언트
static const List<Color> glassGradient = [
Color(0x1AFFFFFF), // 10% white
Color(0x0FFFFFFF), // 6% white
];
static const List<Color> glassGradientDark = [
Color(0x1A000000), // 10% black
Color(0x0F000000), // 6% black
];
// 시간대별 배경 그라디언트
static const List<Color> morningGradient = [
Color(0xFFFED7AA), // 따뜻한 오렌지
Color(0xFFFBBF24), // 부드러운 노랑
];
static const List<Color> dayGradient = [
Color(0xFFDDEAFC), // 연한 하늘색
Color(0xFFBFDBFE), // 맑은 파랑
];
static const List<Color> eveningGradient = [
Color(0xFFFCA5A5), // 부드러운 핑크
Color(0xFFC084FC), // 연한 보라
];
static const List<Color> nightGradient = [
Color(0xFF4338CA), // 깊은 인디고
Color(0xFF1E1B4B), // 다크 네이비
];
}

View File

@@ -21,7 +21,7 @@ class AppTheme {
cardTheme: CardTheme(
color: AppColors.cardColor,
elevation: 1,
shadowColor: Colors.black.withOpacity(0.04),
shadowColor: Colors.black.withValues(alpha: 0.04),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: AppColors.borderColor, width: 0.5),
@@ -265,7 +265,7 @@ class AppTheme {
}),
trackColor: MaterialStateProperty.resolveWith<Color>((states) {
if (states.contains(MaterialState.selected)) {
return AppColors.primaryColor.withOpacity(0.5);
return AppColors.primaryColor.withValues(alpha: 0.5);
}
return AppColors.borderColor;
}),
@@ -300,7 +300,7 @@ class AppTheme {
activeTrackColor: AppColors.primaryColor,
inactiveTrackColor: AppColors.borderColor,
thumbColor: AppColors.primaryColor,
overlayColor: AppColors.primaryColor.withOpacity(0.2),
overlayColor: AppColors.primaryColor.withValues(alpha: 0.2),
trackHeight: 4,
thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10),
overlayShape: const RoundSliderOverlayShape(overlayRadius: 20),