WIP X-plore P3 보충 (캡처에서 찾음) · 경로 막대 누르기로 위 폴더 고르기 (검토 대기)
캡처 (integration_test/windows_p3_shot_test.dart - 해시 · 비교 · 폴더 속성 · 16진, 412/960 × ko/en/ja, release\test\shots_p3_fix) 에서 찾아 고침: - 16진 보기: 폭 412 에서 한 줄 16 바이트면 끝의 글자 칸이 화면 밖으로 잘렸다 → 폭 560 미만은 한 줄 8 바이트 (line(per:), 처음 값 16 그대로) - 해시 창: 닫자마자 글 칸 컨트롤러를 dispose 해 닫히는 동안 "A TextEditingController was used after being disposed" (닫을 때마다) → 닫힌 뒤 dispose 관리자 지시 3 (X-plore 비교표의 작은 것): 위 막대의 경로를 누르면 지금 폴더 · 위 폴더들 (창의 맨 위까지, 가까운 것부터) 이 나와 고르면 그리로 (X-plore 4.4x 경로 막대). 지금 폴더는 고를 수 없게 흐리게. 시험: explorer_test (새: 경로 막대 - 지금 폴더 못 고름 · 바로 위 = left · 고르면 하나 적어짐) · file_tools_test 통과, analyze 문제 없음, l10n 빠짐 0, 캡처 시험 6 통과. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_p3
parent
fba28cb027
commit
8eb1674382
|
|
@ -1778,6 +1778,7 @@
|
||||||
"위 ⇄ 아래": "Top ⇄ bottom",
|
"위 ⇄ 아래": "Top ⇄ bottom",
|
||||||
"위 폴더": "Parent folder",
|
"위 폴더": "Parent folder",
|
||||||
"위 폴더로": "Go up",
|
"위 폴더로": "Go up",
|
||||||
|
"위 폴더로 가기": "Go to a parent folder",
|
||||||
"위로": "Up",
|
"위로": "Up",
|
||||||
"위아래": "Top and bottom",
|
"위아래": "Top and bottom",
|
||||||
"위아래 ⇅": "Stacked ⇅",
|
"위아래 ⇅": "Stacked ⇅",
|
||||||
|
|
|
||||||
|
|
@ -1778,6 +1778,7 @@
|
||||||
"위 ⇄ 아래": "上 ⇄ 下",
|
"위 ⇄ 아래": "上 ⇄ 下",
|
||||||
"위 폴더": "上のフォルダー",
|
"위 폴더": "上のフォルダー",
|
||||||
"위 폴더로": "上のフォルダーへ",
|
"위 폴더로": "上のフォルダーへ",
|
||||||
|
"위 폴더로 가기": "上のフォルダーへ移動",
|
||||||
"위로": "上へ",
|
"위로": "上へ",
|
||||||
"위아래": "上下",
|
"위아래": "上下",
|
||||||
"위아래 ⇅": "上下 ⇅",
|
"위아래 ⇅": "上下 ⇅",
|
||||||
|
|
|
||||||
|
|
@ -1778,6 +1778,7 @@
|
||||||
"위 ⇄ 아래": "上 ⇄ 下",
|
"위 ⇄ 아래": "上 ⇄ 下",
|
||||||
"위 폴더": "上级文件夹",
|
"위 폴더": "上级文件夹",
|
||||||
"위 폴더로": "返回上级文件夹",
|
"위 폴더로": "返回上级文件夹",
|
||||||
|
"위 폴더로 가기": "转到上级文件夹",
|
||||||
"위로": "向上",
|
"위로": "向上",
|
||||||
"위아래": "上下",
|
"위아래": "上下",
|
||||||
"위아래 ⇅": "上下 ⇅",
|
"위아래 ⇅": "上下 ⇅",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:jj_mkvmaker/app/i18n_controller.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/file_tools.dart';
|
||||||
|
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// 관리자 지시 (10/18): X-plore P3 화면을 PNG 로 (JJ_SHOT_DIR, 없으면 임시 폴더) - 해시 창 · 폴더 속성 · 16진 보기.
|
||||||
|
/// 크기 412 (폰 세로) · 960, 언어 ko · en · ja. 시험 파일은 임시 폴더에만 (끝나면 지움). 실행: flutter test integration_test\windows_p3_shot_test.dart -d windows
|
||||||
|
void main() {
|
||||||
|
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_p3');
|
||||||
|
for (final lang in ['ko', 'en', 'ja']) {
|
||||||
|
for (final (w, h) in [(412, 900), (960, 600)]) {
|
||||||
|
testWidgets('P3 화면 $lang ${w}x$h', (t) async {
|
||||||
|
t.view.physicalSize = Size(w.toDouble(), h.toDouble());
|
||||||
|
t.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(t.view.reset);
|
||||||
|
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||||
|
addTearDown(() => i18n.apply('ko', save: false));
|
||||||
|
final tmp = Directory.systemTemp.createTempSync('jj_p3shot_');
|
||||||
|
addTearDown(() => tmp.deleteSync(recursive: true));
|
||||||
|
final file = File(p.join(tmp.path, '영상 폴더', 'ep01 긴 이름의 동영상 파일.mkv'))
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsBytesSync(List.generate(5000, (i) => i % 256));
|
||||||
|
File(p.join(tmp.path, '영상 폴더', '속', 'a.txt'))
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('hello');
|
||||||
|
final navKey = GlobalKey<NavigatorState>();
|
||||||
|
final shotKey = GlobalKey();
|
||||||
|
await t.pumpWidget(RepaintBoundary(
|
||||||
|
key: shotKey,
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: buildTheme(), debugShowCheckedModeBanner: false, navigatorKey: navKey, home: const Scaffold(body: SizedBox.expand())),
|
||||||
|
));
|
||||||
|
BuildContext ctx() => navKey.currentContext!;
|
||||||
|
Future<void> settle([int n = 8]) async {
|
||||||
|
for (var i = 0; i < n; i++) {
|
||||||
|
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 60)));
|
||||||
|
await t.pump();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> shot(String name) async {
|
||||||
|
await settle(3);
|
||||||
|
final b = shotKey.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||||
|
final img = await t.runAsync(() => b.toImage());
|
||||||
|
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||||
|
Directory(out).createSync(recursive: true);
|
||||||
|
File(p.join(out, 'p3_${name}_${lang}_$w.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> closeAll() async {
|
||||||
|
navKey.currentState!.popUntil((r) => r.isFirst);
|
||||||
|
await settle(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 해시 창 (다 계산된 뒤 · 붙여 넣은 값과 비교)
|
||||||
|
showFileHash(ctx(), file.path);
|
||||||
|
await settle(12);
|
||||||
|
await shot('hash');
|
||||||
|
await t.enterText(find.byType(TextField).first, 'abc');
|
||||||
|
await settle();
|
||||||
|
await shot('hash_compare');
|
||||||
|
await closeAll();
|
||||||
|
// 폴더 속성
|
||||||
|
showFolderProperties(ctx(), p.join(tmp.path, '영상 폴더'));
|
||||||
|
await settle(12);
|
||||||
|
await shot('props');
|
||||||
|
await closeAll();
|
||||||
|
// 16진 보기
|
||||||
|
navKey.currentState!.push(MaterialPageRoute<void>(builder: (_) => HexViewerPage(path: file.path)));
|
||||||
|
await settle(12);
|
||||||
|
await shot('hex');
|
||||||
|
await closeAll();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3698,15 +3698,45 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 지금 폴더 · 그 위 폴더들 (창의 맨 위 [pane.root] 까지, 가까운 것부터)
|
||||||
|
List<String> _ancestors(_Pane pane) {
|
||||||
|
final out = <String>[];
|
||||||
|
var d = pane.current;
|
||||||
|
for (var i = 0; i < 64; i++) {
|
||||||
|
out.add(d);
|
||||||
|
if (samePath(d, pane.root)) break;
|
||||||
|
final up = vDirname(d);
|
||||||
|
if (up == d || up.isEmpty) break;
|
||||||
|
d = up;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _topBar() => AppTopBar(
|
Widget _topBar() => AppTopBar(
|
||||||
nav: AppNavButtons(onExplorerPage: !widget.rsync, onRsyncPage: widget.rsync),
|
nav: AppNavButtons(onExplorerPage: !widget.rsync, onRsyncPage: widget.rsync),
|
||||||
actions: AppActions(c: c),
|
actions: AppActions(c: c),
|
||||||
middle: Row(children: [
|
middle: Row(children: [
|
||||||
Text(widget.rsync ? 'Rsync' : tr('파일 탐색기'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
Text(widget.rsync ? 'Rsync' : tr('파일 탐색기'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
// X-plore 경로 막대 (4.4x): 경로를 누르면 위 폴더들 (창의 맨 위까지) 이 나와 골라 간다
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(_ready ? _displayPath(_pane.current) : '',
|
child: !_ready
|
||||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
|
? const SizedBox.shrink()
|
||||||
|
: PopupMenuButton<String>(
|
||||||
|
tooltip: tr('위 폴더로 가기'),
|
||||||
|
position: PopupMenuPosition.under,
|
||||||
|
onSelected: (d) => _goTo(_pane, d),
|
||||||
|
itemBuilder: (_) => [
|
||||||
|
for (final d in _ancestors(_pane))
|
||||||
|
PopupMenuItem(
|
||||||
|
value: d,
|
||||||
|
enabled: !samePath(d, _pane.current),
|
||||||
|
child: Text(_displayPath(d), maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: Text(_displayPath(_pane.current),
|
||||||
|
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
PopupMenuButton<ExplorerButton>(
|
PopupMenuButton<ExplorerButton>(
|
||||||
tooltip: tr('창 배치 · 버튼 구성'),
|
tooltip: tr('창 배치 · 버튼 구성'),
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,8 @@ Future<void> showFileHash(BuildContext context, String path) async {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
compare.dispose();
|
// 창이 닫히는 동안 (애니메이션) 글 칸이 아직 쓰므로 바로 dispose 하면 "used after being disposed" (15 캡처) - 닫힌 뒤에
|
||||||
|
Future<void>.delayed(const Duration(milliseconds: 500), compare.dispose);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 16진 보기: 한 줄 16 바이트 (위치 · 16진 · 글자). 큰 파일은 [더 읽기]
|
/// 16진 보기: 한 줄 16 바이트 (위치 · 16진 · 글자). 큰 파일은 [더 읽기]
|
||||||
|
|
@ -121,12 +122,12 @@ class HexViewerPage extends StatefulWidget {
|
||||||
final int chunk;
|
final int chunk;
|
||||||
const HexViewerPage({super.key, required this.path, this.chunk = 64 * 1024});
|
const HexViewerPage({super.key, required this.path, this.chunk = 64 * 1024});
|
||||||
|
|
||||||
/// 한 줄 (16 바이트): "00000010 41 42 ... AB.."
|
/// 한 줄 ([per] 바이트, 처음 16): "00000010 41 42 ... AB.."
|
||||||
static String line(int offset, List<int> bytes) {
|
static String line(int offset, List<int> bytes, {int per = 16}) {
|
||||||
final hex = StringBuffer();
|
final hex = StringBuffer();
|
||||||
for (var i = 0; i < 16; i++) {
|
for (var i = 0; i < per; i++) {
|
||||||
hex.write(i < bytes.length ? bytes[i].toRadixString(16).padLeft(2, '0') : ' ');
|
hex.write(i < bytes.length ? bytes[i].toRadixString(16).padLeft(2, '0') : ' ');
|
||||||
hex.write(i == 7 ? ' ' : ' ');
|
hex.write(i == 7 && per > 8 ? ' ' : ' ');
|
||||||
}
|
}
|
||||||
final text = String.fromCharCodes([for (final b in bytes) b >= 0x20 && b < 0x7f ? b : 0x2e]);
|
final text = String.fromCharCodes([for (final b in bytes) b >= 0x20 && b < 0x7f ? b : 0x2e]);
|
||||||
return '${offset.toRadixString(16).padLeft(8, '0')} $hex $text';
|
return '${offset.toRadixString(16).padLeft(8, '0')} $hex $text';
|
||||||
|
|
@ -163,7 +164,9 @@ class _HexViewerPageState extends State<HexViewerPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final rows = (_data.length + 15) ~/ 16;
|
// 좁은 화면 (폰 세로) 은 한 줄 8 바이트 - 16 이면 끝의 글자 칸이 화면 밖으로 잘렸다 (15 캡처, 412 폭)
|
||||||
|
final per = MediaQuery.sizeOf(context).width < 560 ? 8 : 16;
|
||||||
|
final rows = (_data.length + per - 1) ~/ per;
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: Text(trf('16진 보기 · {0}', [p.basename(widget.path)]), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
appBar: AppBar(title: Text(trf('16진 보기 · {0}', [p.basename(widget.path)]), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||||
body: _error != null
|
body: _error != null
|
||||||
|
|
@ -176,7 +179,7 @@ class _HexViewerPageState extends State<HexViewerPage> {
|
||||||
itemCount: rows,
|
itemCount: rows,
|
||||||
itemExtent: 18,
|
itemExtent: 18,
|
||||||
itemBuilder: (_, i) => Text(
|
itemBuilder: (_, i) => Text(
|
||||||
HexViewerPage.line(i * 16, _data.sublist(i * 16, (i * 16 + 16).clamp(0, _data.length))),
|
HexViewerPage.line(i * per, _data.sublist(i * per, (i * per + per).clamp(0, _data.length)), per: per),
|
||||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
softWrap: false,
|
softWrap: false,
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,25 @@ void main() {
|
||||||
await calm(tester);
|
await calm(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('X-plore 경로 막대: 위 막대의 경로를 누르면 지금 폴더 · 위 폴더들 (창의 맨 위까지) - 고르면 그리로', (tester) async {
|
||||||
|
await open(tester);
|
||||||
|
await act(tester, () => tester.tap(find.text('sub')));
|
||||||
|
await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty);
|
||||||
|
await act(tester, () => tester.tap(find.byTooltip('위 폴더로 가기')));
|
||||||
|
await calm(tester);
|
||||||
|
final items = find.byType(PopupMenuItem<String>);
|
||||||
|
final n = items.evaluate().length;
|
||||||
|
expect(n, greaterThanOrEqualTo(2), reason: 'sub · 그 위 폴더들 (창의 맨 위까지)');
|
||||||
|
expect(tester.widget<PopupMenuItem<String>>(items.first).enabled, isFalse, reason: '지금 폴더는 고를 수 없음');
|
||||||
|
expect(tester.widget<PopupMenuItem<String>>(items.at(1)).value, left, reason: '바로 위 = left');
|
||||||
|
await act(tester, () => tester.tap(items.at(1)));
|
||||||
|
await settle(tester, () => false, rounds: 15);
|
||||||
|
// 위로 갔으면 다시 열 때 하나 적다 (지금 폴더 = left)
|
||||||
|
await act(tester, () => tester.tap(find.byTooltip('위 폴더로 가기')));
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.byType(PopupMenuItem<String>), findsNWidgets(n - 1));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('X-plore b: 가운데 버튼 길게 누르기 = 짝 동작 - [복사] 길게 = 이동 창 · [삭제] 길게 = 영구 삭제 창', (tester) async {
|
testWidgets('X-plore b: 가운데 버튼 길게 누르기 = 짝 동작 - [복사] 길게 = 이동 창 · [삭제] 길게 = 영구 삭제 창', (tester) async {
|
||||||
await open(tester);
|
await open(tester);
|
||||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue