WIP X-plore 비교 5-1 ⑦ 밝은 테마 바탕: 색이 테마를 따름 · 설정 "화면 밝기" (시험) (검토 대기)
관리자 지시 10/21 (⑦ (가) 바탕): 처음 값은 어둡게 (예전 색 그대로), 설정 "화면 밝기: 어두운 화면 / 밝은 화면 / 기기 따라" 에 "시험" 표시. - JjColors 를 두 벌 (JjPalette.dark = 예전 값 그대로 · light) 에서 고르는 값으로. 그래서 JjColors 를 쓰던 const 는 뺐다 (기계적으로, 62개 파일 294곳) - 밝기를 바꾸면 테마와 함께 화면 조각 전체를 다시 그림 (상태는 그대로) - JjColors 를 직접 읽는 곳도 바로 새 색 - 기기 따라: 기기 밝기가 바뀌면 따라감. 모르는 값 · 없는 값은 어둡게 - 밝은 색은 바탕 · 칸 위 글 · 흐린 글 · 강조 · 경고 · 성공 대비 4.5:1 이상 (시험으로 확인) - 화면별 맞추기 ((나): 탐색기 → 보기 → 설정 → 나머지) 는 다음 커밋들에서 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_p3b
parent
ab735396ac
commit
9324e4100f
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
"화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "画面の中央を左右にスワイプすると別の画面 (ファイルエクスプローラー · ダウンロードなど) に切り替わります。オフにする: 環境設定 > コンポーネント > 左右スワイプで画面を移動",
|
||||
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "画面の文字をすべてこの端末で翻訳します。数分かかることがあります。",
|
||||
"화면 모양 따라": "画面の形に合わせる",
|
||||
"화면 밝기": "画面の明るさ",
|
||||
"화면 방향": "画面の向き",
|
||||
"화면 방향: 가로 고정 → 자동 (기기를 돌리는 대로). 되돌리기: 환경 설정 > 화면 > 화면 방향": "画面の向き: 横向き固定 → 自動 (端末の向きに合わせる)。元に戻す: 環境設定 > 画面 > 画面の向き",
|
||||
"화면 비율": "画面比率",
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
"화면 가운데를 좌우로 밀면 다른 화면 (파일 탐색기 · 다운로드 등) 으로 넘어갑니다. 끄기: 환경 설정 > 컴포넌트 > 좌우로 밀어 화면 이동": "在屏幕中间左右滑动会切换到其他屏幕(文件浏览器 · 下载等)。关闭:设置 > 组件 > 左右滑动切换界面",
|
||||
"화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.": "将在此设备上翻译所有界面文字。可能需要几分钟。",
|
||||
"화면 모양 따라": "跟随屏幕形状",
|
||||
"화면 밝기": "界面亮度",
|
||||
"화면 방향": "屏幕方向",
|
||||
"화면 방향: 가로 고정 → 자동 (기기를 돌리는 대로). 되돌리기: 환경 설정 > 화면 > 화면 방향": "屏幕方向: 固定横屏 → 自动 (随设备旋转)。恢复: 设置 > 屏幕 > 屏幕方向",
|
||||
"화면 비율": "画面比例",
|
||||
|
|
|
|||
|
|
@ -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?)
|
||||
|
|
|
|||
|
|
@ -834,10 +834,10 @@ class JjMkvMakerApp extends StatelessWidget {
|
|||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListenableBuilder(
|
||||
Widget build(BuildContext context) => _BrightnessHost(
|
||||
c: controller,
|
||||
// 화면 언어를 바꾸면 (i18n) Flutter 기본 글도 그 언어로
|
||||
listenable: i18n,
|
||||
builder: (context, _) => _app(context),
|
||||
builder: (context) => ListenableBuilder(listenable: i18n, builder: (context, _) => _app(context)),
|
||||
);
|
||||
|
||||
Widget _app(BuildContext context) {
|
||||
|
|
@ -849,6 +849,8 @@ class JjMkvMakerApp extends StatelessWidget {
|
|||
title: appTitle,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: buildTheme(),
|
||||
// 밝기 바꿈은 화면 전체를 다시 그려 바로 (전환 그림 없이 - JjColors 를 직접 읽는 곳과 같은 때 바뀌게)
|
||||
themeAnimationDuration: Duration.zero,
|
||||
navigatorKey: navigatorKey,
|
||||
scaffoldMessengerKey: messengerKey,
|
||||
navigatorObservers: [browserRouteObserver],
|
||||
|
|
@ -864,6 +866,51 @@ class JjMkvMakerApp extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// X-plore 비교 5-1 ⑦: 환경 설정 "화면 밝기" · 기기 밝기를 따라 [JjColors] 를 바꾸고 화면 전체를 다시 그린다 (상태는 그대로)
|
||||
class _BrightnessHost extends StatefulWidget {
|
||||
final AppController c;
|
||||
final WidgetBuilder builder;
|
||||
const _BrightnessHost({required this.c, required this.builder});
|
||||
@override
|
||||
State<_BrightnessHost> createState() => _BrightnessHostState();
|
||||
}
|
||||
|
||||
class _BrightnessHostState extends State<_BrightnessHost> with WidgetsBindingObserver {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
widget.c.addListener(_check);
|
||||
JjColors.use(_want());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.c.removeListener(_check);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Brightness _want() =>
|
||||
resolveBrightness(widget.c.settings.uiBrightness, WidgetsBinding.instance.platformDispatcher.platformBrightness);
|
||||
|
||||
@override
|
||||
void didChangePlatformBrightness() => _check();
|
||||
|
||||
void _check() {
|
||||
if (!mounted || !JjColors.use(_want())) return;
|
||||
setState(() {});
|
||||
// JjColors 를 직접 읽는 화면 조각도 새 색으로 (그 조각들은 Theme 이 바뀌어도 다시 그려지지 않는다)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final root = WidgetsBinding.instance.rootElement;
|
||||
if (root != null) rebuildAllElements(root);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.builder(context);
|
||||
}
|
||||
|
||||
/// Cupertino 글이 없는 언어만 영어로 (GlobalCupertinoLocalizations 뒤에 두어 그쪽이 아는 언어는 그쪽이 먼저)
|
||||
class _CupertinoFallbackDelegate extends LocalizationsDelegate<CupertinoLocalizations> {
|
||||
const _CupertinoFallbackDelegate();
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ Future<Language?> pickSourceLanguage(BuildContext context, Language detected) =>
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 0, 24, 8),
|
||||
child: Text(tr('처음부터 정하려면 AI 자막 창의 "원어 (영상 속 언어)" 에서 고르세요.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
for (final l in languages)
|
||||
SimpleDialogOption(
|
||||
|
|
@ -172,7 +172,7 @@ class AiPlanSummary extends StatelessWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(width: 84, child: Text(k, style: const TextStyle(fontSize: 12, color: JjColors.textDim))),
|
||||
SizedBox(width: 84, child: Text(k, style: TextStyle(fontSize: 12, color: JjColors.textDim))),
|
||||
Expanded(child: Text(v, style: const TextStyle(fontSize: 13))),
|
||||
]),
|
||||
),
|
||||
|
|
@ -221,7 +221,7 @@ class _AiDialogState extends State<_AiDialog> {
|
|||
/// 259: 번역이 없으면 받지 않는다 · 감지한 뒤에야 알 수 있으면 그때 받는다
|
||||
Widget _nllbStatus() {
|
||||
if (_installed[nllbModel.id] != false || _o.surelyNeedsTranslation) return _status(nllbModel);
|
||||
const style = TextStyle(fontSize: 11, color: JjColors.textDim);
|
||||
final style = TextStyle(fontSize: 11, color: JjColors.textDim);
|
||||
if (!_o.needsTranslation) return Text(tr('이번에는 쓰지 않음 (번역 없음)'), style: style);
|
||||
return Text(trf('감지한 원어가 만들 언어와 다를 때만 내려받기 {0}', [nllbModel.sizeLabel]), style: style);
|
||||
}
|
||||
|
|
@ -239,7 +239,7 @@ class _AiDialogState extends State<_AiDialog> {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(trf('영상의 음성을 인식하고 선택한 언어로 번역합니다. 모든 처리는 이 {0}에서 이루어집니다.', [Platform.isAndroid ? tr('기기') : 'PC']),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 10),
|
||||
// 138: 무엇을 할지 먼저 (바꾸면 따라 바뀐다)
|
||||
AiPlanSummary(videos: _all ? widget.c.videos : widget.videos, o: _o, thenBuild: widget.thenBuild),
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
onPressed: () => setState(() => _showDetail = !_showDetail),
|
||||
child: Text(_showDetail ? tr('자세히 닫기') : tr('자세히')),
|
||||
),
|
||||
if (_showDetail) SelectableText(_detail, style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
if (_showDetail) SelectableText(_detail, style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
]),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -374,7 +374,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(tr('파일이 없습니다 (지워졌거나 옮겨짐)'),
|
||||
textAlign: TextAlign.center, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
textAlign: TextAlign.center, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
|
|
@ -393,7 +393,7 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
child: Image.file(File(g.path), width: 160, height: 160, fit: BoxFit.cover),
|
||||
),
|
||||
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(trf('시드 {0}', [g.seed]), style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
Text(trf('시드 {0}', [g.seed]), style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
IconButton(
|
||||
tooltip: tr('같은 조건으로 다시 (시드 그대로)'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class _AiSettingsState extends State<AiSettings> {
|
|||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(tr(f.terms!)),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(f.licenseUrl, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
SelectableText(f.licenseUrl, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (f.licenseAsset != null)
|
||||
TextButton(
|
||||
onPressed: () => showLicenseDocument(ctx, trf('{0} 라이선스', [tr(f.license)]), f.licenseAsset!),
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ class _AiSubtitleModelsState extends State<AiSubtitleModels> {
|
|||
if (_error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 0),
|
||||
child: Text(_error!, style: const TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
child: Text(_error!, style: TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
),
|
||||
]);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ Future<bool> _download(BuildContext context, AiStore store, List<AiFile> files)
|
|||
for (final f in files) ...[
|
||||
Text(f.name, style: const TextStyle(fontSize: 13)),
|
||||
Text(store.isFileInstalled(f) ? tr('받음') : store.progressText(f.id) ?? tr('준비하는 중'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
]),
|
||||
|
|
@ -225,11 +225,11 @@ Future<String?> upscaleOne(BuildContext context, AppController c, {required Stri
|
|||
: '${(v * 100).round()}% · ${trf('약 {0} 남음', [AiJobs.durationText(d)])}',
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
if (estimate != null) Text(estimate, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (estimate != null) Text(estimate, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (cpuAnime) ...[
|
||||
const SizedBox(height: 10),
|
||||
Text(tr("사진에는 '사진' 모델이 더 낫지만, 이 기기 (그래픽카드 없음) 에서는 한 장에 약 20분 걸립니다."),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
toPhoto = true;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class _AppManagerPageState extends State<AppManagerPage> {
|
|||
ListTile(leading: const Icon(Icons.info_outline), title: Text(tr('앱 정보 (시스템 화면)')), onTap: () => Navigator.pop(ctx, 'details')),
|
||||
if (!a.system)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline, color: JjColors.danger),
|
||||
leading: Icon(Icons.delete_outline, color: JjColors.danger),
|
||||
title: Text(tr('지우기 (시스템 창에서 확인)')),
|
||||
onTap: () => Navigator.pop(ctx, 'uninstall')),
|
||||
]),
|
||||
|
|
@ -110,7 +110,7 @@ class _AppManagerPageState extends State<AppManagerPage> {
|
|||
body: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Text(tr('보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
child: Text(tr('보이는 앱만 나옵니다 (모든 앱 목록 권한은 쓰지 않음)'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
|
|
@ -133,7 +133,7 @@ class _AppManagerPageState extends State<AppManagerPage> {
|
|||
),
|
||||
]),
|
||||
),
|
||||
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: const TextStyle(color: JjColors.danger))),
|
||||
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: TextStyle(color: JjColors.danger))),
|
||||
if (apps == null && _error == null) const Expanded(child: Center(child: CircularProgressIndicator())),
|
||||
if (apps != null)
|
||||
Expanded(
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Future<String?> askArchivePassword(BuildContext context, String name, {bool wron
|
|||
title: Text(tr('비밀번호')),
|
||||
content: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(trf('"{0}" 은(는) 비밀번호가 걸려 있습니다.', [name])),
|
||||
if (wrong) Text(tr('비밀번호가 틀렸습니다'), style: const TextStyle(color: JjColors.danger)),
|
||||
if (wrong) Text(tr('비밀번호가 틀렸습니다'), style: TextStyle(color: JjColors.danger)),
|
||||
TextField(
|
||||
controller: ctl,
|
||||
autofocus: true,
|
||||
|
|
@ -183,7 +183,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
const SizedBox(height: 8),
|
||||
Text(trf('밖으로 나가는 경로라 풀지 않습니다 ({0}개): {1}',
|
||||
[plan.unsafe.length, plan.unsafe.take(5).map((x) => '${x.$1} (${x.$2})').join(', ')]),
|
||||
style: const TextStyle(color: JjColors.danger, fontSize: 12)),
|
||||
style: TextStyle(color: JjColors.danger, fontSize: 12)),
|
||||
],
|
||||
if (plan.badNames.isNotEmpty) ...[
|
||||
const SizedBox(height: 6),
|
||||
|
|
@ -198,11 +198,11 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
],
|
||||
if (plan.encrypted) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(tr('비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(tr('비밀번호가 걸린 파일이 있습니다 (풀 때 묻습니다)'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
],
|
||||
const SizedBox(height: 10),
|
||||
_placeChoice(ctx, choices, dest, (d) => set(() => dest = d)),
|
||||
Text(vDisplay(dest), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(vDisplay(dest), maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 6),
|
||||
// 좁은 화면 · 큰 글자면 고르기 상자를 다음 줄로 (412 · 글자 1.6배에서 화면 밖으로 나갔다 - 253 시험)
|
||||
Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
|
|
@ -227,7 +227,7 @@ Future<String?> extractArchiveFlow(BuildContext context, AppController c, String
|
|||
// 관리자 판단: 풀린 크기가 빈 공간보다 크면 풀기 전에 알리고 묻는다 (압축 폭탄 · 공간 부족)
|
||||
if (short)
|
||||
Text(trf('풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?', [formatSize(plan.totalSize), formatSize(f)]),
|
||||
style: const TextStyle(color: JjColors.danger)),
|
||||
style: TextStyle(color: JjColors.danger)),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
|
|
@ -351,8 +351,8 @@ Future<String?> createZipFlow(BuildContext context, AppController c, List<String
|
|||
]),
|
||||
const SizedBox(height: 6),
|
||||
_placeChoice(ctx, choices, where, (d) => set(() => where = d)),
|
||||
Text(trf('만들 파일: {0}', [p.basename(target())]), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(tr('링크 · junction 은 넣지 않습니다'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(trf('만들 파일: {0}', [p.basename(target())]), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(tr('링크 · junction 은 넣지 않습니다'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 10),
|
||||
TextField(
|
||||
controller: pass,
|
||||
|
|
@ -387,11 +387,11 @@ Future<String?> createZipFlow(BuildContext context, AppController c, List<String
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(tr('비밀번호를 잊으면 이 ZIP 을 풀 수 없습니다 (앱이 기억하지 않습니다). 파일 이름 목록은 암호화되지 않습니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
// zip_aes: 7-Zip 등 다른 프로그램은 영문 밖 글자 ZIP 비밀번호를 받지 않는다 (10/17 시험) - 막지는 않고 알린다
|
||||
if (pass.text.codeUnits.any((c) => c > 0x7e))
|
||||
Text(tr('한글 등 영문 밖 글자가 든 비밀번호는 이 앱에서는 풀리지만 7-Zip 등 다른 프로그램에서는 풀리지 않을 수 있습니다. 다른 기기 · 프로그램과 나눌 ZIP 은 영문 · 숫자 · 기호 비밀번호를 쓰세요.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
],
|
||||
]),
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -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))),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ class _BookmarkEditorState extends State<_BookmarkEditor> {
|
|||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, 'del'),
|
||||
child: Text(widget.justAdded ? tr('추가 취소') : tr('삭제'), style: const TextStyle(color: JjColors.danger)),
|
||||
child: Text(widget.justAdded ? tr('추가 취소') : tr('삭제'), style: TextStyle(color: JjColors.danger)),
|
||||
),
|
||||
if (!widget.justAdded) TextButton(onPressed: () => Navigator.pop(context), child: Text(tr('취소'))),
|
||||
FilledButton(onPressed: _save, child: Text(widget.justAdded ? tr('완료') : tr('저장'))),
|
||||
|
|
@ -177,7 +177,7 @@ Future<void> showNewBookmarkDialog(BuildContext context, BookmarksController bm,
|
|||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(trf('폴더: {0}', [bm.tree.find(parentId)?.title ?? '']),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
@ -218,7 +218,7 @@ Future<String?> askText(BuildContext context, String title, String label, String
|
|||
),
|
||||
if (help != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(help, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(help, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
],
|
||||
]),
|
||||
),
|
||||
|
|
@ -370,7 +370,7 @@ class _BookmarkDraggableState extends State<BookmarkDraggable> {
|
|||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(n.isFolder ? Icons.folder : Icons.public, size: 16, color: n.isFolder ? Colors.amber : null),
|
||||
const SizedBox(width: 6),
|
||||
Text(n.title, style: const TextStyle(fontSize: 12, color: JjColors.text)),
|
||||
Text(n.title, style: TextStyle(fontSize: 12, color: JjColors.text)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
|
@ -509,7 +509,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
|
|||
nav: const AppNavButtons(),
|
||||
actions: const AppActions(),
|
||||
middle: Row(children: [
|
||||
const Icon(Icons.bookmarks_outlined, color: JjColors.accent),
|
||||
Icon(Icons.bookmarks_outlined, color: JjColors.accent),
|
||||
const SizedBox(width: 8),
|
||||
if (!isCompact(context)) ...[
|
||||
Text(tr('즐겨찾기 관리자'), style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
|
|
@ -644,7 +644,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
|
|||
Expanded(
|
||||
child: Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
for (final (i, f) in path.indexed) ...[
|
||||
if (i > 0) const Icon(Icons.chevron_right, size: 16, color: JjColors.textDim),
|
||||
if (i > 0) Icon(Icons.chevron_right, size: 16, color: JjColors.textDim),
|
||||
BookmarkFolderDrop(
|
||||
bm: bm,
|
||||
folderId: f.id,
|
||||
|
|
@ -704,7 +704,7 @@ class _BookmarkManagerPageState extends State<BookmarkManagerPage> {
|
|||
leading: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
ReorderableDragStartListener(
|
||||
index: i,
|
||||
child: const Icon(Icons.drag_indicator, size: 18, color: JjColors.textDim),
|
||||
child: Icon(Icons.drag_indicator, size: 18, color: JjColors.textDim),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
// 아이콘을 끌면 다른 폴더로 옮기기
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
textAlign: TextAlign.center, style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(height: 8),
|
||||
Text(tr('인터넷 연결이나 환경 설정 > 네트워크 · DNS 의 서버 주소를 확인하세요.'),
|
||||
textAlign: TextAlign.center, style: const TextStyle(fontSize: 13, color: JjColors.textDim)),
|
||||
textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: JjColors.textDim)),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(spacing: 8, runSpacing: 8, alignment: WrapAlignment.center, children: [
|
||||
FilledButton(
|
||||
|
|
@ -468,7 +468,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 2, 4, 2),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.swipe, size: 16, color: JjColors.accent),
|
||||
Icon(Icons.swipe, size: 16, color: JjColors.accent),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(tr('웹 브라우저에서는 위쪽 막대를 좌우로 밀면 다른 화면으로 옮깁니다 (가운데는 페이지가 밀립니다).'),
|
||||
|
|
@ -489,12 +489,12 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 2, 4, 2),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.timer_outlined, size: 16, color: JjColors.textDim),
|
||||
Icon(Icons.timer_outlined, size: 16, color: JjColors.textDim),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
trf('Tor 대기 중 ({0}분 뒤 끔) - 이 브라우저는 Tor 를 쓰지 않습니다', [(at.difference(DateTime.now()).inSeconds / 60).ceil().clamp(0, 60)]),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
|
@ -1336,7 +1336,7 @@ class _CefElevatedBlocked extends StatelessWidget {
|
|||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.admin_panel_settings_outlined, size: 40, color: JjColors.textDim),
|
||||
Icon(Icons.admin_panel_settings_outlined, size: 40, color: JjColors.textDim),
|
||||
const SizedBox(height: 12),
|
||||
Text(tr('관리자 권한으로 실행 중이라 Chrome 엔진을 쓸 수 없습니다 (보안)'),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textAlign: TextAlign.center),
|
||||
|
|
@ -1344,7 +1344,7 @@ class _CefElevatedBlocked extends StatelessWidget {
|
|||
Text(
|
||||
tr('내장 Chrome 엔진은 샌드박스가 꺼져 있어 관리자 권한으로 돌리면 위험합니다. '
|
||||
'Edge 엔진을 쓰거나, 관리자 권한 없이 프로그램을 실행하세요.'),
|
||||
style: const TextStyle(color: JjColors.textDim),
|
||||
style: TextStyle(color: JjColors.textDim),
|
||||
textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(spacing: 12, runSpacing: 8, alignment: WrapAlignment.center, children: [
|
||||
|
|
@ -1471,12 +1471,12 @@ class _CefViewState extends State<_CefView> {
|
|||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 520),
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.error_outline, size: 40, color: JjColors.textDim),
|
||||
Icon(Icons.error_outline, size: 40, color: JjColors.textDim),
|
||||
const SizedBox(height: 12),
|
||||
Text(tr('Chrome 엔진을 시작하지 못했습니다'),
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 8),
|
||||
Text('$_error', style: const TextStyle(color: JjColors.textDim), textAlign: TextAlign.center),
|
||||
Text('$_error', style: TextStyle(color: JjColors.textDim), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(spacing: 12, runSpacing: 8, alignment: WrapAlignment.center, children: [
|
||||
if (widget.onUseEdge != null)
|
||||
|
|
@ -1785,7 +1785,7 @@ class _BookmarkPanelState extends State<BookmarkPanel> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 4, 0),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.bookmarks_outlined, size: 18, color: JjColors.accent),
|
||||
Icon(Icons.bookmarks_outlined, size: 18, color: JjColors.accent),
|
||||
const SizedBox(width: 6),
|
||||
Text(tr('즐겨찾기'), style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
const Spacer(),
|
||||
|
|
@ -1816,7 +1816,7 @@ class _BookmarkPanelState extends State<BookmarkPanel> {
|
|||
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
|
||||
child: Wrap(crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
for (final (i, n) in _path().indexed) ...[
|
||||
if (i > 0) const Icon(Icons.chevron_right, size: 14, color: JjColors.textDim),
|
||||
if (i > 0) Icon(Icons.chevron_right, size: 14, color: JjColors.textDim),
|
||||
BookmarkFolderDrop(
|
||||
bm: bm,
|
||||
folderId: n.id,
|
||||
|
|
@ -1903,7 +1903,7 @@ class _BookmarkPanelState extends State<BookmarkPanel> {
|
|||
leading: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
ReorderableDragStartListener(
|
||||
index: i,
|
||||
child: const Icon(Icons.drag_indicator, size: 16, color: JjColors.textDim),
|
||||
child: Icon(Icons.drag_indicator, size: 16, color: JjColors.textDim),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
// 아이콘을 끌어 폴더 줄 · 위 경로에 놓으면 그 폴더로
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ class _CopySyncSettingsState extends State<CopySyncSettings> {
|
|||
)
|
||||
: IconButton(
|
||||
tooltip: tr('내려받은 rsync 지우기'),
|
||||
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
onPressed: () async {
|
||||
await RsyncInstaller.uninstall();
|
||||
await _check();
|
||||
|
|
@ -303,7 +303,7 @@ class _LiveSyncTile extends StatelessWidget {
|
|||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text('$label ${shortPath(path)}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
Text(vDisplay(path), style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
Text(vDisplay(path), style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
]),
|
||||
),
|
||||
TextButton(
|
||||
|
|
@ -474,7 +474,7 @@ class _LiveSyncTile extends StatelessWidget {
|
|||
),
|
||||
IconButton(
|
||||
tooltip: tr('지우기'),
|
||||
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
|
||||
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
|
||||
onPressed: () => removeLiveSyncPair(context, c, pairs[i]),
|
||||
),
|
||||
]),
|
||||
|
|
@ -525,7 +525,7 @@ class LiveSyncRunOptions extends StatelessWidget {
|
|||
),
|
||||
// 257: 폰 세로에서 넘쳤다 - 모자라면 고르기 상자를 아래 줄로
|
||||
Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
Text(tr('다시 켤 때 동기화'), style: const TextStyle(fontSize: 13, color: JjColors.textDim)),
|
||||
Text(tr('다시 켤 때 동기화'), style: TextStyle(fontSize: 13, color: JjColors.textDim)),
|
||||
onStart,
|
||||
]),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ Future<String?> fetchDav(BuildContext context, AppController c, String path) asy
|
|||
LinearProgressIndicator(value: v.$2 > 0 ? v.$1 / v.$2 : null),
|
||||
const SizedBox(height: 6),
|
||||
Text('${formatSize(v.$1)}${v.$2 > 0 ? ' / ${formatSize(v.$2)}' : ''}',
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]),
|
||||
),
|
||||
actions: [
|
||||
|
|
|
|||
|
|
@ -200,9 +200,9 @@ class _DlnaPageState extends State<DlnaPage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
child: Text(tr('보기만 합니다 - 같은 Wi-Fi · 유선 망의 미디어 서버만 찾고, 올리기 · 지우기는 없습니다'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: const TextStyle(color: JjColors.danger))),
|
||||
if (_error != null) Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: TextStyle(color: JjColors.danger))),
|
||||
if (s == null) Expanded(child: _serverList()) else Expanded(child: _entryList()),
|
||||
]),
|
||||
),
|
||||
|
|
@ -234,7 +234,7 @@ class _DlnaPageState extends State<DlnaPage> {
|
|||
ListTile(
|
||||
leading: const Icon(Icons.dns_outlined),
|
||||
title: Text(x.name),
|
||||
subtitle: Text(x.host, style: const TextStyle(color: JjColors.textDim)),
|
||||
subtitle: Text(x.host, style: TextStyle(color: JjColors.textDim)),
|
||||
onTap: () => _open(x, '0', x.name),
|
||||
),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Text(hasSel ? trf('{0}개 선택', [d.selected.length]) : tr('전체 선택'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
@ -185,7 +185,7 @@ class _DownloadsPageState extends State<DownloadsPage> {
|
|||
? tr('다운로드가 없습니다.\nYouTube 주소 · 마그넷 링크를 위 칸에 붙여 넣거나, 웹 브라우저에서 [다운로드] 를 누르세요.')
|
||||
: tr('다운로드가 없습니다.\nYouTube 주소나 마그넷 링크를 복사(Ctrl+C)하면 자동으로 받습니다.'),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(color: JjColors.textDim))))
|
||||
style: TextStyle(color: JjColors.textDim))))
|
||||
: ListView.builder(
|
||||
itemCount: d.tasks.length,
|
||||
itemBuilder: (_, i) => _DownloadRow(d: d, t: d.tasks[i]),
|
||||
|
|
@ -274,7 +274,7 @@ class _DownloadRow extends StatelessWidget {
|
|||
width: 560,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
// 서명 · 키가 든 주소 부분은 가린다
|
||||
SelectableText(redactUrlSecrets(t.source), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
SelectableText(redactUrlSecrets(t.source), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(redactUrlSecrets(t.error ?? ''), style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
|
||||
]),
|
||||
|
|
@ -300,7 +300,7 @@ class _DownloadRow extends StatelessWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: d.selected.contains(t.id) ? JjColors.accent.withValues(alpha: 0.08) : null,
|
||||
border: const Border(bottom: BorderSide(color: JjColors.border)),
|
||||
border: Border(bottom: BorderSide(color: JjColors.border)),
|
||||
),
|
||||
child: Row(children: [
|
||||
Checkbox(value: d.selected.contains(t.id), onChanged: (_) => d.toggle(t)),
|
||||
|
|
@ -328,7 +328,7 @@ class _DownloadRow extends StatelessWidget {
|
|||
])
|
||||
else if (t.state == DownloadState.failed)
|
||||
Wrap(crossAxisAlignment: WrapCrossAlignment.center, spacing: 4, children: [
|
||||
Text('$label · ${_why(t)}', style: const TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
Text('$label · ${_why(t)}', style: TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
if ((t.error ?? '').trim().isNotEmpty)
|
||||
TextButton(
|
||||
style: _small,
|
||||
|
|
@ -356,7 +356,7 @@ class _DownloadRow extends StatelessWidget {
|
|||
),
|
||||
IconButton(
|
||||
tooltip: tr('폴더 열기'),
|
||||
icon: const Icon(Icons.folder_open, size: 18, color: JjColors.textDim),
|
||||
icon: Icon(Icons.folder_open, size: 18, color: JjColors.textDim),
|
||||
onPressed: () {
|
||||
if (Platform.isWindows) {
|
||||
Process.run('explorer', [t.dir]);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -75,10 +75,10 @@ Widget dragFeedback(List<String> paths) => Material(
|
|||
border: Border.all(color: JjColors.accent),
|
||||
),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.drive_file_move_outline, size: 16, color: JjColors.accent),
|
||||
Icon(Icons.drive_file_move_outline, size: 16, color: JjColors.accent),
|
||||
const SizedBox(width: 6),
|
||||
Text(paths.length == 1 ? vBasename(paths.first) : trf('{0}개 · {1} …', [paths.length, vBasename(paths.first)]),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.text)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.text)),
|
||||
]),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -79,14 +79,14 @@ class _ExplorerGridPageState extends State<ExplorerGridPage> {
|
|||
picture = FutureBuilder<File?>(
|
||||
future: widget.videoThumb(e),
|
||||
builder: (_, s) => s.data == null
|
||||
? const Center(child: Icon(Icons.movie_outlined, size: 40, color: JjColors.textDim))
|
||||
? Center(child: Icon(Icons.movie_outlined, size: 40, color: JjColors.textDim))
|
||||
: Image.file(s.data!, fit: BoxFit.cover, cacheWidth: px),
|
||||
);
|
||||
} else {
|
||||
picture = Image.file(File(e.path),
|
||||
fit: BoxFit.cover,
|
||||
cacheWidth: px,
|
||||
errorBuilder: (_, _, _) => const Center(child: Icon(Icons.broken_image_outlined, size: 40, color: JjColors.textDim)));
|
||||
errorBuilder: (_, _, _) => Center(child: Icon(Icons.broken_image_outlined, size: 40, color: JjColors.textDim)));
|
||||
}
|
||||
return Tooltip(
|
||||
message: e.name,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? [
|
||||
|
|
|
|||
|
|
@ -743,7 +743,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Expanded(child: Text(title, style: const TextStyle(fontWeight: FontWeight.w600))),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Text(body, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(body, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Wrap(spacing: 8, children: actions),
|
||||
]),
|
||||
);
|
||||
|
|
@ -2094,7 +2094,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.wifi_tethering, size: 18, color: JjColors.success),
|
||||
Icon(Icons.wifi_tethering, size: 18, color: JjColors.success),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
|
|
@ -2265,7 +2265,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
enabled: false,
|
||||
height: 32,
|
||||
child: Text(trf('{0}개 항목', [targets.length]),
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: JjColors.accent)),
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: JjColors.accent)),
|
||||
),
|
||||
if (icons.isNotEmpty) ...[_MenuIconRow(icons), const PopupMenuDivider(height: 1)],
|
||||
for (final (id, icon, label) in items)
|
||||
|
|
@ -2618,7 +2618,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.arrow_forward, size: 18, color: JjColors.accent),
|
||||
Icon(Icons.arrow_forward, size: 18, color: JjColors.accent),
|
||||
const SizedBox(width: 6),
|
||||
// 412 캡처 (10/18): "갈 곳:" 은 늘 보이고, 폴더 이름이 길면 가운데를 줄인다 (전체 경로는 길게 눌러 · 마우스를 올려)
|
||||
Expanded(
|
||||
|
|
@ -3008,7 +3008,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(trf('앱 휴지통에 {0}일 두었다가 지웁니다 (탐색기 [휴지통] 에서 되살리기).', [c.settings.androidTrashDays]),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
if (canRecycle)
|
||||
CheckboxListTile(
|
||||
|
|
@ -3126,7 +3126,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
],
|
||||
for (final (x, why) in failed.take(30)) ...[
|
||||
Text(vBasename(x), style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text('${vDirname(x)} · $why', style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text('${vDirname(x)} · $why', style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
if (failed.length > 30) Text(trf('… 외 {0}개', [failed.length - 30])),
|
||||
|
|
@ -3485,7 +3485,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
child: Text(
|
||||
trf('환경 설정의 {0} 는 복사에서는 쓰지 않습니다 (이동은 [이동] 으로)',
|
||||
[splitOptions(task.options).where(isSourceRemovingOption).join(' ')]),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
FutureBuilder<List<NameClash>>(
|
||||
future: clashesFuture,
|
||||
|
|
@ -3493,7 +3493,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (snap.connectionState != ConnectionState.done) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Text(tr('같은 이름이 있는지 확인하는 중 …'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
child: Text(tr('같은 이름이 있는지 확인하는 중 …'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
);
|
||||
}
|
||||
final clashes = snap.data ?? const <NameClash>[];
|
||||
|
|
@ -3733,18 +3733,18 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Text(shortPath(t.sources.first), style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text('→ ${shortPath(t.dest)}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text('${vDisplay(t.sources.first)}/ → ${vDisplay(t.dest)}/',
|
||||
style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
// 111: 이 쌍은 처음 실행 때 기억한 옵션을 쓴다 (환경 설정의 rsync 옵션을 바꿔도 그대로)
|
||||
if (center.tasks.any((x) => x.id == t.id))
|
||||
Text(tr('이 폴더 쌍은 기억한 옵션을 씁니다 (환경 설정이 아니라 모니터링 > 복사 · rsync 에서 고칩니다)'),
|
||||
style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
Text('rsync ${runOpts(tasks.indexOf(t))}${removeSource ? ' --remove-source-files' : ''}'
|
||||
'${t.bandwidthKBps > 0 ? ' · ${trf('속도 제한 {0} KB/s', [t.bandwidthKBps])}' : ''}',
|
||||
style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
Text(tr('폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
// 222: 이동이면 동작대로 (예전에는 이동일 때도 "복사 · 원본 폴더는 그대로" 라고 썼다)
|
||||
if (nested)
|
||||
Text(
|
||||
|
|
@ -3755,7 +3755,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 1280×720 등 낮은 화면에서도 보이게: 원본 지우기 체크 · 경고는 비교 목록보다 위에 (178)
|
||||
if (both) ...[
|
||||
Text(keepOptionsTogether(tr('양쪽을 함께: 받는 쪽이 더 새 파일은 덮어쓰지 않습니다 (-u).')),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (optionMove)
|
||||
Text(tr('양쪽 (⇄) 에서는 원본 파일을 지우지 않습니다 (옵션의 --remove-source-files 는 쓰지 않음).'),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
|
||||
|
|
@ -3781,7 +3781,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
Text(tr('끝난 뒤 원본의 빈 폴더를 지웁니다 (find 원본/ -type d -empty -delete). 원본 폴더는:'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 6),
|
||||
PruneChoice(value: prune, onChanged: (v) => set(() => prune = v)),
|
||||
],
|
||||
|
|
@ -3840,7 +3840,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
const SizedBox(height: 4),
|
||||
if (_viaDav([l, r]))
|
||||
Text(tr('WebDAV · SFTP · SMB · FTP: rsync 대신 앱이 직접 맞춥니다 (크기 · 바뀐 때 비교, 옵션 중 -u · --delete · --remove-source-files · 거르기 (--exclude · --include · --filter · --delete-excluded) 를 따름).'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.accent)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.accent)),
|
||||
]),
|
||||
),
|
||||
actions: [
|
||||
|
|
@ -4027,7 +4027,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
const SizedBox(width: 10),
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: Text(right, textAlign: TextAlign.end, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
child: Text(right, textAlign: TextAlign.end, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
|
@ -4040,7 +4040,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Expanded(
|
||||
// Rsync 화면: 원본 → 대상 (⇄ 는 두 줄)
|
||||
child: Text(widget.rsync ? '${job.sources.join(', ')}/ → ${job.dest}/' : job.dest,
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
if (!job.finished) TextButton(onPressed: job.cancel, child: Text(tr('취소'))),
|
||||
]),
|
||||
|
|
@ -4240,11 +4240,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(children: [
|
||||
const Icon(Icons.manage_search, size: 18, color: JjColors.accent),
|
||||
Icon(Icons.manage_search, size: 18, color: JjColors.accent),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(trf('찾기 결과 ({0}) · {1}', [back.cond.summary, back.root]),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
TextButton(onPressed: _backToSearch, child: Text(tr('찾기 결과로 돌아가기'))),
|
||||
IconButton(
|
||||
|
|
@ -4273,7 +4273,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Text(tr('이 앱의 파일 탐색기 목록에서만 빠집니다. 파일은 지우거나 옮기거나 이름을 바꾸지 않고, 다른 앱 · 갤러리에서는 그대로 보입니다.')),
|
||||
const SizedBox(height: 6),
|
||||
Text(tr('목록 맨 아래의 [숨긴 항목 보이기] 나 환경 설정 > 파일 탐색기에서 다시 볼 수 있습니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
|
|
@ -4310,7 +4310,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
: tr('이 폴더의 .nomedia 파일을 지웁니다. 갤러리 · 음악 앱이 이 폴더의 그림 · 동영상 · 음악을 다시 보이게 됩니다.')),
|
||||
const SizedBox(height: 6),
|
||||
Text(tr('폴더 안의 파일은 바꾸지 않습니다. 갤러리에 반영되기까지 잠깐 걸릴 수 있습니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
|
|
@ -4359,7 +4359,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
]),
|
||||
SwitchListTile(value: desc, title: Text(tr('거꾸로')), onChanged: (x) => set(() => desc = x)),
|
||||
if (c.settings.explorerSortPerFolder)
|
||||
Text(trf('이 폴더에만: {0}', [_displayPath(dir)]), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(trf('이 폴더에만: {0}', [_displayPath(dir)]), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))),
|
||||
|
|
@ -4468,7 +4468,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6, left: 4),
|
||||
child: Text(tr('왼쪽에 폴더, 오른쪽에 왼쪽에서 고른 폴더의 파일들을 보여 줍니다 (Windows 탐색기처럼).'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
if (layout != 'single')
|
||||
group(tr('두 창 배치'), orient, [
|
||||
|
|
@ -4485,7 +4485,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 4),
|
||||
child: Text(tr('숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
@ -4523,7 +4523,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
height: 460,
|
||||
child: Column(children: [
|
||||
Text(tr('보일 버튼을 고르고, 끌어서 순서를 바꿉니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: ReorderableListView(
|
||||
|
|
@ -4710,7 +4710,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
),
|
||||
],
|
||||
child: Text(_displayPath(_pane.current),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: JjColors.textDim)),
|
||||
),
|
||||
),
|
||||
PopupMenuButton<Object>(
|
||||
|
|
@ -5082,8 +5082,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(tr('고르기 도구'), style: const TextStyle(fontSize: 13, color: JjColors.accent)),
|
||||
const Icon(Icons.arrow_drop_down, size: 18, color: JjColors.accent),
|
||||
Text(tr('고르기 도구'), style: TextStyle(fontSize: 13, color: JjColors.accent)),
|
||||
Icon(Icons.arrow_drop_down, size: 18, color: JjColors.accent),
|
||||
]),
|
||||
),
|
||||
),
|
||||
|
|
@ -5188,7 +5188,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
height: 40,
|
||||
alignment: Alignment.centerLeft,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Text(_displayPath(dir), maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
|
||||
child: Text(_displayPath(dir), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: JjColors.textDim)),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
if (pane.filterOpen) _filterBar(pane, shown.length),
|
||||
|
|
@ -5207,7 +5207,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
child: pane.loading.contains(dir) && !pane.cache.containsKey(dir)
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: entries.isEmpty && !up
|
||||
? Center(child: Text(tr('비어 있음'), style: const TextStyle(color: JjColors.textDim)))
|
||||
? Center(child: Text(tr('비어 있음'), style: TextStyle(color: JjColors.textDim)))
|
||||
: scaleListText(context, _listTextScale, ListView.builder(
|
||||
controller: pane.listScroll,
|
||||
itemCount: rows.length,
|
||||
|
|
@ -5898,7 +5898,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
feedback: dragFeedback(_dragPaths(pane, e)),
|
||||
child: Tooltip(
|
||||
message: tr('끌어서 다른 창으로'),
|
||||
child: const SizedBox(width: 28, height: 36, child: Icon(Icons.drag_indicator, size: 18, color: JjColors.textDim)),
|
||||
child: SizedBox(width: 28, height: 36, child: Icon(Icons.drag_indicator, size: 18, color: JjColors.textDim)),
|
||||
),
|
||||
),
|
||||
// 표시 동그라미는 두 번 누르기 영역 밖 (기다리지 않고 바로)
|
||||
|
|
@ -6260,7 +6260,7 @@ class _MetaText extends StatelessWidget {
|
|||
builder: (_, s) => Text.rich(
|
||||
TextSpan(children: [
|
||||
if (lead.isNotEmpty) TextSpan(text: lead, style: leadStyle),
|
||||
if ((s.data ?? '').isNotEmpty) TextSpan(text: '${lead.isEmpty ? '' : ' '}${s.data}', style: const TextStyle(fontSize: 11, color: JjColors.accent)),
|
||||
if ((s.data ?? '').isNotEmpty) TextSpan(text: '${lead.isEmpty ? '' : ' '}${s.data}', style: TextStyle(fontSize: 11, color: JjColors.accent)),
|
||||
]),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
|
|
@ -6319,7 +6319,7 @@ class TransferProgressChip extends StatelessWidget {
|
|||
child: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
// 멈춤은 진행 고리 대신 경고 표시 (꽉 찬 고리는 "끝났다" 로 보였다)
|
||||
if (failed)
|
||||
const Icon(Icons.error_outline, size: 16, color: JjColors.danger)
|
||||
Icon(Icons.error_outline, size: 16, color: JjColors.danger)
|
||||
else
|
||||
SizedBox(
|
||||
width: 14,
|
||||
|
|
@ -6387,7 +6387,7 @@ class _Thumb extends StatelessWidget {
|
|||
future: _Thumbs.of(c, entry),
|
||||
builder: (_, s) {
|
||||
final f = s.data;
|
||||
if (f == null) return const Icon(Icons.movie_outlined, size: 28, color: JjColors.textDim);
|
||||
if (f == null) return Icon(Icons.movie_outlined, size: 28, color: JjColors.textDim);
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
child: Stack(alignment: Alignment.center, children: [
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ class _ConditionsDialogState extends State<_ConditionsDialog> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Text(tr('크기 · 바뀐 날 · 종류를 넣으면 파일만 찾습니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
@ -398,7 +398,7 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
|
|||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('${widget.cond.summary} · ${widget.root}',
|
||||
maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
|
|
@ -473,7 +473,7 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
|
|||
for (final (dir, why) in _skipped.take(50))
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text('$dir · $why', style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
child: Text('$dir · $why', style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -483,7 +483,7 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
|
|||
SafeArea(
|
||||
top: false,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(border: Border(top: BorderSide(color: JjColors.border))),
|
||||
decoration: BoxDecoration(border: Border(top: BorderSide(color: JjColors.border))),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: Wrap(spacing: 4, runSpacing: 2, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
Text(_summary(_targets), style: const TextStyle(fontSize: 12)),
|
||||
|
|
@ -499,8 +499,8 @@ class _SearchResultsPageState extends State<SearchResultsPage> {
|
|||
),
|
||||
TextButton.icon(
|
||||
onPressed: () => _act('delete'),
|
||||
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.danger),
|
||||
label: Text(tr('지우기'), style: const TextStyle(color: JjColors.danger)),
|
||||
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.danger),
|
||||
label: Text(tr('지우기'), style: TextStyle(color: JjColors.danger)),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ Future<void> showFileHash(BuildContext context, String path) async {
|
|||
content: SizedBox(
|
||||
width: 560,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
if (error != null) Text(trf('읽지 못했습니다: {0}', [error]), style: const TextStyle(color: JjColors.danger)),
|
||||
if (error != null) Text(trf('읽지 못했습니다: {0}', [error]), style: TextStyle(color: JjColors.danger)),
|
||||
if (result == null && error == null) LinearProgressIndicator(value: progress),
|
||||
if (result != null) ...[
|
||||
line('MD5', result!.md5),
|
||||
|
|
@ -196,7 +196,7 @@ class _HexViewerPageState extends State<HexViewerPage> {
|
|||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Text(trf('{0} / {1} 읽음', [formatSize(_data.length), formatSize(_size)]),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
FilledButton.tonal(onPressed: _more, child: Text(tr('더 읽기'))),
|
||||
]),
|
||||
|
|
@ -246,7 +246,7 @@ Future<void> showFolderProperties(BuildContext context, String dir) async {
|
|||
builder: (_, s) {
|
||||
final r = s.data;
|
||||
return Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SelectableText(dir, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
SelectableText(dir, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 8),
|
||||
if (r == null) const LinearProgressIndicator(),
|
||||
if (r != null) ...[
|
||||
|
|
|
|||
|
|
@ -46,12 +46,12 @@ class FtpSettings extends StatelessWidget {
|
|||
].join(' · '),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: s.tls ? null : const TextStyle(color: JjColors.danger)),
|
||||
style: s.tls ? null : TextStyle(color: JjColors.danger)),
|
||||
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
IconButton(tooltip: tr('고치기'), icon: const Icon(Icons.edit_outlined, size: 18), onPressed: () => editFtpServer(context, c, old: s)),
|
||||
IconButton(
|
||||
tooltip: tr('지우기'),
|
||||
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
|
||||
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
|
||||
onPressed: () => deleteFtpServer(context, c, s),
|
||||
),
|
||||
]),
|
||||
|
|
@ -92,7 +92,7 @@ Future<bool> askTrustFtpCert(BuildContext context, String serverLabel, FtpCertEx
|
|||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
scrollable: true,
|
||||
icon: const Icon(Icons.gpp_bad_outlined, color: JjColors.danger),
|
||||
icon: Icon(Icons.gpp_bad_outlined, color: JjColors.danger),
|
||||
title: Text(trf('"{0}" 서버의 인증서가 바뀌었습니다', [serverLabel])),
|
||||
content: SelectableText(
|
||||
'${tr('중간에서 가로채는 공격일 수 있어 연결하지 않았습니다. 서버 인증서를 바꾼 것이 확실할 때만, 서버 설정의 [믿은 인증서 지우기] 뒤 다시 연결하세요.')}\n\n'
|
||||
|
|
@ -223,7 +223,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
|
|||
),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Text(tr('암호화'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(tr('암호화'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
RadioGroup<String>(
|
||||
groupValue: security,
|
||||
onChanged: (x) => set(() {
|
||||
|
|
@ -238,7 +238,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
|
|||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
value: v,
|
||||
title: Text(ftpSecurityLabel(v), style: v == 'none' ? const TextStyle(color: JjColors.danger) : null),
|
||||
title: Text(ftpSecurityLabel(v), style: v == 'none' ? TextStyle(color: JjColors.danger) : null),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
|
@ -248,7 +248,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
|
|||
color: JjColors.danger.withValues(alpha: 0.12),
|
||||
child: Text(
|
||||
tr('⚠ 평문 FTP 는 아이디 · 비밀번호 · 파일이 암호화되지 않은 채 오갑니다. 같은 망의 다른 사람이 볼 수 있습니다. 서버가 FTPS 를 못 할 때만, 믿을 수 있는 집 안 망에서만 쓰세요.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
),
|
||||
TextField(controller: user, decoration: InputDecoration(labelText: tr('아이디 (비우면 익명)'))),
|
||||
TextField(
|
||||
|
|
@ -273,7 +273,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
|
|||
fingerprint.isEmpty
|
||||
? tr('인증서: 공인 인증서면 그대로 연결, 자체 서명이면 [연결 확인] 때 지문을 보이고 묻습니다')
|
||||
: '${tr('믿은 인증서')}: $fingerprint',
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (fingerprint.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Future<void> showHiddenItems(BuildContext context, AppController c) => showDialo
|
|||
width: 520,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(tr('풀면 파일 탐색기 목록에 다시 보입니다. 파일 자체는 바꾸지 않습니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 8),
|
||||
if (list.isEmpty) Text(tr('숨긴 항목이 없습니다')),
|
||||
for (final x in list)
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class LanguageSettings extends StatelessWidget {
|
|||
final added = i18n.available.where((c) => !I18nController.builtIn.contains(c)).toList();
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.translate, color: JjColors.accent),
|
||||
leading: Icon(Icons.translate, color: JjColors.accent),
|
||||
// 읽을 수 없는 언어를 골라도 찾을 수 있게 영어를 함께
|
||||
title: Text(uiLanguage == 'en' ? 'Language' : '${tr('화면 언어')} · Language'),
|
||||
subtitle: Text(tr('프로그램의 모든 글자를 고른 언어로 보여 줍니다. 목록에 없는 언어는 [언어 추가] 로 '
|
||||
|
|
@ -58,7 +58,7 @@ class LanguageSettings extends StatelessWidget {
|
|||
),
|
||||
if (!i18n.canTranslate)
|
||||
Text(tr('(AI 번역을 쓸 수 없는 기기라 언어를 더할 수 없습니다)'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]),
|
||||
),
|
||||
for (final code in added)
|
||||
|
|
@ -118,7 +118,7 @@ Future<void> addLanguageDialog(BuildContext context) async {
|
|||
LinearProgressIndicator(value: s.$2),
|
||||
const SizedBox(height: 8),
|
||||
Text(tr('화면 글자 전체를 이 기기에서 번역합니다. 몇 분 걸릴 수 있습니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const NllbLicenseNote(),
|
||||
]),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ Future<void> _done(BuildContext context, AppController c, MasterLock lock, List<
|
|||
final setNew = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
icon: const Icon(Icons.check_circle_outline, color: JjColors.accent),
|
||||
icon: Icon(Icons.check_circle_outline, color: JjColors.accent),
|
||||
title: Text(tr('초기화했습니다')),
|
||||
content: Text(done.map((x) => '• $x').join('\n')),
|
||||
actions: [
|
||||
|
|
@ -314,7 +314,7 @@ Future<bool> setPasswordDialog(BuildContext context, AppController c, MasterLock
|
|||
'저장된 비밀번호를 지우고 초기화할 수 있습니다.')),
|
||||
const SizedBox(height: 8),
|
||||
Text(tr('이 기기에는 원문이 아니라 되돌릴 수 없는 해시만 남습니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
TextField(
|
||||
controller: a,
|
||||
obscureText: true,
|
||||
|
|
|
|||
|
|
@ -130,17 +130,17 @@ Future<bool?> _liveDeleteDialog(BuildContext context, LiveSync live, LiveSyncPai
|
|||
child: ListView(children: [
|
||||
if (list.isEmpty)
|
||||
Text(tr('미리 본 지울 것은 없습니다 - 위 규칙 때문에 실제로는 지워질 수 있습니다'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
for (final n in plain.take(500)) Text(n, style: const TextStyle(fontSize: 12, fontFamily: 'Consolas')),
|
||||
if (plain.length > 500) Text(trf('… 외 {0}개', [plain.length - 500])),
|
||||
if (filtered.isNotEmpty) ...[
|
||||
if (plain.isNotEmpty) const SizedBox(height: 10),
|
||||
Text(keepOptionsTogether(trf('거른 것 {0}개도 지워집니다 (--delete-excluded)', [filtered.length])),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: JjColors.danger)),
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: JjColors.danger)),
|
||||
Text(keepOptionsTogether(tr('rsync 옵션에서 뺀 것 (--exclude 등) 이지만 --delete-excluded 가 있어 대상에서 지웁니다. 지키려면 옵션에서 --delete-excluded 를 빼세요.')),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 4),
|
||||
for (final n in filtered.take(500)) Text(n, style: const TextStyle(fontSize: 12, fontFamily: 'Consolas', color: JjColors.danger)),
|
||||
for (final n in filtered.take(500)) Text(n, style: TextStyle(fontSize: 12, fontFamily: 'Consolas', color: JjColors.danger)),
|
||||
if (filtered.length > 500) Text(trf('… 외 {0}개', [filtered.length - 500])),
|
||||
],
|
||||
]),
|
||||
|
|
@ -219,11 +219,11 @@ class _DiskInfo extends StatelessWidget {
|
|||
future: diskSpace(dir),
|
||||
builder: (_, s) {
|
||||
final d = s.data;
|
||||
if (d == null) return Text(tr('대상 디스크: 알 수 없음'), style: const TextStyle(fontSize: 12, color: JjColors.textDim));
|
||||
if (d == null) return Text(tr('대상 디스크: 알 수 없음'), style: TextStyle(fontSize: 12, color: JjColors.textDim));
|
||||
final (free, total) = d;
|
||||
final used = total == 0 ? 0.0 : (total - free) / total;
|
||||
return Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.storage, size: 14, color: JjColors.textDim),
|
||||
Icon(Icons.storage, size: 14, color: JjColors.textDim),
|
||||
const SizedBox(width: 4),
|
||||
// 241 시험에서 찾음: 좁은 화면 (폭 500) 에서 넘쳤다 - 글을 줄인다
|
||||
Flexible(
|
||||
|
|
@ -273,7 +273,7 @@ class TransferProgressLines extends StatelessWidget {
|
|||
),
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: Text(right, textAlign: TextAlign.end, style: const TextStyle(fontSize: 12, color: JjColors.textDim))),
|
||||
child: Text(right, textAlign: TextAlign.end, style: TextStyle(fontSize: 12, color: JjColors.textDim))),
|
||||
]),
|
||||
);
|
||||
final speed = !job.finished && job.speed != null ? '${formatSize(job.speed!.round())}/s' : '';
|
||||
|
|
@ -319,7 +319,7 @@ class _CopyTab extends StatelessWidget {
|
|||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(tr('실행한 rsync 가 없습니다. Rsync 화면에서 → · ← · ⇄ 로 실행하면 여기에 자동으로 등록됩니다.'),
|
||||
textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim)),
|
||||
textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -593,7 +593,7 @@ class _CopyCardState extends State<_CopyCard> {
|
|||
),
|
||||
Text('→ ${shortPath(t.dest)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text('${vDisplay(t.sources.first)}${t.contents ? '/' : ''} → ${vDisplay(t.dest)}',
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
]),
|
||||
),
|
||||
Chip(
|
||||
|
|
@ -710,7 +710,7 @@ class _CopyCardState extends State<_CopyCard> {
|
|||
),
|
||||
IconButton(
|
||||
tooltip: tr('목록에서 지우기 (파일은 그대로)'),
|
||||
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
onPressed: () => center.remove(t.id),
|
||||
),
|
||||
]),
|
||||
|
|
@ -774,7 +774,7 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
SelectableText(
|
||||
'rsync -avHPOg --remove-source-files ${x.source}/ ${x.target}/\n'
|
||||
'find ${x.source}/ -type d -empty -delete',
|
||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontFamily: 'monospace', fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 14),
|
||||
Text(tr('원본 폴더'), style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 6),
|
||||
|
|
@ -830,7 +830,7 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
Expanded(
|
||||
child: Text(
|
||||
tr('원본이 바뀌면 대상에 맞춥니다. 일정을 정하면 그 시간에만 맞추고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
),
|
||||
),
|
||||
TextButton.icon(onPressed: () => addLiveSyncPairDialog(context, c), icon: const Icon(Icons.add), label: Text(tr('추가'))),
|
||||
|
|
@ -852,7 +852,7 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Center(child: Text(tr('실시간 동기화가 없습니다. [추가] 하거나 복사 · rsync 탭에서 [lsync 로 이동] 하세요.'),
|
||||
style: const TextStyle(color: JjColors.textDim))),
|
||||
style: TextStyle(color: JjColors.textDim))),
|
||||
),
|
||||
for (final x in pairs) _liveCard(context, live, x),
|
||||
]);
|
||||
|
|
@ -895,11 +895,11 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
const Icon(Icons.lock_clock_outlined, color: JjColors.accent, size: 20),
|
||||
Icon(Icons.lock_clock_outlined, color: JjColors.accent, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(tr('마스터 비밀번호가 필요해 기다리는 중'),
|
||||
style: const TextStyle(fontWeight: FontWeight.w600, color: JjColors.accent)),
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: JjColors.accent)),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
|
|
@ -948,7 +948,7 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('${vDisplay(x.source)}${e.detail.isEmpty ? '' : ' · ${e.detail}'}',
|
||||
style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(spacing: 8, runSpacing: 4, children: [
|
||||
FilledButton.tonalIcon(
|
||||
|
|
@ -1088,8 +1088,8 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [
|
||||
Text(scheduleSummary(x.schedule), style: TextStyle(fontSize: 12, color: active ? JjColors.accent : Colors.orangeAccent)),
|
||||
if (x.schedule.isNotEmpty)
|
||||
Text(x.schedule.join(' | '), style: const TextStyle(fontSize: 11, fontFamily: 'monospace', color: JjColors.textDim)),
|
||||
Text(tr('누르면 일정 바꾸기'), style: const TextStyle(fontSize: 10, color: JjColors.textDim)),
|
||||
Text(x.schedule.join(' | '), style: TextStyle(fontSize: 11, fontFamily: 'monospace', color: JjColors.textDim)),
|
||||
Text(tr('누르면 일정 바꾸기'), style: TextStyle(fontSize: 10, color: JjColors.textDim)),
|
||||
]),
|
||||
),
|
||||
]),
|
||||
|
|
@ -1109,7 +1109,7 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32, top: 8),
|
||||
child: pend == null
|
||||
? Text(tr('맞출 것을 세는 중…'), style: const TextStyle(fontSize: 12, color: JjColors.textDim))
|
||||
? Text(tr('맞출 것을 세는 중…'), style: TextStyle(fontSize: 12, color: JjColors.textDim))
|
||||
: pend.isEmpty
|
||||
// 244: 대상에만 있는 것이 있으면 "같습니다" 가 아니라 사실대로
|
||||
? Text(
|
||||
|
|
@ -1173,7 +1173,7 @@ class _LiveTabState extends State<_LiveTab> {
|
|||
),
|
||||
IconButton(
|
||||
tooltip: tr('지우기'),
|
||||
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
onPressed: () => removeLiveSyncPair(context, c, x),
|
||||
),
|
||||
]),
|
||||
|
|
@ -1198,7 +1198,7 @@ Future<void> askLiveSyncStart(BuildContext context, LiveSync live) async {
|
|||
width: 520,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(tr('시작할 동기화를 고르세요. 고르지 않은 것은 이번 실행 동안 멈춥니다 (모니터링 > lsync 에서 시작).'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 8),
|
||||
Flexible(
|
||||
child: ListView(shrinkWrap: true, children: [
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ class _DnsSettingsState extends State<DnsSettings> {
|
|||
label: Text(tr('지금 확인')),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: Text(_testing ? tr('확인하는 중…') : _test ?? '', style: const TextStyle(fontSize: 12, color: JjColors.textDim))),
|
||||
Expanded(child: Text(_testing ? tr('확인하는 중…') : _test ?? '', style: TextStyle(fontSize: 12, color: JjColors.textDim))),
|
||||
]),
|
||||
),
|
||||
SettingTile(
|
||||
|
|
@ -247,7 +247,7 @@ class _DnsSettingsState extends State<DnsSettings> {
|
|||
if (desk && s.browserEngine == 'chrome' && CefRuntime.readyThisRun)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: Text(tr('Chrome 엔진은 앱을 다시 시작하면 바뀐 설정을 씁니다.'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
child: Text(tr('Chrome 엔진은 앱을 다시 시작하면 바뀐 설정을 씁니다.'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -379,7 +379,7 @@ class _PlayerPageState extends State<PlayerPage> {
|
|||
Widget item(String label, VoidCallback? onTap, {String? key, bool checked = false}) => MenuItemButton(
|
||||
onPressed: onTap,
|
||||
leadingIcon: SizedBox(width: 18, child: checked ? const Icon(Icons.check, size: 16) : null),
|
||||
trailingIcon: key == null ? null : Text(key, style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
trailingIcon: key == null ? null : Text(key, style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
child: Text(label),
|
||||
);
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class _QuickLookState extends State<QuickLook> {
|
|||
Widget _info(List<String> lines) => Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(_e.isDir ? Icons.folder : Icons.insert_drive_file_outlined, size: 56, color: JjColors.textDim),
|
||||
for (final l in lines) Text(l, textAlign: TextAlign.center, style: const TextStyle(color: JjColors.textDim)),
|
||||
for (final l in lines) Text(l, textAlign: TextAlign.center, style: TextStyle(color: JjColors.textDim)),
|
||||
]),
|
||||
);
|
||||
|
||||
|
|
@ -136,7 +136,7 @@ class _QuickLookState extends State<QuickLook> {
|
|||
builder: (_, s) => s.connectionState != ConnectionState.done
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: s.data == null
|
||||
? const Center(child: Icon(Icons.movie_outlined, size: 56, color: JjColors.textDim))
|
||||
? Center(child: Icon(Icons.movie_outlined, size: 56, color: JjColors.textDim))
|
||||
: Image.file(s.data!, fit: BoxFit.contain),
|
||||
),
|
||||
),
|
||||
|
|
@ -145,9 +145,9 @@ class _QuickLookState extends State<QuickLook> {
|
|||
// 큰 파일 · 느린 디스크에서도 창이 멈추지 않게 (기다리는 동안 "읽는 중", 20초 넘으면 못 읽음)
|
||||
future: widget.probe(e.path).timeout(const Duration(seconds: 20)),
|
||||
builder: (_, s) {
|
||||
if (s.connectionState != ConnectionState.done) return Text(tr('정보를 읽는 중…'), style: const TextStyle(color: JjColors.textDim));
|
||||
if (s.connectionState != ConnectionState.done) return Text(tr('정보를 읽는 중…'), style: TextStyle(color: JjColors.textDim));
|
||||
final info = s.data;
|
||||
if (info == null) return Text(tr('정보를 읽지 못했습니다'), style: const TextStyle(color: JjColors.textDim));
|
||||
if (info == null) return Text(tr('정보를 읽지 못했습니다'), style: TextStyle(color: JjColors.textDim));
|
||||
final v = info.ofType('video').where((x) => (x.width ?? 0) > 0).firstOrNull;
|
||||
final audio = info.ofType('audio');
|
||||
final subs = info.ofType('subtitle');
|
||||
|
|
@ -189,7 +189,7 @@ class _QuickLookState extends State<QuickLook> {
|
|||
title: Row(children: [
|
||||
Expanded(child: Text(e.name, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16))),
|
||||
IconButton(tooltip: tr('앞 항목 (←)'), onPressed: _i > 0 ? () => _go(-1) : null, icon: const Icon(Icons.chevron_left)),
|
||||
Text('${_i + 1}/${widget.entries.length}', style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text('${_i + 1}/${widget.entries.length}', style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
IconButton(
|
||||
tooltip: tr('다음 항목 (→)'),
|
||||
onPressed: _i < widget.entries.length - 1 ? () => _go(1) : null,
|
||||
|
|
@ -203,7 +203,7 @@ class _QuickLookState extends State<QuickLook> {
|
|||
[if (!e.isDir) formatSize(e.size), if (e.modified.year > 1) _date(e.modified), vDirname(e.path)].join(' · '),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: KeyedSubtree(key: ValueKey(e.path), child: _body(e))),
|
||||
|
|
|
|||
|
|
@ -920,7 +920,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
Widget row(String k, Widget v) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
SizedBox(width: 90, child: Text(k, style: const TextStyle(color: JjColors.textDim))),
|
||||
SizedBox(width: 90, child: Text(k, style: TextStyle(color: JjColors.textDim))),
|
||||
Expanded(child: v),
|
||||
]),
|
||||
);
|
||||
|
|
@ -951,12 +951,12 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
child: TextButton(onPressed: () => set(() => showLocation = true), child: Text(tr('위치 보기'))))),
|
||||
if (i.hasLocation)
|
||||
Text(tr('위치는 이 기기에서만 보이며 지도 · 인터넷으로 보내지 않습니다'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (!i.hasExif)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(tr('이 그림에는 Exif 정보가 없습니다 (JPEG 사진만 읽음)'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
],
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ class _RemoteThumbState extends State<RemoteThumb> {
|
|||
Image.file(f, width: 56, height: 44, fit: BoxFit.cover, errorBuilder: (_, _, _) => widget.fallback),
|
||||
if (isVideoFile(widget.entry.path)) const Icon(Icons.play_arrow, size: 18, color: Colors.white70),
|
||||
// 원격에서 온 것 표시 (작게)
|
||||
const Positioned(right: 1, bottom: 1, child: Icon(Icons.cloud_outlined, size: 10, color: JjColors.textDim)),
|
||||
Positioned(right: 1, bottom: 1, child: Icon(Icons.cloud_outlined, size: 10, color: JjColors.textDim)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ Future<String?> installRsyncWithDialog(BuildContext context) async {
|
|||
content: ValueListenableBuilder<(double, String)>(
|
||||
valueListenable: progress,
|
||||
builder: (_, v, _) => Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(v.$2, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(v.$2, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(value: v.$1),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
Expanded(
|
||||
child: Center(
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
const Icon(Icons.lock_outline, size: 48, color: JjColors.textDim),
|
||||
Icon(Icons.lock_outline, size: 48, color: JjColors.textDim),
|
||||
const SizedBox(height: 12),
|
||||
Text(tr('마스터 비밀번호로 잠겨 있습니다')),
|
||||
const SizedBox(height: 12),
|
||||
|
|
@ -416,7 +416,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
_appIconTile(c, desk),
|
||||
if (c.services.updater != null) ...[
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.system_update_alt, color: JjColors.accent),
|
||||
leading: Icon(Icons.system_update_alt, color: JjColors.accent),
|
||||
title: FutureBuilder<String>(
|
||||
future: c.services.updater!.currentVersion(),
|
||||
builder: (_, v) => Text('JJ_MKVMaker v${v.data ?? '…'}'),
|
||||
|
|
@ -497,6 +497,28 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
..uiScale = v),
|
||||
),
|
||||
),
|
||||
// X-plore 비교 5-1 ⑦ (관리자 지시 10/21): 화면 밝기 - 처음 값 어둡게 (예전 그대로), 다 끝날 때까지 "시험"
|
||||
SettingTile(
|
||||
title: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||
Flexible(child: Text(tr('화면 밝기'))),
|
||||
const SizedBox(width: 6),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(border: Border.all(color: Colors.orangeAccent), borderRadius: BorderRadius.circular(4)),
|
||||
child: Text(tr('시험'), style: const TextStyle(fontSize: 11, color: Colors.orangeAccent)),
|
||||
),
|
||||
]),
|
||||
subtitle: Text(tr('밝게 · 기기 따라는 아직 시험 중입니다 - 일부 화면의 색이 덜 맞을 수 있습니다')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.uiBrightness,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'dark', child: Text(tr('어두운 화면'))),
|
||||
DropdownMenuItem(value: 'light', child: Text(tr('밝은 화면'))),
|
||||
DropdownMenuItem(value: 'system', child: Text(tr('기기 따라'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.uiBrightness = v!),
|
||||
),
|
||||
),
|
||||
]),
|
||||
_group('mkv', Icons.movie_creation_outlined, tr('MKV 만들기'), [
|
||||
// 61: 화면 언어와 다른 언어의 자막을 기본으로 원할 수 있다 (예: 화면은 일본어, 자막은 한국어)
|
||||
|
|
@ -592,7 +614,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
s.moveTargets.isEmpty
|
||||
? tr('없음: 세부 정보의 [이동] 을 처음 누를 때 폴더를 골라 하나 만듭니다')
|
||||
: tr('표시 이름은 바로 고칠 수 있습니다. 동영상 줄을 세 번 누르면 첫 번째 버튼의 폴더로 옮깁니다'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
),
|
||||
),
|
||||
]),
|
||||
|
|
@ -726,7 +748,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
if (s.browserEngine == 'edge' && CefRuntime.installed && !CefRuntime.readyThisRun)
|
||||
IconButton(
|
||||
tooltip: tr('내려받은 Chrome 엔진 지우기'),
|
||||
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
onPressed: () async {
|
||||
final ok = await CefRuntime.uninstall();
|
||||
c.note(ok ? tr('내장 Chrome 엔진을 지웠습니다.') : tr('내장 Chrome 엔진을 지우지 못했습니다 (사용 중).'));
|
||||
|
|
@ -1242,7 +1264,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
Text(
|
||||
tr('로그인 쿠키는 다운로드 (yt-dlp) 가 쓰도록 이 기기의 앱 폴더에 cookies_youtube.txt 로 저장됩니다 (평문). '
|
||||
'이 파일을 가진 사람은 그 계정에 로그인할 수 있으니 기기를 남에게 줄 때는 지우세요. 고른 사이트의 쿠키만 넘깁니다:'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(spacing: 6, runSpacing: 6, children: [
|
||||
for (final d in loginCookieDomains)
|
||||
|
|
@ -1332,7 +1354,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
title: Text(tr('창 보이기 / 숨기기 단축키')),
|
||||
subtitle: _hotkeyError == null
|
||||
? Text(tr('어디서나 누르면 JJ_MKVMaker 창이 나타납니다'))
|
||||
: Text(_hotkeyError!, style: const TextStyle(color: JjColors.danger)),
|
||||
: Text(_hotkeyError!, style: TextStyle(color: JjColors.danger)),
|
||||
trailing: SizedBox(
|
||||
width: 260,
|
||||
child: Row(children: [
|
||||
|
|
@ -1386,7 +1408,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
_group('about', Icons.info_outline, tr('프로그램 정보'), [
|
||||
// 164: 지금 버전과 [최신 버전 확인] (결과는 버튼 옆에)
|
||||
SettingTile(
|
||||
leading: const Icon(Icons.info_outline, color: JjColors.accent),
|
||||
leading: Icon(Icons.info_outline, color: JjColors.accent),
|
||||
title: FutureBuilder<String>(
|
||||
future: c.services.updater?.currentVersion() ?? Future.value(''),
|
||||
builder: (_, v) => Text(v.data == null || v.data!.isEmpty ? 'JJ_MKVMaker' : 'JJ_MKVMaker v${v.data}'),
|
||||
|
|
@ -1435,7 +1457,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
c.updateSettings((x) => x.moveTargets = f([...x.moveTargets]));
|
||||
return SettingTile(
|
||||
key: ValueKey('move_${i}_${t.dir}'),
|
||||
leading: const Icon(Icons.drive_file_move_outline, color: JjColors.accent),
|
||||
leading: Icon(Icons.drive_file_move_outline, color: JjColors.accent),
|
||||
title: TextFormField(
|
||||
initialValue: t.name,
|
||||
decoration: InputDecoration(isDense: true, labelText: tr('표시 이름')),
|
||||
|
|
@ -1606,7 +1628,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
|
||||
Widget _subTitle(String t) => Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: Text(t, style: const TextStyle(fontSize: 13, color: JjColors.accent, fontWeight: FontWeight.w600)),
|
||||
child: Text(t, style: TextStyle(fontSize: 13, color: JjColors.accent, fontWeight: FontWeight.w600)),
|
||||
);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class SftpSettings extends StatelessWidget {
|
|||
padding: const EdgeInsets.only(left: 16),
|
||||
child: SettingTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.vpn_key, color: JjColors.accent),
|
||||
leading: Icon(Icons.vpn_key, color: JjColors.accent),
|
||||
title: Text(s.label),
|
||||
subtitle: Text(
|
||||
[
|
||||
|
|
@ -58,7 +58,7 @@ class SftpSettings extends StatelessWidget {
|
|||
),
|
||||
IconButton(
|
||||
tooltip: tr('지우기'),
|
||||
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
|
||||
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
|
||||
onPressed: () => deleteSftpServer(context, c, s),
|
||||
),
|
||||
]),
|
||||
|
|
@ -94,7 +94,7 @@ Future<bool> askTrustHostKey(BuildContext context, String serverLabel, SftpHostK
|
|||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
scrollable: true,
|
||||
icon: const Icon(Icons.gpp_bad_outlined, color: JjColors.danger),
|
||||
icon: Icon(Icons.gpp_bad_outlined, color: JjColors.danger),
|
||||
title: Text(trf('"{0}" 서버의 키가 바뀌었습니다', [serverLabel])),
|
||||
content: SizedBox(
|
||||
width: 520,
|
||||
|
|
@ -290,7 +290,7 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
|
|||
onPressed: pickKey,
|
||||
),
|
||||
if (key.isNotEmpty) ...[
|
||||
Text(tr('개인 키 있음'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(tr('개인 키 있음'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
TextButton(onPressed: () => set(() => key = ''), child: Text(tr('개인 키 빼기'))),
|
||||
],
|
||||
]),
|
||||
|
|
@ -316,7 +316,7 @@ Future<SftpServer?> editSftpServer(BuildContext context, AppController c, {SftpS
|
|||
fingerprint.isEmpty
|
||||
? tr('서버 키: 아직 확인 안 함 ([연결 확인] 을 누르면 지문을 보이고 묻습니다)')
|
||||
: '${tr('믿은 서버 키')}: $fingerprint${keyType.isEmpty ? '' : ' ($keyType)'}',
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (fingerprint.isNotEmpty)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class SmbSettings extends StatelessWidget {
|
|||
padding: const EdgeInsets.only(left: 16),
|
||||
child: SettingTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.dns, color: JjColors.accent),
|
||||
leading: Icon(Icons.dns, color: JjColors.accent),
|
||||
title: Text(s.label),
|
||||
subtitle: Text(
|
||||
[
|
||||
|
|
@ -66,7 +66,7 @@ class SmbSettings extends StatelessWidget {
|
|||
),
|
||||
IconButton(
|
||||
tooltip: tr('지우기'),
|
||||
icon: const Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
|
||||
icon: Icon(Icons.delete_outline, size: 18, color: JjColors.textDim),
|
||||
onPressed: () => deleteSmbServer(context, c, s),
|
||||
),
|
||||
]),
|
||||
|
|
@ -320,14 +320,14 @@ Future<LanHost?> pickLanSmbServer(BuildContext context) async {
|
|||
width: 460,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(tr('먼저 mDNS 로 묻고 (이름을 알리는 NAS), 이어서 이 기기가 연결된 망 (Wi-Fi · 유선) 의 기기들에 SMB (445) 연결만 해 보고 바로 닫습니다. 파일 · 계정 정보는 보내지 않습니다.'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 10),
|
||||
if (list == null && error == null) ...[
|
||||
LinearProgressIndicator(value: total > 0 ? done / total : null),
|
||||
const SizedBox(height: 4),
|
||||
Text(total > 0 ? trf('{0} / {1} 확인', [done, total]) : tr('준비하는 중'), style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
if (error != null) Text('$error', style: const TextStyle(color: JjColors.danger)),
|
||||
if (error != null) Text('$error', style: TextStyle(color: JjColors.danger)),
|
||||
if (list != null && list.isEmpty) Text(tr('찾지 못했습니다. NAS 가 켜져 있고 같은 망에 있는지 확인하거나, 주소를 직접 넣으세요.')),
|
||||
if (list != null)
|
||||
for (final h in list)
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ class _SqliteViewerPageState extends State<SqliteViewerPage> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 0),
|
||||
child: Text(tr('읽기 전용 - 이 앱은 파일을 바꾸지 않습니다. 쓰는 중인 바뀐 것 (-wal) 은 보이지 않을 수 있습니다'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
if (_tables.isNotEmpty)
|
||||
Padding(
|
||||
|
|
@ -166,11 +166,11 @@ class _SqliteViewerPageState extends State<SqliteViewerPage> {
|
|||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (_count != null) Text(trf('{0}행', [_count!]), style: const TextStyle(color: JjColors.textDim)),
|
||||
if (_count != null) Text(trf('{0}행', [_count!]), style: TextStyle(color: JjColors.textDim)),
|
||||
]),
|
||||
),
|
||||
if (_error != null)
|
||||
Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: const TextStyle(color: JjColors.danger))),
|
||||
Padding(padding: const EdgeInsets.all(16), child: Text(_error!, style: TextStyle(color: JjColors.danger))),
|
||||
if (_busy && _rows.isEmpty) const Padding(padding: EdgeInsets.all(24), child: Center(child: CircularProgressIndicator())),
|
||||
if (!_busy && _error == null && _tables.isEmpty) Padding(padding: const EdgeInsets.all(16), child: Text(tr('테이블이 없습니다'))),
|
||||
if (t != null && _rows.isNotEmpty)
|
||||
|
|
|
|||
|
|
@ -158,7 +158,7 @@ class _StorageAnalysisPageState extends State<StorageAnalysisPage> {
|
|||
Text(_error != null ? trf('분석하지 못했습니다: {0}', [_error!]) : trf('읽는 중 · 폴더 {0}개 · {1}', [_dirs, formatSize(_bytes)]),
|
||||
textAlign: TextAlign.center),
|
||||
const SizedBox(height: 4),
|
||||
Text(vDisplay(widget.root), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(vDisplay(widget.root), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
|
@ -176,7 +176,7 @@ class _StorageAnalysisPageState extends State<StorageAnalysisPage> {
|
|||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(vDisplay(_cur), maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
Text(trf('{0} · 파일 {1}개', [formatSize(s.sizeOf(_cur)), s.filesOf(_cur)]),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]),
|
||||
),
|
||||
TextButton.icon(
|
||||
|
|
|
|||
|
|
@ -453,7 +453,7 @@ class _SubtitleEditorPageState extends State<SubtitleEditorPage> {
|
|||
value: _rate,
|
||||
isDense: true,
|
||||
underline: const SizedBox(),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.text),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.text),
|
||||
items: [
|
||||
for (final r in const [0.5, 0.75, 1.0, 1.25, 1.5, 2.0])
|
||||
DropdownMenuItem(value: r, child: Text('${r}x')),
|
||||
|
|
@ -529,7 +529,7 @@ class _SubtitleEditorPageState extends State<SubtitleEditorPage> {
|
|||
value: e.saveCharset,
|
||||
isDense: true,
|
||||
underline: const SizedBox(),
|
||||
style: const TextStyle(fontSize: 13, color: JjColors.text),
|
||||
style: TextStyle(fontSize: 13, color: JjColors.text),
|
||||
items: [
|
||||
for (final c in saveCharsets.entries)
|
||||
DropdownMenuItem(value: c.key, child: Text(c.value)),
|
||||
|
|
@ -591,7 +591,7 @@ class _HeaderRow extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
const _hdr = TextStyle(fontSize: 12, color: JjColors.textDim);
|
||||
final _hdr = TextStyle(fontSize: 12, color: JjColors.textDim);
|
||||
|
||||
class _CueRow extends StatefulWidget {
|
||||
final int index;
|
||||
|
|
@ -661,7 +661,7 @@ class _CueRowState extends State<_CueRow> {
|
|||
? JjColors.accent.withValues(alpha: 0.12)
|
||||
: null,
|
||||
border: Border(
|
||||
bottom: const BorderSide(color: JjColors.border),
|
||||
bottom: BorderSide(color: JjColors.border),
|
||||
left: BorderSide(
|
||||
color: widget.active ? JjColors.accent : Colors.transparent, width: 3),
|
||||
),
|
||||
|
|
@ -701,13 +701,13 @@ class _CueRowState extends State<_CueRow> {
|
|||
IconButton(
|
||||
tooltip: tr('아래에 줄 추가'),
|
||||
iconSize: 18,
|
||||
icon: const Icon(Icons.add, color: JjColors.textDim),
|
||||
icon: Icon(Icons.add, color: JjColors.textDim),
|
||||
onPressed: () => e.insertAfter(widget.index),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: tr('줄 삭제'),
|
||||
iconSize: 18,
|
||||
icon: const Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
icon: Icon(Icons.delete_outline, color: JjColors.textDim),
|
||||
onPressed: () => e.removeAt(widget.index),
|
||||
),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class _SyncPreviewViewState extends State<SyncPreviewView> {
|
|||
return Row(children: [
|
||||
const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
const SizedBox(width: 8),
|
||||
Text(tr('무엇이 바뀌는지 비교하는 중…'), style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
Text(tr('무엇이 바뀌는지 비교하는 중…'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]);
|
||||
}
|
||||
int n(PreviewAction a, [bool? right]) =>
|
||||
|
|
@ -244,10 +244,10 @@ Future<bool> confirmDeletingRun(BuildContext context, CopyTask t, {bool includeM
|
|||
for (final s in t.sources) ...[
|
||||
Text(shortPath(s), style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text('→ ${shortPath(dstOf(s))}', style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text('${vDisplay(s)} → ${vDisplay(dstOf(s))}', style: const TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
Text('${vDisplay(s)} → ${vDisplay(dstOf(s))}', style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
],
|
||||
// 230: 칸에 적힌 옵션이 아니라 실제로 실행할 것 (원본 지우기 옵션은 이동일 때만 붙는다)
|
||||
Text(effectiveRunText(t.method, t.options, move: t.move, skipNewer: skipNewer), style: const TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
|
||||
Text(effectiveRunText(t.method, t.options, move: t.move, skipNewer: skipNewer), style: TextStyle(fontSize: 12, fontFamily: 'monospace', color: JjColors.textDim)),
|
||||
// 210: 이동이면 실행 뒤 원본에서 지워진다는 것을 분명히
|
||||
if (t.move)
|
||||
Padding(
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ class _TextViewerPageState extends State<TextViewerPage> {
|
|||
child: Row(children: [
|
||||
Expanded(
|
||||
child: Text(trf('{0} / {1} 읽음', [formatSize(_bytes.length), formatSize(_size)]),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
FilledButton.tonal(onPressed: _more, child: Text(tr('더 읽기'))),
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ class _SubtitleTimelineState extends State<SubtitleTimeline> {
|
|||
top: 3,
|
||||
child: IgnorePointer(
|
||||
child: Text(trf('보기 {0}초 · Ctrl+휠 확대/축소', [_window.toStringAsFixed(_window < 10 ? 1 : 0)]),
|
||||
style: const TextStyle(fontSize: 10, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 10, color: JjColors.textDim)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -304,7 +304,7 @@ class _RulerPainter extends CustomPainter {
|
|||
if (major) {
|
||||
tp.text = TextSpan(
|
||||
text: _label(t, step),
|
||||
style: const TextStyle(fontSize: 10, color: JjColors.textDim));
|
||||
style: TextStyle(fontSize: 10, color: JjColors.textDim));
|
||||
tp.layout();
|
||||
tp.paint(canvas, Offset(x + 3, 1));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -634,7 +634,7 @@ class _TorPageState extends State<TorPage> with WebProxyVisibility {
|
|||
const SizedBox(height: 12),
|
||||
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), textAlign: TextAlign.center),
|
||||
const SizedBox(height: 8),
|
||||
Text(body, style: const TextStyle(color: JjColors.textDim), textAlign: TextAlign.center),
|
||||
Text(body, style: TextStyle(color: JjColors.textDim), textAlign: TextAlign.center),
|
||||
]),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ class _TrashPageState extends State<TrashPage> {
|
|||
if (items != null && items.isNotEmpty)
|
||||
TextButton.icon(
|
||||
onPressed: _busy ? null : () => _purge(items, all: true),
|
||||
icon: const Icon(Icons.delete_forever_outlined, color: JjColors.danger),
|
||||
label: Text(tr('비우기'), style: const TextStyle(color: JjColors.danger)),
|
||||
icon: Icon(Icons.delete_forever_outlined, color: JjColors.danger),
|
||||
label: Text(tr('비우기'), style: TextStyle(color: JjColors.danger)),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
@ -115,7 +115,7 @@ class _TrashPageState extends State<TrashPage> {
|
|||
widget.days > 0
|
||||
? trf('지운 것은 {0}일 동안 여기에 두었다가 저절로 지웁니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).', [widget.days])
|
||||
: tr('휴지통이 꺼져 있습니다 - 지우면 바로 영구 삭제됩니다 (환경 설정 > 파일 탐색기 > 휴지통 보관).'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim),
|
||||
),
|
||||
),
|
||||
if (_busy) const LinearProgressIndicator(minHeight: 2),
|
||||
|
|
@ -151,7 +151,7 @@ class _TrashPageState extends State<TrashPage> {
|
|||
Expanded(child: Text(trf('{0}개 고름', [chosen.length]))),
|
||||
TextButton(
|
||||
onPressed: _busy ? null : () => _purge(chosen),
|
||||
child: Text(tr('영구 삭제'), style: const TextStyle(color: JjColors.danger)),
|
||||
child: Text(tr('영구 삭제'), style: TextStyle(color: JjColors.danger)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
FilledButton.icon(
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ Future<void> checkForUpdate(BuildContext context, AppController c, {bool manual
|
|||
Text(trf('지금 쓰는 버전: v{0} → 새 버전: v{1}', [current, r.version])),
|
||||
if (r.zipSize > 0)
|
||||
Text(trf('받는 크기: 약 {0}MB · 받은 파일 · AI 모델 · 설정은 그대로 둡니다', [(r.zipSize / 1e6).round()]),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
const SizedBox(height: 12),
|
||||
Text(tr('바뀐 내용'), style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 4),
|
||||
|
|
@ -160,7 +160,7 @@ class _UpdateCheckButtonState extends State<UpdateCheckButton> {
|
|||
Widget build(BuildContext context) => Wrap(spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [
|
||||
OutlinedButton(onPressed: _busy ? null : _check, child: Text(tr('최신 버전 확인'))),
|
||||
if (_busy) const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
if (_result != null) Text(_result!, style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (_result != null) Text(_result!, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +220,7 @@ class _VersionPickerState extends State<_VersionPicker> {
|
|||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
|
||||
child: Text(trf('지금 쓰는 버전: v{0}', [widget.current]),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
),
|
||||
for (final r in all)
|
||||
ListTile(
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
|||
Text(trf('넣을 곳: {0}', [_cwdLabel()]), style: const TextStyle(fontSize: 12.5)),
|
||||
if (remote)
|
||||
Text(tr('원격 파일은 이 기기의 앱 전용 폴더에 받아 넣고, 받은 임시 파일은 바로 지웁니다'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
for (final f in plan.failed.entries.take(3))
|
||||
Text(trf('읽지 못해 빼고 넣습니다: {0} ({1})', [vBasename(f.key), f.value]),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent), maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
|
|
@ -853,7 +853,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
|||
Widget _unlockForm() {
|
||||
final waiting = _waitUntil != null && DateTime.now().isBefore(_waitUntil!);
|
||||
return ListView(padding: const EdgeInsets.all(16), children: [
|
||||
const Icon(Icons.lock_outline, size: 48, color: JjColors.textDim),
|
||||
Icon(Icons.lock_outline, size: 48, color: JjColors.textDim),
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: _pw,
|
||||
|
|
@ -898,7 +898,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
|||
Expanded(
|
||||
child: _cwd.isEmpty
|
||||
? Text(tr('넣기는 파일 탐색기에서 파일 · 폴더를 길게 눌러 [보관함에 넣기]'),
|
||||
style: const TextStyle(fontSize: 12, color: JjColors.textDim))
|
||||
style: TextStyle(fontSize: 12, color: JjColors.textDim))
|
||||
: Text(_cwdLabel(), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
),
|
||||
DropdownButton<String>(
|
||||
|
|
@ -922,7 +922,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
|||
final f = _cwd.isEmpty ? dirs[i] : '$_cwd/${dirs[i]}';
|
||||
final n = v.filesUnder(f).length;
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.folder, color: JjColors.accent),
|
||||
leading: Icon(Icons.folder, color: JjColors.accent),
|
||||
title: Text(dirs[i], maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(trf('파일 {0}개', [n])),
|
||||
onTap: _busy ? null : () => setState(() => _cwd = f),
|
||||
|
|
@ -947,7 +947,7 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
|
|||
}
|
||||
final e = list[i - dirs.length];
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.lock, color: JjColors.accent),
|
||||
leading: Icon(Icons.lock, color: JjColors.accent),
|
||||
title: Text(e.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text('${formatSize(e.size)} · ${_day(e.added)}'),
|
||||
onTap: _busy ? null : () => _view(e),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class _WifiSharePageState extends State<WifiSharePage> {
|
|||
List<Widget> _offView() {
|
||||
final mins = widget.c.settings.wifiShareMinutes;
|
||||
return [
|
||||
Text(tr('공유할 폴더'), style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
Text(tr('공유할 폴더'), style: TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
const SizedBox(height: 4),
|
||||
SelectableText(widget.folder, style: const TextStyle(fontSize: 15)),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -168,9 +168,9 @@ class _WifiSharePageState extends State<WifiSharePage> {
|
|||
final left = s.endsAt?.difference(DateTime.now());
|
||||
return [
|
||||
Row(children: [
|
||||
const Icon(Icons.wifi_tethering, color: JjColors.success),
|
||||
Icon(Icons.wifi_tethering, color: JjColors.success),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(tr('공유 중'), style: const TextStyle(fontSize: 16, color: JjColors.success))),
|
||||
Expanded(child: Text(tr('공유 중'), style: TextStyle(fontSize: 16, color: JjColors.success))),
|
||||
FilledButton.icon(
|
||||
style: FilledButton.styleFrom(backgroundColor: JjColors.danger),
|
||||
onPressed: _stop,
|
||||
|
|
@ -179,15 +179,15 @@ class _WifiSharePageState extends State<WifiSharePage> {
|
|||
),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
SelectableText(s.root, style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
SelectableText(s.root, style: TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
const SizedBox(height: 16),
|
||||
Text(tr('접속 코드'), style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
Text(tr('접속 코드'), style: TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
SelectableText(s.code, style: const TextStyle(fontSize: 40, letterSpacing: 8, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(height: 12),
|
||||
if (hosts.isEmpty)
|
||||
_note(Icons.wifi_off, tr('이 기기의 Wi-Fi 주소를 찾지 못했습니다. Wi-Fi 에 연결돼 있는지 확인하세요.'), color: JjColors.danger)
|
||||
else ...[
|
||||
Text(tr('다른 기기의 브라우저에서 이 주소를 여세요'), style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
Text(tr('다른 기기의 브라우저에서 이 주소를 여세요'), style: TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
for (final h in hosts)
|
||||
Row(children: [
|
||||
Expanded(child: SelectableText('http://$h:${s.port}', style: const TextStyle(fontSize: 16))),
|
||||
|
|
@ -198,7 +198,7 @@ class _WifiSharePageState extends State<WifiSharePage> {
|
|||
),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
Text(tr('또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)'), style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
Text(tr('또는 다른 기기의 카메라로 QR 을 찍으세요 (코드가 들어 있습니다)'), style: TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
const SizedBox(height: 8),
|
||||
Center(child: QrView(data: s.urlWithCode(hosts.first), size: 200)),
|
||||
],
|
||||
|
|
@ -215,9 +215,9 @@ class _WifiSharePageState extends State<WifiSharePage> {
|
|||
]),
|
||||
const Divider(height: 32),
|
||||
Text(trf('접속 기록 ({0}) - 이 화면에만 보이고 저장하지 않습니다', [s.log.length]),
|
||||
style: const TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
style: TextStyle(color: JjColors.textDim, fontSize: 12)),
|
||||
if (s.log.isEmpty)
|
||||
Padding(padding: const EdgeInsets.symmetric(vertical: 8), child: Text(tr('아직 접속한 기기가 없습니다'), style: const TextStyle(color: JjColors.textDim)))
|
||||
Padding(padding: const EdgeInsets.symmetric(vertical: 8), child: Text(tr('아직 접속한 기기가 없습니다'), style: TextStyle(color: JjColors.textDim)))
|
||||
else
|
||||
for (final a in s.log.take(50))
|
||||
Padding(
|
||||
|
|
@ -268,7 +268,9 @@ class _WifiSharePageState extends State<WifiSharePage> {
|
|||
],
|
||||
);
|
||||
|
||||
Widget _note(IconData icon, String text, {Color color = JjColors.textDim}) => Padding(
|
||||
Widget _note(IconData icon, String text, {Color? color}) => _noteIn(icon, text, color ?? JjColors.textDim);
|
||||
|
||||
Widget _noteIn(IconData icon, String text, Color color) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Icon(icon, size: 18, color: color),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,118 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/app/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/settings.dart';
|
||||
import 'package:jj_mkvmaker/main.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/desktop_storage_service.dart';
|
||||
import 'package:jj_mkvmaker/platform/windows/process_media_tool.dart';
|
||||
import 'package:jj_mkvmaker/services/platform_services.dart';
|
||||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
|
||||
/// X-plore 비교 5-1 ⑦ 밝은 테마 (관리자 지시 10/21): 처음 값 어둡게 (예전 그대로) · 밝게 · 기기 따라 · 바꾸면 상태는 그대로 다시 그림
|
||||
double _lum(Color c) {
|
||||
double ch(double v) => v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4).toDouble();
|
||||
return 0.2126 * ch(c.r) + 0.7152 * ch(c.g) + 0.0722 * ch(c.b);
|
||||
}
|
||||
|
||||
/// WCAG 대비
|
||||
double contrast(Color a, Color b) {
|
||||
final x = _lum(a), y = _lum(b);
|
||||
return (max(x, y) + 0.05) / (min(x, y) + 0.05);
|
||||
}
|
||||
|
||||
class _Probe extends StatefulWidget {
|
||||
const _Probe();
|
||||
@override
|
||||
State<_Probe> createState() => _ProbeState();
|
||||
}
|
||||
|
||||
class _ProbeState extends State<_Probe> {
|
||||
int taps = 0;
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: () => setState(() => taps++),
|
||||
child: Text('probe $taps', style: TextStyle(color: JjColors.text)),
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
tearDown(() => JjColors.use(Brightness.dark));
|
||||
|
||||
test('처음 값은 어둡게 (예전 색 그대로) · 모르는 값 · 없는 값도 어둡게 · 고른 값은 그대로', () {
|
||||
expect(AppSettings().uiBrightness, 'dark');
|
||||
expect(AppSettings.fromJson({}).uiBrightness, 'dark');
|
||||
expect(AppSettings.fromJson({'uiBrightness': 'neon'}).uiBrightness, 'dark');
|
||||
expect(AppSettings.fromJson({'uiBrightness': 'light'}).uiBrightness, 'light');
|
||||
expect(AppSettings.fromJson(AppSettings.fromJson({'uiBrightness': 'system'}).toJson()).uiBrightness, 'system');
|
||||
expect(JjColors.bg, const Color(0xFF141417));
|
||||
expect(JjColors.accent, const Color(0xFF00C4CC));
|
||||
expect(JjColors.textDim, const Color(0xFF8E8E99));
|
||||
expect(resolveBrightness('dark', Brightness.light), Brightness.dark);
|
||||
expect(resolveBrightness('light', Brightness.dark), Brightness.light);
|
||||
expect(resolveBrightness('system', Brightness.light), Brightness.light);
|
||||
expect(resolveBrightness('?', Brightness.light), Brightness.dark);
|
||||
});
|
||||
|
||||
test('밝은 색: 바탕 · 칸 위 글자 대비 4.5:1 이상 (글 · 흐린 글 · 강조 · 경고 · 성공 · 강조 위 글자)', () {
|
||||
const l = JjPalette.light;
|
||||
for (final bg in [l.bg, l.panel, l.panelHigh]) {
|
||||
for (final (name, fg) in [('text', l.text), ('textDim', l.textDim), ('accent', l.accent), ('danger', l.danger), ('success', l.success)]) {
|
||||
expect(contrast(fg, bg), greaterThanOrEqualTo(4.5), reason: '$name on ${bg.toARGB32().toRadixString(16)}');
|
||||
}
|
||||
}
|
||||
expect(contrast(l.onAccent, l.accent), greaterThanOrEqualTo(4.5));
|
||||
expect(buildTheme(Brightness.light).brightness, Brightness.light);
|
||||
expect(buildTheme(Brightness.light).scaffoldBackgroundColor, l.bg);
|
||||
expect(buildTheme(Brightness.dark).scaffoldBackgroundColor, JjPalette.dark.bg);
|
||||
});
|
||||
|
||||
testWidgets('색을 바꾸면 JjColors 를 직접 읽는 곳도 다시 그림 · 상태는 그대로', (t) async {
|
||||
await t.pumpWidget(const MaterialApp(home: Scaffold(body: _Probe())));
|
||||
await t.tap(find.text('probe 0'));
|
||||
await t.pump();
|
||||
expect(t.widget<Text>(find.text('probe 1')).style!.color, JjPalette.dark.text);
|
||||
expect(JjColors.use(Brightness.light), isTrue);
|
||||
expect(JjColors.use(Brightness.light), isFalse, reason: '같으면 바꾸지 않음');
|
||||
rebuildAllElements(t.binding.rootElement!);
|
||||
await t.pump();
|
||||
expect(t.widget<Text>(find.text('probe 1')).style!.color, JjPalette.light.text, reason: '눌러 둔 수 (상태) 는 그대로');
|
||||
});
|
||||
|
||||
testWidgets('앱: 설정 "화면 밝기" 를 밝게 → 바로 밝은 테마 · 기기 따라는 기기 밝기를 따라 바뀜 · 어둡게로 돌아옴', (t) async {
|
||||
t.view.physicalSize = const Size(1400, 900);
|
||||
t.view.devicePixelRatio = 1;
|
||||
addTearDown(t.view.reset);
|
||||
addTearDown(t.platformDispatcher.clearPlatformBrightnessTestValue);
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
await t.runAsync(c.init);
|
||||
await t.pumpWidget(JjMkvMakerApp(controller: c));
|
||||
await t.pump();
|
||||
BuildContext ctx() => t.element(find.byType(Scaffold).first);
|
||||
expect(Theme.of(ctx()).brightness, Brightness.dark);
|
||||
expect(JjColors.isLight, isFalse);
|
||||
await t.runAsync(() => c.updateSettings((x) => x.uiBrightness = 'light'));
|
||||
await t.pump();
|
||||
await t.pump();
|
||||
expect(JjColors.isLight, isTrue);
|
||||
expect(Theme.of(ctx()).brightness, Brightness.light);
|
||||
expect(Theme.of(ctx()).scaffoldBackgroundColor, JjPalette.light.bg);
|
||||
// 기기 따라
|
||||
t.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
|
||||
await t.runAsync(() => c.updateSettings((x) => x.uiBrightness = 'system'));
|
||||
await t.pump();
|
||||
await t.pump();
|
||||
expect(JjColors.isLight, isFalse);
|
||||
t.platformDispatcher.platformBrightnessTestValue = Brightness.light;
|
||||
await t.pump();
|
||||
await t.pump();
|
||||
expect(JjColors.isLight, isTrue, reason: '기기 밝기가 바뀌면 따라감');
|
||||
await t.runAsync(() => c.updateSettings((x) => x.uiBrightness = 'dark'));
|
||||
await t.pump();
|
||||
await t.pump();
|
||||
expect(JjColors.isLight, isFalse);
|
||||
expect(Theme.of(ctx()).brightness, Brightness.dark);
|
||||
await t.pumpWidget(const SizedBox());
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue