WIP X-plore 비교 5-1 ⑦ 밝은 테마 바탕: 색이 테마를 따름 · 설정 "화면 밝기" (시험) (검토 대기)

관리자 지시 10/21 (⑦ (가) 바탕): 처음 값은 어둡게 (예전 색 그대로), 설정 "화면 밝기: 어두운 화면 / 밝은 화면 / 기기 따라" 에 "시험" 표시.
- JjColors 를 두 벌 (JjPalette.dark = 예전 값 그대로 · light) 에서 고르는 값으로. 그래서 JjColors 를 쓰던 const 는 뺐다 (기계적으로, 62개 파일 294곳)
- 밝기를 바꾸면 테마와 함께 화면 조각 전체를 다시 그림 (상태는 그대로) - JjColors 를 직접 읽는 곳도 바로 새 색
- 기기 따라: 기기 밝기가 바뀌면 따라감. 모르는 값 · 없는 값은 어둡게
- 밝은 색은 바탕 · 칸 위 글 · 흐린 글 · 강조 · 경고 · 성공 대비 4.5:1 이상 (시험으로 확인)
- 화면별 맞추기 ((나): 탐색기 → 보기 → 설정 → 나머지) 는 다음 커밋들에서

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xplore_p3b
RyuJongJin 2026-10-21 14:08:47 +09:00
parent ab735396ac
commit 9324e4100f
69 changed files with 623 additions and 325 deletions

View File

@ -861,6 +861,7 @@
"기계 번역": "Machine translation",
"기기": "device",
"기기 DNS": "Device DNS",
"기기 따라": "Follow device",
"기기 안에서 그리기 (stable-diffusion.cpp · SD1.5 + LCM)": "Draw on this device (stable-diffusion.cpp · SD1.5 + LCM)",
"기기 안에서 그리려면 모델을 받아야 합니다 (SD1.5 + LCM, 약 4.4GB).": "To draw on this device, download the model first (SD1.5 + LCM, about 4.4 GB).",
"기기 안에서 그리려면 모델을 받아야 합니다 ({0})": "To draw on this device, download the model first ({0})",
@ -1453,7 +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",
"밤 0~6시": "Night 0-6",
"방법: {0}": "Method: {0}",
"방식": "Type",
@ -1826,6 +1829,7 @@
"시작할 때 새 버전 확인 (하루 한 번)": "Check for a new version at startup (once a day)",
"시작할 때마다 설정 창 보기": "Show the setup window at every start",
"시즌": "Season",
"시험": "Beta",
"시험판": "Pre-release",
"실시간 동기화 (lsyncd 처럼)": "Live sync (like lsyncd)",
"실시간 동기화 ({0}): {1} → {2}": "Live sync ({0}): {1} → {2}",
@ -1955,6 +1959,7 @@
"약하게 (0.15)": "Light (0.15)",
"양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "Both ways (⇄) never deletes source files (--remove-source-files in the options is not used).",
"양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "Both ways: newer files on the receiving side are not overwritten (-u).",
"어두운 화면": "Dark",
"어둡게": "Darker",
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "Press anywhere to bring up the JJ_MKVMaker window",
"언어": "Language",
@ -3036,6 +3041,7 @@
"화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "Swiping left or right in the middle of the screen now switches to other screens (file explorer, downloads, etc.). To turn off: Settings > Components > Swipe left/right to switch screens",
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "Translates all of the program's text on this device. It may take a few minutes.",
"화면 모양 따라": "Follow screen shape",
"화면 밝기": "Screen brightness",
"화면 방향": "Screen orientation",
"화면 방향: 가로 고정 → 자동 (기기를 돌리는 대로). 되돌리기: 환경 설정 > 화면 > 화면 방향": "Screen orientation: landscape only → auto (follows the device). To revert: Settings > Display > Screen orientation",
"화면 비율": "Aspect ratio",

View File

@ -861,6 +861,7 @@
"기계 번역": "機械翻訳",
"기기": "端末",
"기기 DNS": "端末の DNS",
"기기 따라": "端末に合わせる",
"기기 안에서 그리기 (stable-diffusion.cpp · SD1.5 + LCM)": "この端末で描く (stable-diffusion.cpp・SD1.5 + LCM)",
"기기 안에서 그리려면 모델을 받아야 합니다 (SD1.5 + LCM, 약 4.4GB).": "この端末で描くにはモデルのダウンロードが必要です (SD1.5 + LCM、約 4.4GB)。",
"기기 안에서 그리려면 모델을 받아야 합니다 ({0})": "この端末で描くにはモデルのダウンロードが必要です ({0})",
@ -1453,7 +1454,9 @@
"받지 않음": "取得しない",
"받지 않음 (처음 쓸 때 내려받기 {0})": "未ダウンロード (初回使用時にダウンロード {0})",
"밝게": "明るく",
"밝게 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다": "明るい・端末に合わせるはまだ試験中です - 一部の画面の色が合わない場合があります",
"밝기": "明るさ",
"밝은 화면": "明るい",
"밤 0~6시": "夜 0~6 時",
"방법: {0}": "方法: {0}",
"방식": "方式",
@ -1826,6 +1829,7 @@
"시작할 때 새 버전 확인 (하루 한 번)": "起動時に新しいバージョンを確認(1日1回)",
"시작할 때마다 설정 창 보기": "起動するたびに設定ウィンドウを表示",
"시즌": "シーズン",
"시험": "試験中",
"시험판": "プレリリース",
"실시간 동기화 (lsyncd 처럼)": "リアルタイム同期 (lsyncd のように)",
"실시간 동기화 ({0}): {1} → {2}": "リアルタイム同期 ({0}): {1} → {2}",
@ -1955,6 +1959,7 @@
"약하게 (0.15)": "弱め (0.15)",
"양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "双方向 (⇄) では元のファイルを削除しません (オプションの --remove-source-files は使いません)。",
"양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双方向: 受け取る側の方が新しいファイルは上書きしません (-u)。",
"어두운 화면": "暗い",
"어둡게": "暗く",
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "どこからでも押すと JJ_MKVMaker ウィンドウが表示されます",
"언어": "言語",
@ -3036,6 +3041,7 @@
"화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "画面の中央を左右にスワイプすると別の画面 (ファイルエクスプローラー · ダウンロードなど) に切り替わります。オフにする: 環境設定 > コンポーネント > 左右スワイプで画面を移動",
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "画面の文字をすべてこの端末で翻訳します。数分かかることがあります。",
"화면 모양 따라": "画面の形に合わせる",
"화면 밝기": "画面の明るさ",
"화면 방향": "画面の向き",
"화면 방향: 가로 고정 → 자동 (기기를 돌리는 대로). 되돌리기: 환경 설정 > 화면 > 화면 방향": "画面の向き: 横向き固定 → 自動 (端末の向きに合わせる)。元に戻す: 環境設定 > 画面 > 画面の向き",
"화면 비율": "画面比率",

View File

@ -861,6 +861,7 @@
"기계 번역": "机器翻译",
"기기": "设备",
"기기 DNS": "设备 DNS",
"기기 따라": "跟随设备",
"기기 안에서 그리기 (stable-diffusion.cpp · SD1.5 + LCM)": "在本设备上绘制 (stable-diffusion.cpp · SD1.5 + LCM)",
"기기 안에서 그리려면 모델을 받아야 합니다 (SD1.5 + LCM, 약 4.4GB).": "要在本设备上绘制,需要先下载模型 (SD1.5 + LCM,约 4.4GB)。",
"기기 안에서 그리려면 모델을 받아야 합니다 ({0})": "要在本设备上绘制,需要先下载模型 ({0})",
@ -1453,7 +1454,9 @@
"받지 않음": "不获取",
"받지 않음 (처음 쓸 때 내려받기 {0})": "未下载 (首次使用时下载 {0})",
"밝게": "调亮",
"밝게 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다": "浅色和跟随设备仍在测试中 - 部分界面的颜色可能还不协调",
"밝기": "亮度",
"밝은 화면": "浅色",
"밤 0~6시": "夜间 0~6 点",
"방법: {0}": "方式: {0}",
"방식": "方式",
@ -1826,6 +1829,7 @@
"시작할 때 새 버전 확인 (하루 한 번)": "启动时检查新版本(每天一次)",
"시작할 때마다 설정 창 보기": "每次启动时显示设置窗口",
"시즌": "季",
"시험": "测试",
"시험판": "预发布",
"실시간 동기화 (lsyncd 처럼)": "实时同步 (类似 lsyncd)",
"실시간 동기화 ({0}): {1} → {2}": "实时同步 ({0}): {1} → {2}",
@ -1955,6 +1959,7 @@
"약하게 (0.15)": "弱 (0.15)",
"양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).": "双向 (⇄) 不会删除源文件(不使用选项中的 --remove-source-files)。",
"양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).": "双向: 接收端较新的文件不会被覆盖 (-u)。",
"어두운 화면": "深色",
"어둡게": "调暗",
"어디서나 누르면 JJ_MKVMaker 창이 나타납니다": "在任何地方按下即可显示 JJ_MKVMaker 窗口",
"언어": "语言",
@ -3036,6 +3041,7 @@
"화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "在屏幕中间左右滑动会切换到其他屏幕(文件浏览器 · 下载等)。关闭:设置 > 组件 > 左右滑动切换界面",
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "将在此设备上翻译所有界面文字。可能需要几分钟。",
"화면 모양 따라": "跟随屏幕形状",
"화면 밝기": "界面亮度",
"화면 방향": "屏幕方向",
"화면 방향: 가로 고정 → 자동 (기기를 돌리는 대로). 되돌리기: 환경 설정 > 화면 > 화면 방향": "屏幕方向: 固定横屏 → 自动 (随设备旋转)。恢复: 设置 > 屏幕 > 屏幕方向",
"화면 비율": "画面比例",

View File

