diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 10a325b..1673d48 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -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", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 0c01a62..102bf1e 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -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 @@ "화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "画面の中央を左右にスワイプすると別の画面 (ファイルエクスプローラー · ダウンロードなど) に切り替わります。オフにする: 環境設定 > コンポーネント > 左右スワイプで画面を移動", "화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "画面の文字をすべてこの端末で翻訳します。数分かかることがあります。", "화면 모양 따라": "画面の形に合わせる", + "화면 밝기": "画面の明るさ", "화면 방향": "画面の向き", "화면 방향: 가로 고정 → 자동 (기기를 돌리는 대로). 되돌리기: 환경 설정 > 화면 > 화면 방향": "画面の向き: 横向き固定 → 自動 (端末の向きに合わせる)。元に戻す: 環境設定 > 画面 > 画面の向き", "화면 비율": "画面比率", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 1aec967..2dc7e9b 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -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 @@ "화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "在屏幕中间左右滑动会切换到其他屏幕(文件浏览器 · 下载等)。关闭:设置 > 组件 > 左右滑动切换界面", "화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "将在此设备上翻译所有界面文字。可能需要几分钟。", "화면 모양 따라": "跟随屏幕形状", + "화면 밝기": "界面亮度", "화면 방향": "屏幕方向", "화면 방향: 가로 고정 → 자동 (기기를 돌리는 대로). 되돌리기: 환경 설정 > 화면 > 화면 방향": "屏幕方向: 固定横屏 → 自动 (随设备旋转)。恢复: 设置 > 屏幕 > 屏幕方向", "화면 비율": "画面比例", diff --git a/lib/app/settings.dart b/lib/app/settings.dart index aebcf34..f1bf14c 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -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?) diff --git a/lib/main.dart b/lib/main.dart index 14812a1..5e4b086 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { const _CupertinoFallbackDelegate(); diff --git a/lib/ui/ai_dialog.dart b/lib/ui/ai_dialog.dart index ca28a19..3eb4694 100644 --- a/lib/ui/ai_dialog.dart +++ b/lib/ui/ai_dialog.dart @@ -94,7 +94,7 @@ Future 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), diff --git a/lib/ui/ai_image_page.dart b/lib/ui/ai_image_page.dart index 5acf2f3..4019dc4 100644 --- a/lib/ui/ai_image_page.dart +++ b/lib/ui/ai_image_page.dart @@ -293,7 +293,7 @@ class _AiImagePageState extends State 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 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 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, diff --git a/lib/ui/ai_settings.dart b/lib/ui/ai_settings.dart index fd6f78b..52a028a 100644 --- a/lib/ui/ai_settings.dart +++ b/lib/ui/ai_settings.dart @@ -111,7 +111,7 @@ class _AiSettingsState extends State { 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!), diff --git a/lib/ui/ai_subtitle_models.dart b/lib/ui/ai_subtitle_models.dart index 7e9a6ed..f218b5f 100644 --- a/lib/ui/ai_subtitle_models.dart +++ b/lib/ui/ai_subtitle_models.dart @@ -84,7 +84,7 @@ class _AiSubtitleModelsState extends State { 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)), ), ]); }, diff --git a/lib/ui/ai_upscale_ui.dart b/lib/ui/ai_upscale_ui.dart index ed94d9b..dea56b9 100644 --- a/lib/ui/ai_upscale_ui.dart +++ b/lib/ui/ai_upscale_ui.dart @@ -122,7 +122,7 @@ Future _download(BuildContext context, AiStore store, List 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 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; diff --git a/lib/ui/android_file_browser.dart b/lib/ui/android_file_browser.dart index 02970e6..942115e 100644 --- a/lib/ui/android_file_browser.dart +++ b/lib/ui/android_file_browser.dart @@ -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), diff --git a/lib/ui/app_actions.dart b/lib/ui/app_actions.dart index 866463d..10ec555 100644 --- a/lib/ui/app_actions.dart +++ b/lib/ui/app_actions.dart @@ -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, ), ]); diff --git a/lib/ui/app_manager_page.dart b/lib/ui/app_manager_page.dart index 27e1a7b..03a991b 100644 --- a/lib/ui/app_manager_page.dart +++ b/lib/ui/app_manager_page.dart @@ -83,7 +83,7 @@ class _AppManagerPageState extends State { 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 { 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 { ), ]), ), - 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( diff --git a/lib/ui/archive_ui.dart b/lib/ui/archive_ui.dart index 1a14a63..2998349 100644 --- a/lib/ui/archive_ui.dart +++ b/lib/ui/archive_ui.dart @@ -45,7 +45,7 @@ Future 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 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 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 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 createZipFlow(BuildContext context, AppController c, List 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 createZipFlow(BuildContext context, AppController c, List c > 0x7e)) Text(tr('한글 등 영문 밖 글자가 든 비밀번호는 이 앱에서는 풀리지만 7-Zip 등 다른 프로그램에서는 풀리지 않을 수 있습니다. 다른 기기 · 프로그램과 나눌 ZIP 은 영문 · 숫자 · 기호 비밀번호를 쓰세요.'), - style: const TextStyle(fontSize: 12, color: JjColors.danger)), + style: TextStyle(fontSize: 12, color: JjColors.danger)), ], ]), actions: [ diff --git a/lib/ui/audio_now_playing.dart b/lib/ui/audio_now_playing.dart index 738fe96..a52fe88 100644 --- a/lib/ui/audio_now_playing.dart +++ b/lib/ui/audio_now_playing.dart @@ -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))), diff --git a/lib/ui/batch_rename_dialog.dart b/lib/ui/batch_rename_dialog.dart index 8a73014..e0451e3 100644 --- a/lib/ui/batch_rename_dialog.dart +++ b/lib/ui/batch_rename_dialog.dart @@ -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)), ), ]), ), diff --git a/lib/ui/bookmark_ui.dart b/lib/ui/bookmark_ui.dart index 1530bcb..6e62b3a 100644 --- a/lib/ui/bookmark_ui.dart +++ b/lib/ui/bookmark_ui.dart @@ -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 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 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 { 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 { 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 { 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 { 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), // 아이콘을 끌면 다른 폴더로 옮기기 diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index e16241c..3f9ba52 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -273,7 +273,7 @@ class _BrowserPageState extends State 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 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 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 { 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 { 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 { 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), // 아이콘을 끌어 폴더 줄 · 위 경로에 놓으면 그 폴더로 diff --git a/lib/ui/cef_setup.dart b/lib/ui/cef_setup.dart index f7eff9a..c1f74bc 100644 --- a/lib/ui/cef_setup.dart +++ b/lib/ui/cef_setup.dart @@ -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: [ diff --git a/lib/ui/cleanup_dialog.dart b/lib/ui/cleanup_dialog.dart index 3fe7509..7cbd6bd 100644 --- a/lib/ui/cleanup_dialog.dart +++ b/lib/ui/cleanup_dialog.dart @@ -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, diff --git a/lib/ui/component_settings.dart b/lib/ui/component_settings.dart index 073fba4..a10a2cc 100644 --- a/lib/ui/component_settings.dart +++ b/lib/ui/component_settings.dart @@ -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( diff --git a/lib/ui/copy_sync_settings.dart b/lib/ui/copy_sync_settings.dart index bb0b5ef..25dfe90 100644 --- a/lib/ui/copy_sync_settings.dart +++ b/lib/ui/copy_sync_settings.dart @@ -219,7 +219,7 @@ class _CopySyncSettingsState extends State { ) : 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, ]), ]); diff --git a/lib/ui/dav_external.dart b/lib/ui/dav_external.dart index cb82765..a0ff66b 100644 --- a/lib/ui/dav_external.dart +++ b/lib/ui/dav_external.dart @@ -30,7 +30,7 @@ Future 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: [ diff --git a/lib/ui/dlna_page.dart b/lib/ui/dlna_page.dart index 0510960..9c0890f 100644 --- a/lib/ui/dlna_page.dart +++ b/lib/ui/dlna_page.dart @@ -200,9 +200,9 @@ class _DlnaPageState extends State { 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 { 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), ), ]); diff --git a/lib/ui/downloads_page.dart b/lib/ui/downloads_page.dart index b25bd4e..8cb5490 100644 --- a/lib/ui/downloads_page.dart +++ b/lib/ui/downloads_page.dart @@ -133,7 +133,7 @@ class _DownloadsPageState extends State { 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 { ? 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]); diff --git a/lib/ui/exit_dialog.dart b/lib/ui/exit_dialog.dart index dfb605f..a70c51a 100644 --- a/lib/ui/exit_dialog.dart +++ b/lib/ui/exit_dialog.dart @@ -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( diff --git a/lib/ui/explorer_bookmarks.dart b/lib/ui/explorer_bookmarks.dart index 27422b2..12b1c6c 100644 --- a/lib/ui/explorer_bookmarks.dart +++ b/lib/ui/explorer_bookmarks.dart @@ -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( diff --git a/lib/ui/explorer_drag.dart b/lib/ui/explorer_drag.dart index 8a278cf..8faa636 100644 --- a/lib/ui/explorer_drag.dart +++ b/lib/ui/explorer_drag.dart @@ -75,10 +75,10 @@ Widget dragFeedback(List 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)), ]), ), ); diff --git a/lib/ui/explorer_grid.dart b/lib/ui/explorer_grid.dart index f462608..090423e 100644 --- a/lib/ui/explorer_grid.dart +++ b/lib/ui/explorer_grid.dart @@ -79,14 +79,14 @@ class _ExplorerGridPageState extends State { picture = FutureBuilder( 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, diff --git a/lib/ui/explorer_keys_page.dart b/lib/ui/explorer_keys_page.dart index fde526f..9472ab6 100644 --- a/lib/ui/explorer_keys_page.dart +++ b/lib/ui/explorer_keys_page.dart @@ -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( diff --git a/lib/ui/explorer_look.dart b/lib/ui/explorer_look.dart index 581d620..fb90372 100644 --- a/lib/ui/explorer_look.dart +++ b/lib/ui/explorer_look.dart @@ -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 ? [ diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index e406b1e..248f1be 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -743,7 +743,7 @@ class _ExplorerPageState extends State { 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 { 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 { 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 { 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 { 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 { ], 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 { 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>( future: clashesFuture, @@ -3493,7 +3493,7 @@ class _ExplorerPageState extends State { 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 []; @@ -3733,18 +3733,18 @@ class _ExplorerPageState extends State { 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 { // 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 { 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 { 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 { 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 { 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 { 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 { 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 { : 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 { ]), 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 { 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 { 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 { 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 { ), ], 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( @@ -5082,8 +5082,8 @@ class _ExplorerPageState extends State { 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 { 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 { 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 { 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: [ diff --git a/lib/ui/explorer_search.dart b/lib/ui/explorer_search.dart index ce434cf..3028e42 100644 --- a/lib/ui/explorer_search.dart +++ b/lib/ui/explorer_search.dart @@ -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 { 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 { 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 { 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 { ), 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)), ), ]), ), diff --git a/lib/ui/file_tools.dart b/lib/ui/file_tools.dart index d90cb2f..3e83386 100644 --- a/lib/ui/file_tools.dart +++ b/lib/ui/file_tools.dart @@ -81,7 +81,7 @@ Future 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 { 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 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) ...[ diff --git a/lib/ui/ftp_settings.dart b/lib/ui/ftp_settings.dart index 7cb9138..ec2c731 100644 --- a/lib/ui/ftp_settings.dart +++ b/lib/ui/ftp_settings.dart @@ -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 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 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( groupValue: security, onChanged: (x) => set(() { @@ -238,7 +238,7 @@ Future 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 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 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, diff --git a/lib/ui/hidden_items_dialog.dart b/lib/ui/hidden_items_dialog.dart index c47352e..a13f430 100644 --- a/lib/ui/hidden_items_dialog.dart +++ b/lib/ui/hidden_items_dialog.dart @@ -28,7 +28,7 @@ Future 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) diff --git a/lib/ui/home_page.dart b/lib/ui/home_page.dart index 6bdba63..273799a 100644 --- a/lib/ui/home_page.dart +++ b/lib/ui/home_page.dart @@ -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), ]), ), diff --git a/lib/ui/language_settings.dart b/lib/ui/language_settings.dart index c251ee1..35978fa 100644 --- a/lib/ui/language_settings.dart +++ b/lib/ui/language_settings.dart @@ -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 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(), ]), ), diff --git a/lib/ui/license_texts.dart b/lib/ui/license_texts.dart index f3547ee..91e4339 100644 --- a/lib/ui/license_texts.dart +++ b/lib/ui/license_texts.dart @@ -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), diff --git a/lib/ui/master_prompt.dart b/lib/ui/master_prompt.dart index 4094bd6..1c98a07 100644 --- a/lib/ui/master_prompt.dart +++ b/lib/ui/master_prompt.dart @@ -262,7 +262,7 @@ Future _done(BuildContext context, AppController c, MasterLock lock, List< final setNew = await showDialog( 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 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, diff --git a/lib/ui/monitor_page.dart b/lib/ui/monitor_page.dart index b68c8b1..c8b498a 100644 --- a/lib/ui/monitor_page.dart +++ b/lib/ui/monitor_page.dart @@ -130,17 +130,17 @@ Future _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 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: [ diff --git a/lib/ui/network_settings.dart b/lib/ui/network_settings.dart index 289b060..8f32731 100644 --- a/lib/ui/network_settings.dart +++ b/lib/ui/network_settings.dart @@ -228,7 +228,7 @@ class _DnsSettingsState extends State { 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 { 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)), ), ]); } diff --git a/lib/ui/player_page.dart b/lib/ui/player_page.dart index 53a41ae..995f902 100644 --- a/lib/ui/player_page.dart +++ b/lib/ui/player_page.dart @@ -379,7 +379,7 @@ class _PlayerPageState extends State { 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 [ diff --git a/lib/ui/quick_look.dart b/lib/ui/quick_look.dart index 2c55b1d..67625fc 100644 --- a/lib/ui/quick_look.dart +++ b/lib/ui/quick_look.dart @@ -124,7 +124,7 @@ class _QuickLookState extends State { Widget _info(List 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 { 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 { // 큰 파일 · 느린 디스크에서도 창이 멈추지 않게 (기다리는 동안 "읽는 중", 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 { 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 { [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))), diff --git a/lib/ui/reader_page.dart b/lib/ui/reader_page.dart index e61e346..0d42080 100644 --- a/lib/ui/reader_page.dart +++ b/lib/ui/reader_page.dart @@ -920,7 +920,7 @@ class _ReaderPageState extends State { 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 { 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)), ), ], ]), diff --git a/lib/ui/remote_thumb_view.dart b/lib/ui/remote_thumb_view.dart index dd158d6..ca33216 100644 --- a/lib/ui/remote_thumb_view.dart +++ b/lib/ui/remote_thumb_view.dart @@ -116,7 +116,7 @@ class _RemoteThumbState extends State { 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)), ]), ); } diff --git a/lib/ui/rsync_setup.dart b/lib/ui/rsync_setup.dart index 6f016ce..e8ce875 100644 --- a/lib/ui/rsync_setup.dart +++ b/lib/ui/rsync_setup.dart @@ -31,7 +31,7 @@ Future 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), ]), diff --git a/lib/ui/schedule_editor.dart b/lib/ui/schedule_editor.dart index 1258853..ec5af8b 100644 --- a/lib/ui/schedule_editor.dart +++ b/lib/ui/schedule_editor.dart @@ -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)), ), ), ), diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 0df599f..ada19db 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -348,7 +348,7 @@ class _SettingsPageState extends State { 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 { _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( future: c.services.updater!.currentVersion(), builder: (_, v) => Text('JJ_MKVMaker v${v.data ?? '…'}'), @@ -497,6 +497,28 @@ class _SettingsPageState extends State { ..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( + 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 { 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 { 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 { 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 { 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 { _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( 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 { 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 { 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)), ); diff --git a/lib/ui/sftp_settings.dart b/lib/ui/sftp_settings.dart index 7d042f4..1936e57 100644 --- a/lib/ui/sftp_settings.dart +++ b/lib/ui/sftp_settings.dart @@ -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 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 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 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, diff --git a/lib/ui/smb_settings.dart b/lib/ui/smb_settings.dart index 04f9b5d..e64f377 100644 --- a/lib/ui/smb_settings.dart +++ b/lib/ui/smb_settings.dart @@ -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 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) diff --git a/lib/ui/sqlite_viewer.dart b/lib/ui/sqlite_viewer.dart index d938797..f29d211 100644 --- a/lib/ui/sqlite_viewer.dart +++ b/lib/ui/sqlite_viewer.dart @@ -151,7 +151,7 @@ class _SqliteViewerPageState extends State { 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 { ), ), 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) diff --git a/lib/ui/storage_page.dart b/lib/ui/storage_page.dart index 08b39ee..9121293 100644 --- a/lib/ui/storage_page.dart +++ b/lib/ui/storage_page.dart @@ -158,7 +158,7 @@ class _StorageAnalysisPageState extends State { 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 { 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( diff --git a/lib/ui/subtitle_editor_page.dart b/lib/ui/subtitle_editor_page.dart index c3caa3b..e96e239 100644 --- a/lib/ui/subtitle_editor_page.dart +++ b/lib/ui/subtitle_editor_page.dart @@ -453,7 +453,7 @@ class _SubtitleEditorPageState extends State { 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 { 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), ), ]), diff --git a/lib/ui/subtitle_search_dialog.dart b/lib/ui/subtitle_search_dialog.dart index 48b9d80..f46a1df 100644 --- a/lib/ui/subtitle_search_dialog.dart +++ b/lib/ui/subtitle_search_dialog.dart @@ -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()), ]), diff --git a/lib/ui/sync_preview_view.dart b/lib/ui/sync_preview_view.dart index 6b3f687..ab8d7f4 100644 --- a/lib/ui/sync_preview_view.dart +++ b/lib/ui/sync_preview_view.dart @@ -93,7 +93,7 @@ class _SyncPreviewViewState extends State { 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 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( diff --git a/lib/ui/text_viewer.dart b/lib/ui/text_viewer.dart index df1e1b0..373d856 100644 --- a/lib/ui/text_viewer.dart +++ b/lib/ui/text_viewer.dart @@ -289,7 +289,7 @@ class _TextViewerPageState extends State { 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('더 읽기'))), ]), diff --git a/lib/ui/theme.dart b/lib/ui/theme.dart index ba172f6..389da51 100644 --- a/lib/ui/theme.dart +++ b/lib/ui/theme.dart @@ -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)), ), ), diff --git a/lib/ui/timeline.dart b/lib/ui/timeline.dart index 64f2b55..17f98d7 100644 --- a/lib/ui/timeline.dart +++ b/lib/ui/timeline.dart @@ -162,7 +162,7 @@ class _SubtitleTimelineState extends State { 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)); } diff --git a/lib/ui/tor_page.dart b/lib/ui/tor_page.dart index 66c0d21..7c522b9 100644 --- a/lib/ui/tor_page.dart +++ b/lib/ui/tor_page.dart @@ -634,7 +634,7 @@ class _TorPageState extends State 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), ]), ), ), diff --git a/lib/ui/translate_dialog.dart b/lib/ui/translate_dialog.dart index d27b582..cdb5c3f 100644 --- a/lib/ui/translate_dialog.dart +++ b/lib/ui/translate_dialog.dart @@ -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)), diff --git a/lib/ui/trash_page.dart b/lib/ui/trash_page.dart index 37dad70..2eef8e9 100644 --- a/lib/ui/trash_page.dart +++ b/lib/ui/trash_page.dart @@ -103,8 +103,8 @@ class _TrashPageState extends State { 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 { 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 { 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( diff --git a/lib/ui/update_dialog.dart b/lib/ui/update_dialog.dart index 3dcbb90..57d1029 100644 --- a/lib/ui/update_dialog.dart +++ b/lib/ui/update_dialog.dart @@ -75,7 +75,7 @@ Future 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 { 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( diff --git a/lib/ui/vault_page.dart b/lib/ui/vault_page.dart index 12fbf2a..9d071f3 100644 --- a/lib/ui/vault_page.dart +++ b/lib/ui/vault_page.dart @@ -343,7 +343,7 @@ class _VaultPageState extends State 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 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 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( @@ -922,7 +922,7 @@ class _VaultPageState extends State 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 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), diff --git a/lib/ui/video_adjust_dialog.dart b/lib/ui/video_adjust_dialog.dart index 6abea8f..99f498f 100644 --- a/lib/ui/video_adjust_dialog.dart +++ b/lib/ui/video_adjust_dialog.dart @@ -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( diff --git a/lib/ui/webdav_settings.dart b/lib/ui/webdav_settings.dart index 7b1c07c..f4a6afa 100644 --- a/lib/ui/webdav_settings.dart +++ b/lib/ui/webdav_settings.dart @@ -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), ), diff --git a/lib/ui/wifi_share_page.dart b/lib/ui/wifi_share_page.dart index d240203..249b498 100644 --- a/lib/ui/wifi_share_page.dart +++ b/lib/ui/wifi_share_page.dart @@ -111,7 +111,7 @@ class _WifiSharePageState extends State { List _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 { 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 { ), ]), 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 { ), ]), 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 { ]), 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 { ], ); - 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), diff --git a/lib/ui/work_panel.dart b/lib/ui/work_panel.dart index 83946e9..37d905d 100644 --- a/lib/ui/work_panel.dart +++ b/lib/ui/work_panel.dart @@ -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), ), ), ], diff --git a/test/theme_test.dart b/test/theme_test.dart new file mode 100644 index 0000000..725d50f --- /dev/null +++ b/test/theme_test.dart @@ -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(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(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()); + }); +}