WIP ⑦ 밝은 화면 (나) 1: 파일 탐색기 · 주의 색 · 캡처 시험 밝은 화면 (검토 대기)

관리자 지시 10/21 (⑦ (나) 화면마다 차례로 - 첫째 파일 탐색기):
- 주황 · 빨강 · 노랑 · 초록 (orangeAccent · redAccent · amber · greenAccent) 을 테마 색 warning · alert · amber · positive 로.
  어두운 화면은 예전 Material 값 그대로, 밝은 화면만 진하게 (글자 4.5:1 · 노랑 아이콘 3:1, 노랑 글자는 amberText)
- 탐색기: 흰 바탕에서 옅은 회색 파일 아이콘을 밝은 화면일 때만 진하게, 저장 공간 그림 "이 폴더의 파일" 칸 글자 (밝은 화면)
- 통합 캡처 시험에 JJ_SHOT_THEME=light (어두운 것과 견줌)
- 설정 설명 글을 새 이름 (밝은 화면 · 기기 따라) 에 맞춤
- 확인: 어두운 화면 캡처 95장을 9324e41 과 픽셀로 견줘 실행마다 바뀌는 곳 (시각 · CPU 숫자 · 임시 이름) 밖의 차이 0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xplore_p3b
RyuJongJin 2026-10-21 15:59:16 +09:00
parent 9324e4100f
commit 808300b69e
46 changed files with 195 additions and 139 deletions

View File

@ -1454,9 +1454,9 @@
"받지 않음": "Off", "받지 않음": "Off",
"받지 않음 (처음 쓸 때 내려받기 {0})": "Not downloaded (downloads on first use, {0})", "받지 않음 (처음 쓸 때 내려받기 {0})": "Not downloaded (downloads on first use, {0})",
"밝게": "Brighter", "밝게": "Brighter",
"밝게 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다": "Light and Follow device are still in testing - colors on some screens may not fit yet",
"밝기": "Brightness", "밝기": "Brightness",
"밝은 화면": "Light", "밝은 화면": "Light",
"밝은 화면 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다": "Light and Follow device are still in testing - colors on some screens may not fit yet",
"밤 0~6시": "Night 0-6", "밤 0~6시": "Night 0-6",
"방법: {0}": "Method: {0}", "방법: {0}": "Method: {0}",
"방식": "Type", "방식": "Type",

View File

@ -1454,9 +1454,9 @@
"받지 않음": "取得しない", "받지 않음": "取得しない",
"받지 않음 (처음 쓸 때 내려받기 {0})": "未ダウンロード (初回使用時にダウンロード {0})", "받지 않음 (처음 쓸 때 내려받기 {0})": "未ダウンロード (初回使用時にダウンロード {0})",
"밝게": "明るく", "밝게": "明るく",
"밝게 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다": "明るい・端末に合わせるはまだ試験中です - 一部の画面の色が合わない場合があります",
"밝기": "明るさ", "밝기": "明るさ",
"밝은 화면": "明るい", "밝은 화면": "明るい",
"밝은 화면 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다": "明るい・端末に合わせるはまだ試験中です - 一部の画面の色が合わない場合があります",
"밤 0~6시": "夜 0~6 時", "밤 0~6시": "夜 0~6 時",
"방법: {0}": "方法: {0}", "방법: {0}": "方法: {0}",
"방식": "方式", "방식": "方式",

View File

@ -1454,9 +1454,9 @@
"받지 않음": "不获取", "받지 않음": "不获取",
"받지 않음 (처음 쓸 때 내려받기 {0})": "未下载 (首次使用时下载 {0})", "받지 않음 (처음 쓸 때 내려받기 {0})": "未下载 (首次使用时下载 {0})",
"밝게": "调亮", "밝게": "调亮",
"밝게 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다": "浅色和跟随设备仍在测试中 - 部分界面的颜色可能还不协调",
"밝기": "亮度", "밝기": "亮度",
"밝은 화면": "浅色", "밝은 화면": "浅色",
"밝은 화면 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다": "浅色和跟随设备仍在测试中 - 部分界面的颜色可能还不协调",
"밤 0~6시": "夜间 0~6 点", "밤 0~6시": "夜间 0~6 点",
"방법: {0}": "方式: {0}", "방법: {0}": "方式: {0}",
"방식": "方式", "방식": "方式",

View File

