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",
|
||||
"위 폴더": "Parent folder",
|
||||
"위 폴더로": "Go up",
|
||||
"위 폴더로 가기": "Go to a parent folder",
|
||||
"위로": "Up",
|
||||
"위아래": "Top and bottom",
|
||||
"위아래 ⇅": "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(
|
||||
nav: AppNavButtons(onExplorerPage: !widget.rsync, onRsyncPage: widget.rsync),
|
||||
actions: AppActions(c: c),
|
||||
middle: Row(children: [
|
||||
Text(widget.rsync ? 'Rsync' : tr('파일 탐색기'), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
const SizedBox(width: 12),
|
||||
// X-plore 경로 막대 (4.4x): 경로를 누르면 위 폴더들 (창의 맨 위까지) 이 나와 골라 간다
|
||||
Expanded(
|
||||
child: Text(_ready ? _displayPath(_pane.current) : '',
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
|
||||
child: !_ready
|
||||
? 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>(
|
||||
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진 · 글자). 큰 파일은 [더 읽기]
|
||||
|
|
@ -121,12 +122,12 @@ class HexViewerPage extends StatefulWidget {
|
|||
final int chunk;
|
||||
const HexViewerPage({super.key, required this.path, this.chunk = 64 * 1024});
|
||||
|
||||
/// 한 줄 (16 바이트): "00000010 41 42 ... AB.."
|
||||
static String line(int offset, List<int> bytes) {
|
||||
/// 한 줄 ([per] 바이트, 처음 16): "00000010 41 42 ... AB.."
|
||||
static String line(int offset, List<int> bytes, {int per = 16}) {
|
||||
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 == 7 ? ' ' : ' ');
|
||||
hex.write(i == 7 && per > 8 ? ' ' : ' ');
|
||||
}
|
||||
final text = String.fromCharCodes([for (final b in bytes) b >= 0x20 && b < 0x7f ? b : 0x2e]);
|
||||
return '${offset.toRadixString(16).padLeft(8, '0')} $hex $text';
|
||||
|
|
@ -163,7 +164,9 @@ class _HexViewerPageState extends State<HexViewerPage> {
|
|||
|
||||
@override
|
||||
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(
|
||||
appBar: AppBar(title: Text(trf('16진 보기 · {0}', [p.basename(widget.path)]), maxLines: 1, overflow: TextOverflow.ellipsis)),
|
||||
body: _error != null
|
||||
|
|
@ -176,7 +179,7 @@ class _HexViewerPageState extends State<HexViewerPage> {
|
|||
itemCount: rows,
|
||||
itemExtent: 18,
|
||||
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),
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
|
|
|
|||
|
|
@ -193,6 +193,25 @@ void main() {
|
|||
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 {
|
||||
await open(tester);
|
||||
await act(tester, () => tester.tap(find.text('doc.txt')));
|
||||
|
|
|
|||
Loading…
Reference in New Issue