From 7970dba2712085814bd4ab79fbb1ed68d6b2cfe2 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:32:36 +0900 Subject: [PATCH] =?UTF-8?q?W=20TailText:=20=EC=B0=BD=20=EC=9C=84=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=A4=84=EB=8F=84=20=EB=81=9D=20(?= =?UTF-8?q?=EC=A7=80=EA=B8=88=20=ED=8F=B4=EB=8D=94)=20=EC=9D=B4=20?= =?UTF-8?q?=EB=B3=B4=EC=9D=B4=EA=B2=8C=20=C2=B7=20=ED=8F=AD=20=EC=9E=AC?= =?UTF-8?q?=EA=B8=B0=EB=8A=94=20path=5Flabel.textWidth=20=ED=95=98?= =?UTF-8?q?=EB=82=98=EB=A1=9C=20+=20=ED=83=90=EC=83=89=EA=B8=B0=20?= =?UTF-8?q?=EC=BA=A1=EC=B2=98=20=ED=9D=94=EB=93=A4=EB=A6=BC=20=EA=B3=A0?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pane_view 의 경로 줄 (폴더 + 파일 목록 · TC 모양 오른쪽): 예전에는 끝을 "…" 로 잘라 지금 폴더가 안 보였다 → TailText (앞을 줄임) - TailText 의 폭 재기를 path_label 의 textWidth 로 (글 방향 · 다 쓴 TextPainter 놓기 - 예전에는 매번 새로 만들고 놓지 않음) - AdaptiveRow · ColumnTable 점검: 좁을 때 모양을 바꾸는 다른 곳은 목록 / 자세히 세로 쌓기 · 칩 줄 따로 등 "앞 묶음 · 뒤 묶음" 꼴이 아니라 그대로. 열 판단은 이미 columnLayout (순수 함수, 312 조합 시험) 하나를 열 머리 · 줄이 같이 씀 - 시험 쪽 흔들림 고정: support/shot_services (shotServices - CPU · 메모리 · 디스크, shotDevice - 드라이브 남은 공간) 를 check_shot · explorer_shot · explorer_text 가 같이 씀. explorer_text 는 JJ_SHOT_TMP 아래 고정 폴더 · 시각, xplore 는 고정 폴더 · 시각 · 압축 안 항목 시각 - 사용자가 보게 될 동작: 탐색기 두 창 · 폴더 + 파일 목록의 오른쪽 위 경로 줄이 "…\Videos\Movies" 처럼 끝이 보임 Co-Authored-By: Claude Opus 5.5 --- integration_test/explorer_shot_test.dart | 8 ++-- integration_test/explorer_text_shot_test.dart | 16 +++++--- integration_test/support/shot_services.dart | 40 +++++++++++++++++++ integration_test/windows_check_shot_test.dart | 34 ++-------------- .../windows_xplore_shot_test.dart | 12 ++++-- lib/ui/explorer/pane_view.dart | 4 +- lib/ui/tail_text.dart | 8 ++-- 7 files changed, 75 insertions(+), 47 deletions(-) create mode 100644 integration_test/support/shot_services.dart diff --git a/integration_test/explorer_shot_test.dart b/integration_test/explorer_shot_test.dart index 4e4db1d..b09e7c8 100644 --- a/integration_test/explorer_shot_test.dart +++ b/integration_test/explorer_shot_test.dart @@ -6,10 +6,11 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; -import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:jj_mkvmaker/ui/theme.dart'; +import 'support/shot_services.dart'; + /// 파일 탐색기 화면을 실제 글꼴 · 실제 FFmpeg (썸네일 · 동영상 정보) 로 그려 PNG 로 저장 (JJ_SHOT_DIR). /// 왼쪽 · 오른쪽 창 폴더: JJ_SHOT_LEFT · JJ_SHOT_RIGHT. 화면 배치 확인용. /// JJ_SHOT_THEME=light 면 밝은 테마로 (⑦ - 파일 이름 끝에 _light) @@ -19,7 +20,8 @@ void main() { testWidgets('파일 탐색기 화면 그림', (tester) async { final env = Platform.environment; final out = env['JJ_SHOT_DIR'] ?? Directory.systemTemp.path; - final c = AppController(PlatformServices.create()); + // 관리자 지시 (10/24): 실행마다 다른 값 (폴더 이름 · 시각 · CPU · 메모리) 을 정한 값으로 (캡처가 흔들리지 않게 - 시험 쪽에서만) + final c = AppController(shotServices()); c.settings.explorer.paths = [env['JJ_SHOT_LEFT'] ?? Directory.current.path, env['JJ_SHOT_RIGHT'] ?? Directory.current.path]; final key = GlobalKey(); final light = env['JJ_SHOT_THEME'] == 'light'; @@ -39,7 +41,7 @@ void main() { tester.view.devicePixelRatio = 1; await tester.runAsync(() => tester.pumpWidget(MaterialApp( theme: buildTheme(), - home: RepaintBoundary(key: key, child: ExplorerPage(c: c)), + home: RepaintBoundary(key: key, child: ExplorerPage(c: c, device: shotDevice())), ))); // 목록 · 썸네일 · 동영상 정보가 나올 때까지 for (var i = 0; i < 40; i++) { diff --git a/integration_test/explorer_text_shot_test.dart b/integration_test/explorer_text_shot_test.dart index 38a9b75..cacfc51 100644 --- a/integration_test/explorer_text_shot_test.dart +++ b/integration_test/explorer_text_shot_test.dart @@ -7,11 +7,13 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:jj_mkvmaker/app/app_controller.dart'; -import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; import 'package:jj_mkvmaker/ui/theme.dart'; import 'package:path/path.dart' as p; +import 'support/shot_services.dart'; +import 'support/temp_dir.dart'; + /// X-plore 비교 40 (관리자 지시 10/20): 412 폭 · 목록 글자 150% · 넓게 (그리고 처음 값) 를 실제 글꼴로 그려 PNG 로 (JJ_SHOT_DIR). /// 세 스타일 모두. 넘치면 (RenderFlex overflow) 시험이 실패한다 void main() { @@ -21,14 +23,18 @@ void main() { testWidgets('40: 목록 글자 크기 · 줄 간격 화면 그림 (412 폭)', (tester) async { final out = Platform.environment['JJ_SHOT_DIR'] ?? Directory.systemTemp.path; - final tmp = Directory.systemTemp.createTempSync('jj_textshot_'); - addTearDown(() => tmp.deleteSync(recursive: true)); + // 관리자 지시 (10/24): 실행마다 다른 값 (폴더 이름 · 시각 · CPU · 메모리) 을 정한 값으로 (캡처가 흔들리지 않게 - 시험 쪽에서만) + // 트리에 둘레 폴더도 보여 JJ_SHOT_TMP (캡처용으로 따로 둔 폴더) 아래에 둔다 + final base = Platform.environment['JJ_SHOT_TMP'] ?? ''; + final tmp = testFixedTempDir('jj_textshot_fixed', base: base); File(p.join(tmp.path, '아주 긴 이름의 동영상 파일 - 글자 크기 시험용 2026 최종본.mp4')).writeAsStringSync('v' * 123456); File(p.join(tmp.path, 'Very long English file name for the text size check.txt')).writeAsStringSync('t' * 4321); File(p.join(tmp.path, 'a.jpg')).writeAsStringSync('x'); Directory(p.join(tmp.path, '폴더 하나')).createSync(); Directory(p.join(tmp.path, 'Another folder with a long name')).createSync(); - final c = AppController(PlatformServices.create()); + // 캡처용 폴더가 따로 있으면 그 폴더의 시각까지 (트리에 보인다). 시스템 임시 폴더면 시험 폴더만 + pinTestTimes(base.isEmpty ? tmp : Directory(base)); + final c = AppController(shotServices()); c.settings.explorer.paths = [tmp.path, tmp.path]; final key = GlobalKey(); // 폰처럼 (Android 는 트리 펼치기 화살표가 44 로 넓다 - Windows 로 그리면 폰보다 넉넉해 보인다) @@ -47,7 +53,7 @@ void main() { theme: buildTheme(), // 기기 글자 크기 (관리자 지시 10/20: 1.3 × 150% 에서 두 창이 모자라면 한 창) builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: TextScaler.linear(device)), child: child!), - home: RepaintBoundary(key: key, child: ExplorerPage(c: c)), + home: RepaintBoundary(key: key, child: ExplorerPage(c: c, device: shotDevice())), ))); for (var i = 0; i < 12; i++) { await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 250))); diff --git a/integration_test/support/shot_services.dart b/integration_test/support/shot_services.dart new file mode 100644 index 0000000..0d592a0 --- /dev/null +++ b/integration_test/support/shot_services.dart @@ -0,0 +1,40 @@ +import 'package:jj_mkvmaker/services/platform_services.dart'; +import 'package:jj_mkvmaker/services/system_usage.dart'; + +import '../../test/support/test_explorer_device.dart'; + +/// 캡처용 (관리자 10/24): 위 막대의 CPU · 메모리는 이 PC 의 실제 값이라 실행마다 달라 캡처가 흔들렸다 - 정한 값 (시험 쪽에서만) +class FixedUsage implements SystemUsage { + const FixedUsage(); + @override + UsageSample? read() => const UsageSample(cpu: 0.05, memUsed: 31, memTotal: 100); + @override + (int, int)? disk(String path) => null; +} + +/// 캡처용 실제 조립 ([PlatformServices.create] 그대로) 에서 실행마다 다른 값만 정한 값으로: +/// CPU · 메모리 ([FixedUsage]) · 디스크 남은 / 전체 (100 GB / 500 GB). 나머지 (엔진 · 저장소 · 기기) 는 실제 것 +PlatformServices shotServices() { + final r = PlatformServices.create(); + return PlatformServices( + mediaTool: r.mediaTool, + storage: r.storage, + createPlayer: r.createPlayer, + createMediaPlayer: r.createMediaPlayer, + createRecognizer: r.createRecognizer, + createTranslator: r.createTranslator, + createDownloadBackends: r.createDownloadBackends, + createSubtitleProviders: r.createSubtitleProviders, + createUpscaler: r.createUpscaler, + findLanSmb: r.findLanSmb, + diskSpace: (_) async => (107374182400, 536870912000), + updater: r.updater, + usage: r.usage == null ? null : const FixedUsage(), + models: r.models, + shell: r.shell, + platform: r.platform, + ); +} + +/// 캡처용 파일 탐색기 기기: 실제 기기 그대로 (목록 · 드라이브 · 휴지통) 에서 드라이브 남은 / 전체 (TC 모양 창 위 줄) 만 정한 값 +TestExplorerDevice shotDevice() => TestExplorerDevice()..space = (_) => (107374182400, 536870912000); diff --git a/integration_test/windows_check_shot_test.dart b/integration_test/windows_check_shot_test.dart index 4317fb2..04c1718 100644 --- a/integration_test/windows_check_shot_test.dart +++ b/integration_test/windows_check_shot_test.dart @@ -10,8 +10,6 @@ import 'package:jj_mkvmaker/app/app_controller.dart'; import 'package:jj_mkvmaker/app/master_lock.dart'; import 'package:jj_mkvmaker/platform/windows/recycle_bin.dart'; import 'package:jj_mkvmaker/services/image_ai.dart'; -import 'package:jj_mkvmaker/services/platform_services.dart'; -import 'package:jj_mkvmaker/services/system_usage.dart'; import 'package:jj_mkvmaker/services/secret_store.dart'; import 'package:jj_mkvmaker/ui/ai_image_page.dart'; import 'package:jj_mkvmaker/ui/explorer_page.dart'; @@ -22,6 +20,7 @@ import 'package:path/path.dart' as p; import '../test/support/fake_recycle_bin.dart'; import '../test/support/test_explorer_device.dart'; +import 'support/shot_services.dart'; import 'support/temp_dir.dart'; /// 기기 감독 (Windows) 확인용: 실제 앱 화면을 띄워 누르고 단계마다 PNG (JJ_SHOT_DIR) 로 남긴다. @@ -69,28 +68,8 @@ void main() { await settle(t); } - /// 실제 조립에서 위 막대의 CPU · 메모리 값만 고정 (실행마다 이 PC 의 실제 CPU % 가 달라 캡처가 흔들렸다 - 시험 쪽에서만) - AppController controller() { - final r = PlatformServices.create(); - return AppController(PlatformServices( - mediaTool: r.mediaTool, - storage: r.storage, - createPlayer: r.createPlayer, - createMediaPlayer: r.createMediaPlayer, - createRecognizer: r.createRecognizer, - createTranslator: r.createTranslator, - createDownloadBackends: r.createDownloadBackends, - createSubtitleProviders: r.createSubtitleProviders, - createUpscaler: r.createUpscaler, - findLanSmb: r.findLanSmb, - diskSpace: r.diskSpace, - updater: r.updater, - usage: r.usage == null ? null : const _FixedUsage(), - models: r.models, - shell: r.shell, - platform: r.platform, - )); - } + /// 실제 조립에서 실행마다 다른 값 (CPU · 메모리 · 디스크) 만 고정 - support/shot_services (시험 쪽에서만) + AppController controller() => AppController(shotServices()); /// 그 글이 화면에 나올 때까지 (목록을 다시 읽는 중일 수 있어) 기다렸다가 누른다 Future tapText(WidgetTester t, String text) async { @@ -336,10 +315,3 @@ Future _purgeRecycled(String dir, {required DateTime since}) async { } /// 늘 같은 값을 내는 사용량 원천 (CPU 5% · 메모리 31%) - 캡처가 실행마다 같게 -class _FixedUsage implements SystemUsage { - const _FixedUsage(); - @override - UsageSample? read() => const UsageSample(cpu: 0.05, memUsed: 31, memTotal: 100); - @override - (int, int)? disk(String path) => null; -} diff --git a/integration_test/windows_xplore_shot_test.dart b/integration_test/windows_xplore_shot_test.dart index f612652..c2baa6e 100644 --- a/integration_test/windows_xplore_shot_test.dart +++ b/integration_test/windows_xplore_shot_test.dart @@ -37,7 +37,8 @@ void main() { addTearDown(t.view.reset); await t.runAsync(() => i18n.apply(lang, save: false)); addTearDown(() => i18n.apply('ko', save: false)); - final tmp = testTempDir('jj_xpshot_'); + // 관리자 지시 (10/24): 실행마다 다른 값 (폴더 이름 · 시각 · CPU · 메모리) 을 정한 값으로 (캡처가 흔들리지 않게 - 시험 쪽에서만) + final tmp = testFixedTempDir('jj_xpshot_fixed'); appTempInto(tmp); // 앱 임시 폴더 (jj_mkvmaker) 도 이 아래로 - 끝에 함께 지움 (관리자 지시 10/20) final a = Directory(p.join(tmp.path, 'Videos'))..createSync(); final b = Directory(p.join(tmp.path, 'Backup'))..createSync(); @@ -47,8 +48,10 @@ void main() { File(p.join(a.path, 'Show S01', 'ep01.mkv')).writeAsBytesSync(List.filled(5000, 0)); File(p.join(a.path, 'notes.txt')).writeAsStringSync('memo'); File(p.join(a.path, 'Photos.zip')).writeAsBytesSync(ZipEncoder().encode(Archive() - ..addFile(ArchiveFile.string('2026/beach.jpg', 'x')) - ..addFile(ArchiveFile.string('readme.txt', 'readme')))); + // 압축 안 항목의 시각도 정해 둔다 (처음 값은 만든 때 - 캡처가 흔들림) + ..addFile(ArchiveFile.string('2026/beach.jpg', 'x')..lastModTime = _fixedZipTime) + ..addFile(ArchiveFile.string('readme.txt', 'readme')..lastModTime = _fixedZipTime))); + pinTestTimes(tmp); device.volumes = () => [(a.path, 'C:'), (b.path, 'D:')]; final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); c.settings @@ -106,3 +109,6 @@ void main() { } } } + +/// 캡처용 압축 안 항목 시각 (유닉스 초) +final _fixedZipTime = DateTime(2026, 10, 22, 9).millisecondsSinceEpoch ~/ 1000; diff --git a/lib/ui/explorer/pane_view.dart b/lib/ui/explorer/pane_view.dart index f8a42e4..0b5805c 100644 --- a/lib/ui/explorer/pane_view.dart +++ b/lib/ui/explorer/pane_view.dart @@ -19,6 +19,7 @@ import 'explorer_pane.dart'; import 'explorer_select.dart'; import 'row_view.dart'; import 'tc_drive_space.dart'; +import '../tail_text.dart'; /// 약속 (E5d-3): 창 하나를 그리는 데 화면이 주는 것. 창의 틀 (활성 · 놓기) · 창 머리 · 안내 · 표시 막대 · 줄 메뉴 · /// 끌어 놓기 · 필터 닫기는 아직 화면의 일이라 여기로 받는다 (나눌 때마다 이 목록이 줄어든다) @@ -226,7 +227,8 @@ class ExplorerListView extends StatelessWidget { height: 40, alignment: Alignment.centerLeft, padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text(ctl.displayPath(dir), maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: JjColors.textDim)), + // 경로는 끝 (지금 폴더) 이 보이게 앞을 줄인다 (TailText - 예전에는 끝을 "…" 로 잘라 지금 폴더가 안 보였다) + child: TailText(ctl.displayPath(dir), style: TextStyle(color: JjColors.textDim)), ), const Divider(height: 1), if (pane.filterOpen) ExplorerFilterBar(pane: pane, shown: shown.length, onClose: () => parts.closeFilter(pane), onChanged: parts.filterChanged), diff --git a/lib/ui/tail_text.dart b/lib/ui/tail_text.dart index 2d599da..3bab30b 100644 --- a/lib/ui/tail_text.dart +++ b/lib/ui/tail_text.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'path_label.dart' show textWidth; + /// 경로처럼 끝 (지금 폴더) 이 중요한 글 (관리자 지시 10/21, 28 지적): 다 들어가면 그대로, 모자라면 앞을 "…" 로 줄인다. /// 구분 ([separators] - " › " · "\" · "/") 에서 자르는 것을 먼저 (가장 긴 뒤쪽), 마지막 조각도 안 들어가면 글자 단위로. /// 지금 글꼴 · 글자 배율로 잰다 @@ -39,10 +41,8 @@ class TailText extends StatelessWidget { @override Widget build(BuildContext context) => LayoutBuilder(builder: (context, box) { - final s = DefaultTextStyle.of(context).style.merge(style); - final scaler = MediaQuery.textScalerOf(context); - double measure(String t) => - (TextPainter(text: TextSpan(text: t, style: s), textDirection: TextDirection.ltr, textScaler: scaler, maxLines: 1)..layout()).width; + // 폭 재기는 path_label 의 textWidth 하나로 (지금 글꼴 · 글자 배율 · 글 방향, 다 쓴 TextPainter 는 놓음) + double measure(String t) => textWidth(context, t, style ?? const TextStyle()); final shown = box.maxWidth.isFinite ? fit(text, box.maxWidth, measure, separators: separators) : text; return Text(shown, style: style, maxLines: 1, overflow: TextOverflow.clip, softWrap: false); });