@ -17,6 +17,8 @@ import 'package:jj_mkvmaker/ui/theme.dart';
/// 환경 설정 > 파일 탐색기 의 복사 · 동기화 부분을 실제 글꼴로 그려 PNG 로 (JJ_SHOT_DIR). 화면 배치 확인용. /// 환경 설정 > 파일 탐색기 의 복사 · 동기화 부분을 실제 글꼴로 그려 PNG 로 (JJ_SHOT_DIR). 화면 배치 확인용.
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('복사 · 동기화 설정 그림', (tester) async { testWidgets('복사 · 동기화 설정 그림', (tester) async {
final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path; final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path;

View File

@ -12,6 +12,7 @@ import 'package:jj_mkvmaker/ui/theme.dart';
/// 파일 탐색기 화면을 실제 글꼴 · 실제 FFmpeg (썸네일 · 동영상 정보) 로 그려 PNG 로 저장 (JJ_SHOT_DIR). /// 파일 탐색기 화면을 실제 글꼴 · 실제 FFmpeg (썸네일 · 동영상 정보) 로 그려 PNG 로 저장 (JJ_SHOT_DIR).
/// 왼쪽 · 오른쪽 창 폴더: JJ_SHOT_LEFT · JJ_SHOT_RIGHT. 화면 배치 확인용. /// 왼쪽 · 오른쪽 창 폴더: JJ_SHOT_LEFT · JJ_SHOT_RIGHT. 화면 배치 확인용.
/// JJ_SHOT_THEME=light 면 밝은 테마로 (⑦ - 파일 이름 끝에 _light)
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@ -21,6 +22,9 @@ void main() {
final c = AppController(PlatformServices.create()); final c = AppController(PlatformServices.create());
c.settings.explorerPaths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path]; c.settings.explorerPaths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path];
final key = GlobalKey(); final key = GlobalKey();
final light = env['JJ_SHOT_THEME'] == 'light';
JjColors.use(light ? Brightness.light : Brightness.dark);
addTearDown(() => JjColors.use(Brightness.dark));
for (final (name, size, layout, style) in [ for (final (name, size, layout, style) in [
('wide', const Size(1600, 900), 'dual', 'xplore'), ('wide', const Size(1600, 900), 'dual', 'xplore'),
('tall', const Size(900, 1400), 'dual', 'xplore'), ('tall', const Size(900, 1400), 'dual', 'xplore'),
@ -45,7 +49,7 @@ void main() {
final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary; final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await tester.runAsync(() => boundary.toImage()); final image = await tester.runAsync(() => boundary.toImage());
final bytes = await tester.runAsync(() => image!.toByteData(format: ui.ImageByteFormat.png)); final bytes = await tester.runAsync(() => image!.toByteData(format: ui.ImageByteFormat.png));
File('$out/explorer_$name.png').writeAsBytesSync(bytes!.buffer.asUint8List()); File('$out/explorer_$name${light ? '_light' : ''}.png').writeAsBytesSync(bytes!.buffer.asUint8List());
await tester.pumpWidget(const SizedBox()); await tester.pumpWidget(const SizedBox());
} }
tester.view.reset(); tester.view.reset();

View File

@ -16,6 +16,8 @@ import 'package:path/path.dart' as p;
/// 세 스타일 모두. 넘치면 (RenderFlex overflow) 시험이 실패한다 /// 세 스타일 모두. 넘치면 (RenderFlex overflow) 시험이 실패한다
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('40: 목록 글자 크기 · 줄 간격 화면 그림 (412 폭)', (tester) async { testWidgets('40: 목록 글자 크기 · 줄 간격 화면 그림 (412 폭)', (tester) async {
final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path; final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path;

View File

@ -19,6 +19,8 @@ import 'package:path/path.dart' as p;
/// 모니터링 화면 (복사 진행 중 · lsync · 일정 격자) 을 실제 글꼴로 그려 PNG 로 (JJ_SHOT_DIR). 화면 배치 확인용. /// 모니터링 화면 (복사 진행 중 · lsync · 일정 격자) 을 실제 글꼴로 그려 PNG 로 (JJ_SHOT_DIR). 화면 배치 확인용.
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('모니터링 화면 그림', (tester) async { testWidgets('모니터링 화면 그림', (tester) async {
final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path; final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path;

View File

@ -15,6 +15,8 @@ import 'package:jj_mkvmaker/ui/theme.dart';
/// 환경 설정 화면을 실제 글꼴로 그려 PNG 로 저장 (JJ_SHOT_DIR). 화면 배치 확인용. /// 환경 설정 화면을 실제 글꼴로 그려 PNG 로 저장 (JJ_SHOT_DIR). 화면 배치 확인용.
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('환경 설정 화면 그림', (tester) async { testWidgets('환경 설정 화면 그림', (tester) async {
final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path; final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path;

View File

@ -15,6 +15,8 @@ import 'package:path/path.dart' as p;
/// 크기 412 (폰 세로) · 960, 언어 ko · en · ja. 시험 파일은 임시 폴더에만 (끝나면 지움). 실행: flutter test integration_test\windows_p3_shot_test.dart -d windows /// 크기 412 (폰 세로) · 960, 언어 ko · en · ja. 시험 파일은 임시 폴더에만 (끝나면 지움). 실행: flutter test integration_test\windows_p3_shot_test.dart -d windows
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_p3'); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_p3');
for (final lang in ['ko', 'en', 'ja']) { for (final lang in ['ko', 'en', 'ja']) {
for (final (w, h) in [(412, 900), (960, 600)]) { for (final (w, h) in [(412, 900), (960, 600)]) {

View File

@ -22,6 +22,8 @@ import 'package:path/path.dart' as p;
/// 크기 412 (폰 세로, 한 창) · 960 (두 창), 언어 ko · en. 시험 파일은 임시 폴더에만, 설정은 메모리에만. /// 크기 412 (폰 세로, 한 창) · 960 (두 창), 언어 ko · en. 시험 파일은 임시 폴더에만, 설정은 메모리에만.
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_xplore'); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_xplore');
for (final lang in ['ko', 'en']) { for (final lang in ['ko', 'en']) {

View File

@ -22,6 +22,8 @@ import 'package:path/path.dart' as p;
/// (JJ_SHOT_DIR, 없으면 임시 폴더). 크기 412 · 960, 언어 ko · en. 시험 파일은 임시 폴더에만. /// (JJ_SHOT_DIR, 없으면 임시 폴더). 크기 412 · 960, 언어 ko · en. 시험 파일은 임시 폴더에만.
void main() { void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized(); IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_xptools'); final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_xptools');
for (final lang in ['ko', 'en']) { for (final lang in ['ko', 'en']) {

View File

@ -224,7 +224,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
_engineRow(), _engineRow(),
if (!_local) if (!_local)
// 인터넷 서비스 · 자기 서버를 쓸 때는 늘 알린다 // 인터넷 서비스 · 자기 서버를 쓸 때는 늘 알린다
_notice(Icons.public, Colors.orangeAccent, _notice(Icons.public, JjColors.warning,
trf('그림 · 프롬프트가 기기 밖으로 나갑니다 ({0})', [_service!.label])), trf('그림 · 프롬프트가 기기 밖으로 나갑니다 ({0})', [_service!.label])),
if (_local && !_modelsReady) if (_local && !_modelsReady)
_notice(Icons.download_outlined, JjColors.accent, _notice(Icons.download_outlined, JjColors.accent,
@ -287,7 +287,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(_error!, style: TextStyle(color: _cancelled ? JjColors.textDim : Colors.redAccent)), Text(_error!, style: TextStyle(color: _cancelled ? JjColors.textDim : JjColors.alert)),
if (_detail.isNotEmpty) if (_detail.isNotEmpty)
TextButton( TextButton(
onPressed: () => setState(() => _showDetail = !_showDetail), onPressed: () => setState(() => _showDetail = !_showDetail),

View File

@ -181,7 +181,7 @@ class _AiSettingsState extends State<AiSettings> {
free == null free == null
? trf('필요한 공간 {0}', [AiStore.sizeText(need)]) ? trf('필요한 공간 {0}', [AiStore.sizeText(need)])
: trf('필요한 공간 {0} · 남은 공간 {1}', [AiStore.sizeText(need), AiStore.sizeText(free)]), : trf('필요한 공간 {0} · 남은 공간 {1}', [AiStore.sizeText(need), AiStore.sizeText(free)]),
style: TextStyle(fontSize: 12, color: free != null && free < need ? Colors.redAccent : JjColors.textDim), style: TextStyle(fontSize: 12, color: free != null && free < need ? JjColors.alert : JjColors.textDim),
); );
}, },
), ),
@ -189,7 +189,7 @@ class _AiSettingsState extends State<AiSettings> {
if (_error != null) if (_error != null)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(_error!, style: const TextStyle(color: Colors.redAccent)), child: Text(_error!, style: TextStyle(color: JjColors.alert)),
), ),
for (final f in _files) _fileRow(f), for (final f in _files) _fileRow(f),
const Divider(), const Divider(),

View File

@ -268,7 +268,7 @@ class _FileBrowserState extends State<_FileBrowser> {
: ListView(children: [ : ListView(children: [
for (final d in _dirs) for (final d in _dirs)
ListTile( ListTile(
leading: const Icon(Icons.folder, color: Colors.amber), leading: Icon(Icons.folder, color: JjColors.amber),
title: Text(p.basename(d.path)), title: Text(p.basename(d.path)),
onTap: () => _open(d.path), onTap: () => _open(d.path),
), ),

View File

@ -189,12 +189,12 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
const SizedBox(height: 6), const SizedBox(height: 6),
Text(trf('풀 수 없는 이름 {0}개 (건너뜀): {1}', Text(trf('풀 수 없는 이름 {0}개 (건너뜀): {1}',
[plan.badNames.length, plan.badNames.take(5).map((x) => '${x.$1} (${x.$2})').join(', ')]), [plan.badNames.length, plan.badNames.take(5).map((x) => '${x.$1} (${x.$2})').join(', ')]),
style: const TextStyle(color: Colors.orangeAccent, fontSize: 12)), style: TextStyle(color: JjColors.warning, fontSize: 12)),
], ],
if (plan.links.isNotEmpty) ...[ if (plan.links.isNotEmpty) ...[
const SizedBox(height: 6), const SizedBox(height: 6),
Text(trf('링크 {0}개는 만들지 않고 건너뜁니다: {1}', [plan.links.length, plan.links.take(5).join(', ')]), Text(trf('링크 {0}개는 만들지 않고 건너뜁니다: {1}', [plan.links.length, plan.links.take(5).join(', ')]),
style: const TextStyle(color: Colors.orangeAccent, fontSize: 12)), style: TextStyle(color: JjColors.warning, fontSize: 12)),
], ],
if (plan.encrypted) ...[ if (plan.encrypted) ...[
const SizedBox(height: 6), const SizedBox(height: 6),
@ -223,7 +223,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
c.settings.copySkipNewer c.settings.copySkipNewer
? tr('덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).') ? tr('덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).')
: tr('덮어쓴 파일은 되돌릴 수 없습니다.'), : tr('덮어쓴 파일은 되돌릴 수 없습니다.'),
style: const TextStyle(color: Colors.orangeAccent, fontSize: 12)), style: TextStyle(color: JjColors.warning, fontSize: 12)),
// 관리자 판단: 풀린 크기가 빈 공간보다 크면 풀기 전에 알리고 묻는다 (압축 폭탄 · 공간 부족) // 관리자 판단: 풀린 크기가 빈 공간보다 크면 풀기 전에 알리고 묻는다 (압축 폭탄 · 공간 부족)
if (short) if (short)
Text(trf('풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?', [formatSize(plan.totalSize), formatSize(f)]), Text(trf('풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?', [formatSize(plan.totalSize), formatSize(f)]),

View File

@ -31,7 +31,7 @@ List<DropdownMenuItem<String>> folderItems(BookmarksController bm, {String? excl
child: Padding( child: Padding(
padding: EdgeInsets.only(left: depth * 14.0), padding: EdgeInsets.only(left: depth * 14.0),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(depth == 0 ? Icons.folder_special_outlined : Icons.folder_outlined, size: 16, color: Colors.amber), Icon(depth == 0 ? Icons.folder_special_outlined : Icons.folder_outlined, size: 16, color: JjColors.amber),
const SizedBox(width: 6), const SizedBox(width: 6),
Text(f.title), Text(f.title),
]), ]),
@ -368,7 +368,7 @@ class _BookmarkDraggableState extends State<BookmarkDraggable> {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(n.isFolder ? Icons.folder : Icons.public, size: 16, color: n.isFolder ? Colors.amber : null), Icon(n.isFolder ? Icons.folder : Icons.public, size: 16, color: n.isFolder ? JjColors.amber : null),
const SizedBox(width: 6), const SizedBox(width: 6),
Text(n.title, style: TextStyle(fontSize: 12, color: JjColors.text)), Text(n.title, style: TextStyle(fontSize: 12, color: JjColors.text)),
]), ]),
@ -610,7 +610,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
), ),
), ),
Icon(depth == 0 ? Icons.folder_special : (selected ? Icons.folder_open : Icons.folder), Icon(depth == 0 ? Icons.folder_special : (selected ? Icons.folder_open : Icons.folder),
size: 18, color: Colors.amber), size: 18, color: JjColors.amber),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded(child: Text(f.title, maxLines: 1, overflow: TextOverflow.ellipsis)), Expanded(child: Text(f.title, maxLines: 1, overflow: TextOverflow.ellipsis)),
]), ]),
@ -711,7 +711,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
BookmarkDraggable( BookmarkDraggable(
n: n, n: n,
onMenu: (at) => _menu(n, folder, at), onMenu: (at) => _menu(n, folder, at),
child: Icon(n.isFolder ? Icons.folder : Icons.public, size: 20, color: n.isFolder ? Colors.amber : JjColors.textDim), child: Icon(n.isFolder ? Icons.folder : Icons.public, size: 20, color: n.isFolder ? JjColors.amber : JjColors.textDim),
), ),
]), ]),
title: Text(n.title, maxLines: 1, overflow: TextOverflow.ellipsis), title: Text(n.title, maxLines: 1, overflow: TextOverflow.ellipsis),
@ -762,7 +762,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
leading: BookmarkDraggable( leading: BookmarkDraggable(
n: n, n: n,
onMenu: (at) => _menu(n, bm.tree.parentOf(n.id) ?? bm.tree.bar, at), onMenu: (at) => _menu(n, bm.tree.parentOf(n.id) ?? bm.tree.bar, at),
child: Icon(n.isFolder ? Icons.folder : Icons.public, size: 20, color: n.isFolder ? Colors.amber : JjColors.textDim), child: Icon(n.isFolder ? Icons.folder : Icons.public, size: 20, color: n.isFolder ? JjColors.amber : JjColors.textDim),
), ),
title: Text(n.title, maxLines: 1, overflow: TextOverflow.ellipsis), title: Text(n.title, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text( subtitle: Text(

View File

@ -870,7 +870,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
suffixIcon: IconButton( suffixIcon: IconButton(
tooltip: starred ? keyHint(tr('즐겨찾기 수정'), 'Ctrl+D') : keyHint(tr('즐겨찾기 추가'), 'Ctrl+D'), tooltip: starred ? keyHint(tr('즐겨찾기 수정'), 'Ctrl+D') : keyHint(tr('즐겨찾기 추가'), 'Ctrl+D'),
icon: Icon(starred ? Icons.star : Icons.star_border, size: 18, icon: Icon(starred ? Icons.star : Icons.star_border, size: 18,
color: starred ? Colors.amber : JjColors.textDim), color: starred ? JjColors.amber : JjColors.textDim),
onPressed: _star, onPressed: _star,
), ),
), ),
@ -1653,7 +1653,7 @@ class _BookmarkBar extends StatelessWidget {
menuChildren: _folderMenu(context, n), menuChildren: _folderMenu(context, n),
builder: (context, ctl, _) => TextButton.icon( builder: (context, ctl, _) => TextButton.icon(
onPressed: () => ctl.isOpen ? ctl.close() : ctl.open(), onPressed: () => ctl.isOpen ? ctl.close() : ctl.open(),
icon: const Icon(Icons.folder, size: 15, color: Colors.amber), icon: Icon(Icons.folder, size: 15, color: JjColors.amber),
label: Text(label, style: const TextStyle(fontSize: 12)), label: Text(label, style: const TextStyle(fontSize: 12)),
), ),
), ),
@ -1693,7 +1693,7 @@ class _BookmarkBar extends StatelessWidget {
for (final c in f.children!) for (final c in f.children!)
c.isFolder c.isFolder
? SubmenuButton( ? SubmenuButton(
leadingIcon: const Icon(Icons.folder, size: 16, color: Colors.amber), leadingIcon: Icon(Icons.folder, size: 16, color: JjColors.amber),
menuChildren: _folderMenu(context, c), menuChildren: _folderMenu(context, c),
child: Text(c.title), child: Text(c.title),
) )
@ -1911,7 +1911,7 @@ class _BookmarkPanelState extends State<BookmarkPanel> {
n: n, n: n,
onMenu: menuAt, onMenu: menuAt,
child: Icon(n.isFolder ? Icons.folder : Icons.public, child: Icon(n.isFolder ? Icons.folder : Icons.public,
size: 18, color: n.isFolder ? Colors.amber : JjColors.textDim), size: 18, color: n.isFolder ? JjColors.amber : JjColors.textDim),
), ),
]), ]),
title: Text(n.title, maxLines: 1, overflow: TextOverflow.ellipsis), title: Text(n.title, maxLines: 1, overflow: TextOverflow.ellipsis),

View File

@ -89,7 +89,7 @@ class _CleanupDialogState extends State<_CleanupDialog> {
), ),
]), ]),
subtitle: Text(g.skipped ?? g.hint, subtitle: Text(g.skipped ?? g.hint,
style: TextStyle(fontSize: 12, color: g.skipped != null ? Colors.amber : JjColors.textDim)), style: TextStyle(fontSize: 12, color: g.skipped != null ? JjColors.amberText : JjColors.textDim)),
), ),
]), ]),
), ),

View File

