import 'package:flutter/material.dart'; /// 화면 색 한 벌 (어두운 · 밝은) class JjPalette { final Brightness brightness; final Color bg, panel, panelHigh, border, accent, text, textDim, danger, success; /// 강조색 바탕 위 글자 (버튼 등) final Color onAccent; const JjPalette({ required this.brightness, required this.bg, required this.panel, required this.panelHigh, required this.border, required this.accent, required this.text, required this.textDim, required this.danger, required this.success, required this.onAccent, }); /// CapCut 참고 어두운 테마 (처음 값 - 예전 그대로) static const dark = JjPalette( brightness: Brightness.dark, bg: Color(0xFF141417), panel: Color(0xFF1E1E23), panelHigh: Color(0xFF28282F), border: Color(0xFF2E2E36), accent: Color(0xFF00C4CC), // CapCut 계열 청록 text: Color(0xFFE8E8EC), textDim: Color(0xFF8E8E99), danger: Color(0xFFFF5C6C), success: Color(0xFF3DD68C), onAccent: Colors.black, ); /// X-plore 비교 5-1 ⑦ 밝은 테마 (관리자 지시 10/21, 시험). 흰 바탕에서 글자 대비 4.5:1 이상이 되게 강조 · 경고 · 성공을 어둡게 static const light = JjPalette( brightness: Brightness.light, bg: Color(0xFFF4F4F6), panel: Color(0xFFFFFFFF), panelHigh: Color(0xFFE9E9EE), border: Color(0xFFD3D3DB), accent: Color(0xFF00747A), text: Color(0xFF1B1B20), textDim: Color(0xFF5E5E6A), danger: Color(0xFFC62833), success: Color(0xFF186B42), onAccent: Colors.white, ); } /// 앱 전체의 색. 테마 (환경 설정 "화면 밝기") 에 따라 [use] 로 바뀐다 - 그래서 const 로 쓰지 않는다 class JjColors { static JjPalette _p = JjPalette.dark; /// 지금 쓰는 색 한 벌 static JjPalette get palette => _p; static Brightness get brightness => _p.brightness; static bool get isLight => _p.brightness == Brightness.light; /// 색 한 벌을 바꾼다 (바꿨으면 true). 화면 전체를 다시 그리는 것은 부르는 쪽 ([rebuildAllElements]) static bool use(Brightness b) { final next = b == Brightness.light ? JjPalette.light : JjPalette.dark; if (identical(next, _p)) return false; _p = next; return true; } static Color get bg => _p.bg; static Color get panel => _p.panel; static Color get panelHigh => _p.panelHigh; static Color get border => _p.border; static Color get accent => _p.accent; static Color get text => _p.text; static Color get textDim => _p.textDim; static Color get danger => _p.danger; static Color get success => _p.success; static Color get onAccent => _p.onAccent; } /// 색을 바꾼 뒤 모든 화면 조각을 다시 그린다 (상태는 그대로 - [JjColors] 를 직접 읽는 곳도 새 색으로) void rebuildAllElements(Element root) { void mark(Element e) { e.markNeedsBuild(); e.visitChildren(mark); } mark(root); } /// 환경 설정 "화면 밝기" 값 ('dark' 처음 값 · 'light' · 'system') → 밝기 Brightness resolveBrightness(String mode, Brightness platform) => switch (mode) { 'light' => Brightness.light, 'system' => platform, _ => Brightness.dark, }; ThemeData buildTheme([Brightness? brightness]) { final c = brightness == null ? JjColors.palette : (brightness == Brightness.light ? JjPalette.light : JjPalette.dark); final light = c.brightness == Brightness.light; final base = ThemeData( brightness: c.brightness, useMaterial3: true, colorScheme: light ? ColorScheme.light(primary: c.accent, secondary: c.accent, surface: c.panel, error: c.danger, onPrimary: c.onAccent) : ColorScheme.dark(primary: c.accent, secondary: c.accent, surface: c.panel, error: c.danger), scaffoldBackgroundColor: c.bg, fontFamily: 'Malgun Gothic', ); return base.copyWith( // 66-2: 좌우로 밀어 화면을 옮길 때는 그 방향으로 미끄러지게 (그 밖의 화면 열기는 기본 전환) pageTransitionsTheme: const PageTransitionsTheme(builders: { TargetPlatform.android: NavSlideTransitionsBuilder(), TargetPlatform.windows: NavSlideTransitionsBuilder(), TargetPlatform.linux: NavSlideTransitionsBuilder(), TargetPlatform.macOS: NavSlideTransitionsBuilder(), TargetPlatform.iOS: NavSlideTransitionsBuilder(), }), dividerColor: c.border, textTheme: base.textTheme.apply( bodyColor: c.text, displayColor: c.text, ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( backgroundColor: c.accent, foregroundColor: c.onAccent, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: OutlinedButton.styleFrom( foregroundColor: c.text, side: BorderSide(color: c.border), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)), ), ), ); } /// 66-2: 다음 화면으로 열리는 화면이 어느 쪽에서 들어올지 (+1 오른쪽에서 = 다음, −1 왼쪽에서 = 이전, 0 기본 전환). /// 좌우로 밀기 · 이전 · 다음으로 옮길 때만 정하고, 화면이 열리면 (그 화면에 기억한 뒤) 0 으로 돌린다. class NavSlide { static int pending = 0; static final _dir = Expando('navSlide'); } class NavSlideTransitionsBuilder extends PageTransitionsBuilder { const NavSlideTransitionsBuilder(); @override Widget buildTransitions(PageRoute route, BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { // 처음 그릴 때의 방향을 그 화면에 기억한다 (전환 중에 다시 불려도 같은 방향) var dir = NavSlide._dir[route]; if (dir == null) { dir = NavSlide._dir[route] = NavSlide.pending; NavSlide.pending = 0; } if (dir == 0) { return const PageTransitionsTheme().buildTransitions(route, context, animation, secondaryAnimation, child); } return SlideTransition( position: Tween(begin: Offset(dir.toDouble(), 0), end: Offset.zero) .animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)), child: child, ); } }