WIP 좁은 창 열 숨기기 · 보기 화면 창 띄우기 (관리자 지시 10/20): ① Windows · TC 스타일에서 이름 열이 120 (× 글자 배율) 보다 좁으면 유형 (TC 확장자) → 날짜 순으로 숨김, 크기는 남김 - 화면이 모자랄 때만, 넓어지면 다시 보임, 열 설정은 그대로 (열 머리 · 줄이 같은 창 폭으로 정함) · 글자 배율을 곱한 것은 d3 판단 (1.95 배 글자의 120px 은 네 글자쯤) ② 보기 화면 PDF 창 (페이지 지우기 · 그림 넣기) 과 저장 안 한 변경 창도 지금 장면이 끝난 뒤 띄움 ('node.built' 와 같은 모양 - 미리 막음, 저장 안 한 변경 창은 d3 판단) · 시험 (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_view
parent
cb50d28486
commit
77710c2a5f
|
|
@ -78,6 +78,23 @@ class _TimesScaler extends TextScaler {
|
|||
int get hashCode => Object.hash(base, factor);
|
||||
}
|
||||
|
||||
/// 관리자 지시 10/20: 좁은 창의 열 (Windows 탐색기 · Total Commander) - 이름 열이 120 (× 글자 배율) 보다 좁으면 유형 (TC 는 확장자) →
|
||||
/// 날짜 순으로 숨긴다 (크기는 남김). 0 = 다 보임 · 1 = 유형 숨김 · 2 = 유형 · 날짜 숨김. 화면이 모자랄 때만 - 열 설정은 그대로.
|
||||
/// [width] 창 폭, [marks] 줄 끝 ✔ 칸 (36) 이 있는지, [textScale] 목록 글자 배율 (기기 × 목록 글자 크기)
|
||||
int columnHideLevel(ExplorerStyle style, double width, {bool marks = false, double textScale = 1}) {
|
||||
if (!style.columns) return 0;
|
||||
// 이름 열 120 (글자가 크면 그만큼 - 1.95 배 글자의 120 은 네 글자쯤이었다)
|
||||
final minName = 120 * (textScale < 1 ? 1 : textScale);
|
||||
final inner = width - 16 - (marks ? 36 : 0);
|
||||
// 열 비율: Windows 이름 6 · 날짜 3 · 유형 2 · 크기 2, TC 이름 6 · 확장자 1 · 크기 2 · (8px) · 날짜 3
|
||||
final flexes = style == ExplorerStyle.windows ? [13, 11, 8] : [12, 11, 8];
|
||||
final extra = style == ExplorerStyle.totalcmd ? 8 : 0;
|
||||
for (var level = 0; level < 2; level++) {
|
||||
if ((inner - (level < 2 ? extra : 0)) * 6 / flexes[level] >= minName) return level;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
/// 파일 종류별 아이콘 · 색 (스타일마다 다르게)
|
||||
(IconData, Color) fileIcon(ExplorerStyle style, FileEntry e, {bool open = false}) {
|
||||
final x = e.ext;
|
||||
|
|
@ -146,6 +163,9 @@ class ExplorerColumnsHeader extends StatelessWidget {
|
|||
/// 선택 모드: 줄 끝의 선택 동그라미 칸만큼 비운다
|
||||
final bool markColumn;
|
||||
|
||||
/// 관리자 지시 10/20: 좁아서 숨긴 열 ([columnHideLevel]: 1 = 유형, 2 = 유형 · 날짜)
|
||||
final int hide;
|
||||
|
||||
/// 84: 지금 정렬 ('name' · 'date' · 'size' · 'type') · 거꾸로 - 그 열에 ▲ ▼
|
||||
final String? sortBy;
|
||||
final bool sortDesc;
|
||||
|
|
@ -153,7 +173,7 @@ class ExplorerColumnsHeader extends StatelessWidget {
|
|||
/// 84: 열 머리를 누르면 (같은 열을 다시 누르면 거꾸로 - 부르는 쪽이 정한다)
|
||||
final void Function(String by)? onSort;
|
||||
const ExplorerColumnsHeader(
|
||||
{super.key, required this.style, this.markColumn = false, this.sortBy, this.sortDesc = false, this.onSort});
|
||||
{super.key, required this.style, this.markColumn = false, this.hide = 0, this.sortBy, this.sortDesc = false, this.onSort});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
|
@ -193,16 +213,15 @@ class ExplorerColumnsHeader extends StatelessWidget {
|
|||
children: style == ExplorerStyle.windows
|
||||
? [
|
||||
cell(tr('이름'), 6, by: 'name'),
|
||||
cell(tr('수정한 날짜'), 3, by: 'date'),
|
||||
cell(tr('유형'), 2, by: 'type'),
|
||||
if (hide < 2) cell(tr('수정한 날짜'), 3, by: 'date'),
|
||||
if (hide < 1) cell(tr('유형'), 2, by: 'type'),
|
||||
cell(tr('크기'), 2, align: TextAlign.end, by: 'size'),
|
||||
]
|
||||
: [
|
||||
cell(tr('이름'), 6, by: 'name'),
|
||||
cell(tr('확장자'), 1, by: 'type'),
|
||||
if (hide < 1) cell(tr('확장자'), 1, by: 'type'),
|
||||
cell(tr('크기'), 2, align: TextAlign.end, by: 'size'),
|
||||
const SizedBox(width: 8),
|
||||
cell(tr('날짜'), 3, by: 'date'),
|
||||
if (hide < 2) ...[const SizedBox(width: 8), cell(tr('날짜'), 3, by: 'date')],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -234,6 +234,9 @@ enum ExplorerButton {
|
|||
class _Pane extends ChangeNotifier {
|
||||
String root;
|
||||
|
||||
/// 관리자 지시 10/20: 좁아서 숨긴 열 ([columnHideLevel]) - 열을 그리는 창 (목록) 의 폭으로 정하고 열 머리 · 줄이 같이 쓴다
|
||||
int colHide = 0;
|
||||
|
||||
/// 지금 폴더. 75: 바뀔 때마다 뒤로 가기 내역에 (뒤로 · 앞으로로 움직일 때는 빼고)
|
||||
String get current => _current;
|
||||
set current(String v) {
|
||||
|
|
@ -4929,7 +4932,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
final rows = _rows(pane, foldersOnly: foldersOnly);
|
||||
final look = _look;
|
||||
// 38: 창 높이를 알아야 안내 상자의 높이를 제한할 수 있다
|
||||
return LayoutBuilder(builder: (context, box) => _frame(i, [
|
||||
return LayoutBuilder(builder: (context, box) {
|
||||
// 열을 그리는 창만 숨길 열을 정한다 ("폴더 + 파일 목록" 의 트리는 열이 없고 목록과 같은 창을 쓴다)
|
||||
if (look.columns && !foldersOnly) {
|
||||
pane.colHide = columnHideLevel(look, box.maxWidth,
|
||||
marks: _selecting, textScale: MediaQuery.textScalerOf(context).scale(14) / 14 * _listTextScale);
|
||||
}
|
||||
return _frame(i, [
|
||||
_paneHeader(pane),
|
||||
const Divider(height: 1),
|
||||
// 보이는 수는 맞는 것만 (맞는 것이 든 위 폴더는 빼고)
|
||||
|
|
@ -4941,6 +4950,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
ExplorerColumnsHeader(
|
||||
style: look,
|
||||
markColumn: _selecting,
|
||||
hide: pane.colHide,
|
||||
sortBy: _sortOf(pane.current).$1,
|
||||
sortDesc: _sortOf(pane.current).$2,
|
||||
onSort: (by) => _sortByColumn(pane, by),
|
||||
|
|
@ -4962,7 +4972,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
]),
|
||||
),
|
||||
if ((_selecting || (_alwaysMarks && pane.marked.isNotEmpty)) && !foldersOnly) _marksBar(pane),
|
||||
]));
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/// 76: 목록 줄의 키 = 항목 경로. 키가 없으면 줄 목록이 바뀔 때 (빠른 필터 · 펼치기 · 새로 고침) 같은 자리의 줄 위젯을
|
||||
|
|
@ -4992,7 +5003,12 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
];
|
||||
final look = _look;
|
||||
// 38: 창 높이를 알아야 안내 상자의 높이를 제한할 수 있다
|
||||
return LayoutBuilder(builder: (context, box) => _frame(0, [
|
||||
return LayoutBuilder(builder: (context, box) {
|
||||
if (look.columns) {
|
||||
pane.colHide = columnHideLevel(look, box.maxWidth,
|
||||
marks: _selecting, textScale: MediaQuery.textScalerOf(context).scale(14) / 14 * _listTextScale);
|
||||
}
|
||||
return _frame(0, [
|
||||
Container(
|
||||
height: 40,
|
||||
alignment: Alignment.centerLeft,
|
||||
|
|
@ -5006,6 +5022,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
ExplorerColumnsHeader(
|
||||
style: look,
|
||||
markColumn: _selecting,
|
||||
hide: pane.colHide,
|
||||
sortBy: _sortOf(dir).$1,
|
||||
sortDesc: _sortOf(dir).$2,
|
||||
onSort: (by) => _sortByColumn(pane, by),
|
||||
|
|
@ -5026,7 +5043,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
)),
|
||||
),
|
||||
if (_selecting) _marksBar(pane, listDir: dir),
|
||||
]));
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
/// 76: 빠른 필터 열기 ([text] 가 있으면 그 글자로 시작)
|
||||
|
|
@ -5613,21 +5631,22 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
flex: flex,
|
||||
child: Text(t, style: dim, textAlign: align, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
);
|
||||
// 관리자 지시 10/20: 좁으면 유형 → 날짜 열을 숨김 (크기는 남김, [columnHideLevel])
|
||||
final hide = pane.colHide;
|
||||
final cells = !look.columns || compact || !real
|
||||
? <Widget>[Expanded(child: nameCell())]
|
||||
: look == ExplorerStyle.windows
|
||||
? [
|
||||
Expanded(flex: 6, child: nameCell()),
|
||||
cell(_date(e.modified), 3),
|
||||
cell(typeLabel(e), 2),
|
||||
if (hide < 2) cell(_date(e.modified), 3),
|
||||
if (hide < 1) cell(typeLabel(e), 2),
|
||||
cell(e.isDir ? (_dirSize(pane, e) ?? '') : formatSize(e.size), 2, align: TextAlign.end),
|
||||
]
|
||||
: [
|
||||
Expanded(flex: 6, child: nameCell()),
|
||||
cell(e.isDir ? '' : e.ext, 1),
|
||||
if (hide < 1) cell(e.isDir ? '' : e.ext, 1),
|
||||
cell(e.isDir ? (_dirSize(pane, e) ?? '<DIR>') : formatSize(e.size), 2, align: TextAlign.end),
|
||||
const SizedBox(width: 8),
|
||||
cell(_date(e.modified), 3),
|
||||
if (hide < 2) ...[const SizedBox(width: 8), cell(_date(e.modified), 3)],
|
||||
];
|
||||
|
||||
final paneIndex = _panes.indexOf(pane);
|
||||
|
|
|
|||
|
|
@ -609,7 +609,13 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
|
||||
PdfSource? get _pdf => src is PdfSource ? src as PdfSource : null;
|
||||
|
||||
/// 28 지적 (10/20): 위 막대 단추를 누른 그 이벤트 안에서 창을 띄우면 (장을 손으로 넘긴 뒤) 창을 닫을 때 디버그 판에서
|
||||
/// 의미 트리 assertion ('node.built') 이 났다 - 창은 지금 장면이 끝난 뒤에 띄운다 (부른 쪽은 뒤에 mounted 확인)
|
||||
Future<void> _afterFrame() => WidgetsBinding.instance.endOfFrame;
|
||||
|
||||
Future<void> _deletePage() async {
|
||||
await _afterFrame();
|
||||
if (!mounted) return;
|
||||
final pdf = _pdf!;
|
||||
if (pdf.length <= 1) {
|
||||
_snack(tr('마지막 한 페이지는 지울 수 없습니다.'));
|
||||
|
|
@ -634,6 +640,8 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
}
|
||||
|
||||
Future<void> _insertImages() async {
|
||||
await _afterFrame();
|
||||
if (!mounted) return;
|
||||
final pdf = _pdf!;
|
||||
final page = _cur.page;
|
||||
final files = await c.services.storage.pickFiles(title: tr('PDF 에 넣을 그림 선택'), extensions: c.settings.imageExts);
|
||||
|
|
@ -662,6 +670,8 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
Future<bool> _confirmLeave() async {
|
||||
final pdf = _pdf;
|
||||
if (pdf == null || !pdf.dirty) return true;
|
||||
await _afterFrame();
|
||||
if (!mounted) return false;
|
||||
final r = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
|
|
@ -882,10 +892,8 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
/// X-plore 맞추기 (관리자 지시 10/20): 지금 그림의 사진 정보 (찍은 날 · 기기 · 크기 · 위치). 위치는 [위치 보기] 를 누를 때만
|
||||
/// 보이고, 지도 · 인터넷으로 보내지 않는다 (링크 없음)
|
||||
Future<void> _showPhotoInfo() async {
|
||||
// 28 지적 (10/20): 장을 손으로 몇 번 넘긴 뒤 위 막대 단추를 누른 그 이벤트 안에서 창을 띄우면, 창을 닫을 때 디버그 판에서
|
||||
// 의미 트리 assertion ('node.built', rendering/object.dart) 이 났다 (Windows 통합 시험에서 재현 - 넘기지 않거나, 시험에서 따로 띄운
|
||||
// 창은 괜찮음). 지금 장면이 끝난 뒤에 띄우면 나지 않는다 (재현 시험 4번 → 0번)
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
// 지금 장면이 끝난 뒤에 띄운다 ([_afterFrame], Windows 통합 시험에서 재현 4번 → 0번)
|
||||
await _afterFrame();
|
||||
if (!mounted) return;
|
||||
final s = src;
|
||||
if (s is! ImageFilesSource || _v.isEmpty) return;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'package:jj_mkvmaker/app/settings.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/explorer_look.dart';
|
||||
import 'package:jj_mkvmaker/ui/explorer_page.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
|
|
@ -131,6 +132,43 @@ void main() {
|
|||
});
|
||||
}
|
||||
|
||||
// 관리자 지시 10/20: 좁은 창의 열 - 이름 열이 120 보다 좁으면 유형 (TC 확장자) → 날짜 순으로 숨김, 크기는 남김
|
||||
test('숨길 열 정하기: 넓으면 0 · 좁아질수록 유형 → 날짜 · 열 없는 X-plore 는 0', () {
|
||||
expect(columnHideLevel(ExplorerStyle.windows, 1280), 0);
|
||||
expect(columnHideLevel(ExplorerStyle.windows, 300), 0, reason: '이름 열 (300-16)*6/13 = 131');
|
||||
expect(columnHideLevel(ExplorerStyle.windows, 300, marks: true), 1, reason: '✔ 칸 (36) 이 있으면 114');
|
||||
expect(columnHideLevel(ExplorerStyle.windows, 260), 1);
|
||||
expect(columnHideLevel(ExplorerStyle.windows, 180), 2);
|
||||
expect(columnHideLevel(ExplorerStyle.totalcmd, 1280), 0);
|
||||
expect(columnHideLevel(ExplorerStyle.totalcmd, 180), 2);
|
||||
expect(columnHideLevel(ExplorerStyle.xplore, 100), 0);
|
||||
});
|
||||
|
||||
for (final style in ['windows', 'totalcmd']) {
|
||||
testWidgets('좁은 한 창 ($style, 412 · 기기 글자 1.3): 이름 열이 좁으면 유형 → 날짜 열을 숨기고 크기는 남김 · 넓으면 다 보임', (tester) async {
|
||||
tester.view.physicalSize = const Size(412, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final type = style == 'windows' ? '유형' : '확장자';
|
||||
final date = style == 'windows' ? '수정한 날짜' : '날짜';
|
||||
final sub = style == 'totalcmd' ? '[sub]' : 'sub';
|
||||
final c = await open(tester, style, sub, device: 1.3, scale: 150);
|
||||
await tester.pump();
|
||||
expect(c.settings.explorerLayout, 'split');
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text(type), findsNothing, reason: '좁아서 유형 열 숨김');
|
||||
expect(find.text('크기'), findsOneWidget, reason: '크기는 남김');
|
||||
await close(tester);
|
||||
tester.view.physicalSize = const Size(1600, 900);
|
||||
await open(tester, style, sub);
|
||||
await tester.pump();
|
||||
expect(find.text(type), findsOneWidget);
|
||||
expect(find.text(date), findsOneWidget);
|
||||
expect(find.text('크기'), findsOneWidget);
|
||||
await close(tester);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets('넓은 화면 (1280) 의 폴더 + 파일 목록은 그대로 (2 : 3, 트리에도 ✔)', (tester) async {
|
||||
tester.view.physicalSize = const Size(1280, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
|
|||
Loading…
Reference in New Issue