@ -327,10 +327,10 @@ class _LiveSyncTile extends StatelessWidget {
// 34: 안쪽 → 바깥은 지우기 없이만, 바깥 → 안쪽은 안 됨 (끝없이 돎) // 34: 안쪽 → 바깥은 지우기 없이만, 바깥 → 안쪽은 안 됨 (끝없이 돎)
if (LiveSync.nestingProblem(LiveSyncPair(src!, dst!, delete: delete), allowInnerToOuter: c.settings.allowInnerToOuter) if (LiveSync.nestingProblem(LiveSyncPair(src!, dst!, delete: delete), allowInnerToOuter: c.settings.allowInnerToOuter)
case final nest?) case final nest?)
Text(nest, style: const TextStyle(fontSize: 12, color: Colors.redAccent)) Text(nest, style: TextStyle(fontSize: 12, color: JjColors.alert))
else if (isSameOrInside(src!, dst!)) else if (isSameOrInside(src!, dst!))
Text(tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (지우기는 쓸 수 없음).'), Text(tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (지우기는 쓸 수 없음).'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
const SizedBox(height: 12), const SizedBox(height: 12),
Wrap(spacing: 6, children: [ Wrap(spacing: 6, children: [
for (final m in CopyMethod.values) for (final m in CopyMethod.values)

View File

@ -255,7 +255,7 @@ class _DlnaPageState extends State<DlnaPage> {
final e = _entries[i]; final e = _entries[i];
return ListTile( return ListTile(
dense: true, dense: true,
leading: Icon(_icon(e), color: e.isContainer ? Colors.amber : JjColors.textDim), leading: Icon(_icon(e), color: e.isContainer ? JjColors.amber : JjColors.textDim),
title: Text(e.title, maxLines: 1, overflow: TextOverflow.ellipsis), title: Text(e.title, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: e.isContainer subtitle: e.isContainer
? (e.childCount == null ? null : Text(trf('{0}개', [e.childCount!]))) ? (e.childCount == null ? null : Text(trf('{0}개', [e.childCount!])))

View File

@ -289,7 +289,7 @@ class _DownloadRow extends StatelessWidget {
DownloadState.queued => (tr('대기 중'), JjColors.textDim), DownloadState.queued => (tr('대기 중'), JjColors.textDim),
DownloadState.downloading when t.extra['waiting'] == true => (tr('대기 중'), JjColors.textDim), DownloadState.downloading when t.extra['waiting'] == true => (tr('대기 중'), JjColors.textDim),
DownloadState.downloading => (tr('받는 중'), JjColors.accent), DownloadState.downloading => (tr('받는 중'), JjColors.accent),
DownloadState.paused => (tr('일시정지'), Colors.amber), DownloadState.paused => (tr('일시정지'), JjColors.amberText),
DownloadState.done => (tr('완료'), JjColors.success), DownloadState.done => (tr('완료'), JjColors.success),
DownloadState.failed => (tr('실패'), JjColors.danger), DownloadState.failed => (tr('실패'), JjColors.danger),
DownloadState.cancelled => (tr('취소됨'), JjColors.textDim), DownloadState.cancelled => (tr('취소됨'), JjColors.textDim),
@ -321,7 +321,7 @@ class _DownloadRow extends StatelessWidget {
Text( Text(
'${trf('트래커 · 피어에 닿지 않습니다 ({0}초째)', [((DateTime.now().millisecondsSinceEpoch - ((t.extra['stallSince'] as int?) ?? 0)) / 1000).round()])} · ' '${trf('트래커 · 피어에 닿지 않습니다 ({0}초째)', [((DateTime.now().millisecondsSinceEpoch - ((t.extra['stallSince'] as int?) ?? 0)) / 1000).round()])} · '
'${trf('이유 후보: {0}', [[for (final r in (t.extra['stallReasons'] as List?) ?? const []) stallReasonText('$r')].join(' · ')])}', '${trf('이유 후보: {0}', [[for (final r in (t.extra['stallReasons'] as List?) ?? const []) stallReasonText('$r')].join(' · ')])}',
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
if (((t.extra['stallReasons'] as List?) ?? const []).contains('dns') && d.useSystemDns != null) if (((t.extra['stallReasons'] as List?) ?? const []).contains('dns') && d.useSystemDns != null)
TextButton(style: _small, onPressed: () => d.useSystemDns!(), child: Text(tr('시스템 DNS 로 바꾸기'))), TextButton(style: _small, onPressed: () => d.useSystemDns!(), child: Text(tr('시스템 DNS 로 바꾸기'))),
TextButton(style: _small, onPressed: () => d.retryStalled(t), child: Text(tr('다시 시도'))), TextButton(style: _small, onPressed: () => d.retryStalled(t), child: Text(tr('다시 시도'))),
@ -404,10 +404,10 @@ class DownloadProgressBar extends StatelessWidget {
// 주황으로 "멈춤" - 움직이는 "준비 중…" 이면 받는 중인 것처럼 보였다 // 주황으로 "멈춤" - 움직이는 "준비 중…" 이면 받는 중인 것처럼 보였다
final stalled = t.state == DownloadState.downloading && t.extra['stalled'] == true; final stalled = t.state == DownloadState.downloading && t.extra['stalled'] == true;
final color = switch (t.state) { final color = switch (t.state) {
_ when stalled => Colors.orangeAccent, _ when stalled => JjColors.warning,
DownloadState.done => JjColors.success, DownloadState.done => JjColors.success,
DownloadState.failed => JjColors.danger, DownloadState.failed => JjColors.danger,
DownloadState.paused => Colors.amber, DownloadState.paused => JjColors.amberText,
DownloadState.cancelled => JjColors.textDim, DownloadState.cancelled => JjColors.textDim,
_ => JjColors.accent, _ => JjColors.accent,
}; };

View File

@ -93,7 +93,7 @@ class _BookmarksDialogState extends State<_BookmarksDialog> {
for (final b in mine) for (final b in mine)
ListTile( ListTile(
dense: true, dense: true,
leading: Icon(b.isPair ? Icons.compare_arrows : Icons.star, color: Colors.amber, size: 20), leading: Icon(b.isPair ? Icons.compare_arrows : Icons.star, color: JjColors.amber, size: 20),
title: Text(b.name, maxLines: 1, overflow: TextOverflow.ellipsis), title: Text(b.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(b.isPair ? '${vDisplay(b.path)}\n⇄ ${vDisplay(b.right!)}' : vDisplay(b.path), subtitle: Text(b.isPair ? '${vDisplay(b.path)}\n⇄ ${vDisplay(b.right!)}' : vDisplay(b.path),
maxLines: b.isPair ? 2 : 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)), maxLines: b.isPair ? 2 : 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)),

View File

@ -129,7 +129,7 @@ class _KeyCaptureDialogState extends State<_KeyCaptureDialog> {
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text(trf('이미 "{0}" 에 쓰는 키입니다 - 저장하면 그 동작의 이 키는 없어집니다', [explorerKeyLabel(clash)]), child: Text(trf('이미 "{0}" 에 쓰는 키입니다 - 저장하면 그 동작의 이 키는 없어집니다', [explorerKeyLabel(clash)]),
style: const TextStyle(color: Colors.orangeAccent, fontSize: 12)), style: TextStyle(color: JjColors.warning, fontSize: 12)),
), ),
]), ]),
), ),

View File

@ -100,7 +100,7 @@ int columnHideLevel(ExplorerStyle style, double width, {bool marks = false, doub
final x = e.ext; final x = e.ext;
switch (style) { switch (style) {
case ExplorerStyle.xplore: case ExplorerStyle.xplore:
if (e.isDir) return (open ? Icons.folder_open : Icons.folder, Colors.amber); if (e.isDir) return (open ? Icons.folder_open : Icons.folder, JjColors.amber);
return ( return (
switch (x) { switch (x) {
_ when _subs.contains(x) => Icons.subtitles_outlined, _ when _subs.contains(x) => Icons.subtitles_outlined,
@ -128,16 +128,17 @@ int columnHideLevel(ExplorerStyle style, double width, {bool marks = false, doub
if (_sheets.contains(x)) return (Icons.table_chart, const Color(0xFF4CAF7A)); if (_sheets.contains(x)) return (Icons.table_chart, const Color(0xFF4CAF7A));
if (_slides.contains(x)) return (Icons.slideshow, const Color(0xFFE8804F)); if (_slides.contains(x)) return (Icons.slideshow, const Color(0xFFE8804F));
if (_programs.contains(x)) return (x == 'apk' ? Icons.android : Icons.terminal, const Color(0xFF8BC34A)); if (_programs.contains(x)) return (x == 'apk' ? Icons.android : Icons.terminal, const Color(0xFF8BC34A));
if (_texts.contains(x)) return (Icons.description, const Color(0xFFB0BEC5)); // ⑦ 밝은 화면: 옅은 회색은 흰 바탕에서 안 보여 진하게
return (Icons.insert_drive_file, const Color(0xFFB0BEC5)); if (_texts.contains(x)) return (Icons.description, JjColors.isLight ? const Color(0xFF78909C) : const Color(0xFFB0BEC5));
return (Icons.insert_drive_file, JjColors.isLight ? const Color(0xFF78909C) : const Color(0xFFB0BEC5));
case ExplorerStyle.totalcmd: case ExplorerStyle.totalcmd:
// Total Commander 느낌: 단순한 작은 아이콘, 폴더는 노랑, 실행 파일은 초록 // Total Commander 느낌: 단순한 작은 아이콘, 폴더는 노랑, 실행 파일은 초록
if (e.isDir) return (Icons.folder, const Color(0xFFE8C547)); if (e.isDir) return (Icons.folder, const Color(0xFFE8C547));
if (_programs.contains(x)) return (Icons.settings_applications, const Color(0xFF7CB342)); if (_programs.contains(x)) return (Icons.settings_applications, const Color(0xFF7CB342));
if (isVideoFile(e.path)) return (Icons.movie, const Color(0xFF64B5F6)); if (isVideoFile(e.path)) return (Icons.movie, const Color(0xFF64B5F6));
if (_images.contains(x)) return (Icons.image, const Color(0xFF64B5F6)); if (_images.contains(x)) return (Icons.image, const Color(0xFF64B5F6));
if (_archives.contains(x)) return (Icons.inventory_2, const Color(0xFFBCAAA4)); if (_archives.contains(x)) return (Icons.inventory_2, JjColors.isLight ? const Color(0xFF8D6E63) : const Color(0xFFBCAAA4));
return (Icons.description, const Color(0xFFCFD8DC)); return (Icons.description, JjColors.isLight ? const Color(0xFF78909C) : const Color(0xFFCFD8DC));
} }
} }

View File

@ -612,7 +612,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
? tr('목록 · 보기 · 꺼내기 (복사) 만 됩니다. 시스템 · 다른 앱의 개인 자료가 있으니 꺼낸 파일을 조심해서 다루세요') ? tr('목록 · 보기 · 꺼내기 (복사) 만 됩니다. 시스템 · 다른 앱의 개인 자료가 있으니 꺼낸 파일을 조심해서 다루세요')
: tr('목록 · 보기 · 꺼내기 (복사) 만 됩니다. 다른 앱의 데이터가 있을 수 있으니 꺼낸 파일을 조심해서 다루세요'), : tr('목록 · 보기 · 꺼내기 (복사) 만 됩니다. 다른 앱의 데이터가 있을 수 있으니 꺼낸 파일을 조심해서 다루세요'),
actions: const [], actions: const [],
color: Colors.orangeAccent, color: JjColors.warning,
); );
} }
@ -623,7 +623,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
scrollable: true, scrollable: true,
icon: const Icon(Icons.security, color: Colors.orangeAccent), icon: Icon(Icons.security, color: JjColors.warning),
title: Text(tr('루트 폴더 보기 (읽기 전용)')), title: Text(tr('루트 폴더 보기 (읽기 전용)')),
content: Text(tr('루트 (su) 권한으로 시스템 · 다른 앱의 폴더 전체를 봅니다. 다른 앱의 개인 자료 (대화 · 로그인 정보 · 비밀번호 등) 도 ' content: Text(tr('루트 (su) 권한으로 시스템 · 다른 앱의 폴더 전체를 봅니다. 다른 앱의 개인 자료 (대화 · 로그인 정보 · 비밀번호 등) 도 '
'보일 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.')), '보일 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.')),
@ -662,7 +662,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
scrollable: true, scrollable: true,
icon: const Icon(Icons.admin_panel_settings_outlined, color: Colors.orangeAccent), icon: Icon(Icons.admin_panel_settings_outlined, color: JjColors.warning),
title: Text(tr('시스템 폴더 보기 (읽기 전용)')), title: Text(tr('시스템 폴더 보기 (읽기 전용)')),
content: Text(tr('Shizuku 의 권한으로 Android/data · obb 처럼 막힌 폴더를 봅니다. 다른 앱의 데이터 (대화 · 로그인 정보 등) 가 ' content: Text(tr('Shizuku 의 권한으로 Android/data · obb 처럼 막힌 폴더를 봅니다. 다른 앱의 데이터 (대화 · 로그인 정보 등) 가 '
'있을 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.')), '있을 수 있습니다. 이 앱은 읽기만 하고 (쓰기 · 지우기 · 이름 바꾸기 없음), 꺼낸 파일은 고른 곳에만 둡니다.')),
@ -726,7 +726,10 @@ class _ExplorerPageState extends State<ExplorerPage> {
required String title, required String title,
required String body, required String body,
required List<Widget> actions, required List<Widget> actions,
Color color = Colors.redAccent}) => Color? color}) =>
_noticeBox(icon, title, body, actions, color ?? JjColors.alert);
Widget _noticeBox(IconData icon, String title, String body, List<Widget> actions, Color color) =>
Container( Container(
width: double.infinity, width: double.infinity,
margin: const EdgeInsets.fromLTRB(6, 6, 6, 0), margin: const EdgeInsets.fromLTRB(6, 6, 6, 0),
@ -764,7 +767,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
if (list.isEmpty) return null; if (list.isEmpty) return null;
return _notice( return _notice(
icon: Icons.broken_image_outlined, icon: Icons.broken_image_outlined,
color: Colors.orangeAccent, color: JjColors.warning,
title: trf('만들다 만 파일 {0}개', [list.length]), title: trf('만들다 만 파일 {0}개', [list.length]),
body: tr('복사 · 동기화가 중간에 끊겨 남은 임시 파일입니다 (.jjpart). 완성된 파일이 아니므로 지워도 됩니다. ' body: tr('복사 · 동기화가 중간에 끊겨 남은 임시 파일입니다 (.jjpart). 완성된 파일이 아니므로 지워도 됩니다. '
'같은 파일을 다시 복사하면 새로 만듭니다.'), '같은 파일을 다시 복사하면 새로 만듭니다.'),
@ -1531,8 +1534,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
volume, volume,
false, false,
), ),
_Row.favorites => (Icons.star, Colors.amber, tr('즐겨찾기'), pane.favoritesOpen), _Row.favorites => (Icons.star, JjColors.amber, tr('즐겨찾기'), pane.favoritesOpen),
_ => (Icons.star_outline, Colors.amber, fav!.name, null), _ => (Icons.star_outline, JjColors.amber, fav!.name, null),
}; };
void tap() { void tap() {
setState(() => _active = _panes.indexOf(pane)); setState(() => _active = _panes.indexOf(pane));
@ -2990,19 +2993,19 @@ class _ExplorerPageState extends State<ExplorerPage> {
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text(trf('그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께', [paths.where(vIsDirSync).length]), child: Text(trf('그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께', [paths.where(vIsDirSync).length]),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
), ),
if (hiddenInside > 0) if (hiddenInside > 0)
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text(trf('지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다', [hiddenInside]), child: Text(trf('지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다', [hiddenInside]),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
), ),
if (local && !canRecycle) if (local && !canRecycle)
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text(tr('이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.'), child: Text(tr('이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
), ),
if (canTrash && !permanent) if (canTrash && !permanent)
Padding( Padding(
@ -3080,7 +3083,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
scrollable: true, scrollable: true,
icon: const Icon(Icons.warning_amber_rounded, color: Colors.orangeAccent), icon: Icon(Icons.warning_amber_rounded, color: JjColors.warning),
title: Text(trf('{0}개를 휴지통으로 옮기지 못했습니다', [notTrashed.length])), title: Text(trf('{0}개를 휴지통으로 옮기지 못했습니다', [notTrashed.length])),
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
for (final (x, why) in notTrashed.take(10)) Text('· ${vBasename(x)} - $why', style: const TextStyle(fontSize: 12)), for (final (x, why) in notTrashed.take(10)) Text('· ${vBasename(x)} - $why', style: const TextStyle(fontSize: 12)),
@ -3112,7 +3115,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
scrollable: true, scrollable: true,
icon: const Icon(Icons.error_outline, color: Colors.redAccent), icon: Icon(Icons.error_outline, color: JjColors.alert),
title: Text(trf('{0}개 중 {1}개를 지우지 못했습니다', [paths.length, failed.length + tooLong.length])), title: Text(trf('{0}개 중 {1}개를 지우지 못했습니다', [paths.length, failed.length + tooLong.length])),
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
// 144: 긴 경로는 이유 한 줄 + 이름만 (영구 삭제는 아래 버튼으로) // 144: 긴 경로는 이유 한 줄 + 이름만 (영구 삭제는 아래 버튼으로)
@ -3138,7 +3141,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
if (tooLong.isNotEmpty) ...[ if (tooLong.isNotEmpty) ...[
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('그대로 두기'))), TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('그대로 두기'))),
FilledButton( FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.redAccent), style: FilledButton.styleFrom(backgroundColor: JjColors.alert),
onPressed: () => Navigator.pop(ctx, true), onPressed: () => Navigator.pop(ctx, true),
child: Text(trf('영구 삭제 ({0}개, 되돌릴 수 없음)', [tooLong.length])), child: Text(trf('영구 삭제 ({0}개, 되돌릴 수 없음)', [tooLong.length])),
), ),
@ -3501,7 +3504,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
const SizedBox(height: 12), const SizedBox(height: 12),
Text(trf('같은 이름이 이미 있습니다: {0}', [_namesOf([for (final x in clashes) x.src])]), Text(trf('같은 이름이 이미 있습니다: {0}', [_namesOf([for (final x in clashes) x.src])]),
style: const TextStyle(color: Colors.orangeAccent)), style: TextStyle(color: JjColors.warning)),
// 172: 두 파일의 크기 · 바뀐 날 (3개까지) // 172: 두 파일의 크기 · 바뀐 날 (3개까지)
for (final x in clashes.take(3)) ...[ for (final x in clashes.take(3)) ...[
Padding( Padding(
@ -3522,7 +3525,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
: c.settings.copySkipNewer : c.settings.copySkipNewer
? trf('안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)', [n.data]) ? trf('안쪽에 받는 쪽이 더 새 파일 {0}개 - 합쳐도 건너뜁니다 (원본에 남음)', [n.data])
: trf('안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다', [n.data]), : trf('안쪽에 받는 쪽이 더 새 파일 {0}개 - 합치면 예전 것으로 덮어씁니다', [n.data]),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent), style: TextStyle(fontSize: 12, color: JjColors.warning),
), ),
), ),
), ),
@ -3751,14 +3754,14 @@ class _ExplorerPageState extends State<ExplorerPage> {
removeSource && !both removeSource && !both
? tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래로 옮겨집니다 (원본 파일은 지워지고, 원본 폴더는 아래에서 고른 대로).') ? tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래로 옮겨집니다 (원본 파일은 지워지고, 원본 폴더는 아래에서 고른 대로).')
: tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (원본 폴더는 그대로 남음).'), : tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (원본 폴더는 그대로 남음).'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
// 1280×720 등 낮은 화면에서도 보이게: 원본 지우기 체크 · 경고는 비교 목록보다 위에 (178) // 1280×720 등 낮은 화면에서도 보이게: 원본 지우기 체크 · 경고는 비교 목록보다 위에 (178)
if (both) ...[ if (both) ...[
Text(keepOptionsTogether(tr('양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).')), Text(keepOptionsTogether(tr('양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).')),
style: TextStyle(fontSize: 12, color: JjColors.textDim)), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
if (optionMove) if (optionMove)
Text(tr('양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).'), Text(tr('양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
] else ...[ ] else ...[
CheckboxListTile( CheckboxListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@ -3778,7 +3781,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
optionMove optionMove
? tr('원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.') ? tr('원본 파일이 지워집니다 (이동). 환경 설정의 rsync 옵션에 --remove-source-files 가 있어 켜 두었습니다 - 끄면 이번에는 원본을 남깁니다.')
: tr('원본 파일이 지워집니다 (이동).'), : tr('원본 파일이 지워집니다 (이동).'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)), style: TextStyle(fontSize: 12, color: JjColors.warning, fontWeight: FontWeight.w600)),
const SizedBox(height: 4), const SizedBox(height: 4),
Text(tr('끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:'), Text(tr('끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:'),
style: TextStyle(fontSize: 12, color: JjColors.textDim)), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
@ -3824,7 +3827,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
), ),
if (nestedDelete()) if (nestedDelete())
Text(tr('원본이 대상 안에 있으면 지우기 (--delete) 를 함께 쓸 수 없습니다 (대상의 다른 파일이 모두 지워집니다).'), Text(tr('원본이 대상 안에 있으면 지우기 (--delete) 를 함께 쓸 수 없습니다 (대상의 다른 파일이 모두 지워집니다).'),
style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)), style: TextStyle(fontSize: 12, color: JjColors.alert, fontWeight: FontWeight.w600)),
// 110: 이 줄은 늘 같은 자리를 차지한다 (글이 사라져 창 높이가 바뀌면 버튼이 움직인다) // 110: 이 줄은 늘 같은 자리를 차지한다 (글이 사라져 창 높이가 바뀌면 버튼이 움직인다)
SizedBox( SizedBox(
height: 34, height: 34,
@ -3833,7 +3836,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
previewError != null previewError != null
? tr('지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.') ? tr('지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.')
: tr('지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.'), : tr('지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent), style: TextStyle(fontSize: 12, color: JjColors.warning),
) )
: null, : null,
), ),
@ -5485,7 +5488,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
icon: Icon( icon: Icon(
c.settings.folderBookmarks.any((b) => !b.isPair && samePath(b.path, pane.current)) ? Icons.star : Icons.star_outline, c.settings.folderBookmarks.any((b) => !b.isPair && samePath(b.path, pane.current)) ? Icons.star : Icons.star_outline,
size: 16, size: 16,
color: Colors.amber), color: JjColors.amber),
onPressed: () => _bookmarks(pane), onPressed: () => _bookmarks(pane),
), ),
// 76: 빠른 필터 (켜져 있으면 강조) // 76: 빠른 필터 (켜져 있으면 강조)
@ -5562,7 +5565,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
// 165: 열 수 없는 드라이브는 숨기지 않고 "연결 안 됨" - 누르면 다시 연결 (다시 읽기) // 165: 열 수 없는 드라이브는 숨기지 않고 "연결 안 됨" - 누르면 다시 연결 (다시 읽기)
label: _driveOk[path] == false label: _driveOk[path] == false
? Text('$label · ${_removableDrives.contains(path) ? tr('비어 있음') : tr('연결 안 됨')} ⟳', ? Text('$label · ${_removableDrives.contains(path) ? tr('비어 있음') : tr('연결 안 됨')} ⟳',
style: TextStyle(color: _removableDrives.contains(path) ? JjColors.textDim : Colors.orangeAccent)) style: TextStyle(color: _removableDrives.contains(path) ? JjColors.textDim : JjColors.warning))
: Text(label), : Text(label),
onPressed: () { onPressed: () {
setState(() => _active = _panes.indexOf(pane)); setState(() => _active = _panes.indexOf(pane));
@ -5830,7 +5833,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
if (slim) if (slim)
const SizedBox.shrink() const SizedBox.shrink()
else if (look.rich && e.isDir && pane.errors[e.path] != null) else if (look.rich && e.isDir && pane.errors[e.path] != null)
Text(tr('읽을 수 없음'), style: dim.copyWith(color: Colors.redAccent)) Text(tr('읽을 수 없음'), style: dim.copyWith(color: JjColors.alert))
// 80: 열 없는 모양 (X-plore · 좁은 화면) 은 폴더 이름 아래에 크기 // 80: 열 없는 모양 (X-plore · 좁은 화면) 은 폴더 이름 아래에 크기
else if (e.isDir && !row.isRoot && !row.isUp && (!look.columns || compact) && _dirSize(pane, e) != null) else if (e.isDir && !row.isRoot && !row.isUp && (!look.columns || compact) && _dirSize(pane, e) != null)
Text(_dirSize(pane, e)!, style: dim, maxLines: 1), Text(_dirSize(pane, e)!, style: dim, maxLines: 1),

View File

@ -451,13 +451,13 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Text(tr('처음 찾을 때 끝까지 찾기 전의 결과입니다. 더 찾으려면 다시 찾으세요.'), child: Text(tr('처음 찾을 때 끝까지 찾기 전의 결과입니다. 더 찾으려면 다시 찾으세요.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
), ),
if (_paused) if (_paused)
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 4),
child: Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ child: Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
Text(trf('{0}개에서 멈췄습니다', [_found.length]), style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), Text(trf('{0}개에서 멈췄습니다', [_found.length]), style: TextStyle(fontSize: 12, color: JjColors.warning)),
OutlinedButton(onPressed: _more, child: Text(trf('{0}개 더 보기', [widget.chunk]))), OutlinedButton(onPressed: _more, child: Text(trf('{0}개 더 보기', [widget.chunk]))),
]), ]),
), ),
@ -468,7 +468,7 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
tilePadding: EdgeInsets.zero, tilePadding: EdgeInsets.zero,
dense: true, dense: true,
title: Text(trf('읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다', [_skipped.length]), title: Text(trf('읽지 못한 폴더 {0}개 (권한 · 연결 문제) - 그 안은 찾지 못했습니다', [_skipped.length]),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
children: [ children: [
for (final (dir, why) in _skipped.take(50)) for (final (dir, why) in _skipped.take(50))
Align( Align(

View File

@ -96,7 +96,7 @@ Future<void> showFileHash(BuildContext context, String path) async {
Padding( Padding(
padding: const EdgeInsets.only(top: 6), padding: const EdgeInsets.only(top: 6),
child: Text(match ? tr('같습니다 ✔') : tr('다릅니다 - 파일이 바뀌었거나 다른 파일입니다'), child: Text(match ? tr('같습니다 ✔') : tr('다릅니다 - 파일이 바뀌었거나 다른 파일입니다'),
style: TextStyle(color: match ? Colors.greenAccent : JjColors.danger)), style: TextStyle(color: match ? JjColors.positive : JjColors.danger)),
), ),
], ],
]), ]),

View File

@ -11,6 +11,7 @@ import '../core/webdav.dart';
import '../l10n/tr.dart'; import '../l10n/tr.dart';
import 'android_file_browser.dart'; import 'android_file_browser.dart';
import 'file_error.dart'; import 'file_error.dart';
import 'theme.dart';
/// 폴더 고르기. Android 는 앱 안 화면 (내장 저장소 · SD 카드 · USB 를 실제 경로로 고를 수 있음), PC 는 Windows 폴더 선택 창 /// 폴더 고르기. Android 는 앱 안 화면 (내장 저장소 · SD 카드 · USB 를 실제 경로로 고를 수 있음), PC 는 Windows 폴더 선택 창
Future<String?> pickFolder(BuildContext context, String title, [String? initial]) => Platform.isAndroid Future<String?> pickFolder(BuildContext context, String title, [String? initial]) => Platform.isAndroid
@ -94,9 +95,9 @@ class _DavFolderDialogState extends State<_DavFolderDialog> {
final locked = isLockedError(error); final locked = isLockedError(error);
return Center( return Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(locked ? Icons.lock_outline : Icons.cloud_off_outlined, color: Colors.redAccent), Icon(locked ? Icons.lock_outline : Icons.cloud_off_outlined, color: JjColors.alert),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(title, textAlign: TextAlign.center, style: const TextStyle(color: Colors.redAccent, fontWeight: FontWeight.w600)), Text(title, textAlign: TextAlign.center, style: TextStyle(color: JjColors.alert, fontWeight: FontWeight.w600)),
if (body.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text(body, textAlign: TextAlign.center)), if (body.isNotEmpty) Padding(padding: const EdgeInsets.only(top: 4), child: Text(body, textAlign: TextAlign.center)),
const SizedBox(height: 10), const SizedBox(height: 10),
FilledButton.tonal( FilledButton.tonal(

View File

@ -291,7 +291,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
), ),
], ],
if (testing) const LinearProgressIndicator(), if (testing) const LinearProgressIndicator(),
if (result != null) Text(result!, style: TextStyle(fontSize: 12, color: passed ? Colors.greenAccent : Colors.redAccent)), if (result != null) Text(result!, style: TextStyle(fontSize: 12, color: passed ? JjColors.positive : JjColors.alert)),
]), ]),
), ),
actions: [ actions: [
@ -300,7 +300,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
onPressed: () async { onPressed: () async {
if (await deleteFtpServer(ctx, c, old) && ctx.mounted) Navigator.pop(ctx); if (await deleteFtpServer(ctx, c, old) && ctx.mounted) Navigator.pop(ctx);
}, },
style: TextButton.styleFrom(foregroundColor: Colors.redAccent), style: TextButton.styleFrom(foregroundColor: JjColors.alert),
child: Text(tr('지우기')), child: Text(tr('지우기')),
), ),
TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))), TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))),

