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

View File

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

View File

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

View File

@ -17,6 +17,8 @@ import 'package:jj_mkvmaker/ui/theme.dart';
/// 환경 설정 > 파일 탐색기 의 복사 · 동기화 부분을 실제 글꼴로 그려 PNG 로 (JJ_SHOT_DIR). 화면 배치 확인용.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('복사 · 동기화 설정 그림', (tester) async {
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).
/// 왼쪽 · 오른쪽 창 폴더: JJ_SHOT_LEFT · JJ_SHOT_RIGHT. 화면 배치 확인용.
/// JJ_SHOT_THEME=light 면 밝은 테마로 (⑦ - 파일 이름 끝에 _light)
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@ -21,6 +22,9 @@ void main() {
final c = AppController(PlatformServices.create());
c.settings.explorerPaths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path];
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 [
('wide', const Size(1600, 900), 'dual', 'xplore'),
('tall', const Size(900, 1400), 'dual', 'xplore'),
@ -45,7 +49,7 @@ void main() {
final boundary = key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
final image = await tester.runAsync(() => boundary.toImage());
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());
}
tester.view.reset();

View File

@ -16,6 +16,8 @@ import 'package:path/path.dart' as p;
/// 세 스타일 모두. 넘치면 (RenderFlex overflow) 시험이 실패한다
void main() {
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 {
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). 화면 배치 확인용.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('모니터링 화면 그림', (tester) async {
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). 화면 배치 확인용.
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// ⑦ 밝은 화면 캡처: JJ_SHOT_THEME=light (JJ_SHOT_DIR 를 따로 두어 어두운 것과 견줌)
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
testWidgets('환경 설정 화면 그림', (tester) async {
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
void main() {
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');
for (final lang in ['ko', 'en', 'ja']) {
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. 시험 파일은 임시 폴더에만, 설정은 메모리에만.
void main() {
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');
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. 시험 파일은 임시 폴더에만.
void main() {
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');
for (final lang in ['ko', 'en']) {

View File

@ -224,7 +224,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
_engineRow(),
if (!_local)
// 인터넷 서비스 · 자기 서버를 쓸 때는 늘 알린다
_notice(Icons.public, Colors.orangeAccent,
_notice(Icons.public, JjColors.warning,
trf('그림 · 프롬프트가 기기 밖으로 나갑니다 ({0})', [_service!.label])),
if (_local && !_modelsReady)
_notice(Icons.download_outlined, JjColors.accent,
@ -287,7 +287,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
Padding(
padding: const EdgeInsets.only(top: 8),
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)
TextButton(
onPressed: () => setState(() => _showDetail = !_showDetail),

View File

@ -181,7 +181,7 @@ class _AiSettingsState extends State<AiSettings> {
free == null
? trf('필요한 공간 {0}', [AiStore.sizeText(need)])
: 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)
Padding(
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),
const Divider(),

View File

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

View File

@ -189,12 +189,12 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
const SizedBox(height: 6),
Text(trf('풀 수 없는 이름 {0}개 (건너뜀): {1}',
[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) ...[
const SizedBox(height: 6),
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) ...[
const SizedBox(height: 6),
@ -223,7 +223,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
c.settings.copySkipNewer
? tr('덮어쓴 파일은 되돌릴 수 없습니다 (받는 쪽이 더 새 파일은 그대로 둡니다).')
: tr('덮어쓴 파일은 되돌릴 수 없습니다.'),
style: const TextStyle(color: Colors.orangeAccent, fontSize: 12)),
style: TextStyle(color: JjColors.warning, fontSize: 12)),
// 관리자 판단: 풀린 크기가 빈 공간보다 크면 풀기 전에 알리고 묻는다 (압축 폭탄 · 공간 부족)
if (short)
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(
padding: EdgeInsets.only(left: depth * 14.0),
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),
Text(f.title),
]),
@ -368,7 +368,7 @@ class _BookmarkDraggableState extends State<BookmarkDraggable> {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
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),
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),
size: 18, color: Colors.amber),
size: 18, color: JjColors.amber),
const SizedBox(width: 8),
Expanded(child: Text(f.title, maxLines: 1, overflow: TextOverflow.ellipsis)),
]),
@ -711,7 +711,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
BookmarkDraggable(
n: n,
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),
@ -762,7 +762,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
leading: BookmarkDraggable(
n: n,
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),
subtitle: Text(

View File

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

View File

@ -89,7 +89,7 @@ class _CleanupDialogState extends State<_CleanupDialog> {
),
]),
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: 안쪽 → 바깥은 지우기 없이만, 바깥 → 안쪽은 안 됨 (끝없이 돎)
if (LiveSync.nestingProblem(LiveSyncPair(src!, dst!, delete: delete), allowInnerToOuter: c.settings.allowInnerToOuter)
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!))
Text(tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (지우기는 쓸 수 없음).'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
style: TextStyle(fontSize: 12, color: JjColors.warning)),
const SizedBox(height: 12),
Wrap(spacing: 6, children: [
for (final m in CopyMethod.values)

View File

@ -255,7 +255,7 @@ class _DlnaPageState extends State<DlnaPage> {
final e = _entries[i];
return ListTile(
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),
subtitle: e.isContainer
? (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.downloading when t.extra['waiting'] == true => (tr('대기 중'), JjColors.textDim),
DownloadState.downloading => (tr('받는 중'), JjColors.accent),
DownloadState.paused => (tr('일시정지'), Colors.amber),
DownloadState.paused => (tr('일시정지'), JjColors.amberText),
DownloadState.done => (tr('완료'), JjColors.success),
DownloadState.failed => (tr('실패'), JjColors.danger),
DownloadState.cancelled => (tr('취소됨'), JjColors.textDim),
@ -321,7 +321,7 @@ class _DownloadRow extends StatelessWidget {
Text(
'${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(' · ')])}',
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)
TextButton(style: _small, onPressed: () => d.useSystemDns!(), child: Text(tr('시스템 DNS 로 바꾸기'))),
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 color = switch (t.state) {
_ when stalled => Colors.orangeAccent,
_ when stalled => JjColors.warning,
DownloadState.done => JjColors.success,
DownloadState.failed => JjColors.danger,
DownloadState.paused => Colors.amber,
DownloadState.paused => JjColors.amberText,
DownloadState.cancelled => JjColors.textDim,
_ => JjColors.accent,
};

View File

@ -93,7 +93,7 @@ class _BookmarksDialogState extends State<_BookmarksDialog> {
for (final b in mine)
ListTile(
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),
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)),

View File

@ -129,7 +129,7 @@ class _KeyCaptureDialogState extends State<_KeyCaptureDialog> {
Padding(
padding: const EdgeInsets.only(top: 8),
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;
switch (style) {
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 (
switch (x) {
_ 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 (_slides.contains(x)) return (Icons.slideshow, const Color(0xFFE8804F));
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:
// Total Commander 느낌: 단순한 작은 아이콘, 폴더는 노랑, 실행 파일은 초록
if (e.isDir) return (Icons.folder, const Color(0xFFE8C547));
if (_programs.contains(x)) return (Icons.settings_applications, const Color(0xFF7CB342));
if (isVideoFile(e.path)) return (Icons.movie, const Color(0xFF64B5F6));
if (_images.contains(x)) return (Icons.image, const Color(0xFF64B5F6));
if (_archives.contains(x)) return (Icons.inventory_2, const Color(0xFFBCAAA4));
return (Icons.description, const Color(0xFFCFD8DC));
if (_archives.contains(x)) return (Icons.inventory_2, JjColors.isLight ? const Color(0xFF8D6E63) : const Color(0xFFBCAAA4));
return (Icons.description, JjColors.isLight ? const Color(0xFF78909C) : const Color(0xFFCFD8DC));
}
}

View File

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

View File

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

View File

@ -96,7 +96,7 @@ Future<void> showFileHash(BuildContext context, String path) async {
Padding(
padding: const EdgeInsets.only(top: 6),
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 'android_file_browser.dart';
import 'file_error.dart';
import 'theme.dart';
/// 폴더 고르기. Android 는 앱 안 화면 (내장 저장소 · SD 카드 · USB 를 실제 경로로 고를 수 있음), PC 는 Windows 폴더 선택 창
Future<String?> pickFolder(BuildContext context, String title, [String? initial]) => Platform.isAndroid
@ -94,9 +95,9 @@ class _DavFolderDialogState extends State<_DavFolderDialog> {
final locked = isLockedError(error);
return Center(
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),
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)),
const SizedBox(height: 10),
FilledButton.tonal(

View File

@ -291,7 +291,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
),
],
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: [
@ -300,7 +300,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
onPressed: () async {
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('지우기')),
),
TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))),

View File

@ -116,7 +116,7 @@ class _MasterDialogState extends State<_MasterDialog> {
Padding(
padding: const EdgeInsets.only(top: 8),
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 (!widget.forChange)

View File

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

View File

@ -230,7 +230,7 @@ class _WeekGridState extends State<_WeekGrid> {
onTap: () => _toggleRow(d),
child: Center(
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/settings.dart';
import '../l10n/tr.dart';
import 'theme.dart';
/// 안전 저장소 문제 (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}) {
final lost = issue.kind == SecretIssueKind.lost;
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: [
Text(
lost ? tr('저장된 비밀번호를 되살리지 못했습니다') : tr('이 기기의 안전 저장소를 쓸 수 없어 비밀번호를 저장하지 못했습니다'),
@ -61,10 +62,10 @@ MaterialBanner secretIssueBanner(SecretIssue issue, {required VoidCallback onRet
// 91: 새로 넣은 비밀번호가 앱을 끄면 어떻게 되는지 분명히
if (issue.revertsToOld)
Text(tr('새 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 예전 비밀번호로 돌아갑니다.'),
style: const TextStyle(color: Colors.orangeAccent, fontWeight: FontWeight.w600)),
style: TextStyle(color: JjColors.warning, fontWeight: FontWeight.w600)),
if (issue.lostOnExit && !issue.revertsToOld)
Text(tr('새로 넣은 비밀번호는 아직 저장되지 않았습니다. 앱을 끄면 사라집니다.'),
style: const TextStyle(color: Colors.orangeAccent, fontWeight: FontWeight.w600)),
style: TextStyle(color: JjColors.warning, fontWeight: FontWeight.w600)),
]),
actions: [
if (!lost) TextButton(onPressed: onRetry, child: Text(tr('다시 시도'))),

View File

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

View File

@ -336,7 +336,7 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
),
if (testing) const LinearProgressIndicator(),
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: [
@ -345,7 +345,7 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
onPressed: () async {
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('지우기')),
),
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),
if (testing) const LinearProgressIndicator(),
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)
Padding(
padding: const EdgeInsets.only(top: 6),
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),
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 {
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('지우기')),
),
TextButton(onPressed: !ok || testing ? null : test, child: Text(tr('연결 확인'))),

View File

@ -107,10 +107,10 @@ class StorageTreemap extends StatelessWidget {
Text(b.label,
maxLines: 1,
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)
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.links > 0) trf('링크 {0}개는 세지 않음', [s.links]),
].join(' · '),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent),
style: TextStyle(fontSize: 12, color: JjColors.warning),
),
),
Padding(
@ -226,7 +226,7 @@ class _StorageAnalysisPageState extends State<StorageAnalysisPage> {
onSecondaryTapUp: (x) => _menuAt(x.globalPosition, d),
child: ListTile(
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),
subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const SizedBox(height: 4),

View File

@ -262,7 +262,7 @@ class _SearchDialogState extends State<_SearchDialog> {
),
secondary: Row(mainAxisSize: MainAxisSize.min, children: [
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) {
return Row(children: [
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),
]);
@ -120,26 +120,26 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
Expanded(
child: Text(
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),
]),
if (dels > 0)
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 등) 도 지워진다는 것을 따로
if (partials > 0)
Text(trf('받다 만 파일 {0}개도 지워집니다', [partials]),
style: const TextStyle(fontSize: 12, color: Colors.redAccent)),
style: TextStyle(fontSize: 12, color: JjColors.alert)),
// 211: 내용이 다른 같은 이름은 받는 쪽 파일이 바뀐다 - 목록의 "~" 만으로는 알아채기 어려웠다
if (overwrites > 0)
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 가 없을 때)
if (overNewer > 0)
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)
Container(
margin: const EdgeInsets.only(top: 6),
@ -171,8 +171,8 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
fontSize: 11.5,
fontFamily: 'monospace',
color: switch (x.action) {
PreviewAction.delete => Colors.redAccent,
PreviewAction.update || PreviewAction.skipDir => Colors.orangeAccent,
PreviewAction.delete => JjColors.alert,
PreviewAction.update || PreviewAction.skipDir => JjColors.warning,
PreviewAction.onlyTarget || PreviewAction.skipNewer || PreviewAction.skipLink => JjColors.textDim,
_ => null,
},
@ -253,7 +253,7 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(tr('이동: 대상에 옮긴 파일은 원본에서 지워집니다'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
style: TextStyle(fontSize: 12, color: JjColors.warning)),
),
const SizedBox(height: 8),
// 15 검토 F2: 미리 볼 수 없는 거르기 규칙 (늘 같은 자리 - 비교 결과와 상관없이 옵션으로 정해짐)
@ -288,7 +288,7 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
: deletes
? tr('지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.')
: tr('원본을 지우는 실행이라, 옮길 목록이 나온 뒤에 실행할 수 있습니다.'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent),
style: TextStyle(fontSize: 12, color: JjColors.warning),
)
: null,
),
@ -362,7 +362,7 @@ class OptionProblemNote extends StatelessWidget {
child: Text(
// 28 지적 (10/20): 끝 문구는 문제 종류에 맞게 (따옴표 닫기 · - 로 시작 · 값 적기 · 곧은 글자로)
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
Widget build(BuildContext context) => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
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)
CheckboxListTile(
dense: true,

View File

@ -7,6 +7,12 @@ class JjPalette {
/// 강조색 바탕 위 글자 (버튼 등)
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({
required this.brightness,
required this.bg,
@ -19,6 +25,11 @@ class JjPalette {
required this.danger,
required this.success,
required this.onAccent,
required this.warning,
required this.alert,
required this.amber,
required this.positive,
required this.amberText,
});
/// CapCut 참고 어두운 테마 (처음 값 - 예전 그대로)
@ -34,6 +45,11 @@ class JjPalette {
danger: Color(0xFFFF5C6C),
success: Color(0xFF3DD68C),
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 이상이 되게 강조 · 경고 · 성공을 어둡게
@ -49,6 +65,11 @@ class JjPalette {
danger: Color(0xFFC62833),
success: Color(0xFF186B42),
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 success => _p.success;
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] 를 직접 읽는 곳도 새 색으로)

View File

@ -133,7 +133,7 @@ class _TrashPageState extends State<TrashPage> {
value: on,
onChanged: (v) => setState(() => v == true ? _picked.add(i.entryDir) : _picked.remove(i.entryDir)),
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),
subtitle: Text('${i.original}\n${_date(i.deletedAt)} · ${formatSize(i.size)}',
maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11)),

View File

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

View File

@ -346,7 +346,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
for (final f in plan.failed.entries.take(3))
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),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
@ -358,7 +358,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
),
if (move) ...[
Text(tr('원본은 휴지통으로 갑니다 - 휴지통에 평문 (암호화하지 않은 원본) 이 남습니다'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
style: TextStyle(fontSize: 12, color: JjColors.warning)),
RadioGroup<bool>(
groupValue: permanent,
onChanged: (v) => set(() => permanent = v ?? false),

View File

@ -174,13 +174,13 @@ Future<DavServer?> editDavServer(BuildContext context, AppController c, {DavServ
Padding(
padding: const EdgeInsets.only(top: 6),
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),
Expanded(
child: Text(
tr('http:// 주소는 아이디 · 비밀번호와 파일이 암호화되지 않고 그대로 갑니다. 같은 네트워크의 다른 사람이 볼 수 있으니 '
'서버가 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(
alignment: Alignment.centerLeft,
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)
Align(
@ -232,7 +232,7 @@ Future<DavServer?> editDavServer(BuildContext context, AppController c, {DavServ
onPressed: () async {
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('지우기')),
),
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.accent, const Color(0xFF00C4CC));
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('light', Brightness.dark), Brightness.light);
expect(resolveBrightness('system', Brightness.light), Brightness.light);
@ -58,11 +60,15 @@ void main() {
test('밝은 색: 바탕 · 칸 위 글자 대비 4.5:1 이상 (글 · 흐린 글 · 강조 · 경고 · 성공 · 강조 위 글자)', () {
const l = JjPalette.light;
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(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).scaffoldBackgroundColor, l.bg);
expect(buildTheme(Brightness.dark).scaffoldBackgroundColor, JjPalette.dark.bg);