173 lines
6.2 KiB
Dart
173 lines
6.2 KiB
Dart
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<int>('navSlide');
|
||
}
|
||
|
||
class NavSlideTransitionsBuilder extends PageTransitionsBuilder {
|
||
const NavSlideTransitionsBuilder();
|
||
|
||
@override
|
||
Widget buildTransitions<T>(PageRoute<T> route, BuildContext context, Animation<double> animation,
|
||
Animation<double> 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,
|
||
);
|
||
}
|
||
}
|