View File

@ -116,7 +116,7 @@ class _MasterDialogState extends State<_MasterDialog> {
Padding( Padding(
padding: const EdgeInsets.only(top: 8), padding: const EdgeInsets.only(top: 8),
child: Text(trf('여러 번 틀려 {0}초 뒤에 다시 넣을 수 있습니다', [wait.inSeconds + 1]), child: Text(trf('여러 번 틀려 {0}초 뒤에 다시 넣을 수 있습니다', [wait.inSeconds + 1]),
style: const TextStyle(color: Colors.redAccent)), style: TextStyle(color: JjColors.alert)),
), ),
if (_busy) const Padding(padding: EdgeInsets.only(top: 8), child: LinearProgressIndicator()), if (_busy) const Padding(padding: EdgeInsets.only(top: 8), child: LinearProgressIndicator()),
if (!widget.forChange) if (!widget.forChange)

View File

@ -120,7 +120,7 @@ Future<bool?> _liveDeleteDialog(BuildContext context, LiveSync live, LiveSyncPai
// 관리자 3a 판단 (10/20): 확인한 쌍인데 옵션이 바뀌어 지울 것이 늘어 다시 묻는 것 // 관리자 3a 판단 (10/20): 확인한 쌍인데 옵션이 바뀌어 지울 것이 늘어 다시 묻는 것
if (!now && live.reasked.contains(LiveSync.keyOf(x))) ...[ if (!now && live.reasked.contains(LiveSync.keyOf(x))) ...[
Text(tr('지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.'), Text(tr('지우기를 확인한 뒤 rsync 옵션이 바뀌어 지울 것이 늘었습니다. 다시 확인해 주세요.'),
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.orangeAccent)), style: TextStyle(fontWeight: FontWeight.w600, color: JjColors.warning)),
const SizedBox(height: 6), const SizedBox(height: 6),
], ],
Text(trf('"{0}" 에는 없고 대상 "{1}" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).', Text(trf('"{0}" 에는 없고 대상 "{1}" 에만 있는 항목입니다. 지우기 포함으로 맞추면 대상에서 지워집니다 (되돌릴 수 없음).',
@ -238,7 +238,7 @@ class _DiskInfo extends StatelessWidget {
child: LinearProgressIndicator( child: LinearProgressIndicator(
minHeight: 6, minHeight: 6,
value: used, value: used,
color: used > 0.9 ? Colors.redAccent : JjColors.accent, color: used > 0.9 ? JjColors.alert : JjColors.accent,
backgroundColor: JjColors.border, backgroundColor: JjColors.border,
), ),
), ),
@ -524,7 +524,7 @@ class _CopyCardState extends State<_CopyCard> {
if (running && t.move) const SizedBox(height: 10), if (running && t.move) const SizedBox(height: 10),
if (t.move) if (t.move)
Text(tr('실시간 동기화는 원본을 지우지 않습니다 (복사로 맞춤). 원본을 비우려면 lsync 의 [최종 정리] 를 쓰세요.'), Text(tr('실시간 동기화는 원본을 지우지 않습니다 (복사로 맞춤). 원본을 비우려면 lsync 의 [최종 정리] 를 쓰세요.'),
style: const TextStyle(color: Colors.orangeAccent)), style: TextStyle(color: JjColors.warning)),
]), ]),
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))), TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
@ -558,12 +558,12 @@ class _CopyCardState extends State<_CopyCard> {
// 235: 이동인데 원본에 남은 것이 있으면 "끝" 만 보이지 않게 (주황 · 남은 수) // 235: 이동인데 원본에 남은 것이 있으면 "끝" 만 보이지 않게 (주황 · 남은 수)
'done' when t.lastLeft > 0 => ( 'done' when t.lastLeft > 0 => (
Icons.warning_amber, Icons.warning_amber,
Colors.orangeAccent, JjColors.warning,
'${trf('끝 · {0} · 파일 {1}개', [_hm(t.lastRun), t.lastFiles])} · ${trf('{0}개는 원본에 남음 (건너뜀)', [t.lastLeft])}' '${trf('끝 · {0} · 파일 {1}개', [_hm(t.lastRun), t.lastFiles])} · ${trf('{0}개는 원본에 남음 (건너뜀)', [t.lastLeft])}'
), ),
'done' => (Icons.check_circle, Colors.greenAccent, trf('끝 · {0} · 파일 {1}개', [_hm(t.lastRun), t.lastFiles])), 'done' => (Icons.check_circle, JjColors.positive, trf('끝 · {0} · 파일 {1}개', [_hm(t.lastRun), t.lastFiles])),
'failed' => (Icons.error, Colors.redAccent, trf('실패 · {0}', [_hm(t.lastRun)])), 'failed' => (Icons.error, JjColors.alert, trf('실패 · {0}', [_hm(t.lastRun)])),
'cancelled' => (Icons.pause_circle, Colors.orangeAccent, trf('멈춤 · {0}', [_hm(t.lastRun)])), 'cancelled' => (Icons.pause_circle, JjColors.warning, trf('멈춤 · {0}', [_hm(t.lastRun)])),
_ => (Icons.schedule, JjColors.textDim, tr('아직 실행 안 함')), _ => (Icons.schedule, JjColors.textDim, tr('아직 실행 안 함')),
}; };
final optsHint = switch (CopyMethod.of(_method)) { final optsHint = switch (CopyMethod.of(_method)) {
@ -612,7 +612,7 @@ class _CopyCardState extends State<_CopyCard> {
Padding( Padding(
padding: const EdgeInsets.only(left: 28, top: 4), padding: const EdgeInsets.only(left: 28, top: 4),
child: Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 4, children: [ child: Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 4, children: [
Text(t.lastMessage, maxLines: 3, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, color: Colors.redAccent)), Text(t.lastMessage, maxLines: 3, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11, color: JjColors.alert)),
// 237: 원문 (예외 이름 · 명령 줄) 은 [자세히] 에서 - 보고 복사해 물어볼 수 있게 (다운로드 실패 줄 136 과 같게) // 237: 원문 (예외 이름 · 명령 줄) 은 [자세히] 에서 - 보고 복사해 물어볼 수 있게 (다운로드 실패 줄 136 과 같게)
if (t.lastDetail.trim().isNotEmpty) if (t.lastDetail.trim().isNotEmpty)
TextButton( TextButton(
@ -660,13 +660,13 @@ class _CopyCardState extends State<_CopyCard> {
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text( Text(
'${t.move ? tr('이동') : tr('복사')} · ${trf('실제 실행: {0}', [effectiveRunText(_method, _options.text.trim(), move: t.move, skipNewer: s.copySkipNewer)])}', '${t.move ? tr('이동') : tr('복사')} · ${trf('실제 실행: {0}', [effectiveRunText(_method, _options.text.trim(), move: t.move, skipNewer: s.copySkipNewer)])}',
style: TextStyle(fontSize: 12, color: t.move ? Colors.orangeAccent : JjColors.textDim), style: TextStyle(fontSize: 12, color: t.move ? JjColors.warning : JjColors.textDim),
), ),
// 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션 (실행은 작업이 막는다) // 관리자 지시 (10/20, 28 확인): 고쳐 읽어도 알아볼 수 없는 옵션 (실행은 작업이 막는다)
if (_method == 'rsync' && optionProblems(_options.text).isNotEmpty) OptionProblemNote(optionProblems(_options.text)), if (_method == 'rsync' && optionProblems(_options.text).isNotEmpty) OptionProblemNote(optionProblems(_options.text)),
if (!t.move && optionsRemoveSource(_options.text)) if (!t.move && optionsRemoveSource(_options.text))
Text(tr('복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요'), Text(tr('복사 작업이라 칸의 원본 지우기 옵션 (--remove-source-files · /MOV) 은 쓰지 않습니다. 옮기려면 Rsync 화면에서 [원본 파일 지우기] 를 켜세요'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
]), ]),
), ),
SizedBox( SizedBox(
@ -781,7 +781,7 @@ class _LiveTabState extends State<_LiveTab> {
PruneChoice(value: prune, onChanged: (v) => set(() => prune = v)), PruneChoice(value: prune, onChanged: (v) => set(() => prune = v)),
const SizedBox(height: 12), const SizedBox(height: 12),
Text(tr('원본이 비므로 이 실시간 동기화는 끕니다. 진행은 [복사 · rsync] 탭에서 봅니다.'), Text(tr('원본이 비므로 이 실시간 동기화는 끕니다. 진행은 [복사 · rsync] 탭에서 봅니다.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
]), ]),
), ),
actions: [ actions: [
@ -928,12 +928,12 @@ class _LiveTabState extends State<_LiveTab> {
), ),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [ Row(children: [
Icon(e.empty ? Icons.folder_off_outlined : Icons.error_outline, color: Colors.redAccent, size: 20), Icon(e.empty ? Icons.folder_off_outlined : Icons.error_outline, color: JjColors.alert, size: 20),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
e.empty ? tr('원본 폴더가 비어 있어 멈췄습니다') : tr('원본을 읽을 수 없어 멈췄습니다'), e.empty ? tr('원본 폴더가 비어 있어 멈췄습니다') : tr('원본을 읽을 수 없어 멈췄습니다'),
style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.redAccent), style: TextStyle(fontWeight: FontWeight.w600, color: JjColors.alert),
), ),
), ),
]), ]),
@ -1002,7 +1002,7 @@ class _LiveTabState extends State<_LiveTab> {
: active : active
? Icons.sync ? Icons.sync
: Icons.bedtime_outlined, : Icons.bedtime_outlined,
color: x.enabled && !held ? (active ? JjColors.accent : Colors.orangeAccent) : JjColors.textDim), color: x.enabled && !held ? (active ? JjColors.accent : JjColors.warning) : JjColors.textDim),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
// 67: 끝 폴더가 보이게 짧게 · 69: 길게 누르면 (오른쪽 클릭) 고치기 · 지우기 // 67: 끝 폴더가 보이게 짧게 · 69: 길게 누르면 (오른쪽 클릭) 고치기 · 지우기
@ -1037,16 +1037,16 @@ class _LiveTabState extends State<_LiveTab> {
Padding( Padding(
padding: const EdgeInsets.only(left: 32, top: 2), padding: const EdgeInsets.only(left: 32, top: 2),
child: Text(tr('멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다'), child: Text(tr('멈춤 (이번 실행 동안) · 시작 버튼을 누르면 다시 동기화합니다'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
), ),
// 42: 지우기를 아직 확인하지 않았고 지울 것이 있다 → 지우지 않고 맞추는 중. 목록을 보고 결정 // 42: 지우기를 아직 확인하지 않았고 지울 것이 있다 → 지우지 않고 맞추는 중. 목록을 보고 결정
if (live != null && (live.toDelete[k]?.isNotEmpty ?? false)) if (live != null && (live.toDelete[k]?.isNotEmpty ?? false))
Padding( Padding(
padding: const EdgeInsets.only(left: 32, top: 4), padding: const EdgeInsets.only(left: 32, top: 4),
child: Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ child: Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
const Icon(Icons.warning_amber_rounded, size: 16, color: Colors.orangeAccent), Icon(Icons.warning_amber_rounded, size: 16, color: JjColors.warning),
Text(trf('대상에만 있는 {0}개를 지울지 확인이 필요합니다 (확인 전에는 지우지 않음)', [live.toDelete[k]!.length]), Text(trf('대상에만 있는 {0}개를 지울지 확인이 필요합니다 (확인 전에는 지우지 않음)', [live.toDelete[k]!.length]),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
OutlinedButton( OutlinedButton(
onPressed: () => confirmLiveDeletes(context, live, x), onPressed: () => confirmLiveDeletes(context, live, x),
child: Text(tr('지울 목록 보고 결정')), child: Text(tr('지울 목록 보고 결정')),
@ -1058,10 +1058,10 @@ class _LiveTabState extends State<_LiveTab> {
Padding( Padding(
padding: const EdgeInsets.only(left: 32, top: 4), padding: const EdgeInsets.only(left: 32, top: 4),
child: Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ child: Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
const Icon(Icons.block, size: 16, color: Colors.redAccent), Icon(Icons.block, size: 16, color: JjColors.alert),
Text(tr('이 옵션의 일부 거르기 규칙은 미리 볼 수 없어 자동으로 지우지 않습니다'), Text(tr('이 옵션의 일부 거르기 규칙은 미리 볼 수 없어 자동으로 지우지 않습니다'),
style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)), style: TextStyle(fontSize: 12, color: JjColors.alert, fontWeight: FontWeight.w600)),
Text('(${live.noAutoDelete[k]!.map(unbreakableOption).join(' · ')})', style: const TextStyle(fontSize: 12, color: Colors.redAccent)), Text('(${live.noAutoDelete[k]!.map(unbreakableOption).join(' · ')})', style: TextStyle(fontSize: 12, color: JjColors.alert)),
OutlinedButton( OutlinedButton(
onPressed: () => confirmLiveDeleteNow(context, live, x), onPressed: () => confirmLiveDeleteNow(context, live, x),
child: Text(tr('지금 지우기 (확인)')), child: Text(tr('지금 지우기 (확인)')),
@ -1086,7 +1086,7 @@ class _LiveTabState extends State<_LiveTab> {
// 257: 폰 세로에서 글이 줄을 바꿀 수 있게 (넘쳤다) // 257: 폰 세로에서 글이 줄을 바꿀 수 있게 (넘쳤다)
Flexible( Flexible(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text(scheduleSummary(x.schedule), style: TextStyle(fontSize: 12, color: active ? JjColors.accent : Colors.orangeAccent)), Text(scheduleSummary(x.schedule), style: TextStyle(fontSize: 12, color: active ? JjColors.accent : JjColors.warning)),
if (x.schedule.isNotEmpty) if (x.schedule.isNotEmpty)
Text(x.schedule.join(' | '), style: TextStyle(fontSize: 11, fontFamily: 'monospace', color: JjColors.textDim)), Text(x.schedule.join(' | '), style: TextStyle(fontSize: 11, fontFamily: 'monospace', color: JjColors.textDim)),
Text(tr('누르면 일정 바꾸기'), style: TextStyle(fontSize: 10, color: JjColors.textDim)), Text(tr('누르면 일정 바꾸기'), style: TextStyle(fontSize: 10, color: JjColors.textDim)),
@ -1097,7 +1097,7 @@ class _LiveTabState extends State<_LiveTab> {
), ),
if (st != null) if (st != null)
Text('${tr('마지막')}: ${st.$1.hour.toString().padLeft(2, '0')}:${st.$1.minute.toString().padLeft(2, '0')} ${st.$2}', Text('${tr('마지막')}: ${st.$1.hour.toString().padLeft(2, '0')}:${st.$1.minute.toString().padLeft(2, '0')} ${st.$2}',
style: TextStyle(fontSize: 12, color: live?.problems[k] != null ? Colors.redAccent : JjColors.textDim)), style: TextStyle(fontSize: 12, color: live?.problems[k] != null ? JjColors.alert : JjColors.textDim)),
_DiskInfo(path: x.target), _DiskInfo(path: x.target),
]), ]),
), ),
@ -1116,11 +1116,11 @@ class _LiveTabState extends State<_LiveTab> {
(live?.onlyTarget[k] ?? 0) > 0 (live?.onlyTarget[k] ?? 0) > 0
? trf('원본의 것은 모두 대상에 있습니다 (대상에만 있는 것 {0}개는 그대로 둠)', [live!.onlyTarget[k]]) ? trf('원본의 것은 모두 대상에 있습니다 (대상에만 있는 것 {0}개는 그대로 둠)', [live!.onlyTarget[k]])
: tr('맞출 것 없음 (원본과 대상이 같습니다)'), : tr('맞출 것 없음 (원본과 대상이 같습니다)'),
style: const TextStyle(fontSize: 12, color: Colors.greenAccent)) style: TextStyle(fontSize: 12, color: JjColors.positive))
: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ : Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text( Text(
trf('맞출 것 {0}개{1}', [pend.length >= 500 ? '500+' : pend.length, active ? '' : tr(' · 다음 시작 시간에 맞춥니다')]), trf('맞출 것 {0}개{1}', [pend.length >= 500 ? '500+' : pend.length, active ? '' : tr(' · 다음 시작 시간에 맞춥니다')]),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent), style: TextStyle(fontSize: 12, color: JjColors.warning),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Container( Container(
@ -1131,7 +1131,7 @@ class _LiveTabState extends State<_LiveTab> {
for (final f in pend.take(100)) for (final f in pend.take(100))
Text(f, Text(f,
style: TextStyle( style: TextStyle(
fontSize: 11.5, fontFamily: 'monospace', color: f.startsWith('− ') ? Colors.redAccent : null)), fontSize: 11.5, fontFamily: 'monospace', color: f.startsWith('− ') ? JjColors.alert : null)),
if (pend.length > 100) Text(trf('… 외 {0}개', [pend.length - 100]), style: const TextStyle(fontSize: 11)), if (pend.length > 100) Text(trf('… 외 {0}개', [pend.length - 100]), style: const TextStyle(fontSize: 11)),
]), ]),
), ),

View File

@ -230,7 +230,7 @@ class _WeekGridState extends State<_WeekGrid> {
onTap: () => _toggleRow(d), onTap: () => _toggleRow(d),
child: Center( child: Center(
child: Text(names[d], child: Text(names[d],
style: TextStyle(fontSize: 12, color: d == 0 ? Colors.redAccent : d == 6 ? Colors.lightBlueAccent : null)), style: TextStyle(fontSize: 12, color: d == 0 ? JjColors.alert : d == 6 ? Colors.lightBlueAccent : null)),
), ),
), ),
), ),

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import '../app/app_controller.dart'; import '../app/app_controller.dart';
import '../app/settings.dart'; import '../app/settings.dart';
import '../l10n/tr.dart'; import '../l10n/tr.dart';
import 'theme.dart';
/// 안전 저장소 문제 (40-1 · 40-3 · 40-5) 를 화면 위 알림으로 보여 준다: 생길 때마다 · 켤 때 이미 있으면 바로. /// 안전 저장소 문제 (40-1 · 40-3 · 40-5) 를 화면 위 알림으로 보여 준다: 생길 때마다 · 켤 때 이미 있으면 바로.
/// 비밀번호를 평문으로 몰래 저장하지 않는 대신, 저장되지 않았다는 것을 분명히 알리고 [다시 시도] 를 둔다. /// 비밀번호를 평문으로 몰래 저장하지 않는 대신, 저장되지 않았다는 것을 분명히 알리고 [다시 시도] 를 둔다.
@ -45,7 +46,7 @@ void watchSecretIssue(GlobalKey<ScaffoldMessengerState> messengerKey, AppControl
MaterialBanner secretIssueBanner(SecretIssue issue, {required VoidCallback onRetry, required VoidCallback onClose}) { MaterialBanner secretIssueBanner(SecretIssue issue, {required VoidCallback onRetry, required VoidCallback onClose}) {
final lost = issue.kind == SecretIssueKind.lost; final lost = issue.kind == SecretIssueKind.lost;
return MaterialBanner( return MaterialBanner(
leading: const Icon(Icons.key_off_outlined, color: Colors.orangeAccent), leading: Icon(Icons.key_off_outlined, color: JjColors.warning),
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text( Text(
lost ? tr('저장된 비밀번호를 되살리지 못했습니다') : tr('이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다'), lost ? tr('저장된 비밀번호를 되살리지 못했습니다') : tr('이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다'),
@ -61,10 +62,10 @@ MaterialBanner secretIssueBanner(SecretIssue issue, {required VoidCallback onRet
// 91: 새로 넣은 비밀번호가 앱을 끄면 어떻게 되는지 분명히 // 91: 새로 넣은 비밀번호가 앱을 끄면 어떻게 되는지 분명히
if (issue.revertsToOld) if (issue.revertsToOld)
Text(tr('새 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 예전 비밀번호로 돌아갑니다.'), Text(tr('새 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 예전 비밀번호로 돌아갑니다.'),
style: const TextStyle(color: Colors.orangeAccent, fontWeight: FontWeight.w600)), style: TextStyle(color: JjColors.warning, fontWeight: FontWeight.w600)),
if (issue.lostOnExit && !issue.revertsToOld) if (issue.lostOnExit && !issue.revertsToOld)
Text(tr('새로 넣은 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 사라집니다.'), Text(tr('새로 넣은 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 사라집니다.'),
style: const TextStyle(color: Colors.orangeAccent, fontWeight: FontWeight.w600)), style: TextStyle(color: JjColors.warning, fontWeight: FontWeight.w600)),
]), ]),
actions: [ actions: [
if (!lost) TextButton(onPressed: onRetry, child: Text(tr('다시 시도'))), if (!lost) TextButton(onPressed: onRetry, child: Text(tr('다시 시도'))),

View File

@ -504,11 +504,11 @@ class _SettingsPageState extends State<SettingsPage> {
const SizedBox(width: 6), const SizedBox(width: 6),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(border: Border.all(color: Colors.orangeAccent), borderRadius: BorderRadius.circular(4)), decoration: BoxDecoration(border: Border.all(color: JjColors.warning), borderRadius: BorderRadius.circular(4)),
child: Text(tr('시험'), style: const TextStyle(fontSize: 11, color: Colors.orangeAccent)), child: Text(tr('시험'), style: TextStyle(fontSize: 11, color: JjColors.warning)),
), ),
]), ]),
subtitle: Text(tr('밝게 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다')), subtitle: Text(tr('밝은 화면 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다')),
trailing: DropdownButton<String>( trailing: DropdownButton<String>(
value: s.uiBrightness, value: s.uiBrightness,
items: [ items: [
@ -1680,7 +1680,7 @@ Future<bool> confirmReadOnlyView(BuildContext context, {required IconData icon,
return StatefulBuilder( return StatefulBuilder(
builder: (ctx, set) => AlertDialog( builder: (ctx, set) => AlertDialog(
scrollable: true, scrollable: true,
icon: Icon(icon, color: Colors.orangeAccent), icon: Icon(icon, color: JjColors.warning),
title: Text(title), title: Text(title),
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(body), Text(body),

View File

@ -336,7 +336,7 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
), ),
if (testing) const LinearProgressIndicator(), if (testing) const LinearProgressIndicator(),
if (result != null) if (result != null)
Text(result!, style: TextStyle(fontSize: 12, color: passed ? Colors.greenAccent : Colors.redAccent)), Text(result!, style: TextStyle(fontSize: 12, color: passed ? JjColors.positive : JjColors.alert)),
]), ]),
), ),
actions: [ actions: [
@ -345,7 +345,7 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
onPressed: () async { onPressed: () async {
if (await deleteSftpServer(ctx, c, old) && ctx.mounted) Navigator.pop(ctx); if (await deleteSftpServer(ctx, c, old) && ctx.mounted) Navigator.pop(ctx);
}, },
style: TextButton.styleFrom(foregroundColor: Colors.redAccent), style: TextButton.styleFrom(foregroundColor: JjColors.alert),
child: Text(tr('지우기')), child: Text(tr('지우기')),
), ),
TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))), TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))),

View File

@ -245,14 +245,14 @@ Future<SmbServer?> editSmbServer(BuildContext context, AppController c, {SmbServ
const SizedBox(height: 8), const SizedBox(height: 8),
if (testing) const LinearProgressIndicator(), if (testing) const LinearProgressIndicator(),
if (result != null) if (result != null)
Text(result!, style: TextStyle(fontSize: 12, color: passed ? Colors.greenAccent : Colors.redAccent)), Text(result!, style: TextStyle(fontSize: 12, color: passed ? JjColors.positive : JjColors.alert)),
if (warning != null) if (warning != null)
Padding( Padding(
padding: const EdgeInsets.only(top: 6), padding: const EdgeInsets.only(top: 6),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Icon(Icons.warning_amber_rounded, size: 18, color: Colors.orangeAccent), Icon(Icons.warning_amber_rounded, size: 18, color: JjColors.warning),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded(child: Text(warning!, style: const TextStyle(fontSize: 12, color: Colors.orangeAccent))), Expanded(child: Text(warning!, style: TextStyle(fontSize: 12, color: JjColors.warning))),
]), ]),
), ),
]), ]),
@ -263,7 +263,7 @@ Future<SmbServer?> editSmbServer(BuildContext context, AppController c, {SmbServ
onPressed: () async { onPressed: () async {
if (await deleteSmbServer(ctx, c, old) && ctx.mounted) Navigator.pop(ctx); if (await deleteSmbServer(ctx, c, old) && ctx.mounted) Navigator.pop(ctx);
}, },
style: TextButton.styleFrom(foregroundColor: Colors.redAccent), style: TextButton.styleFrom(foregroundColor: JjColors.alert),
child: Text(tr('지우기')), child: Text(tr('지우기')),
), ),
TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))), TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))),

View File

@ -107,10 +107,10 @@ class StorageTreemap extends StatelessWidget {
Text(b.label, Text(b.label,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w600)), style: TextStyle(fontSize: 12, color: JjColors.isLight && b.path == null ? JjColors.text : Colors.white, fontWeight: FontWeight.w600)),
if (c.maxHeight >= 46) if (c.maxHeight >= 46)
Text('${formatSize(b.size)} · ${pct.toStringAsFixed(pct < 10 ? 1 : 0)}%', Text('${formatSize(b.size)} · ${pct.toStringAsFixed(pct < 10 ? 1 : 0)}%',
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, color: Colors.white70)), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11, color: JjColors.isLight && b.path == null ? JjColors.textDim : Colors.white70)),
]), ]),
), ),
), ),

View File

@ -194,7 +194,7 @@ class _StorageAnalysisPageState extends State<StorageAnalysisPage> {
if (s.unreadable > 0) trf('읽지 못한 폴더 {0}개 (크기가 실제보다 작을 수 있음)', [s.unreadable]), if (s.unreadable > 0) trf('읽지 못한 폴더 {0}개 (크기가 실제보다 작을 수 있음)', [s.unreadable]),
if (s.links > 0) trf('링크 {0}개는 세지 않음', [s.links]), if (s.links > 0) trf('링크 {0}개는 세지 않음', [s.links]),
].join(' · '), ].join(' · '),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent), style: TextStyle(fontSize: 12, color: JjColors.warning),
), ),
), ),
Padding( Padding(
@ -226,7 +226,7 @@ class _StorageAnalysisPageState extends State<StorageAnalysisPage> {
onSecondaryTapUp: (x) => _menuAt(x.globalPosition, d), onSecondaryTapUp: (x) => _menuAt(x.globalPosition, d),
child: ListTile( child: ListTile(
dense: true, dense: true,
leading: const Icon(Icons.folder, color: Colors.amber), leading: Icon(Icons.folder, color: JjColors.amber),
title: Text(scanName(d), maxLines: 1, overflow: TextOverflow.ellipsis), title: Text(scanName(d), maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const SizedBox(height: 4), const SizedBox(height: 4),

View File

@ -262,7 +262,7 @@ class _SearchDialogState extends State<_SearchDialog> {
), ),
secondary: Row(mainAxisSize: MainAxisSize.min, children: [ secondary: Row(mainAxisSize: MainAxisSize.min, children: [
if (s.hashMatch) _badge(tr('이 파일용'), JjColors.success), if (s.hashMatch) _badge(tr('이 파일용'), JjColors.success),
if (s.machineTranslated) _badge(tr('기계 번역'), Colors.amber), if (s.machineTranslated) _badge(tr('기계 번역'), JjColors.amberText),
]), ]),
); );
}, },

View File

@ -84,7 +84,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
if (_error != null) { if (_error != null) {
return Row(children: [ return Row(children: [
Expanded( Expanded(
child: Text(trf('비교하지 못했습니다: {0}', [_error!]), style: const TextStyle(fontSize: 12, color: Colors.redAccent)), child: Text(trf('비교하지 못했습니다: {0}', [_error!]), style: TextStyle(fontSize: 12, color: JjColors.alert)),
), ),
IconButton(tooltip: tr('다시 비교'), icon: const Icon(Icons.refresh, size: 18), onPressed: _run), IconButton(tooltip: tr('다시 비교'), icon: const Icon(Icons.refresh, size: 18), onPressed: _run),
]); ]);
@ -120,26 +120,26 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
Expanded( Expanded(
child: Text( child: Text(
items.isEmpty ? tr('바뀌는 것 없음 (양쪽이 같습니다)') : parts.join(' · ') + (more ? tr(' · 더 있음') : ''), items.isEmpty ? tr('바뀌는 것 없음 (양쪽이 같습니다)') : parts.join(' · ') + (more ? tr(' · 더 있음') : ''),
style: TextStyle(fontSize: 12, color: items.isEmpty ? Colors.greenAccent : null), style: TextStyle(fontSize: 12, color: items.isEmpty ? JjColors.positive : null),
), ),
), ),
IconButton(tooltip: tr('다시 비교'), icon: const Icon(Icons.refresh, size: 18), onPressed: _run), IconButton(tooltip: tr('다시 비교'), icon: const Icon(Icons.refresh, size: 18), onPressed: _run),
]), ]),
if (dels > 0) if (dels > 0)
Text(keepOptionsTogether(trf('받는 쪽에서 {0}개가 지워집니다 (--delete, 되돌릴 수 없음)', [dels])), Text(keepOptionsTogether(trf('받는 쪽에서 {0}개가 지워집니다 (--delete, 되돌릴 수 없음)', [dels])),
style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)), style: TextStyle(fontSize: 12, color: JjColors.alert, fontWeight: FontWeight.w600)),
// 15 검토 F4: 받는 쪽에만 있는 받다 만 파일 (*.jjpart 등) 도 지워진다는 것을 따로 // 15 검토 F4: 받는 쪽에만 있는 받다 만 파일 (*.jjpart 등) 도 지워진다는 것을 따로
if (partials > 0) if (partials > 0)
Text(trf('받다 만 파일 {0}개도 지워집니다', [partials]), Text(trf('받다 만 파일 {0}개도 지워집니다', [partials]),
style: const TextStyle(fontSize: 12, color: Colors.redAccent)), style: TextStyle(fontSize: 12, color: JjColors.alert)),
// 211: 내용이 다른 같은 이름은 받는 쪽 파일이 바뀐다 - 목록의 "~" 만으로는 알아채기 어려웠다 // 211: 내용이 다른 같은 이름은 받는 쪽 파일이 바뀐다 - 목록의 "~" 만으로는 알아채기 어려웠다
if (overwrites > 0) if (overwrites > 0)
Text(trf('받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)', [overwrites]), Text(trf('받는 쪽에서 {0}개를 덮어씁니다 (내용이 다른 같은 이름 - 원래 것은 없어짐)', [overwrites]),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)), style: TextStyle(fontSize: 12, color: JjColors.warning, fontWeight: FontWeight.w600)),
// 229: 받는 쪽이 더 새 파일을 예전 원본으로 덮어쓰게 되면 따로 크게 (건너뛰기를 끄거나 rsync 에 -u 가 없을 때) // 229: 받는 쪽이 더 새 파일을 예전 원본으로 덮어쓰게 되면 따로 크게 (건너뛰기를 끄거나 rsync 에 -u 가 없을 때)
if (overNewer > 0) if (overNewer > 0)
Text(trf('그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다', [overNewer]), Text(trf('그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다', [overNewer]),
style: const TextStyle(fontSize: 12.5, color: Colors.orangeAccent, fontWeight: FontWeight.w800)), style: TextStyle(fontSize: 12.5, color: JjColors.warning, fontWeight: FontWeight.w800)),
if (items.isNotEmpty) if (items.isNotEmpty)
Container( Container(
margin: const EdgeInsets.only(top: 6), margin: const EdgeInsets.only(top: 6),
@ -171,8 +171,8 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
fontSize: 11.5, fontSize: 11.5,
fontFamily: 'monospace', fontFamily: 'monospace',
color: switch (x.action) { color: switch (x.action) {
PreviewAction.delete => Colors.redAccent, PreviewAction.delete => JjColors.alert,
PreviewAction.update || PreviewAction.skipDir => Colors.orangeAccent, PreviewAction.update || PreviewAction.skipDir => JjColors.warning,
PreviewAction.onlyTarget || PreviewAction.skipNewer || PreviewAction.skipLink => JjColors.textDim, PreviewAction.onlyTarget || PreviewAction.skipNewer || PreviewAction.skipLink => JjColors.textDim,
_ => null, _ => null,
}, },
@ -253,7 +253,7 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
Padding( Padding(
padding: const EdgeInsets.only(top: 4), padding: const EdgeInsets.only(top: 4),
child: Text(tr('이동: 대상에 옮긴 파일은 원본에서 지워집니다'), child: Text(tr('이동: 대상에 옮긴 파일은 원본에서 지워집니다'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// 15 검토 F2: 미리 볼 수 없는 거르기 규칙 (늘 같은 자리 - 비교 결과와 상관없이 옵션으로 정해짐) // 15 검토 F2: 미리 볼 수 없는 거르기 규칙 (늘 같은 자리 - 비교 결과와 상관없이 옵션으로 정해짐)
@ -288,7 +288,7 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
: deletes : deletes
? tr('지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.') ? tr('지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.')
: tr('원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.'), : tr('원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent), style: TextStyle(fontSize: 12, color: JjColors.warning),
) )
: null, : null,
), ),
@ -362,7 +362,7 @@ class OptionProblemNote extends StatelessWidget {
child: Text( child: Text(
// 28 지적 (10/20): 끝 문구는 문제 종류에 맞게 (따옴표 닫기 · - 로 시작 · 값 적기 · 곧은 글자로) // 28 지적 (10/20): 끝 문구는 문제 종류에 맞게 (따옴표 닫기 · - 로 시작 · 값 적기 · 곧은 글자로)
optionProblemText(bad), optionProblemText(bad),
style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)), style: TextStyle(fontSize: 12, color: JjColors.alert, fontWeight: FontWeight.w600)),
); );
} }
@ -376,7 +376,7 @@ class FilterLimitNote extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Widget build(BuildContext context) => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(trf('이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}', [unsupported.map(unbreakableOption).join(' · ')]), Text(trf('이 옵션의 일부 거르기 규칙은 미리 볼 수 없습니다 - 실제로 더 지워질 수 있습니다: {0}', [unsupported.map(unbreakableOption).join(' · ')]),
style: const TextStyle(fontSize: 12, color: Colors.redAccent, fontWeight: FontWeight.w600)), style: TextStyle(fontSize: 12, color: JjColors.alert, fontWeight: FontWeight.w600)),
if (deletes) if (deletes)
CheckboxListTile( CheckboxListTile(
dense: true, dense: true,

View File

@ -7,6 +7,12 @@ class JjPalette {
/// 강조색 바탕 위 글자 (버튼 등) /// 강조색 바탕 위 글자 (버튼 등)
final Color onAccent; final Color onAccent;
/// 주의 (주황) · 경고 (빨강) · 노랑 · 초록 강조 - 어두운 화면은 예전에 쓰던 Material 색 그대로 (orangeAccent · redAccent · amber · greenAccent)
final Color warning, alert, amber, positive;
/// 노랑을 글자에 쓸 때 (밝은 화면은 [amber] (아이콘 · 폴더 · 별, 대비 3:1) 보다 진하게 - 글자 대비 4.5:1)
final Color amberText;
const JjPalette({ const JjPalette({
required this.brightness, required this.brightness,
required this.bg, required this.bg,
@ -19,6 +25,11 @@ class JjPalette {
required this.danger, required this.danger,
required this.success, required this.success,
required this.onAccent, required this.onAccent,
required this.warning,
required this.alert,
required this.amber,
required this.positive,
required this.amberText,
}); });
/// CapCut 참고 어두운 테마 (처음 값 - 예전 그대로) /// CapCut 참고 어두운 테마 (처음 값 - 예전 그대로)
@ -34,6 +45,11 @@ class JjPalette {
danger: Color(0xFFFF5C6C), danger: Color(0xFFFF5C6C),
success: Color(0xFF3DD68C), success: Color(0xFF3DD68C),
onAccent: Colors.black, onAccent: Colors.black,
warning: Color(0xFFFFAB40),
alert: Color(0xFFFF5252),
amber: Color(0xFFFFC107),
positive: Color(0xFF69F0AE),
amberText: Color(0xFFFFC107),
); );
/// X-plore 비교 5-1 ⑦ 밝은 테마 (관리자 지시 10/21, 시험). 흰 바탕에서 글자 대비 4.5:1 이상이 되게 강조 · 경고 · 성공을 어둡게 /// X-plore 비교 5-1 ⑦ 밝은 테마 (관리자 지시 10/21, 시험). 흰 바탕에서 글자 대비 4.5:1 이상이 되게 강조 · 경고 · 성공을 어둡게
@ -49,6 +65,11 @@ class JjPalette {
danger: Color(0xFFC62833), danger: Color(0xFFC62833),
success: Color(0xFF186B42), success: Color(0xFF186B42),
onAccent: Colors.white, onAccent: Colors.white,
warning: Color(0xFFA14A00),
alert: Color(0xFFC62828),
amber: Color(0xFFB07800),
positive: Color(0xFF186B42),
amberText: Color(0xFF7A5900),
); );
} }
@ -79,6 +100,11 @@ class JjColors {
static Color get danger => _p.danger; static Color get danger => _p.danger;
static Color get success => _p.success; static Color get success => _p.success;
static Color get onAccent => _p.onAccent; static Color get onAccent => _p.onAccent;
static Color get warning => _p.warning;
static Color get alert => _p.alert;
static Color get amber => _p.amber;
static Color get positive => _p.positive;
static Color get amberText => _p.amberText;
} }
/// 색을 바꾼 뒤 모든 화면 조각을 다시 그린다 (상태는 그대로 - [JjColors] 를 직접 읽는 곳도 새 색으로) /// 색을 바꾼 뒤 모든 화면 조각을 다시 그린다 (상태는 그대로 - [JjColors] 를 직접 읽는 곳도 새 색으로)