@ -342,6 +342,11 @@ class AppSettings {
double uiScaleDefault = 1.0;
// 18: 50 ~ 250% (사용자 결정)
static const double uiScaleMin = 0.5, uiScaleMax = 2.5;
/// X-plore 비교 5-1 ⑦ 화면 밝기 (관리자 지시 10/21): 'dark' 어둡게 (처음 값 - 예전 그대로) · 'light' 밝게 · 'system' 기기 따라.
/// 다 끝날 때까지 설정에 "시험" 표시. 모르는 값은 어둡게
String uiBrightness = 'dark';
static const uiBrightnessModes = ['dark', 'light', 'system'];
static double clampUiScale(num? v) =>
((v ?? 1.0).toDouble().clamp(uiScaleMin, uiScaleMax) * 20).round() / 20;
@ -846,6 +851,7 @@ class AppSettings {
'startScreen': startScreen,
'uiScale': uiScale,
'uiScaleDefault': uiScaleDefault,
'uiBrightness': uiBrightness,
'openFileAction': openFileAction,
'screenOrientation': screenOrientation,
// 기본이 가로 고정이던 때의 값은 한 번 자동으로 옮긴다 (사용자가 정한 적 없는 가로 고정)
@ -1075,6 +1081,7 @@ class AppSettings {
..startScreen = j['startScreen'] as String? ?? 'home'
..uiScale = clampUiScale(j['uiScale'] as num?)
..uiScaleDefault = clampUiScale(j['uiScaleDefault'] as num?)
..uiBrightness = uiBrightnessModes.contains(j['uiBrightness']) ? j['uiBrightness'] as String : 'dark'
..openFileAction = j['openFileAction'] == 'add' ? 'add' : 'play'
..uiLanguage = j['uiLanguage'] as String? ?? 'ko'
..appIcon = appIconOf(j['appIcon'] as String?)

View File

@ -834,10 +834,10 @@ class JjMkvMakerApp extends StatelessWidget {
];
@override
Widget build(BuildContext context) => ListenableBuilder(
Widget build(BuildContext context) => _BrightnessHost(
c: controller,
// 화면 언어를 바꾸면 (i18n) Flutter 기본 글도 그 언어로
listenable: i18n,
builder: (context, _) => _app(context),
builder: (context) => ListenableBuilder(listenable: i18n, builder: (context, _) => _app(context)),
);
Widget _app(BuildContext context) {
@ -849,6 +849,8 @@ class JjMkvMakerApp extends StatelessWidget {
title: appTitle,
debugShowCheckedModeBanner: false,
theme: buildTheme(),
// 밝기 바꿈은 화면 전체를 다시 그려 바로 (전환 그림 없이 - JjColors 를 직접 읽는 곳과 같은 때 바뀌게)
themeAnimationDuration: Duration.zero,
navigatorKey: navigatorKey,
scaffoldMessengerKey: messengerKey,
navigatorObservers: [browserRouteObserver],
@ -864,6 +866,51 @@ class JjMkvMakerApp extends StatelessWidget {
}
}
/// X-plore 비교 5-1 ⑦: 환경 설정 "화면 밝기" · 기기 밝기를 따라 [JjColors] 를 바꾸고 화면 전체를 다시 그린다 (상태는 그대로)
class _BrightnessHost extends StatefulWidget {
final AppController c;
final WidgetBuilder builder;
const _BrightnessHost({required this.c, required this.builder});
@override
State<_BrightnessHost> createState() => _BrightnessHostState();
}
class _BrightnessHostState extends State<_BrightnessHost> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
widget.c.addListener(_check);
JjColors.use(_want());
}
@override
void dispose() {
widget.c.removeListener(_check);
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
Brightness _want() =>
resolveBrightness(widget.c.settings.uiBrightness, WidgetsBinding.instance.platformDispatcher.platformBrightness);
@override
void didChangePlatformBrightness() => _check();
void _check() {
if (!mounted || !JjColors.use(_want())) return;
setState(() {});
// JjColors 를 직접 읽는 화면 조각도 새 색으로 (그 조각들은 Theme 이 바뀌어도 다시 그려지지 않는다)
WidgetsBinding.instance.addPostFrameCallback((_) {
final root = WidgetsBinding.instance.rootElement;
if (root != null) rebuildAllElements(root);
});
}
@override
Widget build(BuildContext context) => widget.builder(context);
}
/// Cupertino 글이 없는 언어만 영어로 (GlobalCupertinoLocalizations 뒤에 두어 그쪽이 아는 언어는 그쪽이 먼저)
class _CupertinoFallbackDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
const _CupertinoFallbackDelegate();

View File

@ -94,7 +94,7 @@ Future<Language?> pickSourceLanguage(BuildContext context, Language detected) =>
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 8),
child: Text(tr('처음부터 정하려면 AI 자막 창의 "원어 (영상 속 언어)" 에서 고르세요.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
for (final l in languages)
SimpleDialogOption(
@ -172,7 +172,7 @@ class AiPlanSummary extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(width: 84, child: Text(k, style: const TextStyle(fontSize: 12, color: JjColors.textDim))),
SizedBox(width: 84, child: Text(k, style: TextStyle(fontSize: 12, color: JjColors.textDim))),
Expanded(child: Text(v, style: const TextStyle(fontSize: 13))),
]),
),
@ -221,7 +221,7 @@ class _AiDialogState extends State<_AiDialog> {
/// 259: 번역이 없으면 받지 않는다 · 감지한 뒤에야 알 수 있으면 그때 받는다
Widget _nllbStatus() {
if (_installed[nllbModel.id] != false || _o.surelyNeedsTranslation) return _status(nllbModel);
const style = TextStyle(fontSize: 11, color: JjColors.textDim);
final style = TextStyle(fontSize: 11, color: JjColors.textDim);
if (!_o.needsTranslation) return Text(tr('이번에는 쓰지 않음 (번역 없음)'), style: style);
return Text(trf('감지한 원어가 만들 언어와 다를 때만 내려받기 {0}', [nllbModel.sizeLabel]), style: style);
}
@ -239,7 +239,7 @@ class _AiDialogState extends State<_AiDialog> {
mainAxisSize: MainAxisSize.min,
children: [
Text(trf('영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0}에서 이루어집니다.', [Platform.isAndroid ? tr('기기') : 'PC']),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 10),
// 138: 무엇을 할지 먼저 (바꾸면 따라 바뀐다)
AiPlanSummary(videos: _all ? widget.c.videos : widget.videos, o: _o, thenBuild: widget.thenBuild),

View File

@ -293,7 +293,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
onPressed: () => setState(() => _showDetail = !_showDetail),
child: Text(_showDetail ? tr('자세히 닫기') : tr('자세히')),
),
if (_showDetail) SelectableText(_detail, style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
if (_showDetail) SelectableText(_detail, style: TextStyle(fontSize: 11, color: JjColors.textDim)),
]),
),
const SizedBox(height: 12),
@ -374,7 +374,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
alignment: Alignment.center,
padding: const EdgeInsets.all(8),
child: Text(tr('파일이 없습니다 (지워졌거나 옮겨짐)'),
textAlign: TextAlign.center, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
textAlign: TextAlign.center, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
TextButton(
onPressed: () {
@ -393,7 +393,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
child: Image.file(File(g.path), width: 160, height: 160, fit: BoxFit.cover),
),
Row(mainAxisSize: MainAxisSize.min, children: [
Text(trf('시드 {0}', [g.seed]), style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
Text(trf('시드 {0}', [g.seed]), style: TextStyle(fontSize: 11, color: JjColors.textDim)),
IconButton(
tooltip: tr('같은 조건으로 다시 (시드 그대로)'),
visualDensity: VisualDensity.compact,

View File

@ -111,7 +111,7 @@ class _AiSettingsState extends State<AiSettings> {
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(tr(f.terms!)),
const SizedBox(height: 8),
SelectableText(f.licenseUrl, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
SelectableText(f.licenseUrl, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
if (f.licenseAsset != null)
TextButton(
onPressed: () => showLicenseDocument(ctx, trf('{0} 라이선스', [tr(f.license)]), f.licenseAsset!),

View File

@ -84,7 +84,7 @@ class _AiSubtitleModelsState extends State<AiSubtitleModels> {
if (_error != null)
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
child: Text(_error!, style: const TextStyle(fontSize: 12, color: JjColors.danger)),
child: Text(_error!, style: TextStyle(fontSize: 12, color: JjColors.danger)),
),
]);
},

View File

@ -122,7 +122,7 @@ Future<bool> _download(BuildContext context, AiStore store, List<AiFile> files)
for (final f in files) ...[
Text(f.name, style: const TextStyle(fontSize: 13)),
Text(store.isFileInstalled(f) ? tr('받음') : store.progressText(f.id) ?? tr('준비하는 중'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 8),
],
]),
@ -225,11 +225,11 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
: '${(v * 100).round()}% · ${trf('약 {0} 남음', [AiJobs.durationText(d)])}',
style: const TextStyle(fontSize: 12)),
),
if (estimate != null) Text(estimate, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
if (estimate != null) Text(estimate, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
if (cpuAnime) ...[
const SizedBox(height: 10),
Text(tr("사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다."),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
TextButton(
onPressed: () {
toPhoto = true;

View File

@ -256,7 +256,7 @@ class _FileBrowserState extends State<_FileBrowser> {
dir == null || vol == null
? ''
: (atRoot ? _volumeName(vol) : '${_volumeName(vol)} / ${p.relative(dir, from: root)}'),
style: const TextStyle(color: JjColors.textDim),
style: TextStyle(color: JjColors.textDim),
),
),
const Divider(height: 1),
@ -264,7 +264,7 @@ class _FileBrowserState extends State<_FileBrowser> {
child: _dirs.isEmpty && _files.isEmpty
? Center(
child: Text(widget.folder ? tr('하위 폴더가 없습니다') : tr('이 폴더에는 고를 파일이 없습니다'),
style: const TextStyle(color: JjColors.textDim)))
style: TextStyle(color: JjColors.textDim)))
: ListView(children: [
for (final d in _dirs)
ListTile(
@ -312,7 +312,7 @@ class _FileBrowserState extends State<_FileBrowser> {
tr('지금은 듀얼 앱 (복제한 앱) 입니다. Android 가 듀얼 앱에는 이 권한을 주지 않을 수 있습니다. '
'허용 화면에서 켤 수 없으면 배지 없는 원래 JJ_MKVMaker 아이콘으로 여세요.'),
textAlign: TextAlign.center,
style: const TextStyle(color: JjColors.textDim),
style: TextStyle(color: JjColors.textDim),
),
],
const SizedBox(height: 16),

View File

@ -490,7 +490,7 @@ class UsageView extends StatelessWidget {
width: width,
child: Text.rich(
TextSpan(children: [
TextSpan(text: '$label ', style: const TextStyle(color: JjColors.textDim)),
TextSpan(text: '$label ', style: TextStyle(color: JjColors.textDim)),
TextSpan(
text: value,
style: TextStyle(
@ -679,7 +679,7 @@ class AppActions extends StatelessWidget {
PopupMenuItem(
value: 'exit',
child: ListTile(
leading: const Icon(Icons.power_settings_new, color: JjColors.danger),
leading: Icon(Icons.power_settings_new, color: JjColors.danger),
title: Text(tr('종료')),
dense: true)),
],
@ -706,7 +706,7 @@ class AppActions extends StatelessWidget {
if (exit != null)
IconButton(
tooltip: tr('종료'),
icon: const Icon(Icons.power_settings_new, color: JjColors.danger),
icon: Icon(Icons.power_settings_new, color: JjColors.danger),
onPressed: exit,
),
]);

View File

@ -83,7 +83,7 @@ class _AppManagerPageState extends State<AppManagerPage> {
ListTile(leading: const Icon(Icons.info_outline), title: Text(tr('앱 정보 (시스템 화면)')), onTap: () => Navigator.pop(ctx, 'details')),
if (!a.system)
ListTile(
leading: const Icon(Icons.delete_outline, color: JjColors.danger),
leading: Icon(Icons.delete_outline, color: JjColors.danger),
title: Text(tr('지우기 (시스템 창에서 확인)')),
onTap: () => Navigator.pop(ctx, 'uninstall')),
]),
@ -110,7 +110,7 @@ class _AppManagerPageState extends State<AppManagerPage> {
body: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
child: Text(tr('보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
child: Text(tr('보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
@ -133,7 +133,7 @@ class _AppManagerPageState extends State<AppManagerPage> {
),
]),
),
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: const TextStyle(color: JjColors.danger))),
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: TextStyle(color: JjColors.danger))),
if (apps == null && _error == null) const Expanded(child: Center(child: CircularProgressIndicator())),
if (apps != null)
Expanded(

View File

@ -45,7 +45,7 @@ Future<String?> askArchivePassword(BuildContext context, String name, {bool wron
title: Text(tr('비밀번호')),
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(trf('"{0}" 은(는) 비밀번호가 걸려 있습니다.', [name])),
if (wrong) Text(tr('비밀번호가 틀렸습니다'), style: const TextStyle(color: JjColors.danger)),
if (wrong) Text(tr('비밀번호가 틀렸습니다'), style: TextStyle(color: JjColors.danger)),
TextField(
controller: ctl,
autofocus: true,
@ -183,7 +183,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
const SizedBox(height: 8),
Text(trf('밖으로 나가는 경로라 풀지 않습니다 ({0}개): {1}',
[plan.unsafe.length, plan.unsafe.take(5).map((x) => '${x.$1} (${x.$2})').join(', ')]),
style: const TextStyle(color: JjColors.danger, fontSize: 12)),
style: TextStyle(color: JjColors.danger, fontSize: 12)),
],
if (plan.badNames.isNotEmpty) ...[
const SizedBox(height: 6),
@ -198,11 +198,11 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
],
if (plan.encrypted) ...[
const SizedBox(height: 6),
Text(tr('비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(tr('비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
],
const SizedBox(height: 10),
_placeChoice(ctx, choices, dest, (d) => set(() => dest = d)),
Text(vDisplay(dest), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(vDisplay(dest), maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 6),
// 좁은 화면 · 큰 글자면 고르기 상자를 다음 줄로 (412 · 글자 1.6배에서 화면 밖으로 나갔다 - 253 시험)
Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
@ -227,7 +227,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
// 관리자 판단: 풀린 크기가 빈 공간보다 크면 풀기 전에 알리고 묻는다 (압축 폭탄 · 공간 부족)
if (short)
Text(trf('풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?', [formatSize(plan.totalSize), formatSize(f)]),
style: const TextStyle(color: JjColors.danger)),
style: TextStyle(color: JjColors.danger)),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
@ -351,8 +351,8 @@ Future<String?> createZipFlow(BuildContext context, AppController c, List<String
]),
const SizedBox(height: 6),
_placeChoice(ctx, choices, where, (d) => set(() => where = d)),
Text(trf('만들 파일: {0}', [p.basename(target())]), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(tr('링크 · junction 은 넣지 않습니다'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(trf('만들 파일: {0}', [p.basename(target())]), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(tr('링크 · junction 은 넣지 않습니다'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 10),
TextField(
controller: pass,
@ -387,11 +387,11 @@ Future<String?> createZipFlow(BuildContext context, AppController c, List<String
),
const SizedBox(height: 4),
Text(tr('비밀번호를 잊으면 이 ZIP 을 풀 수 없습니다 (앱이 기억하지 않습니다). 파일 이름 목록은 암호화되지 않습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
// zip_aes: 7-Zip 등 다른 프로그램은 영문 밖 글자 ZIP 비밀번호를 받지 않는다 (10/17 시험) - 막지는 않고 알린다
if (pass.text.codeUnits.any((c) => c > 0x7e))
Text(tr('한글 등 영문 밖 글자가 든 비밀번호는 이 앱에서는 풀리지만 7-Zip 등 다른 프로그램에서는 풀리지 않을 수 있습니다. 다른 기기 · 프로그램과 나눌 ZIP 은 영문 · 숫자 · 기호 비밀번호를 쓰세요.'),
style: const TextStyle(fontSize: 12, color: JjColors.danger)),
style: TextStyle(fontSize: 12, color: JjColors.danger)),
],
]),
actions: [

View File

@ -50,7 +50,7 @@ class AudioNowPlaying extends StatelessWidget {
valueListenable: state,
builder: (_, s, _) => Text(
'${_t(s.position)} / ${_t(s.duration)}',
style: const TextStyle(color: JjColors.textDim, fontSize: 14, fontFeatures: [FontFeature.tabularFigures()]),
style: TextStyle(color: JjColors.textDim, fontSize: 14, fontFeatures: [FontFeature.tabularFigures()]),
),
),
]),
@ -61,7 +61,7 @@ class _Note extends StatelessWidget {
const _Note();
@override
Widget build(BuildContext context) => const AspectRatio(
Widget build(BuildContext context) => AspectRatio(
aspectRatio: 1,
child: DecoratedBox(
decoration: BoxDecoration(color: Color(0xFF1E1E1E), borderRadius: BorderRadius.all(Radius.circular(12))),

View File

@ -245,7 +245,7 @@ class _BatchRenameDialogState extends State<_BatchRenameDialog> {
PopupMenuItem(value: t, child: Text('$t $label')),
],
// 412 · 큰 글자에서 이 줄이 넘쳐 (253 시험) 글 대신 작은 아이콘
child: const Padding(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 4, vertical: 6),
child: Icon(Icons.music_note_outlined, size: 20, color: JjColors.accent),
),
@ -317,7 +317,7 @@ class _BatchRenameDialogState extends State<_BatchRenameDialog> {
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Expanded(
child: Text(r.oldName,
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
const Padding(padding: EdgeInsets.symmetric(horizontal: 6), child: Text('→', style: TextStyle(fontSize: 12))),
Expanded(
@ -339,7 +339,7 @@ class _BatchRenameDialogState extends State<_BatchRenameDialog> {
if (_error != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(_error!, style: const TextStyle(fontSize: 12, color: JjColors.danger)),
child: Text(_error!, style: TextStyle(fontSize: 12, color: JjColors.danger)),
),
]),
),

View File

@ -135,7 +135,7 @@ class _BookmarkEditorState extends State<_BookmarkEditor> {
actions: [
TextButton(
onPressed: () => Navigator.pop(context, 'del'),
child: Text(widget.justAdded ? tr('추가 취소') : tr('삭제'), style: const TextStyle(color: JjColors.danger)),
child: Text(widget.justAdded ? tr('추가 취소') : tr('삭제'), style: TextStyle(color: JjColors.danger)),
),
if (!widget.justAdded) TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))),
FilledButton(onPressed: _save, child: Text(widget.justAdded ? tr('완료') : tr('저장'))),
@ -177,7 +177,7 @@ Future<void> showNewBookmarkDialog(BuildContext context, BookmarksController bm,
Align(
alignment: Alignment.centerLeft,
child: Text(trf('폴더: {0}', [bm.tree.find(parentId)?.title ?? '']),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
]),
),
@ -218,7 +218,7 @@ Future<String?> askText(BuildContext context, String title, String label, String
),
if (help != null) ...[
const SizedBox(height: 6),
Text(help, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(help, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
],
]),
),
@ -370,7 +370,7 @@ class _BookmarkDraggableState extends State<BookmarkDraggable> {
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(n.isFolder ? Icons.folder : Icons.public, size: 16, color: n.isFolder ? Colors.amber : null),
const SizedBox(width: 6),
Text(n.title, style: const TextStyle(fontSize: 12, color: JjColors.text)),
Text(n.title, style: TextStyle(fontSize: 12, color: JjColors.text)),
]),
),
);
@ -509,7 +509,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
nav: const AppNavButtons(),
actions: const AppActions(),
middle: Row(children: [
const Icon(Icons.bookmarks_outlined, color: JjColors.accent),
Icon(Icons.bookmarks_outlined, color: JjColors.accent),
const SizedBox(width: 8),
if (!isCompact(context)) ...[
Text(tr('즐겨찾기 관리자'), style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
@ -644,7 +644,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
Expanded(
child: Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [
for (final (i, f) in path.indexed) ...[
if (i > 0) const Icon(Icons.chevron_right, size: 16, color: JjColors.textDim),
if (i > 0) Icon(Icons.chevron_right, size: 16, color: JjColors.textDim),
BookmarkFolderDrop(
bm: bm,
folderId: f.id,
@ -704,7 +704,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
leading: Row(mainAxisSize: MainAxisSize.min, children: [
ReorderableDragStartListener(
index: i,
child: const Icon(Icons.drag_indicator, size: 18, color: JjColors.textDim),
child: Icon(Icons.drag_indicator, size: 18, color: JjColors.textDim),
),
const SizedBox(width: 6),
// 아이콘을 끌면 다른 폴더로 옮기기

View File

@ -273,7 +273,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
textAlign: TextAlign.center, style: const TextStyle(fontSize: 16)),
const SizedBox(height: 8),
Text(tr('인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.'),
textAlign: TextAlign.center, style: const TextStyle(fontSize: 13, color: JjColors.textDim)),
textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: JjColors.textDim)),
const SizedBox(height: 16),
Wrap(spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: [
FilledButton(
@ -468,7 +468,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 2, 4, 2),
child: Row(children: [
const Icon(Icons.swipe, size: 16, color: JjColors.accent),
Icon(Icons.swipe, size: 16, color: JjColors.accent),
const SizedBox(width: 8),
Expanded(
child: Text(tr('웹 브라우저에서는 위쪽 막대를 좌우로 밀면 다른 화면으로 옮깁니다 (가운데는 페이지가 밀립니다).'),
@ -489,12 +489,12 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
child: Padding(
padding: const EdgeInsets.fromLTRB(12, 2, 4, 2),
child: Row(children: [
const Icon(Icons.timer_outlined, size: 16, color: JjColors.textDim),
Icon(Icons.timer_outlined, size: 16, color: JjColors.textDim),
const SizedBox(width: 8),
Expanded(
child: Text(
trf('Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다', [(at.difference(DateTime.now()).inSeconds / 60).ceil().clamp(0, 60)]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
style: TextStyle(fontSize: 12, color: JjColors.textDim),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
@ -1336,7 +1336,7 @@ class _CefElevatedBlocked extends StatelessWidget {
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 560),
child: Column(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.admin_panel_settings_outlined, size: 40, color: JjColors.textDim),
Icon(Icons.admin_panel_settings_outlined, size: 40, color: JjColors.textDim),
const SizedBox(height: 12),
Text(tr('관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)'),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textAlign: TextAlign.center),
@ -1344,7 +1344,7 @@ class _CefElevatedBlocked extends StatelessWidget {
Text(
tr('내장 Chrome 엔진은 샌드박스가 꺼져 있어 관리자 권한으로 돌리면 위험합니다. '
'Edge 엔진을 쓰거나, 관리자 권한 없이 프로그램을 실행하세요.'),
style: const TextStyle(color: JjColors.textDim),
style: TextStyle(color: JjColors.textDim),
textAlign: TextAlign.center),
const SizedBox(height: 16),
Wrap(spacing: 12, runSpacing: 8, alignment: WrapAlignment.center, children: [
@ -1471,12 +1471,12 @@ class _CefViewState extends State<_CefView> {
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 520),
child: Column(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.error_outline, size: 40, color: JjColors.textDim),
Icon(Icons.error_outline, size: 40, color: JjColors.textDim),
const SizedBox(height: 12),
Text(tr('Chrome 엔진을 시작하지 못했습니다'),
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textAlign: TextAlign.center),
const SizedBox(height: 8),
Text('$_error', style: const TextStyle(color: JjColors.textDim), textAlign: TextAlign.center),
Text('$_error', style: TextStyle(color: JjColors.textDim), textAlign: TextAlign.center),
const SizedBox(height: 16),
Wrap(spacing: 12, runSpacing: 8, alignment: WrapAlignment.center, children: [
if (widget.onUseEdge != null)
@ -1785,7 +1785,7 @@ class _BookmarkPanelState extends State<BookmarkPanel> {
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 4, 0),
child: Row(children: [
const Icon(Icons.bookmarks_outlined, size: 18, color: JjColors.accent),
Icon(Icons.bookmarks_outlined, size: 18, color: JjColors.accent),
const SizedBox(width: 6),
Text(tr('즐겨찾기'), style: TextStyle(fontWeight: FontWeight.w600)),
const Spacer(),
@ -1816,7 +1816,7 @@ class _BookmarkPanelState extends State<BookmarkPanel> {
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
child: Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [
for (final (i, n) in _path().indexed) ...[
if (i > 0) const Icon(Icons.chevron_right, size: 14, color: JjColors.textDim),
if (i > 0) Icon(Icons.chevron_right, size: 14, color: JjColors.textDim),
BookmarkFolderDrop(
bm: bm,
folderId: n.id,
@ -1903,7 +1903,7 @@ class _BookmarkPanelState extends State<BookmarkPanel> {
leading: Row(mainAxisSize: MainAxisSize.min, children: [
ReorderableDragStartListener(
index: i,
child: const Icon(Icons.drag_indicator, size: 16, color: JjColors.textDim),
child: Icon(Icons.drag_indicator, size: 16, color: JjColors.textDim),
),
const SizedBox(width: 4),
// 아이콘을 끌어 폴더 줄 · 위 경로에 놓으면 그 폴더로

View File

@ -109,7 +109,7 @@ class _InstallProgressState extends State<_InstallProgress> {
const SizedBox(height: 8),
LinearProgressIndicator(value: _p <= 0 ? null : _p),
] else
Text(trf('설치하지 못했습니다.\n{0}', [_error]), style: const TextStyle(color: JjColors.danger)),
Text(trf('설치하지 못했습니다.\n{0}', [_error]), style: TextStyle(color: JjColors.danger)),
]),
),
actions: [

View File

@ -59,7 +59,7 @@ class _CleanupDialogState extends State<_CleanupDialog> {
final groups = _groups;
return AlertDialog(
title: Row(children: [
const Icon(Icons.cleaning_services_outlined, color: JjColors.accent),
Icon(Icons.cleaning_services_outlined, color: JjColors.accent),
const SizedBox(width: 10),
Text(tr('저장 공간 정리')),
]),
@ -85,7 +85,7 @@ class _CleanupDialogState extends State<_CleanupDialog> {
Expanded(child: Text(g.title)),
Text(
g.items.isEmpty ? tr('없음') : trf('{0}개 · {1}', [g.items.length, Cleaner.sizeText(g.bytes)]),
style: const TextStyle(fontSize: 13, color: JjColors.textDim),
style: TextStyle(fontSize: 13, color: JjColors.textDim),
),
]),
subtitle: Text(g.skipped ?? g.hint,

View File

@ -240,7 +240,7 @@ class ComponentSettings extends StatelessWidget {
ListTile(
key: ValueKey(p.id),
dense: true,
leading: Text('${i + 1}', style: const TextStyle(color: JjColors.textDim)),
leading: Text('${i + 1}', style: TextStyle(color: JjColors.textDim)),
title: Row(children: [Icon(p.icon, size: 18), const SizedBox(width: 10), Flexible(child: Text(tr(p.name)))]),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(

View File

@ -219,7 +219,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
)
: IconButton(
tooltip: tr('내려받은 rsync 지우기'),
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
onPressed: () async {
await RsyncInstaller.uninstall();
await _check();
@ -303,7 +303,7 @@ class _LiveSyncTile extends StatelessWidget {
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text('$label ${shortPath(path)}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
Text(vDisplay(path), style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
Text(vDisplay(path), style: TextStyle(fontSize: 11, color: JjColors.textDim)),
]),
),
TextButton(
@ -474,7 +474,7 @@ class _LiveSyncTile extends StatelessWidget {
),
IconButton(
tooltip: tr('지우기'),
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
onPressed: () => removeLiveSyncPair(context, c, pairs[i]),
),
]),
@ -525,7 +525,7 @@ class LiveSyncRunOptions extends StatelessWidget {
),
// 257: 폰 세로에서 넘쳤다 - 모자라면 고르기 상자를 아래 줄로
Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
Text(tr('다시 켤 때 동기화'), style: const TextStyle(fontSize: 13, color: JjColors.textDim)),
Text(tr('다시 켤 때 동기화'), style: TextStyle(fontSize: 13, color: JjColors.textDim)),
onStart,
]),
]);

View File

@ -30,7 +30,7 @@ Future<String?> fetchDav(BuildContext context, AppController c, String path) asy
LinearProgressIndicator(value: v.$2 > 0 ? v.$1 / v.$2 : null),
const SizedBox(height: 6),
Text('${formatSize(v.$1)}${v.$2 > 0 ? ' / ${formatSize(v.$2)}' : ''}',
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
),
actions: [

View File

@ -200,9 +200,9 @@ class _DlnaPageState extends State<DlnaPage> {
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: Text(tr('보기만 합니다 - 같은 Wi-Fi · 유선 망의 미디어 서버만 찾고, 올리기 · 지우기는 없습니다'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: const TextStyle(color: JjColors.danger))),
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: TextStyle(color: JjColors.danger))),
if (s == null) Expanded(child: _serverList()) else Expanded(child: _entryList()),
]),
),
@ -234,7 +234,7 @@ class _DlnaPageState extends State<DlnaPage> {
ListTile(
leading: const Icon(Icons.dns_outlined),
title: Text(x.name),
subtitle: Text(x.host, style: const TextStyle(color: JjColors.textDim)),
subtitle: Text(x.host, style: TextStyle(color: JjColors.textDim)),
onTap: () => _open(x, '0', x.name),
),
]);

View File

@ -133,7 +133,7 @@ class _DownloadsPageState extends State<DownloadsPage> {
Padding(
padding: const EdgeInsets.only(right: 8),
child: Text(hasSel ? trf('{0}개 선택', [d.selected.length]) : tr('전체 선택'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
]),
),
@ -185,7 +185,7 @@ class _DownloadsPageState extends State<DownloadsPage> {
? tr('다운로드가 없습니다.\nYouTube 주소 · 마그넷 링크를 위 칸에 붙여 넣거나, 웹 브라우저에서 [다운로드] 를 누르세요.')
: tr('다운로드가 없습니다.\nYouTube 주소나 마그넷 링크를 복사(Ctrl+C)하면 자동으로 받습니다.'),
textAlign: TextAlign.center,
style: const TextStyle(color: JjColors.textDim))))
style: TextStyle(color: JjColors.textDim))))
: ListView.builder(
itemCount: d.tasks.length,
itemBuilder: (_, i) => _DownloadRow(d: d, t: d.tasks[i]),
@ -274,7 +274,7 @@ class _DownloadRow extends StatelessWidget {
width: 560,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
// 서명 · 키가 든 주소 부분은 가린다
SelectableText(redactUrlSecrets(t.source), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
SelectableText(redactUrlSecrets(t.source), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 8),
SelectableText(redactUrlSecrets(t.error ?? ''), style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
]),
@ -300,7 +300,7 @@ class _DownloadRow extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: d.selected.contains(t.id) ? JjColors.accent.withValues(alpha: 0.08) : null,
border: const Border(bottom: BorderSide(color: JjColors.border)),
border: Border(bottom: BorderSide(color: JjColors.border)),
),
child: Row(children: [
Checkbox(value: d.selected.contains(t.id), onChanged: (_) => d.toggle(t)),
@ -328,7 +328,7 @@ class _DownloadRow extends StatelessWidget {
])
else if (t.state == DownloadState.failed)
Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 4, children: [
Text('$label · ${_why(t)}', style: const TextStyle(fontSize: 12, color: JjColors.danger)),
Text('$label · ${_why(t)}', style: TextStyle(fontSize: 12, color: JjColors.danger)),
if ((t.error ?? '').trim().isNotEmpty)
TextButton(
style: _small,
@ -356,7 +356,7 @@ class _DownloadRow extends StatelessWidget {
),
IconButton(
tooltip: tr('폴더 열기'),
icon: const Icon(Icons.folder_open, size: 18, color: JjColors.textDim),
icon: Icon(Icons.folder_open, size: 18, color: JjColors.textDim),
onPressed: () {
if (Platform.isWindows) {
Process.run('explorer', [t.dir]);

View File

@ -35,7 +35,7 @@ class _CloseChoiceState extends State<_CloseChoice> {
Text(trf('지금: {0}', [widget.running]), style: const TextStyle(fontSize: 13)),
const SizedBox(height: 10),
Text(trf('· 백그라운드로: 창만 숨기고 다운로드 · 변환을 계속합니다 ({0} 또는 트레이 아이콘으로 다시 열기)\n' '· 모두 종료: 다운로드 · 변환을 멈추고 프로그램을 끝냅니다', [widget.hotkey]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 8),
InkWell(
onTap: () => setState(() => _remember = !_remember),
@ -125,7 +125,7 @@ class _ExitProgressState extends State<_ExitProgress> {
Text(trf('바로 전 작업: {0}', [widget.lastWork]),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 12),
for (final (i, s) in widget.steps.indexed)
Padding(
@ -135,10 +135,10 @@ class _ExitProgressState extends State<_ExitProgress> {
width: 18,
height: 18,
child: i < _done
? const Icon(Icons.check_circle, size: 18, color: JjColors.success)
? Icon(Icons.check_circle, size: 18, color: JjColors.success)
: i == _done
? const CircularProgressIndicator(strokeWidth: 2)
: const Icon(Icons.circle_outlined, size: 18, color: JjColors.textDim),
: Icon(Icons.circle_outlined, size: 18, color: JjColors.textDim),
),
const SizedBox(width: 10),
Expanded(

View File

@ -88,7 +88,7 @@ class _BookmarksDialogState extends State<_BookmarksDialog> {
if (mine.isEmpty)
Padding(
padding: const EdgeInsets.all(12),
child: Text(tr('아직 없습니다. 아래에서 지금 폴더를 넣으세요.'), style: const TextStyle(color: JjColors.textDim)),
child: Text(tr('아직 없습니다. 아래에서 지금 폴더를 넣으세요.'), style: TextStyle(color: JjColors.textDim)),
),
for (final b in mine)
ListTile(

View File

@ -75,10 +75,10 @@ Widget dragFeedback(List<String> paths) => Material(
border: Border.all(color: JjColors.accent),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.drive_file_move_outline, size: 16, color: JjColors.accent),
Icon(Icons.drive_file_move_outline, size: 16, color: JjColors.accent),
const SizedBox(width: 6),
Text(paths.length == 1 ? vBasename(paths.first) : trf('{0}개 · {1} …', [paths.length, vBasename(paths.first)]),
style: const TextStyle(fontSize: 12, color: JjColors.text)),
style: TextStyle(fontSize: 12, color: JjColors.text)),
]),
),
);

View File

@ -79,14 +79,14 @@ class _ExplorerGridPageState extends State<ExplorerGridPage> {
picture = FutureBuilder<File?>(
future: widget.videoThumb(e),
builder: (_, s) => s.data == null
? const Center(child: Icon(Icons.movie_outlined, size: 40, color: JjColors.textDim))
? Center(child: Icon(Icons.movie_outlined, size: 40, color: JjColors.textDim))
: Image.file(s.data!, fit: BoxFit.cover, cacheWidth: px),
);
} else {
picture = Image.file(File(e.path),
fit: BoxFit.cover,
cacheWidth: px,
errorBuilder: (_, _, _) => const Center(child: Icon(Icons.broken_image_outlined, size: 40, color: JjColors.textDim)));
errorBuilder: (_, _, _) => Center(child: Icon(Icons.broken_image_outlined, size: 40, color: JjColors.textDim)));
}
return Tooltip(
message: e.name,

View File

@ -52,7 +52,7 @@ class ExplorerKeysPage extends StatelessWidget {
child: Text(
trf('지금 스타일 ({0}) 의 키입니다. 바꾼 키는 모든 스타일에 쓰이고, 다른 동작과 겹치면 그 동작의 키는 없어집니다. Esc 는 바꿀 수 없습니다',
[ExplorerStyle.of(c.settings.explorerStyle).label]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
for (final id in explorerKeyActions)
ListTile(
@ -123,7 +123,7 @@ class _KeyCaptureDialogState extends State<_KeyCaptureDialog> {
if (clash == 'escape')
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(tr('Esc 는 선택 · 빠른 필터를 닫는 키라 쓸 수 없습니다'), style: const TextStyle(color: JjColors.danger, fontSize: 12)),
child: Text(tr('Esc 는 선택 · 빠른 필터를 닫는 키라 쓸 수 없습니다'), style: TextStyle(color: JjColors.danger, fontSize: 12)),
)
else if (clash != null)
Padding(

View File

@ -177,7 +177,7 @@ class ExplorerColumnsHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
const st = TextStyle(fontSize: 11.5, color: JjColors.textDim);
final st = TextStyle(fontSize: 11.5, color: JjColors.textDim);
Widget cell(String t, int flex, {TextAlign align = TextAlign.start, String? by}) {
final on = by != null && by == sortBy;
final label = Row(
@ -208,7 +208,7 @@ class ExplorerColumnsHeader extends StatelessWidget {
return Container(
height: 24,
padding: EdgeInsets.only(left: 10, right: markColumn ? 40 : 4),
decoration: const BoxDecoration(border: Border(bottom: BorderSide(color: JjColors.border))),
decoration: BoxDecoration(border: Border(bottom: BorderSide(color: JjColors.border))),
child: Row(
children: style == ExplorerStyle.windows
? [

View File

@ -743,7 +743,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
Expanded(child: Text(title, style: const TextStyle(fontWeight: FontWeight.w600))),
]),
const SizedBox(height: 4),
Text(body, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(body, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
Wrap(spacing: 8, children: actions),
]),
);
@ -2094,7 +2094,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(children: [
const Icon(Icons.wifi_tethering, size: 18, color: JjColors.success),
Icon(Icons.wifi_tethering, size: 18, color: JjColors.success),
const SizedBox(width: 6),
Expanded(
child: Text(
@ -2265,7 +2265,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
enabled: false,
height: 32,
child: Text(trf('{0}개 항목', [targets.length]),
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: JjColors.accent)),
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: JjColors.accent)),
),
if (icons.isNotEmpty) ...[_MenuIconRow(icons), const PopupMenuDivider(height: 1)],
for (final (id, icon, label) in items)
@ -2618,7 +2618,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Row(children: [
const Icon(Icons.arrow_forward, size: 18, color: JjColors.accent),
Icon(Icons.arrow_forward, size: 18, color: JjColors.accent),
const SizedBox(width: 6),
// 412 캡처 (10/18): "갈 곳:" 은 늘 보이고, 폴더 이름이 길면 가운데를 줄인다 (전체 경로는 길게 눌러 · 마우스를 올려)
Expanded(
@ -3008,7 +3008,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(trf('앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).', [c.settings.androidTrashDays]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
if (canRecycle)
CheckboxListTile(
@ -3126,7 +3126,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
],
for (final (x, why) in failed.take(30)) ...[
Text(vBasename(x), style: const TextStyle(fontWeight: FontWeight.w600)),
Text('${vDirname(x)} · $why', style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text('${vDirname(x)} · $why', style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 4),
],
if (failed.length > 30) Text(trf('… 외 {0}개', [failed.length - 30])),
@ -3485,7 +3485,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
child: Text(
trf('환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)',
[splitOptions(task.options).where(isSourceRemovingOption).join(' ')]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
FutureBuilder<List<NameClash>>(
future: clashesFuture,
@ -3493,7 +3493,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
if (snap.connectionState != ConnectionState.done) {
return Padding(
padding: const EdgeInsets.only(top: 12),
child: Text(tr('같은 이름이 있는지 확인하는 중 …'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
child: Text(tr('같은 이름이 있는지 확인하는 중 …'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
);
}
final clashes = snap.data ?? const <NameClash>[];
@ -3733,18 +3733,18 @@ class _ExplorerPageState extends State<ExplorerPage> {
Text(shortPath(t.sources.first), style: const TextStyle(fontWeight: FontWeight.w600)),
Text('→ ${shortPath(t.dest)}', style: const TextStyle(fontWeight: FontWeight.w600)),
Text('${vDisplay(t.sources.first)}/ → ${vDisplay(t.dest)}/',
style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
style: TextStyle(fontSize: 11, color: JjColors.textDim)),
// 111: 이 쌍은 처음 실행 때 기억한 옵션을 쓴다 (환경 설정의 rsync 옵션을 바꿔도 그대로)
if (center.tasks.any((x) => x.id == t.id))
Text(tr('이 폴더 쌍은 기억한 옵션을 씁니다 (환경 설정이 아니라 모니터링 > 복사 · rsync 에서 고칩니다)'),
style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
style: TextStyle(fontSize: 11, color: JjColors.textDim)),
Text('rsync ${runOpts(tasks.indexOf(t))}${removeSource ? ' --remove-source-files' : ''}'
'${t.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [t.bandwidthKBps])}' : ''}',
style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
style: TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
const SizedBox(height: 10),
],
Text(tr('폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
// 222: 이동이면 동작대로 (예전에는 이동일 때도 "복사 · 원본 폴더는 그대로" 라고 썼다)
if (nested)
Text(
@ -3755,7 +3755,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
// 1280×720 등 낮은 화면에서도 보이게: 원본 지우기 체크 · 경고는 비교 목록보다 위에 (178)
if (both) ...[
Text(keepOptionsTogether(tr('양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).')),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
if (optionMove)
Text(tr('양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
@ -3781,7 +3781,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
Text(tr('끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 6),
PruneChoice(value: prune, onChanged: (v) => set(() => prune = v)),
],
@ -3840,7 +3840,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
const SizedBox(height: 4),
if (_viaDav([l, r]))
Text(tr('WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files · 거르기 (--exclude · --include · --filter · --delete-excluded) 를 따름).'),
style: const TextStyle(fontSize: 12, color: JjColors.accent)),
style: TextStyle(fontSize: 12, color: JjColors.accent)),
]),
),
actions: [
@ -4027,7 +4027,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
const SizedBox(width: 10),
SizedBox(
width: 150,
child: Text(right, textAlign: TextAlign.end, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
child: Text(right, textAlign: TextAlign.end, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
]),
);
@ -4040,7 +4040,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
Expanded(
// Rsync 화면: 원본 → 대상 (⇄ 는 두 줄)
child: Text(widget.rsync ? '${job.sources.join(', ')}/ → ${job.dest}/' : job.dest,
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
if (!job.finished) TextButton(onPressed: job.cancel, child: Text(tr('취소'))),
]),
@ -4240,11 +4240,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(children: [
const Icon(Icons.manage_search, size: 18, color: JjColors.accent),
Icon(Icons.manage_search, size: 18, color: JjColors.accent),
const SizedBox(width: 6),
Expanded(
child: Text(trf('찾기 결과 ({0}) · {1}', [back.cond.summary, back.root]),
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
TextButton(onPressed: _backToSearch, child: Text(tr('찾기 결과로 돌아가기'))),
IconButton(
@ -4273,7 +4273,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
Text(tr('이 앱의 파일 탐색기 목록에서만 빠집니다. 파일은 지우거나 옮기거나 이름을 바꾸지 않고, 다른 앱 · 갤러리에서는 그대로 보입니다.')),
const SizedBox(height: 6),
Text(tr('목록 맨 아래의 [숨긴 항목 보이기] 나 환경 설정 > 파일 탐색기에서 다시 볼 수 있습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
controlAffinity: ListTileControlAffinity.leading,
@ -4310,7 +4310,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
: tr('이 폴더의 .nomedia 파일을 지웁니다. 갤러리 · 음악 앱이 이 폴더의 그림 · 동영상 · 음악을 다시 보이게 됩니다.')),
const SizedBox(height: 6),
Text(tr('폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 잠깐 걸릴 수 있습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
@ -4359,7 +4359,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
]),
SwitchListTile(value: desc, title: Text(tr('거꾸로')), onChanged: (x) => set(() => desc = x)),
if (c.settings.explorerSortPerFolder)
Text(trf('이 폴더에만: {0}', [_displayPath(dir)]), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(trf('이 폴더에만: {0}', [_displayPath(dir)]), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
@ -4468,7 +4468,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
Padding(
padding: const EdgeInsets.only(top: 6, left: 4),
child: Text(tr('왼쪽에 폴더, 오른쪽에 왼쪽에서 고른 폴더의 파일들을 보여 줍니다 (Windows 탐색기처럼).'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
if (layout != 'single')
group(tr('두 창 배치'), orient, [
@ -4485,7 +4485,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
Padding(
padding: const EdgeInsets.only(top: 8, left: 4),
child: Text(tr('숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
]),
),
@ -4523,7 +4523,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
height: 460,
child: Column(children: [
Text(tr('보일 버튼을 고르고, 끌어서 순서를 바꿉니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 8),
Expanded(
child: ReorderableListView(
@ -4710,7 +4710,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
),
],
child: Text(_displayPath(_pane.current),
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: JjColors.textDim)),
),
),
PopupMenuButton<Object>(
@ -5082,8 +5082,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Text(tr('고르기 도구'), style: const TextStyle(fontSize: 13, color: JjColors.accent)),
const Icon(Icons.arrow_drop_down, size: 18, color: JjColors.accent),
Text(tr('고르기 도구'), style: TextStyle(fontSize: 13, color: JjColors.accent)),
Icon(Icons.arrow_drop_down, size: 18, color: JjColors.accent),
]),
),
),
@ -5188,7 +5188,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
height: 40,
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(_displayPath(dir), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
child: Text(_displayPath(dir), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: JjColors.textDim)),
),
const Divider(height: 1),
if (pane.filterOpen) _filterBar(pane, shown.length),
@ -5207,7 +5207,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
child: pane.loading.contains(dir) && !pane.cache.containsKey(dir)
? const Center(child: CircularProgressIndicator())
: entries.isEmpty && !up
? Center(child: Text(tr('비어 있음'), style: const TextStyle(color: JjColors.textDim)))
? Center(child: Text(tr('비어 있음'), style: TextStyle(color: JjColors.textDim)))
: scaleListText(context, _listTextScale, ListView.builder(
controller: pane.listScroll,
itemCount: rows.length,
@ -5898,7 +5898,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
feedback: dragFeedback(_dragPaths(pane, e)),
child: Tooltip(
message: tr('끌어서 다른 창으로'),
child: const SizedBox(width: 28, height: 36, child: Icon(Icons.drag_indicator, size: 18, color: JjColors.textDim)),
child: SizedBox(width: 28, height: 36, child: Icon(Icons.drag_indicator, size: 18, color: JjColors.textDim)),
),
),
// 표시 동그라미는 두 번 누르기 영역 밖 (기다리지 않고 바로)
@ -6260,7 +6260,7 @@ class _MetaText extends StatelessWidget {
builder: (_, s) => Text.rich(
TextSpan(children: [
if (lead.isNotEmpty) TextSpan(text: lead, style: leadStyle),
if ((s.data ?? '').isNotEmpty) TextSpan(text: '${lead.isEmpty ? '' : ' '}${s.data}', style: const TextStyle(fontSize: 11, color: JjColors.accent)),
if ((s.data ?? '').isNotEmpty) TextSpan(text: '${lead.isEmpty ? '' : ' '}${s.data}', style: TextStyle(fontSize: 11, color: JjColors.accent)),
]),
maxLines: 1,
overflow: TextOverflow.ellipsis),
@ -6319,7 +6319,7 @@ class TransferProgressChip extends StatelessWidget {
child: Row(mainAxisSize: MainAxisSize.min, children: [
// 멈춤은 진행 고리 대신 경고 표시 (꽉 찬 고리는 "끝났다" 로 보였다)
if (failed)
const Icon(Icons.error_outline, size: 16, color: JjColors.danger)
Icon(Icons.error_outline, size: 16, color: JjColors.danger)
else
SizedBox(
width: 14,
@ -6387,7 +6387,7 @@ class _Thumb extends StatelessWidget {
future: _Thumbs.of(c, entry),
builder: (_, s) {
final f = s.data;
if (f == null) return const Icon(Icons.movie_outlined, size: 28, color: JjColors.textDim);
if (f == null) return Icon(Icons.movie_outlined, size: 28, color: JjColors.textDim);
return ClipRRect(
borderRadius: BorderRadius.circular(3),
child: Stack(alignment: Alignment.center, children: [

View File

@ -224,7 +224,7 @@ class _ConditionsDialogState extends State<_ConditionsDialog> {
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(tr('크기 · 바뀐 날 · 종류를 넣으면 파일만 찾습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
]),
),
@ -398,7 +398,7 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
child: Align(
alignment: Alignment.centerLeft,
child: Text('${widget.cond.summary} · ${widget.root}',
maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
),
Expanded(
@ -473,7 +473,7 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
for (final (dir, why) in _skipped.take(50))
Align(
alignment: Alignment.centerLeft,
child: Text('$dir · $why', style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
child: Text('$dir · $why', style: TextStyle(fontSize: 11, color: JjColors.textDim)),
),
],
),
@ -483,7 +483,7 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
SafeArea(
top: false,
child: Container(
decoration: const BoxDecoration(border: Border(top: BorderSide(color: JjColors.border))),
decoration: BoxDecoration(border: Border(top: BorderSide(color: JjColors.border))),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Wrap(spacing: 4, runSpacing: 2, crossAxisAlignment: WrapCrossAlignment.center, children: [
Text(_summary(_targets), style: const TextStyle(fontSize: 12)),
@ -499,8 +499,8 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
),
TextButton.icon(
onPressed: () => _act('delete'),
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.danger),
label: Text(tr('지우기'), style: const TextStyle(color: JjColors.danger)),
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.danger),
label: Text(tr('지우기'), style: TextStyle(color: JjColors.danger)),
),
]),
),

View File

@ -81,7 +81,7 @@ Future<void> showFileHash(BuildContext context, String path) async {
content: SizedBox(
width: 560,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
if (error != null) Text(trf('읽지 못했습니다: {0}', [error]), style: const TextStyle(color: JjColors.danger)),
if (error != null) Text(trf('읽지 못했습니다: {0}', [error]), style: TextStyle(color: JjColors.danger)),
if (result == null && error == null) LinearProgressIndicator(value: progress),
if (result != null) ...[
line('MD5', result!.md5),
@ -196,7 +196,7 @@ class _HexViewerPageState extends State<HexViewerPage> {
child: Row(children: [
Expanded(
child: Text(trf('{0} / {1} 읽음', [formatSize(_data.length), formatSize(_size)]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
FilledButton.tonal(onPressed: _more, child: Text(tr('더 읽기'))),
]),
@ -246,7 +246,7 @@ Future<void> showFolderProperties(BuildContext context, String dir) async {
builder: (_, s) {
final r = s.data;
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
SelectableText(dir, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
SelectableText(dir, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 8),
if (r == null) const LinearProgressIndicator(),
if (r != null) ...[

View File

@ -46,12 +46,12 @@ class FtpSettings extends StatelessWidget {
].join(' · '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: s.tls ? null : const TextStyle(color: JjColors.danger)),
style: s.tls ? null : TextStyle(color: JjColors.danger)),
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
IconButton(tooltip: tr('고치기'), icon: const Icon(Icons.edit_outlined, size: 18), onPressed: () => editFtpServer(context, c, old: s)),
IconButton(
tooltip: tr('지우기'),
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
onPressed: () => deleteFtpServer(context, c, s),
),
]),
@ -92,7 +92,7 @@ Future<bool> askTrustFtpCert(BuildContext context, String serverLabel, FtpCertEx
context: context,
builder: (ctx) => AlertDialog(
scrollable: true,
icon: const Icon(Icons.gpp_bad_outlined, color: JjColors.danger),
icon: Icon(Icons.gpp_bad_outlined, color: JjColors.danger),
title: Text(trf('"{0}" 서버의 인증서가 바뀌었습니다', [serverLabel])),
content: SelectableText(
'${tr('중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 서버 인증서를 바꾼 것이 확실할 때만, 서버 설정의 [믿은 인증서 지우기] 뒤 다시 연결하세요.')}\n\n'
@ -223,7 +223,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
),
]),
const SizedBox(height: 8),
Text(tr('암호화'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(tr('암호화'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
RadioGroup<String>(
groupValue: security,
onChanged: (x) => set(() {
@ -238,7 +238,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
contentPadding: EdgeInsets.zero,
dense: true,
value: v,
title: Text(ftpSecurityLabel(v), style: v == 'none' ? const TextStyle(color: JjColors.danger) : null),
title: Text(ftpSecurityLabel(v), style: v == 'none' ? TextStyle(color: JjColors.danger) : null),
),
]),
),
@ -248,7 +248,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
color: JjColors.danger.withValues(alpha: 0.12),
child: Text(
tr('⚠ 평문 FTP 는 아이디 · 비밀번호 · 파일이 암호화되지 않은 채 오갑니다. 같은 망의 다른 사람이 볼 수 있습니다. 서버가 FTPS 를 못 할 때만, 믿을 수 있는 집 안 망에서만 쓰세요.'),
style: const TextStyle(fontSize: 12, color: JjColors.danger)),
style: TextStyle(fontSize: 12, color: JjColors.danger)),
),
TextField(controller: user, decoration: InputDecoration(labelText: tr('아이디 (비우면 익명)'))),
TextField(
@ -273,7 +273,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
fingerprint.isEmpty
? tr('인증서: 공인 인증서면 그대로 연결, 자체 서명이면 [연결 확인] 때 지문을 보이고 묻습니다')
: '${tr('믿은 인증서')}: $fingerprint',
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
if (fingerprint.isNotEmpty)
Align(
alignment: Alignment.centerLeft,

View File

@ -28,7 +28,7 @@ Future<void> showHiddenItems(BuildContext context, AppController c) => showDialo
width: 520,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(tr('풀면 파일 탐색기 목록에 다시 보입니다. 파일 자체는 바꾸지 않습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 8),
if (list.isEmpty) Text(tr('숨긴 항목이 없습니다')),
for (final x in list)

View File

@ -155,7 +155,7 @@ class _TopBar extends StatelessWidget {
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(trf('지금: {0}', [targets.map((v) => v.fileName).join(', ')])),
const SizedBox(height: 4),
Text(tr('하나씩 멈추려면 목록 줄의 ■ 를 누르세요.'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(tr('하나씩 멈추려면 목록 줄의 ■ 를 누르세요.'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('그대로 두기'))),
@ -185,7 +185,7 @@ class _TopBar extends StatelessWidget {
Text(trf('지금: {0}', [now.isEmpty ? (c.currentJob ?? '') : now])),
const SizedBox(height: 4),
Text(trf('남은 것: MKV 만들기 {0}개{1}', [inBatch, c.pendingJobs.isEmpty ? '' : ' · ${c.pendingJobs.join(' · ')}']),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('그대로 두기'))),
@ -218,7 +218,7 @@ class _TopBar extends StatelessWidget {
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(trf('지금: {0}', [c.currentJob ?? ''])),
const SizedBox(height: 4),
Text(trf('대기 중: {0}', [c.pendingJobs.join(' · ')]), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(trf('대기 중: {0}', [c.pendingJobs.join(' · ')]), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
actions: [
TextButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('닫기'))),
@ -395,7 +395,7 @@ class _TopBar extends StatelessWidget {
// 138: AI 버튼은 [MKV 만들기] 와 떨어뜨려 맨 뒤에, 구분선 너머로
if (hasAi) ...[
const SizedBox(width: 8),
const SizedBox(height: 28, child: VerticalDivider(width: 16, color: JjColors.border)),
SizedBox(height: 28, child: VerticalDivider(width: 16, color: JjColors.border)),
...aiButtons(true),
],
]),
@ -568,7 +568,7 @@ class _EncodeBar extends StatelessWidget {
Widget label(String t) => Padding(
padding: const EdgeInsets.only(right: 6),
child: Text(t, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
child: Text(t, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
);
// 253: 글자가 크면 고르기 상자가 화면보다 넓어져 넘쳤다 - 제 폭 (가장 긴 항목) 을 쓰되 화면 폭을 넘지 않게 (글은 …)
final maxDrop = (MediaQuery.sizeOf(context).width - 40).clamp(120.0, double.infinity);
@ -582,7 +582,7 @@ class _EncodeBar extends StatelessWidget {
isDense: true,
isExpanded: true,
underline: const SizedBox(),
style: const TextStyle(fontSize: 13, color: JjColors.text),
style: TextStyle(fontSize: 13, color: JjColors.text),
items: [
for (final i in items)
DropdownMenuItem(
@ -741,7 +741,7 @@ class _VideoList extends StatelessWidget {
? trf('동영상 {0}개 · 전체 선택', [c.videos.length])
: trf('선택 {0} / {1}개', [c.checked.length, c.videos.length]),
maxLines: 1,
style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
style: TextStyle(color: JjColors.textDim, fontSize: 12)),
),
),
),
@ -753,14 +753,14 @@ class _VideoList extends StatelessWidget {
tooltip: tr('선택한 동영상을 목록에서 제거'),
iconSize: 18,
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.playlist_remove, color: JjColors.textDim),
icon: Icon(Icons.playlist_remove, color: JjColors.textDim),
onPressed: c.removeChecked,
),
IconButton(
tooltip: tr('모두 지우기'),
iconSize: 18,
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.clear_all, color: JjColors.textDim),
icon: Icon(Icons.clear_all, color: JjColors.textDim),
// 46: 확인 뒤 (목록에서만 빼고 파일은 그대로)
onPressed: c.busy || c.videos.isEmpty
? null
@ -917,10 +917,10 @@ class _VideoTile extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(top: 3),
child: Text(v.phase!,
style: const TextStyle(fontSize: 11, color: JjColors.accent)),
style: TextStyle(fontSize: 11, color: JjColors.accent)),
),
] else if (v.phase != null)
Text(v.phase!, style: const TextStyle(fontSize: 11, color: JjColors.textDim))
Text(v.phase!, style: TextStyle(fontSize: 11, color: JjColors.textDim))
else
Text(
v.status == JobStatus.failed
@ -943,7 +943,7 @@ class _VideoTile extends StatelessWidget {
tooltip: c.inCurrentBatch(v) ? tr('다시 만들기 대기로 (지금 묶음이 끝난 뒤)') : tr('다시 만들기 대기로'),
iconSize: 16,
onPressed: c.inCurrentBatch(v) ? null : () => c.resetStatus(v),
icon: const Icon(Icons.refresh, color: JjColors.textDim),
icon: Icon(Icons.refresh, color: JjColors.textDim),
),
// 240: 만드는 중인 동영상 하나만 취소 (다른 것 · 남은 것은 그대로)
if (v.status == JobStatus.running && c.buildingMkv)
@ -951,13 +951,13 @@ class _VideoTile extends StatelessWidget {
tooltip: tr('이 동영상만 취소'),
iconSize: 16,
onPressed: () => c.cancelVideos([v]),
icon: const Icon(Icons.stop, color: JjColors.danger),
icon: Icon(Icons.stop, color: JjColors.danger),
),
IconButton(
tooltip: tr('목록에서 제거'),
iconSize: 16,
onPressed: v.status == JobStatus.running ? null : () => c.removeVideo(v),
icon: const Icon(Icons.close, color: JjColors.textDim),
icon: Icon(Icons.close, color: JjColors.textDim),
),
],
),
@ -973,16 +973,16 @@ class _StatusIcon extends StatelessWidget {
@override
Widget build(BuildContext context) => switch (s) {
JobStatus.ready =>
const Icon(Icons.movie_outlined, size: 20, color: JjColors.textDim),
Icon(Icons.movie_outlined, size: 20, color: JjColors.textDim),
JobStatus.running =>
const Icon(Icons.autorenew, size: 20, color: JjColors.accent),
Icon(Icons.autorenew, size: 20, color: JjColors.accent),
JobStatus.done =>
const Icon(Icons.check_circle, size: 20, color: JjColors.success),
Icon(Icons.check_circle, size: 20, color: JjColors.success),
JobStatus.failed =>
const Icon(Icons.error, size: 20, color: JjColors.danger),
Icon(Icons.error, size: 20, color: JjColors.danger),
// 187: 취소는 실패가 아니라 회색
JobStatus.cancelled =>
const Icon(Icons.cancel_outlined, size: 20, color: JjColors.textDim),
Icon(Icons.cancel_outlined, size: 20, color: JjColors.textDim),
};
}
@ -1032,12 +1032,12 @@ class _PickHint extends StatelessWidget {
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.touch_app_outlined, size: 40, color: JjColors.textDim),
Icon(Icons.touch_app_outlined, size: 40, color: JjColors.textDim),
const SizedBox(height: 8),
Text(tr('고른 동영상이 없습니다'), textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim)),
Text(tr('고른 동영상이 없습니다'), textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim)),
const SizedBox(height: 4),
Text(tr('목록에서 동영상을 누르면 여기에 자세히 보입니다'),
textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim, fontSize: 12)),
]),
),
);
@ -1056,10 +1056,10 @@ class _EmptyHint extends StatelessWidget {
children: [
Icon(Icons.video_library_outlined, size: 56, color: JjColors.textDim),
const SizedBox(height: 12),
Text(tr('"동영상 추가" 로 파일을 선택하세요'), textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim)),
Text(tr('"동영상 추가" 로 파일을 선택하세요'), textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim)),
const SizedBox(height: 4),
Text(tr('같은 폴더의 자막은 자동으로 추가됩니다'),
textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim, fontSize: 12)),
if (onAdd != null) ...[
const SizedBox(height: 16),
FilledButton.icon(onPressed: onAdd, icon: const Icon(Icons.add), label: Text(tr('동영상 추가'))),
@ -1142,7 +1142,7 @@ class _VideoDetail extends StatelessWidget {
? null
: Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 8, children: [
Text(trf('감지한 언어: {0} (자동 감지)', [languageOf(v.aiDetected).name]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
TextButton(
onPressed: c.busy
? null
@ -1207,7 +1207,7 @@ class _VideoDetail extends StatelessWidget {
]),
const SizedBox(height: 4),
SelectableText(v.path,
style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
style: TextStyle(color: JjColors.textDim, fontSize: 12)),
const SizedBox(height: 12),
Wrap(spacing: 8, runSpacing: 8, children: [
if (info?.duration != null) _Chip(trf('길이 {0}', [_fmt(info!.duration!)])),
@ -1237,7 +1237,7 @@ class _VideoDetail extends StatelessWidget {
borderRadius: BorderRadius.circular(6),
),
child: SelectableText(v.message!,
style: const TextStyle(color: JjColors.danger, fontSize: 12)),
style: TextStyle(color: JjColors.danger, fontSize: 12)),
),
],
if (!compact) ...[
@ -1254,7 +1254,7 @@ class _VideoDetail extends StatelessWidget {
_SubtitleRow(c: c, v: v, s: s, locked: locked),
const SizedBox(height: 24),
Text(trf('출력: {0}', [outputMkvPath(v.path)]),
style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
style: TextStyle(color: JjColors.textDim, fontSize: 12)),
],
);
}
@ -1345,14 +1345,14 @@ class _SubtitleRow extends StatelessWidget {
onPressed: locked || off || !c.canEdit(s)
? null
: () => openSubtitleEditor(context, c, v, s),
icon: const Icon(Icons.edit_note, color: JjColors.accent),
icon: Icon(Icons.edit_note, color: JjColors.accent),
),
if (c.services.createTranslator != null)
IconButton(
tooltip: c.canEdit(s) ? trf('다른 언어로 번역 (AI · 이 {0}에서)', [Platform.isAndroid ? tr('기기') : 'PC']) : tr('이미지 자막은 번역할 수 없습니다'),
iconSize: 18,
onPressed: locked || off || !c.canEdit(s) ? null : () => showTranslateDialog(context, c, v, s),
icon: const Icon(Icons.translate, color: JjColors.accent),
icon: Icon(Icons.translate, color: JjColors.accent),
),
IconButton(
tooltip: embedded ? (off ? tr('되살리기') : tr('삭제 (출력에서 제외)')) : tr('목록에서 제거'),
@ -1381,7 +1381,7 @@ class _LanguageDropdown extends StatelessWidget {
value: items.contains(value) ? value : undetermined,
isDense: true,
underline: const SizedBox(),
style: const TextStyle(fontSize: 12, color: JjColors.text),
style: TextStyle(fontSize: 12, color: JjColors.text),
items: [
for (final l in items)
DropdownMenuItem(
@ -1404,7 +1404,7 @@ class _CharsetDropdown extends StatelessWidget {
value: supportedCharsets.containsKey(value) ? value : 'UTF-8',
isDense: true,
underline: const SizedBox(),
style: const TextStyle(fontSize: 12, color: JjColors.text),
style: TextStyle(fontSize: 12, color: JjColors.text),
items: [
for (final e in supportedCharsets.entries)
DropdownMenuItem(value: e.key, child: Text(e.value)),
@ -1433,7 +1433,7 @@ class _LogPanel extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 44, 0),
child: Text(tr('작업 기록이 아직 없습니다. MKV 만들기 · 자막 · 다운로드를 하면 여기에 차례로 남습니다.'),
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
),
Positioned.fill(
@ -1443,7 +1443,7 @@ class _LogPanel extends StatelessWidget {
itemCount: logs.length,
itemBuilder: (_, i) => SelectableText(
logs[logs.length - 1 - i],
style: const TextStyle(
style: TextStyle(
fontSize: 12, fontFamily: 'Consolas', color: JjColors.textDim),
),
),
@ -1456,7 +1456,7 @@ class _LogPanel extends StatelessWidget {
tooltip: tr('작업 기록 숨기기'),
iconSize: 16,
visualDensity: VisualDensity.compact,
icon: const Icon(Icons.close, color: JjColors.textDim),
icon: Icon(Icons.close, color: JjColors.textDim),
onPressed: onClose,
),
),
@ -1483,10 +1483,10 @@ class _LogStrip extends StatelessWidget {
child: Text(last,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 11, fontFamily: 'Consolas', color: JjColors.textDim)),
style: TextStyle(fontSize: 11, fontFamily: 'Consolas', color: JjColors.textDim)),
),
Text(tr('작업 기록 보기'), style: TextStyle(fontSize: 11, color: JjColors.accent)),
const Icon(Icons.expand_less, size: 16, color: JjColors.accent),
Icon(Icons.expand_less, size: 16, color: JjColors.accent),
const SizedBox(width: 12),
]),
),

View File

@ -19,7 +19,7 @@ class LanguageSettings extends StatelessWidget {
final added = i18n.available.where((c) => !I18nController.builtIn.contains(c)).toList();
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
SettingTile(
leading: const Icon(Icons.translate, color: JjColors.accent),
leading: Icon(Icons.translate, color: JjColors.accent),
// 읽을 수 없는 언어를 골라도 찾을 수 있게 영어를 함께
title: Text(uiLanguage == 'en' ? 'Language' : '${tr('화면 언어')} · Language'),
subtitle: Text(tr('프로그램의 모든 글자를 고른 언어로 보여 줍니다. 목록에 없는 언어는 [언어 추가] 로 '
@ -58,7 +58,7 @@ class LanguageSettings extends StatelessWidget {
),
if (!i18n.canTranslate)
Text(tr('(AI 번역을 쓸 수 없는 기기라 언어를 더할 수 없습니다)'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
),
for (final code in added)
@ -118,7 +118,7 @@ Future<void> addLanguageDialog(BuildContext context) async {
LinearProgressIndicator(value: s.$2),
const SizedBox(height: 8),
Text(tr('화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const NllbLicenseNote(),
]),
),

View File

@ -104,7 +104,7 @@ class NllbLicenseNote extends StatelessWidget {
@override
Widget build(BuildContext context) => Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [
Text(tr('번역 모델 라이선스: CC BY-NC 4.0 (비상업적 이용만 허용)'),
style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
style: TextStyle(fontSize: 11, color: JjColors.textDim)),
TextButton(
style: TextButton.styleFrom(visualDensity: VisualDensity.compact, padding: const EdgeInsets.symmetric(horizontal: 6)),
onPressed: () => showLicenseDocument(context, 'CC BY-NC 4.0', nllbLicenseAsset),

View File

@ -262,7 +262,7 @@ Future<void> _done(BuildContext context, AppController c, MasterLock lock, List<
final setNew = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
icon: const Icon(Icons.check_circle_outline, color: JjColors.accent),
icon: Icon(Icons.check_circle_outline, color: JjColors.accent),
title: Text(tr('초기화했습니다')),
content: Text(done.map((x) => '• $x').join('\n')),
actions: [
@ -314,7 +314,7 @@ Future<bool> setPasswordDialog(BuildContext context, AppController c, MasterLock
'저장된 비밀번호를 지우고 초기화할 수 있습니다.')),
const SizedBox(height: 8),
Text(tr('이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
TextField(
controller: a,
obscureText: true,

View File

@ -130,17 +130,17 @@ Future<bool?> _liveDeleteDialog(BuildContext context, LiveSync live, LiveSyncPai
child: ListView(children: [
if (list.isEmpty)
Text(tr('미리 본 지울 것은 없습니다 - 위 규칙 때문에 실제로는 지워질 수 있습니다'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
for (final n in plain.take(500)) Text(n, style: const TextStyle(fontSize: 12, fontFamily: 'Consolas')),
if (plain.length > 500) Text(trf('… 외 {0}개', [plain.length - 500])),
if (filtered.isNotEmpty) ...[
if (plain.isNotEmpty) const SizedBox(height: 10),
Text(keepOptionsTogether(trf('거른 것 {0}개도 지워집니다 (--delete-excluded)', [filtered.length])),
style: const TextStyle(fontWeight: FontWeight.w600, color: JjColors.danger)),
style: TextStyle(fontWeight: FontWeight.w600, color: JjColors.danger)),
Text(keepOptionsTogether(tr('rsync 옵션에서 뺀 것 (--exclude 등) 이지만 --delete-excluded 가 있어 대상에서 지웁니다. 지키려면 옵션에서 --delete-excluded 를 빼세요.')),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 4),
for (final n in filtered.take(500)) Text(n, style: const TextStyle(fontSize: 12, fontFamily: 'Consolas', color: JjColors.danger)),
for (final n in filtered.take(500)) Text(n, style: TextStyle(fontSize: 12, fontFamily: 'Consolas', color: JjColors.danger)),
if (filtered.length > 500) Text(trf('… 외 {0}개', [filtered.length - 500])),
],
]),
@ -219,11 +219,11 @@ class _DiskInfo extends StatelessWidget {
future: diskSpace(dir),
builder: (_, s) {
final d = s.data;
if (d == null) return Text(tr('대상 디스크: 알 수 없음'), style: const TextStyle(fontSize: 12, color: JjColors.textDim));
if (d == null) return Text(tr('대상 디스크: 알 수 없음'), style: TextStyle(fontSize: 12, color: JjColors.textDim));
final (free, total) = d;
final used = total == 0 ? 0.0 : (total - free) / total;
return Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.storage, size: 14, color: JjColors.textDim),
Icon(Icons.storage, size: 14, color: JjColors.textDim),
const SizedBox(width: 4),
// 241 시험에서 찾음: 좁은 화면 (폭 500) 에서 넘쳤다 - 글을 줄인다
Flexible(
@ -273,7 +273,7 @@ class TransferProgressLines extends StatelessWidget {
),
SizedBox(
width: 130,
child: Text(right, textAlign: TextAlign.end, style: const TextStyle(fontSize: 12, color: JjColors.textDim))),
child: Text(right, textAlign: TextAlign.end, style: TextStyle(fontSize: 12, color: JjColors.textDim))),
]),
);
final speed = !job.finished && job.speed != null ? '${formatSize(job.speed!.round())}/s' : '';
@ -319,7 +319,7 @@ class _CopyTab extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(tr('실행한 rsync 가 없습니다. Rsync 화면에서 → · ← · ⇄ 로 실행하면 여기에 자동으로 등록됩니다.'),
textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim)),
textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim)),
),
);
}
@ -593,7 +593,7 @@ class _CopyCardState extends State<_CopyCard> {
),
Text('→ ${shortPath(t.dest)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w600)),
Text('${vDisplay(t.sources.first)}${t.contents ? '/' : ''} → ${vDisplay(t.dest)}',
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11, color: JjColors.textDim)),
]),
),
Chip(
@ -710,7 +710,7 @@ class _CopyCardState extends State<_CopyCard> {
),
IconButton(
tooltip: tr('목록에서 지우기 (파일은 그대로)'),
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
onPressed: () => center.remove(t.id),
),
]),
@ -774,7 +774,7 @@ class _LiveTabState extends State<_LiveTab> {
SelectableText(
'rsync -avHPOg --remove-source-files ${x.source}/ ${x.target}/\n'
'find ${x.source}/ -type d -empty -delete',
style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontFamily: 'monospace', fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 14),
Text(tr('원본 폴더'), style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 6),
@ -830,7 +830,7 @@ class _LiveTabState extends State<_LiveTab> {
Expanded(
child: Text(
tr('원본이 바뀌면 대상에 맞춥니다. 일정을 정하면 그 시간에만 맞추고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
style: TextStyle(fontSize: 12, color: JjColors.textDim),
),
),
TextButton.icon(onPressed: () => addLiveSyncPairDialog(context, c), icon: const Icon(Icons.add), label: Text(tr('추가'))),
@ -852,7 +852,7 @@ class _LiveTabState extends State<_LiveTab> {
Padding(
padding: const EdgeInsets.all(24),
child: Center(child: Text(tr('실시간 동기화가 없습니다. [추가] 하거나 복사 · rsync 탭에서 [lsync 로 이동] 하세요.'),
style: const TextStyle(color: JjColors.textDim))),
style: TextStyle(color: JjColors.textDim))),
),
for (final x in pairs) _liveCard(context, live, x),
]);
@ -895,11 +895,11 @@ class _LiveTabState extends State<_LiveTab> {
),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
const Icon(Icons.lock_clock_outlined, color: JjColors.accent, size: 20),
Icon(Icons.lock_clock_outlined, color: JjColors.accent, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(tr('마스터 비밀번호가 필요해 기다리는 중'),
style: const TextStyle(fontWeight: FontWeight.w600, color: JjColors.accent)),
style: TextStyle(fontWeight: FontWeight.w600, color: JjColors.accent)),
),
]),
const SizedBox(height: 4),
@ -948,7 +948,7 @@ class _LiveTabState extends State<_LiveTab> {
),
const SizedBox(height: 4),
Text('${vDisplay(x.source)}${e.detail.isEmpty ? '' : ' · ${e.detail}'}',
style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
style: TextStyle(fontSize: 11, color: JjColors.textDim)),
const SizedBox(height: 6),
Wrap(spacing: 8, runSpacing: 4, children: [
FilledButton.tonalIcon(
@ -1088,8 +1088,8 @@ class _LiveTabState extends State<_LiveTab> {
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
Text(scheduleSummary(x.schedule), style: TextStyle(fontSize: 12, color: active ? JjColors.accent : Colors.orangeAccent)),
if (x.schedule.isNotEmpty)
Text(x.schedule.join(' | '), style: const TextStyle(fontSize: 11, fontFamily: 'monospace', color: JjColors.textDim)),
Text(tr('누르면 일정 바꾸기'), style: const TextStyle(fontSize: 10, 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)),
]),
),
]),
@ -1109,7 +1109,7 @@ class _LiveTabState extends State<_LiveTab> {
Padding(
padding: const EdgeInsets.only(left: 32, top: 8),
child: pend == null
? Text(tr('맞출 것을 세는 중…'), style: const TextStyle(fontSize: 12, color: JjColors.textDim))
? Text(tr('맞출 것을 세는 중…'), style: TextStyle(fontSize: 12, color: JjColors.textDim))
: pend.isEmpty
// 244: 대상에만 있는 것이 있으면 "같습니다" 가 아니라 사실대로
? Text(
@ -1173,7 +1173,7 @@ class _LiveTabState extends State<_LiveTab> {
),
IconButton(
tooltip: tr('지우기'),
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
onPressed: () => removeLiveSyncPair(context, c, x),
),
]),
@ -1198,7 +1198,7 @@ Future<void> askLiveSyncStart(BuildContext context, LiveSync live) async {
width: 520,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(tr('시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 8),
Flexible(
child: ListView(shrinkWrap: true, children: [

View File

@ -228,7 +228,7 @@ class _DnsSettingsState extends State<DnsSettings> {
label: Text(tr('지금 확인')),
),
const SizedBox(width: 12),
Expanded(child: Text(_testing ? tr('확인하는 중…') : _test ?? '', style: const TextStyle(fontSize: 12, color: JjColors.textDim))),
Expanded(child: Text(_testing ? tr('확인하는 중…') : _test ?? '', style: TextStyle(fontSize: 12, color: JjColors.textDim))),
]),
),
SettingTile(
@ -247,7 +247,7 @@ class _DnsSettingsState extends State<DnsSettings> {
if (desk && s.browserEngine == 'chrome' && CefRuntime.readyThisRun)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
child: Text(tr('Chrome 엔진은 앱을 다시 시작하면 바뀐 설정을 씁니다.'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
child: Text(tr('Chrome 엔진은 앱을 다시 시작하면 바뀐 설정을 씁니다.'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
]);
}

View File

@ -379,7 +379,7 @@ class _PlayerPageState extends State<PlayerPage> {
Widget item(String label, VoidCallback? onTap, {String? key, bool checked = false}) => MenuItemButton(
onPressed: onTap,
leadingIcon: SizedBox(width: 18, child: checked ? const Icon(Icons.check, size: 16) : null),
trailingIcon: key == null ? null : Text(key, style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
trailingIcon: key == null ? null : Text(key, style: TextStyle(fontSize: 11, color: JjColors.textDim)),
child: Text(label),
);
return [

View File

@ -124,7 +124,7 @@ class _QuickLookState extends State<QuickLook> {
Widget _info(List<String> lines) => Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(_e.isDir ? Icons.folder : Icons.insert_drive_file_outlined, size: 56, color: JjColors.textDim),
for (final l in lines) Text(l, textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim)),
for (final l in lines) Text(l, textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim)),
]),
);
@ -136,7 +136,7 @@ class _QuickLookState extends State<QuickLook> {
builder: (_, s) => s.connectionState != ConnectionState.done
? const Center(child: CircularProgressIndicator())
: s.data == null
? const Center(child: Icon(Icons.movie_outlined, size: 56, color: JjColors.textDim))
? Center(child: Icon(Icons.movie_outlined, size: 56, color: JjColors.textDim))
: Image.file(s.data!, fit: BoxFit.contain),
),
),
@ -145,9 +145,9 @@ class _QuickLookState extends State<QuickLook> {
// 큰 파일 · 느린 디스크에서도 창이 멈추지 않게 (기다리는 동안 "읽는 중", 20초 넘으면 못 읽음)
future: widget.probe(e.path).timeout(const Duration(seconds: 20)),
builder: (_, s) {
if (s.connectionState != ConnectionState.done) return Text(tr('정보를 읽는 중…'), style: const TextStyle(color: JjColors.textDim));
if (s.connectionState != ConnectionState.done) return Text(tr('정보를 읽는 중…'), style: TextStyle(color: JjColors.textDim));
final info = s.data;
if (info == null) return Text(tr('정보를 읽지 못했습니다'), style: const TextStyle(color: JjColors.textDim));
if (info == null) return Text(tr('정보를 읽지 못했습니다'), style: TextStyle(color: JjColors.textDim));
final v = info.ofType('video').where((x) => (x.width ?? 0) > 0).firstOrNull;
final audio = info.ofType('audio');
final subs = info.ofType('subtitle');
@ -189,7 +189,7 @@ class _QuickLookState extends State<QuickLook> {
title: Row(children: [
Expanded(child: Text(e.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16))),
IconButton(tooltip: tr('앞 항목 (←)'), onPressed: _i > 0 ? () => _go(-1) : null, icon: const Icon(Icons.chevron_left)),
Text('${_i + 1}/${widget.entries.length}', style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text('${_i + 1}/${widget.entries.length}', style: TextStyle(fontSize: 12, color: JjColors.textDim)),
IconButton(
tooltip: tr('다음 항목 (→)'),
onPressed: _i < widget.entries.length - 1 ? () => _go(1) : null,
@ -203,7 +203,7 @@ class _QuickLookState extends State<QuickLook> {
[if (!e.isDir) formatSize(e.size), if (e.modified.year > 1) _date(e.modified), vDirname(e.path)].join(' · '),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
style: TextStyle(fontSize: 12, color: JjColors.textDim),
),
const SizedBox(height: 8),
Expanded(child: KeyedSubtree(key: ValueKey(e.path), child: _body(e))),

View File

@ -920,7 +920,7 @@ class _ReaderPageState extends State<ReaderPage> {
Widget row(String k, Widget v) => Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
SizedBox(width: 90, child: Text(k, style: const TextStyle(color: JjColors.textDim))),
SizedBox(width: 90, child: Text(k, style: TextStyle(color: JjColors.textDim))),
Expanded(child: v),
]),
);
@ -951,12 +951,12 @@ class _ReaderPageState extends State<ReaderPage> {
child: TextButton(onPressed: () => set(() => showLocation = true), child: Text(tr('위치 보기'))))),
if (i.hasLocation)
Text(tr('위치는 이 기기에서만 보이며 지도 · 인터넷으로 보내지 않습니다'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
if (!i.hasExif)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(tr('이 그림에는 Exif 정보가 없습니다 (JPEG 사진만 읽음)'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
],
]),

View File

@ -116,7 +116,7 @@ class _RemoteThumbState extends State<RemoteThumb> {
Image.file(f, width: 56, height: 44, fit: BoxFit.cover, errorBuilder: (_, _, _) => widget.fallback),
if (isVideoFile(widget.entry.path)) const Icon(Icons.play_arrow, size: 18, color: Colors.white70),
// 원격에서 온 것 표시 (작게)
const Positioned(right: 1, bottom: 1, child: Icon(Icons.cloud_outlined, size: 10, color: JjColors.textDim)),
Positioned(right: 1, bottom: 1, child: Icon(Icons.cloud_outlined, size: 10, color: JjColors.textDim)),
]),
);
}

View File

@ -31,7 +31,7 @@ Future<String?> installRsyncWithDialog(BuildContext context) async {
content: ValueListenableBuilder<(double, String)>(
valueListenable: progress,
builder: (_, v, _) => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(v.$2, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(v.$2, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 8),
LinearProgressIndicator(value: v.$1),
]),

View File

@ -101,7 +101,7 @@ class _ScheduleDialogState extends State<_ScheduleDialog> {
? tr('앱이 켜져 있는 동안 늘 동기화합니다.')
: tr('칠한 시간에만 동기화하고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다. '
'칸을 누르거나 끌어서 칠합니다 (요일 이름 · 시 숫자를 누르면 그 줄 전체).'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
style: TextStyle(fontSize: 12, color: JjColors.textDim),
),
if (!_always) ...[
const SizedBox(height: 10),
@ -136,7 +136,7 @@ class _ScheduleDialogState extends State<_ScheduleDialog> {
),
const SizedBox(height: 8),
Text(lines.isEmpty ? tr('칠한 시간이 없어 동기화하지 않습니다.') : scheduleSummary(lines),
style: const TextStyle(fontSize: 12, color: JjColors.accent)),
style: TextStyle(fontSize: 12, color: JjColors.accent)),
],
]),
),
@ -215,7 +215,7 @@ class _WeekGridState extends State<_WeekGrid> {
child: InkWell(
onTap: () => _toggleCol(h),
child: Center(
child: Text(h % 3 == 0 ? '$h' : '', style: const TextStyle(fontSize: 10, color: JjColors.textDim)),
child: Text(h % 3 == 0 ? '$h' : '', style: TextStyle(fontSize: 10, color: JjColors.textDim)),
),
),
),

View File

@ -348,7 +348,7 @@ class _SettingsPageState extends State<SettingsPage> {
Expanded(
child: Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.lock_outline, size: 48, color: JjColors.textDim),
Icon(Icons.lock_outline, size: 48, color: JjColors.textDim),
const SizedBox(height: 12),
Text(tr('마스터 비밀번호로 잠겨 있습니다')),
const SizedBox(height: 12),
@ -416,7 +416,7 @@ class _SettingsPageState extends State<SettingsPage> {
_appIconTile(c, desk),
if (c.services.updater != null) ...[
SettingTile(
leading: const Icon(Icons.system_update_alt, color: JjColors.accent),
leading: Icon(Icons.system_update_alt, color: JjColors.accent),
title: FutureBuilder<String>(
future: c.services.updater!.currentVersion(),
builder: (_, v) => Text('JJ_MKVMaker v${v.data ?? '…'}'),
@ -497,6 +497,28 @@ class _SettingsPageState extends State<SettingsPage> {
..uiScale = v),
),
),
// X-plore 비교 5-1 ⑦ (관리자 지시 10/21): 화면 밝기 - 처음 값 어둡게 (예전 그대로), 다 끝날 때까지 "시험"
SettingTile(
title: Row(mainAxisSize: MainAxisSize.min, children: [
Flexible(child: Text(tr('화면 밝기'))),
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)),
),
]),
subtitle: Text(tr('밝게 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다')),
trailing: DropdownButton<String>(
value: s.uiBrightness,
items: [
DropdownMenuItem(value: 'dark', child: Text(tr('어두운 화면'))),
DropdownMenuItem(value: 'light', child: Text(tr('밝은 화면'))),
DropdownMenuItem(value: 'system', child: Text(tr('기기 따라'))),
],
onChanged: (v) => c.updateSettings((x) => x.uiBrightness = v!),
),
),
]),
_group('mkv', Icons.movie_creation_outlined, tr('MKV 만들기'), [
// 61: 화면 언어와 다른 언어의 자막을 기본으로 원할 수 있다 (예: 화면은 일본어, 자막은 한국어)
@ -592,7 +614,7 @@ class _SettingsPageState extends State<SettingsPage> {
s.moveTargets.isEmpty
? tr('없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다')
: tr('표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
style: TextStyle(fontSize: 12, color: JjColors.textDim),
),
),
]),
@ -726,7 +748,7 @@ class _SettingsPageState extends State<SettingsPage> {
if (s.browserEngine == 'edge' && CefRuntime.installed && !CefRuntime.readyThisRun)
IconButton(
tooltip: tr('내려받은 Chrome 엔진 지우기'),
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
onPressed: () async {
final ok = await CefRuntime.uninstall();
c.note(ok ? tr('내장 Chrome 엔진을 지웠습니다.') : tr('내장 Chrome 엔진을 지우지 못했습니다 (사용 중).'));
@ -1242,7 +1264,7 @@ class _SettingsPageState extends State<SettingsPage> {
Text(
tr('로그인 쿠키는 다운로드 (yt-dlp) 가 쓰도록 이 기기의 앱 폴더에 cookies_youtube.txt 로 저장됩니다 (평문). '
'이 파일을 가진 사람은 그 계정에 로그인할 수 있으니 기기를 남에게 줄 때는 지우세요. 고른 사이트의 쿠키만 넘깁니다:'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 6),
Wrap(spacing: 6, runSpacing: 6, children: [
for (final d in loginCookieDomains)
@ -1332,7 +1354,7 @@ class _SettingsPageState extends State<SettingsPage> {
title: Text(tr('창 보이기 / 숨기기 단축키')),
subtitle: _hotkeyError == null
? Text(tr('어디서나 누르면 JJ_MKVMaker 창이 나타납니다'))
: Text(_hotkeyError!, style: const TextStyle(color: JjColors.danger)),
: Text(_hotkeyError!, style: TextStyle(color: JjColors.danger)),
trailing: SizedBox(
width: 260,
child: Row(children: [
@ -1386,7 +1408,7 @@ class _SettingsPageState extends State<SettingsPage> {
_group('about', Icons.info_outline, tr('프로그램 정보'), [
// 164: 지금 버전과 [최신 버전 확인] (결과는 버튼 옆에)
SettingTile(
leading: const Icon(Icons.info_outline, color: JjColors.accent),
leading: Icon(Icons.info_outline, color: JjColors.accent),
title: FutureBuilder<String>(
future: c.services.updater?.currentVersion() ?? Future.value(''),
builder: (_, v) => Text(v.data == null || v.data!.isEmpty ? 'JJ_MKVMaker' : 'JJ_MKVMaker v${v.data}'),
@ -1435,7 +1457,7 @@ class _SettingsPageState extends State<SettingsPage> {
c.updateSettings((x) => x.moveTargets = f([...x.moveTargets]));
return SettingTile(
key: ValueKey('move_${i}_${t.dir}'),
leading: const Icon(Icons.drive_file_move_outline, color: JjColors.accent),
leading: Icon(Icons.drive_file_move_outline, color: JjColors.accent),
title: TextFormField(
initialValue: t.name,
decoration: InputDecoration(isDense: true, labelText: tr('표시 이름')),
@ -1606,7 +1628,7 @@ class _SettingsPageState extends State<SettingsPage> {
Widget _subTitle(String t) => Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Text(t, style: const TextStyle(fontSize: 13, color: JjColors.accent, fontWeight: FontWeight.w600)),
child: Text(t, style: TextStyle(fontSize: 13, color: JjColors.accent, fontWeight: FontWeight.w600)),
);

View File

@ -38,7 +38,7 @@ class SftpSettings extends StatelessWidget {
padding: const EdgeInsets.only(left: 16),
child: SettingTile(
dense: true,
leading: const Icon(Icons.vpn_key, color: JjColors.accent),
leading: Icon(Icons.vpn_key, color: JjColors.accent),
title: Text(s.label),
subtitle: Text(
[
@ -58,7 +58,7 @@ class SftpSettings extends StatelessWidget {
),
IconButton(
tooltip: tr('지우기'),
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
onPressed: () => deleteSftpServer(context, c, s),
),
]),
@ -94,7 +94,7 @@ Future<bool> askTrustHostKey(BuildContext context, String serverLabel, SftpHostK
context: context,
builder: (ctx) => AlertDialog(
scrollable: true,
icon: const Icon(Icons.gpp_bad_outlined, color: JjColors.danger),
icon: Icon(Icons.gpp_bad_outlined, color: JjColors.danger),
title: Text(trf('"{0}" 서버의 키가 바뀌었습니다', [serverLabel])),
content: SizedBox(
width: 520,
@ -290,7 +290,7 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
onPressed: pickKey,
),
if (key.isNotEmpty) ...[
Text(tr('개인 키 있음'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(tr('개인 키 있음'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
TextButton(onPressed: () => set(() => key = ''), child: Text(tr('개인 키 빼기'))),
],
]),
@ -316,7 +316,7 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
fingerprint.isEmpty
? tr('서버 키: 아직 확인 안 함 ([연결 확인] 을 누르면 지문을 보이고 묻습니다)')
: '${tr('믿은 서버 키')}: $fingerprint${keyType.isEmpty ? '' : ' ($keyType)'}',
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
if (fingerprint.isNotEmpty)
Align(
alignment: Alignment.centerLeft,

View File

@ -49,7 +49,7 @@ class SmbSettings extends StatelessWidget {
padding: const EdgeInsets.only(left: 16),
child: SettingTile(
dense: true,
leading: const Icon(Icons.dns, color: JjColors.accent),
leading: Icon(Icons.dns, color: JjColors.accent),
title: Text(s.label),
subtitle: Text(
[
@ -66,7 +66,7 @@ class SmbSettings extends StatelessWidget {
),
IconButton(
tooltip: tr('지우기'),
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
onPressed: () => deleteSmbServer(context, c, s),
),
]),
@ -320,14 +320,14 @@ Future<LanHost?> pickLanSmbServer(BuildContext context) async {
width: 460,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(tr('먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 10),
if (list == null && error == null) ...[
LinearProgressIndicator(value: total > 0 ? done / total : null),
const SizedBox(height: 4),
Text(total > 0 ? trf('{0} / {1} 확인', [done, total]) : tr('준비하는 중'), style: const TextStyle(fontSize: 12)),
],
if (error != null) Text('$error', style: const TextStyle(color: JjColors.danger)),
if (error != null) Text('$error', style: TextStyle(color: JjColors.danger)),
if (list != null && list.isEmpty) Text(tr('찾지 못했습니다. NAS 가 켜져 있고 같은 망에 있는지 확인하거나, 주소를 직접 넣으세요.')),
if (list != null)
for (final h in list)

View File

@ -151,7 +151,7 @@ class _SqliteViewerPageState extends State<SqliteViewerPage> {
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
child: Text(tr('읽기 전용 - 이 앱은 파일을 바꾸지 않습니다. 쓰는 중인 바뀐 것 (-wal) 은 보이지 않을 수 있습니다'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
if (_tables.isNotEmpty)
Padding(
@ -166,11 +166,11 @@ class _SqliteViewerPageState extends State<SqliteViewerPage> {
),
),
const SizedBox(width: 8),
if (_count != null) Text(trf('{0}행', [_count!]), style: const TextStyle(color: JjColors.textDim)),
if (_count != null) Text(trf('{0}행', [_count!]), style: TextStyle(color: JjColors.textDim)),
]),
),
if (_error != null)
Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: const TextStyle(color: JjColors.danger))),
Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: TextStyle(color: JjColors.danger))),
if (_busy && _rows.isEmpty) const Padding(padding: EdgeInsets.all(24), child: Center(child: CircularProgressIndicator())),
if (!_busy && _error == null && _tables.isEmpty) Padding(padding: const EdgeInsets.all(16), child: Text(tr('테이블이 없습니다'))),
if (t != null && _rows.isNotEmpty)

View File

@ -158,7 +158,7 @@ class _StorageAnalysisPageState extends State<StorageAnalysisPage> {
Text(_error != null ? trf('분석하지 못했습니다: {0}', [_error!]) : trf('읽는 중 · 폴더 {0}개 · {1}', [_dirs, formatSize(_bytes)]),
textAlign: TextAlign.center),
const SizedBox(height: 4),
Text(vDisplay(widget.root), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(vDisplay(widget.root), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
),
)
@ -176,7 +176,7 @@ class _StorageAnalysisPageState extends State<StorageAnalysisPage> {
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(vDisplay(_cur), maxLines: 2, overflow: TextOverflow.ellipsis),
Text(trf('{0} · 파일 {1}개', [formatSize(s.sizeOf(_cur)), s.filesOf(_cur)]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]),
),
TextButton.icon(

View File

@ -453,7 +453,7 @@ class _SubtitleEditorPageState extends State<SubtitleEditorPage> {
value: _rate,
isDense: true,
underline: const SizedBox(),
style: const TextStyle(fontSize: 12, color: JjColors.text),
style: TextStyle(fontSize: 12, color: JjColors.text),
items: [
for (final r in const [0.5, 0.75, 1.0, 1.25, 1.5, 2.0])
DropdownMenuItem(value: r, child: Text('${r}x')),
@ -529,7 +529,7 @@ class _SubtitleEditorPageState extends State<SubtitleEditorPage> {
value: e.saveCharset,
isDense: true,
underline: const SizedBox(),
style: const TextStyle(fontSize: 13, color: JjColors.text),
style: TextStyle(fontSize: 13, color: JjColors.text),
items: [
for (final c in saveCharsets.entries)
DropdownMenuItem(value: c.key, child: Text(c.value)),
@ -591,7 +591,7 @@ class _HeaderRow extends StatelessWidget {
);
}
const _hdr = TextStyle(fontSize: 12, color: JjColors.textDim);
final _hdr = TextStyle(fontSize: 12, color: JjColors.textDim);
class _CueRow extends StatefulWidget {
final int index;
@ -661,7 +661,7 @@ class _CueRowState extends State<_CueRow> {
? JjColors.accent.withValues(alpha: 0.12)
: null,
border: Border(
bottom: const BorderSide(color: JjColors.border),
bottom: BorderSide(color: JjColors.border),
left: BorderSide(
color: widget.active ? JjColors.accent : Colors.transparent, width: 3),
),
@ -701,13 +701,13 @@ class _CueRowState extends State<_CueRow> {
IconButton(
tooltip: tr('아래에 줄 추가'),
iconSize: 18,
icon: const Icon(Icons.add, color: JjColors.textDim),
icon: Icon(Icons.add, color: JjColors.textDim),
onPressed: () => e.insertAfter(widget.index),
),
IconButton(
tooltip: tr('줄 삭제'),
iconSize: 18,
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
onPressed: () => e.removeAt(widget.index),
),
]),

View File

@ -129,7 +129,7 @@ class _SearchDialogState extends State<_SearchDialog> {
if (_error != null)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SelectableText(_error!, style: const TextStyle(color: JjColors.danger)),
child: SelectableText(_error!, style: TextStyle(color: JjColors.danger)),
),
Expanded(child: _list()),
]),

View File

@ -93,7 +93,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
return Row(children: [
const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)),
const SizedBox(width: 8),
Text(tr('무엇이 바뀌는지 비교하는 중…'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
Text(tr('무엇이 바뀌는지 비교하는 중…'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]);
}
int n(PreviewAction a, [bool? right]) =>
@ -244,10 +244,10 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
for (final s in t.sources) ...[
Text(shortPath(s), style: const TextStyle(fontWeight: FontWeight.w600)),
Text('→ ${shortPath(dstOf(s))}', style: const TextStyle(fontWeight: FontWeight.w600)),
Text('${vDisplay(s)} → ${vDisplay(dstOf(s))}', style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
Text('${vDisplay(s)} → ${vDisplay(dstOf(s))}', style: TextStyle(fontSize: 11, color: JjColors.textDim)),
],
// 230: 칸에 적힌 옵션이 아니라 실제로 실행할 것 (원본 지우기 옵션은 이동일 때만 붙는다)
Text(effectiveRunText(t.method, t.options, move: t.move, skipNewer: skipNewer), style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
Text(effectiveRunText(t.method, t.options, move: t.move, skipNewer: skipNewer), style: TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
// 210: 이동이면 실행 뒤 원본에서 지워진다는 것을 분명히
if (t.move)
Padding(

View File

@ -289,7 +289,7 @@ class _TextViewerPageState extends State<TextViewerPage> {
child: Row(children: [
Expanded(
child: Text(trf('{0} / {1} 읽음', [formatSize(_bytes.length), formatSize(_size)]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
FilledButton.tonal(onPressed: _more, child: Text(tr('더 읽기'))),
]),

View File

@ -1,29 +1,113 @@
import 'package:flutter/material.dart';
/// CapCut 참고 어두운 테마
class JjColors {
static const bg = Color(0xFF141417);
static const panel = Color(0xFF1E1E23);
static const panelHigh = Color(0xFF28282F);
static const border = Color(0xFF2E2E36);
static const accent = Color(0xFF00C4CC); // CapCut 계열 청록
static const text = Color(0xFFE8E8EC);
static const textDim = Color(0xFF8E8E99);
static const danger = Color(0xFFFF5C6C);
static const success = Color(0xFF3DD68C);
/// 화면 색 한 벌 (어두운 · 밝은)
class JjPalette {
final Brightness brightness;
final Color bg, panel, panelHigh, border, accent, text, textDim, danger, success;
/// 강조색 바탕 위 글자 (버튼 등)
final Color onAccent;
const JjPalette({
required this.brightness,
required this.bg,
required this.panel,
required this.panelHigh,
required this.border,
required this.accent,
required this.text,
required this.textDim,
required this.danger,
required this.success,
required this.onAccent,
});
/// CapCut 참고 어두운 테마 (처음 값 - 예전 그대로)
static const dark = JjPalette(
brightness: Brightness.dark,
bg: Color(0xFF141417),
panel: Color(0xFF1E1E23),
panelHigh: Color(0xFF28282F),
border: Color(0xFF2E2E36),
accent: Color(0xFF00C4CC), // CapCut 계열 청록
text: Color(0xFFE8E8EC),
textDim: Color(0xFF8E8E99),
danger: Color(0xFFFF5C6C),
success: Color(0xFF3DD68C),
onAccent: Colors.black,
);
/// X-plore 비교 5-1 ⑦ 밝은 테마 (관리자 지시 10/21, 시험). 흰 바탕에서 글자 대비 4.5:1 이상이 되게 강조 · 경고 · 성공을 어둡게
static const light = JjPalette(
brightness: Brightness.light,
bg: Color(0xFFF4F4F6),
panel: Color(0xFFFFFFFF),
panelHigh: Color(0xFFE9E9EE),
border: Color(0xFFD3D3DB),
accent: Color(0xFF00747A),
text: Color(0xFF1B1B20),
textDim: Color(0xFF5E5E6A),
danger: Color(0xFFC62833),
success: Color(0xFF186B42),
onAccent: Colors.white,
);
}
ThemeData buildTheme() {
/// 앱 전체의 색. 테마 (환경 설정 "화면 밝기") 에 따라 [use] 로 바뀐다 - 그래서 const 로 쓰지 않는다
class JjColors {
static JjPalette _p = JjPalette.dark;
/// 지금 쓰는 색 한 벌
static JjPalette get palette => _p;
static Brightness get brightness => _p.brightness;
static bool get isLight => _p.brightness == Brightness.light;
/// 색 한 벌을 바꾼다 (바꿨으면 true). 화면 전체를 다시 그리는 것은 부르는 쪽 ([rebuildAllElements])
static bool use(Brightness b) {
final next = b == Brightness.light ? JjPalette.light : JjPalette.dark;
if (identical(next, _p)) return false;
_p = next;
return true;
}
static Color get bg => _p.bg;
static Color get panel => _p.panel;
static Color get panelHigh => _p.panelHigh;
static Color get border => _p.border;
static Color get accent => _p.accent;
static Color get text => _p.text;
static Color get textDim => _p.textDim;
static Color get danger => _p.danger;
static Color get success => _p.success;
static Color get onAccent => _p.onAccent;
}
/// 색을 바꾼 뒤 모든 화면 조각을 다시 그린다 (상태는 그대로 - [JjColors] 를 직접 읽는 곳도 새 색으로)
void rebuildAllElements(Element root) {
void mark(Element e) {
e.markNeedsBuild();
e.visitChildren(mark);
}
mark(root);
}
/// 환경 설정 "화면 밝기" 값 ('dark' 처음 값 · 'light' · 'system') → 밝기
Brightness resolveBrightness(String mode, Brightness platform) => switch (mode) {
'light' => Brightness.light,
'system' => platform,
_ => Brightness.dark,
};
ThemeData buildTheme([Brightness? brightness]) {
final c = brightness == null ? JjColors.palette : (brightness == Brightness.light ? JjPalette.light : JjPalette.dark);
final light = c.brightness == Brightness.light;
final base = ThemeData(
brightness: Brightness.dark,
brightness: c.brightness,
useMaterial3: true,
colorScheme: const ColorScheme.dark(
primary: JjColors.accent,
secondary: JjColors.accent,
surface: JjColors.panel,
error: JjColors.danger,
),
scaffoldBackgroundColor: JjColors.bg,
colorScheme: light
? ColorScheme.light(primary: c.accent, secondary: c.accent, surface: c.panel, error: c.danger, onPrimary: c.onAccent)
: ColorScheme.dark(primary: c.accent, secondary: c.accent, surface: c.panel, error: c.danger),
scaffoldBackgroundColor: c.bg,
fontFamily: 'Malgun Gothic',
);
return base.copyWith(
@ -35,22 +119,22 @@ ThemeData buildTheme() {
TargetPlatform.macOS: NavSlideTransitionsBuilder(),
TargetPlatform.iOS: NavSlideTransitionsBuilder(),
}),
dividerColor: JjColors.border,
dividerColor: c.border,
textTheme: base.textTheme.apply(
bodyColor: JjColors.text,
displayColor: JjColors.text,
bodyColor: c.text,
displayColor: c.text,
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
backgroundColor: JjColors.accent,
foregroundColor: Colors.black,
backgroundColor: c.accent,
foregroundColor: c.onAccent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: JjColors.text,
side: const BorderSide(color: JjColors.border),
foregroundColor: c.text,
side: BorderSide(color: c.border),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
),

View File

@ -162,7 +162,7 @@ class _SubtitleTimelineState extends State<SubtitleTimeline> {
top: 3,
child: IgnorePointer(
child: Text(trf('보기 {0}초 · Ctrl+휠 확대/축소', [_window.toStringAsFixed(_window < 10 ? 1 : 0)]),
style: const TextStyle(fontSize: 10, color: JjColors.textDim)),
style: TextStyle(fontSize: 10, color: JjColors.textDim)),
),
),
],
@ -304,7 +304,7 @@ class _RulerPainter extends CustomPainter {
if (major) {
tp.text = TextSpan(
text: _label(t, step),
style: const TextStyle(fontSize: 10, color: JjColors.textDim));
style: TextStyle(fontSize: 10, color: JjColors.textDim));
tp.layout();
tp.paint(canvas, Offset(x + 3, 1));
}

View File

@ -634,7 +634,7 @@ class _TorPageState extends State<TorPage> with WebProxyVisibility {
const SizedBox(height: 12),
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textAlign: TextAlign.center),
const SizedBox(height: 8),
Text(body, style: const TextStyle(color: JjColors.textDim), textAlign: TextAlign.center),
Text(body, style: TextStyle(color: JjColors.textDim), textAlign: TextAlign.center),
]),
),
),

View File

@ -67,7 +67,7 @@ class _TranslateDialogState extends State<_TranslateDialog> {
src == undetermined
? tr('원어: 자동 감지 (글자로 추정)')
: trf('원어: {0} ({1})', [src.name, src.code]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 16),
Text(tr('번역할 언어 (jj_mkv\\파일명_언어코드.srt 로 저장 후 MKV 에 추가)'),
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),

View File

@ -103,8 +103,8 @@ class _TrashPageState extends State<TrashPage> {
if (items != null && items.isNotEmpty)
TextButton.icon(
onPressed: _busy ? null : () => _purge(items, all: true),
icon: const Icon(Icons.delete_forever_outlined, color: JjColors.danger),
label: Text(tr('비우기'), style: const TextStyle(color: JjColors.danger)),
icon: Icon(Icons.delete_forever_outlined, color: JjColors.danger),
label: Text(tr('비우기'), style: TextStyle(color: JjColors.danger)),
),
],
),
@ -115,7 +115,7 @@ class _TrashPageState extends State<TrashPage> {
widget.days > 0
? trf('지운 것은 {0}일 동안 여기에 두었다가 저절로 지웁니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).', [widget.days])
: tr('휴지통이 꺼져 있습니다 - 지우면 바로 영구 삭제됩니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
style: TextStyle(fontSize: 12, color: JjColors.textDim),
),
),
if (_busy) const LinearProgressIndicator(minHeight: 2),
@ -151,7 +151,7 @@ class _TrashPageState extends State<TrashPage> {
Expanded(child: Text(trf('{0}개 고름', [chosen.length]))),
TextButton(
onPressed: _busy ? null : () => _purge(chosen),
child: Text(tr('영구 삭제'), style: const TextStyle(color: JjColors.danger)),
child: Text(tr('영구 삭제'), style: TextStyle(color: JjColors.danger)),
),
const SizedBox(width: 8),
FilledButton.icon(

View File

@ -75,7 +75,7 @@ Future<void> checkForUpdate(BuildContext context, AppController c, {bool manual
Text(trf('지금 쓰는 버전: v{0} → 새 버전: v{1}', [current, r.version])),
if (r.zipSize > 0)
Text(trf('받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다', [(r.zipSize / 1e6).round()]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 12),
Text(tr('바뀐 내용'), style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 4),
@ -160,7 +160,7 @@ class _UpdateCheckButtonState extends State<UpdateCheckButton> {
Widget build(BuildContext context) => Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
OutlinedButton(onPressed: _busy ? null : _check, child: Text(tr('최신 버전 확인'))),
if (_busy) const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)),
if (_result != null) Text(_result!, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
if (_result != null) Text(_result!, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
]);
}
@ -220,7 +220,7 @@ class _VersionPickerState extends State<_VersionPicker> {
Padding(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
child: Text(trf('지금 쓰는 버전: v{0}', [widget.current]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
for (final r in all)
ListTile(

View File

@ -343,7 +343,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
Text(trf('넣을 곳: {0}', [_cwdLabel()]), style: const TextStyle(fontSize: 12.5)),
if (remote)
Text(tr('원격 파일은 이 기기의 앱 전용 폴더에 받아 넣고, 받은 임시 파일은 바로 지웁니다'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
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),
@ -853,7 +853,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
Widget _unlockForm() {
final waiting = _waitUntil != null && DateTime.now().isBefore(_waitUntil!);
return ListView(padding: const EdgeInsets.all(16), children: [
const Icon(Icons.lock_outline, size: 48, color: JjColors.textDim),
Icon(Icons.lock_outline, size: 48, color: JjColors.textDim),
const SizedBox(height: 8),
TextField(
controller: _pw,
@ -898,7 +898,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
Expanded(
child: _cwd.isEmpty
? Text(tr('넣기는 파일 탐색기에서 파일 · 폴더를 길게 눌러 [보관함에 넣기]'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim))
style: TextStyle(fontSize: 12, color: JjColors.textDim))
: Text(_cwdLabel(), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w600)),
),
DropdownButton<String>(
@ -922,7 +922,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
final f = _cwd.isEmpty ? dirs[i] : '$_cwd/${dirs[i]}';
final n = v.filesUnder(f).length;
return ListTile(
leading: const Icon(Icons.folder, color: JjColors.accent),
leading: Icon(Icons.folder, color: JjColors.accent),
title: Text(dirs[i], maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(trf('파일 {0}개', [n])),
onTap: _busy ? null : () => setState(() => _cwd = f),
@ -947,7 +947,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
}
final e = list[i - dirs.length];
return ListTile(
leading: const Icon(Icons.lock, color: JjColors.accent),
leading: Icon(Icons.lock, color: JjColors.accent),
title: Text(e.name, maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text('${formatSize(e.size)} · ${_day(e.added)}'),
onTap: _busy ? null : () => _view(e),

View File

@ -123,13 +123,13 @@ class _VideoAdjustState extends State<_VideoAdjust> {
color: JjColors.panelHigh,
padding: const EdgeInsets.fromLTRB(16, 8, 8, 8),
child: Row(children: [
const Icon(Icons.tune, color: JjColors.accent, size: 20),
Icon(Icons.tune, color: JjColors.accent, size: 20),
const SizedBox(width: 8),
Text(tr('화면 · 색 보정'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
const SizedBox(width: 12),
Expanded(
child: Text(tr('MKV 만들기에 적용됩니다 (영상을 다시 인코딩)'),
overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
TextButton.icon(
onPressed: s.adjusts ? () => _set(s.resetAdjust()) : null,
@ -161,12 +161,12 @@ class _VideoAdjustState extends State<_VideoAdjust> {
child: Stack(children: [
Positioned.fill(
child: v == null
? Center(child: Text(tr('미리보기: 목록에서 동영상을 고르세요'), style: const TextStyle(color: JjColors.textDim)))
? Center(child: Text(tr('미리보기: 목록에서 동영상을 고르세요'), style: TextStyle(color: JjColors.textDim)))
: _error != null
? Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(_error!, style: const TextStyle(color: JjColors.danger, fontSize: 12)),
child: Text(_error!, style: TextStyle(color: JjColors.danger, fontSize: 12)),
))
: img == null
? const Center(child: CircularProgressIndicator())
@ -209,7 +209,7 @@ class _VideoAdjustState extends State<_VideoAdjust> {
Widget _inspector() {
Widget head(String t) => Padding(
padding: const EdgeInsets.fromLTRB(0, 16, 0, 8),
child: Text(t, style: const TextStyle(fontSize: 13, color: JjColors.accent, fontWeight: FontWeight.w600)),
child: Text(t, style: TextStyle(fontSize: 13, color: JjColors.accent, fontWeight: FontWeight.w600)),
);
const frameIcons = {
FrameChoice.original: Icons.crop_original,
@ -258,7 +258,7 @@ class _VideoAdjustState extends State<_VideoAdjust> {
const SizedBox(height: 12),
Text(
s.adjusts ? '${tr('적용')}: ${s.adjustSummary}' : tr('바꾼 것이 없습니다 (원본 그대로)'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
style: TextStyle(fontSize: 12, color: JjColors.textDim),
),
]),
);
@ -272,7 +272,7 @@ class _VideoAdjustState extends State<_VideoAdjust> {
Text(label, style: const TextStyle(fontSize: 13)),
if (hint != null) ...[
const SizedBox(width: 6),
Text(hint, style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
Text(hint, style: TextStyle(fontSize: 11, color: JjColors.textDim)),
],
const Spacer(),
SizedBox(

View File

@ -37,7 +37,7 @@ class WebDavSettings extends StatelessWidget {
padding: const EdgeInsets.only(left: 16),
child: SettingTile(
dense: true,
leading: const Icon(Icons.cloud, color: JjColors.accent),
leading: Icon(Icons.cloud, color: JjColors.accent),
title: Text(s.label),
subtitle: Text(
'${s.url}${s.user.isEmpty ? '' : ' · ${s.user}'}${s.insecure ? ' · ${tr('인증서 확인 안 함')}' : ''}'
@ -51,7 +51,7 @@ class WebDavSettings extends StatelessWidget {
),
IconButton(
tooltip: tr('지우기'),
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
// 46: 확인 뒤 (저장된 비밀번호도 함께 지워진다)
onPressed: () => deleteDavServer(context, c, s),
),

View File

@ -111,7 +111,7 @@ class _WifiSharePageState extends State<WifiSharePage> {
List<Widget> _offView() {
final mins = widget.c.settings.wifiShareMinutes;
return [
Text(tr('공유할 폴더'), style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
Text(tr('공유할 폴더'), style: TextStyle(color: JjColors.textDim, fontSize: 12)),
const SizedBox(height: 4),
SelectableText(widget.folder, style: const TextStyle(fontSize: 15)),
const SizedBox(height: 16),
@ -168,9 +168,9 @@ class _WifiSharePageState extends State<WifiSharePage> {
final left = s.endsAt?.difference(DateTime.now());
return [
Row(children: [
const Icon(Icons.wifi_tethering, color: JjColors.success),
Icon(Icons.wifi_tethering, color: JjColors.success),
const SizedBox(width: 8),
Expanded(child: Text(tr('공유 중'), style: const TextStyle(fontSize: 16, color: JjColors.success))),
Expanded(child: Text(tr('공유 중'), style: TextStyle(fontSize: 16, color: JjColors.success))),
FilledButton.icon(
style: FilledButton.styleFrom(backgroundColor: JjColors.danger),
onPressed: _stop,
@ -179,15 +179,15 @@ class _WifiSharePageState extends State<WifiSharePage> {
),
]),
const SizedBox(height: 8),
SelectableText(s.root, style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
SelectableText(s.root, style: TextStyle(color: JjColors.textDim, fontSize: 12)),
const SizedBox(height: 16),
Text(tr('접속 코드'), style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
Text(tr('접속 코드'), style: TextStyle(color: JjColors.textDim, fontSize: 12)),
SelectableText(s.code, style: const TextStyle(fontSize: 40, letterSpacing: 8, fontWeight: FontWeight.w600)),
const SizedBox(height: 12),
if (hosts.isEmpty)
_note(Icons.wifi_off, tr('이 기기의 Wi-Fi 주소를 찾지 못했습니다. Wi-Fi 에 연결돼 있는지 확인하세요.'), color: JjColors.danger)
else ...[
Text(tr('다른 기기의 브라우저에서 이 주소를 여세요'), style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
Text(tr('다른 기기의 브라우저에서 이 주소를 여세요'), style: TextStyle(color: JjColors.textDim, fontSize: 12)),
for (final h in hosts)
Row(children: [
Expanded(child: SelectableText('http://$h:${s.port}', style: const TextStyle(fontSize: 16))),
@ -198,7 +198,7 @@ class _WifiSharePageState extends State<WifiSharePage> {
),
]),
const SizedBox(height: 8),
Text(tr('또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)'), style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
Text(tr('또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)'), style: TextStyle(color: JjColors.textDim, fontSize: 12)),
const SizedBox(height: 8),
Center(child: QrView(data: s.urlWithCode(hosts.first), size: 200)),
],
@ -215,9 +215,9 @@ class _WifiSharePageState extends State<WifiSharePage> {
]),
const Divider(height: 32),
Text(trf('접속 기록 ({0}) - 이 화면에만 보이고 저장하지 않습니다', [s.log.length]),
style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
style: TextStyle(color: JjColors.textDim, fontSize: 12)),
if (s.log.isEmpty)
Padding(padding: const EdgeInsets.symmetric(vertical: 8), child: Text(tr('아직 접속한 기기가 없습니다'), style: const TextStyle(color: JjColors.textDim)))
Padding(padding: const EdgeInsets.symmetric(vertical: 8), child: Text(tr('아직 접속한 기기가 없습니다'), style: TextStyle(color: JjColors.textDim)))
else
for (final a in s.log.take(50))
Padding(
@ -268,7 +268,9 @@ class _WifiSharePageState extends State<WifiSharePage> {
],
);
Widget _note(IconData icon, String text, {Color color = JjColors.textDim}) => Padding(
Widget _note(IconData icon, String text, {Color? color}) => _noteIn(icon, text, color ?? JjColors.textDim);
Widget _noteIn(IconData icon, String text, Color color) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
Icon(icon, size: 18, color: color),

View File

@ -144,7 +144,7 @@ class WorkPanel extends StatelessWidget {
padding: const EdgeInsets.fromLTRB(12, 6, 4, 6),
child: Row(
children: [
const Icon(Icons.dashboard_outlined, size: 18, color: JjColors.accent),
Icon(Icons.dashboard_outlined, size: 18, color: JjColors.accent),
const SizedBox(width: 6),
Expanded(
child: Text(tr('작업 현황'), style: TextStyle(fontWeight: FontWeight.w600)),
@ -158,7 +158,7 @@ class WorkPanel extends StatelessWidget {
IconButton(
tooltip: c.busy ? tr('MKV 만들기 (대기열에 추가)') : tr('MKV 만들기'),
iconSize: 18,
icon: const Icon(Icons.play_arrow, color: JjColors.accent),
icon: Icon(Icons.play_arrow, color: JjColors.accent),
onPressed: c.videos.isEmpty || c.ffmpegVersion == null
? null
: c.buildAll,
@ -167,7 +167,7 @@ class WorkPanel extends StatelessWidget {
IconButton(
tooltip: tr('모든 작업 취소'),
iconSize: 18,
icon: const Icon(Icons.stop, color: JjColors.danger),
icon: Icon(Icons.stop, color: JjColors.danger),
onPressed: c.cancel,
),
IconButton(
@ -192,7 +192,7 @@ class WorkPanel extends StatelessWidget {
_title(tr('작업')),
// 123: AI 그림 (남은 장 · 예상 시간)
if (AiJobs.instance.statusLine case final ai?) ...[
Text('▶ $ai', style: const TextStyle(fontSize: 12, color: JjColors.accent)),
Text('▶ $ai', style: TextStyle(fontSize: 12, color: JjColors.accent)),
LinearProgressIndicator(value: AiJobs.instance.progress),
],
// 157: 끝난 AI 일 - 어디에 저장했는지와 [열기]
@ -209,12 +209,12 @@ class WorkPanel extends StatelessWidget {
else if (c.busy) ...[
Text(
'▶ ${c.currentJob ?? tr('작업 중')}',
style: const TextStyle(fontSize: 12, color: JjColors.accent),
style: TextStyle(fontSize: 12, color: JjColors.accent),
),
for (final (i, j) in c.pendingJobs.indexed)
Text(
trf('{0}. {1} (대기)', [i + 1, j]),
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
style: TextStyle(fontSize: 12, color: JjColors.textDim),
),
],
const SizedBox(height: 12),
@ -227,7 +227,7 @@ class WorkPanel extends StatelessWidget {
Expanded(
child: Text(
t,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: JjColors.textDim,
@ -248,7 +248,7 @@ class _Dim extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
text,
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
style: TextStyle(fontSize: 12, color: JjColors.textDim),
),
);
}
@ -434,7 +434,7 @@ class JobIndicator extends StatelessWidget {
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: const TextStyle(fontSize: 12, color: JjColors.accent),
style: TextStyle(fontSize: 12, color: JjColors.accent),
),
),
],
@ -450,7 +450,7 @@ class JobIndicator extends StatelessWidget {
: '${c.currentJob ?? tr('작업 중')}${p == null ? '' : ' ${(p * 100).round()}%'}${wait > 0 ? trf(' · 대기 {0}', [wait]) : ''}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 12, color: JjColors.accent),
style: TextStyle(fontSize: 12, color: JjColors.accent),
),
),
],

118
test/theme_test.dart Normal file
View File

@ -0,0 +1,118 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/app/app_controller.dart';
import 'package:jj_mkvmaker/app/settings.dart';
import 'package:jj_mkvmaker/main.dart';
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/ui/theme.dart';
/// X-plore 비교 5-1 ⑦ 밝은 테마 (관리자 지시 10/21): 처음 값 어둡게 (예전 그대로) · 밝게 · 기기 따라 · 바꾸면 상태는 그대로 다시 그림
double _lum(Color c) {
double ch(double v) => v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4).toDouble();
return 0.2126 * ch(c.r) + 0.7152 * ch(c.g) + 0.0722 * ch(c.b);
}
/// WCAG 대비
double contrast(Color a, Color b) {
final x = _lum(a), y = _lum(b);
return (max(x, y) + 0.05) / (min(x, y) + 0.05);
}
class _Probe extends StatefulWidget {
const _Probe();
@override
State<_Probe> createState() => _ProbeState();
}
class _ProbeState extends State<_Probe> {
int taps = 0;
@override
Widget build(BuildContext context) => GestureDetector(
onTap: () => setState(() => taps++),
child: Text('probe $taps', style: TextStyle(color: JjColors.text)),
);
}
void main() {
tearDown(() => JjColors.use(Brightness.dark));
test('처음 값은 어둡게 (예전 색 그대로) · 모르는 값 · 없는 값도 어둡게 · 고른 값은 그대로', () {
expect(AppSettings().uiBrightness, 'dark');
expect(AppSettings.fromJson({}).uiBrightness, 'dark');
expect(AppSettings.fromJson({'uiBrightness': 'neon'}).uiBrightness, 'dark');
expect(AppSettings.fromJson({'uiBrightness': 'light'}).uiBrightness, 'light');
expect(AppSettings.fromJson(AppSettings.fromJson({'uiBrightness': 'system'}).toJson()).uiBrightness, 'system');
expect(JjColors.bg, const Color(0xFF141417));
expect(JjColors.accent, const Color(0xFF00C4CC));
expect(JjColors.textDim, const Color(0xFF8E8E99));
expect(resolveBrightness('dark', Brightness.light), Brightness.dark);
expect(resolveBrightness('light', Brightness.dark), Brightness.light);
expect(resolveBrightness('system', Brightness.light), Brightness.light);
expect(resolveBrightness('?', Brightness.light), Brightness.dark);
});
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)]) {
expect(contrast(fg, bg), greaterThanOrEqualTo(4.5), reason: '$name on ${bg.toARGB32().toRadixString(16)}');
}
}
expect(contrast(l.onAccent, l.accent), greaterThanOrEqualTo(4.5));
expect(buildTheme(Brightness.light).brightness, Brightness.light);
expect(buildTheme(Brightness.light).scaffoldBackgroundColor, l.bg);
expect(buildTheme(Brightness.dark).scaffoldBackgroundColor, JjPalette.dark.bg);
});
testWidgets('색을 바꾸면 JjColors 를 직접 읽는 곳도 다시 그림 · 상태는 그대로', (t) async {
await t.pumpWidget(const MaterialApp(home: Scaffold(body: _Probe())));
await t.tap(find.text('probe 0'));
await t.pump();
expect(t.widget<Text>(find.text('probe 1')).style!.color, JjPalette.dark.text);
expect(JjColors.use(Brightness.light), isTrue);
expect(JjColors.use(Brightness.light), isFalse, reason: '같으면 바꾸지 않음');
rebuildAllElements(t.binding.rootElement!);
await t.pump();
expect(t.widget<Text>(find.text('probe 1')).style!.color, JjPalette.light.text, reason: '눌러 둔 수 (상태) 는 그대로');
});
testWidgets('앱: 설정 "화면 밝기" 를 밝게 → 바로 밝은 테마 · 기기 따라는 기기 밝기를 따라 바뀜 · 어둡게로 돌아옴', (t) async {
t.view.physicalSize = const Size(1400, 900);
t.view.devicePixelRatio = 1;
addTearDown(t.view.reset);
addTearDown(t.platformDispatcher.clearPlatformBrightnessTestValue);
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
await t.runAsync(c.init);
await t.pumpWidget(JjMkvMakerApp(controller: c));
await t.pump();
BuildContext ctx() => t.element(find.byType(Scaffold).first);
expect(Theme.of(ctx()).brightness, Brightness.dark);
expect(JjColors.isLight, isFalse);
await t.runAsync(() => c.updateSettings((x) => x.uiBrightness = 'light'));
await t.pump();
await t.pump();
expect(JjColors.isLight, isTrue);
expect(Theme.of(ctx()).brightness, Brightness.light);
expect(Theme.of(ctx()).scaffoldBackgroundColor, JjPalette.light.bg);
// 기기 따라
t.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
await t.runAsync(() => c.updateSettings((x) => x.uiBrightness = 'system'));
await t.pump();
await t.pump();
expect(JjColors.isLight, isFalse);
t.platformDispatcher.platformBrightnessTestValue = Brightness.light;
await t.pump();
await t.pump();
expect(JjColors.isLight, isTrue, reason: '기기 밝기가 바뀌면 따라감');
await t.runAsync(() => c.updateSettings((x) => x.uiBrightness = 'dark'));
await t.pump();
await t.pump();
expect(JjColors.isLight, isFalse);
expect(Theme.of(ctx()).brightness, Brightness.dark);
await t.pumpWidget(const SizedBox());
});
}