View File

@ -133,7 +133,7 @@ class _TrashPageState extends State<TrashPage> {
value: on, value: on,
onChanged: (v) => setState(() => v == true ? _picked.add(i.entryDir) : _picked.remove(i.entryDir)), onChanged: (v) => setState(() => v == true ? _picked.add(i.entryDir) : _picked.remove(i.entryDir)),
secondary: Icon(i.isDir ? Icons.folder : Icons.insert_drive_file_outlined, secondary: Icon(i.isDir ? Icons.folder : Icons.insert_drive_file_outlined,
color: i.isDir ? Colors.amber : null), color: i.isDir ? JjColors.amber : null),
title: Text(i.name, maxLines: 1, overflow: TextOverflow.ellipsis), title: Text(i.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text('${i.original}\n${_date(i.deletedAt)} · ${formatSize(i.size)}', subtitle: Text('${i.original}\n${_date(i.deletedAt)} · ${formatSize(i.size)}',
maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)),

View File

@ -230,8 +230,8 @@ class _VersionPickerState extends State<_VersionPicker> {
title: Row(children: [ title: Row(children: [
Text('v${r.version}', style: const TextStyle(fontWeight: FontWeight.w600)), Text('v${r.version}', style: const TextStyle(fontWeight: FontWeight.w600)),
if (r.version == widget.current) _Badge(tr('지금'), JjColors.accent), if (r.version == widget.current) _Badge(tr('지금'), JjColors.accent),
if (r.version == newest) _Badge(tr('최신'), Colors.greenAccent), if (r.version == newest) _Badge(tr('최신'), JjColors.positive),
if (r.prerelease) _Badge(tr('시험판'), Colors.orangeAccent), if (r.prerelease) _Badge(tr('시험판'), JjColors.warning),
]), ]),
subtitle: Text(r.zipUrl == null ? '${_date(r.published)} · ${tr('이 기기용 파일 없음')}' : _date(r.published), subtitle: Text(r.zipUrl == null ? '${_date(r.published)} · ${tr('이 기기용 파일 없음')}' : _date(r.published),
style: const TextStyle(fontSize: 11)), style: const TextStyle(fontSize: 11)),
@ -255,7 +255,7 @@ class _VersionPickerState extends State<_VersionPicker> {
tr('Android 에서는 앱을 지운 뒤 예전 버전을 설치합니다. 받은 AI 모델 · 비밀번호 · 브라우저 로그인 등은 ' tr('Android 에서는 앱을 지운 뒤 예전 버전을 설치합니다. 받은 AI 모델 · 비밀번호 · 브라우저 로그인 등은 '
'사라집니다 (남는 것과 사라지는 것은 다음 창에서 보여 줍니다).'), '사라집니다 (남는 것과 사라지는 것은 다음 창에서 보여 줍니다).'),
].join('\n'), ].join('\n'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(tr('바뀐 내용'), style: const TextStyle(fontWeight: FontWeight.w600)), Text(tr('바뀐 내용'), style: const TextStyle(fontWeight: FontWeight.w600)),
@ -547,8 +547,8 @@ Future<void> _androidRollback(BuildContext context, AppController c, Updater up,
Text(tr('남는 것'), style: const TextStyle(fontWeight: FontWeight.w600)), Text(tr('남는 것'), style: const TextStyle(fontWeight: FontWeight.w600)),
for (final x in kept) Text('· $x'), for (final x in kept) Text('· $x'),
const SizedBox(height: 8), const SizedBox(height: 8),
Text(tr('사라지는 것'), style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.orangeAccent)), Text(tr('사라지는 것'), style: TextStyle(fontWeight: FontWeight.w600, color: JjColors.warning)),
for (final x in lost) Text('· $x', style: const TextStyle(color: Colors.orangeAccent)), for (final x in lost) Text('· $x', style: TextStyle(color: JjColors.warning)),
if (modelSize > 0) ...[ if (modelSize > 0) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
CheckboxListTile( CheckboxListTile(
@ -569,7 +569,7 @@ Future<void> _androidRollback(BuildContext context, AppController c, Updater up,
], ],
if (!targetRestoresSettings) if (!targetRestoresSettings)
Text(trf('v{0} 은 저장해 둔 설정을 되살리지 못해 설정을 다시 해야 합니다.', [r.version]), Text(trf('v{0} 은 저장해 둔 설정을 되살리지 못해 설정을 다시 해야 합니다.', [r.version]),
style: const TextStyle(color: Colors.orangeAccent)), style: TextStyle(color: JjColors.warning)),
]), ]),
), ),
actions: [ actions: [
@ -598,7 +598,7 @@ Future<void> _androidRollback(BuildContext context, AppController c, Updater up,
actions: [ actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))), TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
FilledButton( FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.redAccent), style: FilledButton.styleFrom(backgroundColor: JjColors.alert),
onPressed: () => Navigator.pop(ctx, true), onPressed: () => Navigator.pop(ctx, true),
child: Text(tr('되돌리기 계속')), child: Text(tr('되돌리기 계속')),
), ),

View File

@ -346,7 +346,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
style: TextStyle(fontSize: 12, color: JjColors.textDim)), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
for (final f in plan.failed.entries.take(3)) for (final f in plan.failed.entries.take(3))
Text(trf('읽지 못해 빼고 넣습니다: {0} ({1})', [vBasename(f.key), f.value]), Text(trf('읽지 못해 빼고 넣습니다: {0} ({1})', [vBasename(f.key), f.value]),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent), maxLines: 2, overflow: TextOverflow.ellipsis), style: TextStyle(fontSize: 12, color: JjColors.warning), maxLines: 2, overflow: TextOverflow.ellipsis),
const SizedBox(height: 8), const SizedBox(height: 8),
CheckboxListTile( CheckboxListTile(
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
@ -358,7 +358,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
), ),
if (move) ...[ if (move) ...[
Text(tr('원본은 휴지통으로 갑니다 - 휴지통에 평문 (암호화하지 않은 원본) 이 남습니다'), Text(tr('원본은 휴지통으로 갑니다 - 휴지통에 평문 (암호화하지 않은 원본) 이 남습니다'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
RadioGroup<bool>( RadioGroup<bool>(
groupValue: permanent, groupValue: permanent,
onChanged: (v) => set(() => permanent = v ?? false), onChanged: (v) => set(() => permanent = v ?? false),

View File

@ -174,13 +174,13 @@ Future<DavServer?> editDavServer(BuildContext context, AppController c, {DavServ
Padding( Padding(
padding: const EdgeInsets.only(top: 6), padding: const EdgeInsets.only(top: 6),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Icon(Icons.warning_amber_rounded, size: 18, color: Colors.orangeAccent), Icon(Icons.warning_amber_rounded, size: 18, color: JjColors.warning),
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Text( child: Text(
tr('http:// 주소는 아이디 · 비밀번호와 파일이 암호화되지 않고 그대로 갑니다. 같은 네트워크의 다른 사람이 볼 수 있으니 ' tr('http:// 주소는 아이디 · 비밀번호와 파일이 암호화되지 않고 그대로 갑니다. 같은 네트워크의 다른 사람이 볼 수 있으니 '
'서버가 https 를 지원하면 https:// 로 바꾸세요.'), '서버가 https 를 지원하면 https:// 로 바꾸세요.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), style: TextStyle(fontSize: 12, color: JjColors.warning)),
), ),
]), ]),
), ),
@ -208,7 +208,7 @@ Future<DavServer?> editDavServer(BuildContext context, AppController c, {DavServ
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text(result!, child: Text(result!,
style: TextStyle(fontSize: 12, color: passed ? Colors.greenAccent : Colors.redAccent)), style: TextStyle(fontSize: 12, color: passed ? JjColors.positive : JjColors.alert)),
), ),
if (toHttps) if (toHttps)
Align( Align(
@ -232,7 +232,7 @@ Future<DavServer?> editDavServer(BuildContext context, AppController c, {DavServ
onPressed: () async { onPressed: () async {
if (await deleteDavServer(ctx, c, old) && ctx.mounted) Navigator.pop(ctx); if (await deleteDavServer(ctx, c, old) && ctx.mounted) Navigator.pop(ctx);
}, },
style: TextButton.styleFrom(foregroundColor: Colors.redAccent), style: TextButton.styleFrom(foregroundColor: JjColors.alert),
child: Text(tr('지우기')), child: Text(tr('지우기')),
), ),
TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))), TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))),

View File

@ -49,6 +49,8 @@ void main() {
expect(JjColors.bg, const Color(0xFF141417)); expect(JjColors.bg, const Color(0xFF141417));
expect(JjColors.accent, const Color(0xFF00C4CC)); expect(JjColors.accent, const Color(0xFF00C4CC));
expect(JjColors.textDim, const Color(0xFF8E8E99)); expect(JjColors.textDim, const Color(0xFF8E8E99));
// 예전에 쓰던 Material 색 그대로 (어두운 화면이 바뀌지 않게)
expect([JjColors.warning, JjColors.alert, JjColors.amber, JjColors.amberText, JjColors.positive], [Colors.orangeAccent, Colors.redAccent, Colors.amber, Colors.amber, Colors.greenAccent].map((c) => Color(c.toARGB32())).toList());
expect(resolveBrightness('dark', Brightness.light), Brightness.dark); expect(resolveBrightness('dark', Brightness.light), Brightness.dark);
expect(resolveBrightness('light', Brightness.dark), Brightness.light); expect(resolveBrightness('light', Brightness.dark), Brightness.light);
expect(resolveBrightness('system', Brightness.light), Brightness.light); expect(resolveBrightness('system', Brightness.light), Brightness.light);
@ -58,11 +60,15 @@ void main() {
test('밝은 색: 바탕 · 칸 위 글자 대비 4.5:1 이상 (글 · 흐린 글 · 강조 · 경고 · 성공 · 강조 위 글자)', () { test('밝은 색: 바탕 · 칸 위 글자 대비 4.5:1 이상 (글 · 흐린 글 · 강조 · 경고 · 성공 · 강조 위 글자)', () {
const l = JjPalette.light; const l = JjPalette.light;
for (final bg in [l.bg, l.panel, l.panelHigh]) { for (final bg in [l.bg, l.panel, l.panelHigh]) {
for (final (name, fg) in [('text', l.text), ('textDim', l.textDim), ('accent', l.accent), ('danger', l.danger), ('success', l.success)]) { for (final (name, fg) in [('text', l.text), ('textDim', l.textDim), ('accent', l.accent), ('danger', l.danger), ('success', l.success), ('warning', l.warning), ('alert', l.alert), ('amberText', l.amberText), ('positive', l.positive)]) {
expect(contrast(fg, bg), greaterThanOrEqualTo(4.5), reason: '$name on ${bg.toARGB32().toRadixString(16)}'); expect(contrast(fg, bg), greaterThanOrEqualTo(4.5), reason: '$name on ${bg.toARGB32().toRadixString(16)}');
} }
} }
expect(contrast(l.onAccent, l.accent), greaterThanOrEqualTo(4.5)); expect(contrast(l.onAccent, l.accent), greaterThanOrEqualTo(4.5));
// 아이콘 (폴더 · 별) 은 3:1
for (final bg in [l.bg, l.panel, l.panelHigh]) {
expect(contrast(l.amber, bg), greaterThanOrEqualTo(3));
}
expect(buildTheme(Brightness.light).brightness, Brightness.light); expect(buildTheme(Brightness.light).brightness, Brightness.light);
expect(buildTheme(Brightness.light).scaffoldBackgroundColor, l.bg); expect(buildTheme(Brightness.light).scaffoldBackgroundColor, l.bg);
expect(buildTheme(Brightness.dark).scaffoldBackgroundColor, JjPalette.dark.bg); expect(buildTheme(Brightness.dark).scaffoldBackgroundColor, JjPalette.dark.bg);