rc2: 탐색기 Windows · TC 날짜 열이 "2026-10-2…" 로 잘리던 것 - 모자라면 짧은 꼴, 그래도 모자라면 숨김 (28 지적, 관리자 지시 10/21)
- 날짜 열 폭 (열 비율 · 창 폭) 과 날짜 글 폭 (줄의 흐린 글 모양 · 지금 글꼴 · 기기 × 목록 글자 배율, TextPainter) 을 견줘:
다 들어가면 "2026-10-21 14:05" (예전 그대로) → 아니면 짧은 꼴 (올해 "10-21 14:05" · 아니면 "2025-10-21") → 아니면 유형을 먼저 숨기고 (아직이면)
다시 봄 → 그래도 모자라면 날짜 숨김. 열 머리 · 줄이 같은 결정을 씀 (두 창 · 폴더 + 파일 목록 모두)
- 일본어는 / ("2026/10/21 14:05" · "10/21 14:05"), 한국어 · 중국어 · 영어는 예전 꼴 그대로
- 함께 찾은 것: 300 폭 · 글자 1.3 × 150% 에서 위 막대 제목 ("파일 탐색기") 이 넘쳐 그리기 오류 → 경로 막대에 80 남짓 남을 때까지는
제목 폭 그대로, 그보다 좁으면 제목을 "…" 로 (보통 화면은 바뀌지 않음)
- 시험 integration_test/windows_datecol_shot_test.dart: 실제 글꼴로 312 조합 (폭 13 × 스타일 2 × 언어 ko · ja · zh × 글자 배율 4) -
날짜 칸 잘림 0 (RenderParagraph.didExceedMaxLines), 넓은 창은 다 쓴 꼴, 짧은 꼴 칸 162 · 날짜 숨김 84 조합, 360 · 412 는 PNG.
목록이 빈 채로 남는 일이 가끔 있어 (빙글이 없이, 3번 중 2번 실행에서 1~2 조합) 그림을 남기고 [새로 고침] 뒤 확인 - 따로 알림
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc2
parent
d4266a7236
commit
c65f4851f7
|
|
@ -0,0 +1,122 @@
|
|||
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/app_controller.dart';
|
||||
import 'package:jj_mkvmaker/app/i18n_controller.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_page.dart';
|
||||
import 'package:jj_mkvmaker/ui/theme.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
import 'support/temp_dir.dart';
|
||||
|
||||
/// 관리자 지시 (10/21, 28 지적): Windows · TC 스타일의 날짜 열이 시간까지 들어가지 않으면 "2026-10-2…" 로 잘리던 것 -
|
||||
/// 모자라면 짧은 꼴 (올해 "10-21 14:05" · 아니면 "2025-10-21"), 그래도 모자라면 숨김. 실제 글꼴로 312 조합
|
||||
/// (폭 13 × 스타일 2 × 언어 3 × 글자 배율 4) 에서 날짜 칸이 잘리지 않는지 (RenderParagraph.didExceedMaxLines) 와
|
||||
/// 날짜를 보일 수 있는 넓은 창에서는 다 쓴 꼴이 나오는지 본다. 좁은 몇 개는 PNG (JJ_SHOT_DIR).
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
if (Platform.environment['JJ_SHOT_THEME'] == 'light') JjColors.use(Brightness.light);
|
||||
final out = Platform.environment['JJ_SHOT_DIR'] ?? p.join(Directory.systemTemp.path, 'jj_shots_datecol');
|
||||
final dateLike = RegExp(r'^(\d{4}[-/])?\d{2}[-/]\d{2}( \d{2}:\d{2})?$');
|
||||
|
||||
testWidgets('날짜 열: 312 조합에서 잘림 0', (t) async {
|
||||
final tmp = testTempDir('jj_datecol_');
|
||||
final dir = Directory(p.join(tmp.path, 'files'))..createSync();
|
||||
final now = DateTime.now();
|
||||
for (final (n, at) in [
|
||||
('올해 사진.jpg', DateTime(now.year, 12, 28, 23, 58).isAfter(now) ? now : DateTime(now.year, 12, 28, 23, 58)),
|
||||
('작년 문서.pdf', DateTime(now.year - 1, 12, 28, 23, 58)),
|
||||
]) {
|
||||
File(p.join(dir.path, n))
|
||||
..writeAsStringSync('x')
|
||||
..setLastModifiedSync(at);
|
||||
}
|
||||
ExplorerPage.debugVolumes = () => [(dir.path, 'T:')];
|
||||
addTearDown(() => ExplorerPage.debugVolumes = null);
|
||||
final key = GlobalKey();
|
||||
var checked = 0, short = 0, hidden = 0;
|
||||
final emptyOnce = <String>[];
|
||||
final widths = <double>[300, 340, 360, 390, 412, 460, 520, 600, 700, 800, 960, 1280, 1600];
|
||||
final scales = [(1.0, 80), (1.0, 100), (1.0, 150), (1.3, 150)];
|
||||
for (final lang in ['ko', 'ja', 'zh-Hans']) {
|
||||
await t.runAsync(() => i18n.apply(lang, save: false));
|
||||
for (final style in ['windows', 'totalcmd']) {
|
||||
for (final (dev, list) in scales) {
|
||||
for (final w in widths) {
|
||||
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||
c.settings
|
||||
..explorerStyle = style
|
||||
..explorerLayout = 'single'
|
||||
..explorerTextScale = list
|
||||
..explorerPaths = [dir.path, dir.path];
|
||||
t.view.physicalSize = Size(w, 800);
|
||||
t.view.devicePixelRatio = 1;
|
||||
await t.runAsync(() => t.pumpWidget(RepaintBoundary(
|
||||
key: key,
|
||||
child: MaterialApp(
|
||||
theme: buildTheme(),
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: (ctx, child) => MediaQuery(data: MediaQuery.of(ctx).copyWith(textScaler: TextScaler.linear(dev)), child: child!),
|
||||
home: ExplorerPage(key: UniqueKey(), c: c),
|
||||
),
|
||||
)));
|
||||
for (var i = 0; i < 200 && find.textContaining('작년 문서').evaluate().isEmpty; i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 50)));
|
||||
await t.pump();
|
||||
}
|
||||
await t.pump(const Duration(milliseconds: 100));
|
||||
final name = '${lang}_${style}_${w.round()}_d${dev}_l$list';
|
||||
if (find.textContaining('작년 문서').evaluate().isEmpty) {
|
||||
// 목록이 빈 채로 남는 일이 가끔 있다 (빙글이 없이 - 3a 에 알림). 그림을 남기고 [새로 고침] 으로 한 번 더
|
||||
emptyOnce.add(name);
|
||||
final r = key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
final img = await t.runAsync(() => r.toImage());
|
||||
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||
Directory(out).createSync(recursive: true);
|
||||
File(p.join(out, 'datecol_EMPTY_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
await t.tap(find.byIcon(Icons.refresh).first);
|
||||
for (var i = 0; i < 200 && find.textContaining('작년 문서').evaluate().isEmpty; i++) {
|
||||
await t.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 50)));
|
||||
await t.pump();
|
||||
}
|
||||
await t.pump(const Duration(milliseconds: 100));
|
||||
}
|
||||
expect(find.textContaining('작년 문서'), findsWidgets, reason: '$name 목록');
|
||||
final dates = find.byWidgetPredicate((x) => x is Text && x.data != null && dateLike.hasMatch(x.data!));
|
||||
for (final e in dates.evaluate()) {
|
||||
final para = e.findRenderObject()! as RenderParagraph;
|
||||
expect(para.didExceedMaxLines, isFalse, reason: '$name: "${(e.widget as Text).data}" 잘림');
|
||||
if (!(e.widget as Text).data!.contains(':') || !(e.widget as Text).data!.startsWith('20')) short++;
|
||||
}
|
||||
if (dates.evaluate().isEmpty) hidden++;
|
||||
// 넓은 창 (1280 이상) 에서는 다 쓴 꼴 (연도 + 시각)
|
||||
if (w >= 1280 && dev == 1.0 && list <= 100) {
|
||||
expect(dates.evaluate().map((e) => (e.widget as Text).data!).every((x) => x.length == 16), isTrue, reason: '$name 다 쓴 꼴');
|
||||
}
|
||||
if (w == 360 || w == 412) {
|
||||
final r = key.currentContext!.findRenderObject()! as RenderRepaintBoundary;
|
||||
final img = await t.runAsync(() => r.toImage());
|
||||
final bytes = await t.runAsync(() => img!.toByteData(format: ui.ImageByteFormat.png));
|
||||
Directory(out).createSync(recursive: true);
|
||||
File(p.join(out, 'datecol_$name.png')).writeAsBytesSync(bytes!.buffer.asUint8List());
|
||||
}
|
||||
checked++;
|
||||
await t.pumpWidget(const SizedBox());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await t.runAsync(() => i18n.apply('ko', save: false));
|
||||
t.view.reset();
|
||||
expect(checked, 312);
|
||||
// ignore: avoid_print
|
||||
print('날짜 열 조합 $checked · 짧은 꼴 칸 $short · 날짜 숨긴 조합 $hidden · 목록이 빈 채였던 조합 $emptyOnce');
|
||||
}, timeout: const Timeout(Duration(minutes: 30)));
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../core/file_ops.dart';
|
||||
|
|
@ -95,6 +97,41 @@ int columnHideLevel(ExplorerStyle style, double width, {bool marks = false, doub
|
|||
return 2;
|
||||
}
|
||||
|
||||
/// 날짜 열의 글 (관리자 지시 10/21): 다 쓰면 "2026-10-21 14:05", [short] 면 올해는 "10-21 14:05" · 아니면 "2025-10-21".
|
||||
/// 일본어는 / ("2026/10/21") - 한국어 · 중국어 · 영어는 - (예전 그대로)
|
||||
String columnDateText(DateTime d, {bool short = false, DateTime? now, String? lang}) {
|
||||
if (d.year <= 1970) return '—';
|
||||
final s = (lang ?? uiLanguage) == 'ja' ? '/' : '-';
|
||||
String two(int x) => x.toString().padLeft(2, '0');
|
||||
final md = '${two(d.month)}$s${two(d.day)}';
|
||||
final hm = '${two(d.hour)}:${two(d.minute)}';
|
||||
if (!short) return '${d.year}$s$md $hm';
|
||||
return d.year == (now ?? DateTime.now()).year ? '$md $hm' : '${d.year}$s$md';
|
||||
}
|
||||
|
||||
/// 좁은 창의 열 (관리자 지시 10/20 · 10/21): (숨김 단계 [columnHideLevel], 날짜를 짧게 쓸지).
|
||||
/// 날짜 열에 다 들어가지 않으면 먼저 짧은 꼴로, 그래도 모자라면 유형을 숨겨 (아직이면) 다시 보고, 그래도 모자라면 날짜를 숨긴다.
|
||||
/// [measure] 지금 글꼴 · 글자 배율로 그 글의 폭 (목록의 흐린 글 모양)
|
||||
(int, bool) columnLayout(ExplorerStyle style, double width,
|
||||
{bool marks = false, double textScale = 1, required double Function(String text) measure, String? lang}) {
|
||||
if (!style.columns) return (0, false);
|
||||
var level = columnHideLevel(style, width, marks: marks, textScale: textScale);
|
||||
final inner = width - 16 - (marks ? 36 : 0);
|
||||
final extra = style == ExplorerStyle.totalcmd ? 8.0 : 0.0;
|
||||
// 날짜 열 몫: Windows 이름 6 · 날짜 3 · 유형 2 · 크기 2 (유형 숨기면 11), TC 이름 6 · 확장자 1 · 크기 2 · 날짜 3 (확장자 숨기면 11)
|
||||
final sums = style == ExplorerStyle.windows ? [13, 11] : [12, 11];
|
||||
final sample = DateTime(2026, 12, 28, 23, 58);
|
||||
final full = measure(columnDateText(sample, lang: lang));
|
||||
final short = max(measure(columnDateText(sample, short: true, now: DateTime(2026), lang: lang)),
|
||||
measure(columnDateText(DateTime(2025, 12, 28), short: true, now: DateTime(2026), lang: lang)));
|
||||
for (; level < 2; level++) {
|
||||
final w = (inner - extra) * 3 / sums[level] - 2;
|
||||
if (full <= w) return (level, false);
|
||||
if (short <= w) return (level, true);
|
||||
}
|
||||
return (2, false);
|
||||
}
|
||||
|
||||
/// 파일 종류별 아이콘 · 색 (스타일마다 다르게)
|
||||
(IconData, Color) fileIcon(ExplorerStyle style, FileEntry e, {bool open = false}) {
|
||||
final x = e.ext;
|
||||
|
|
|
|||
|
|
@ -258,6 +258,9 @@ class _Pane extends ChangeNotifier {
|
|||
/// 관리자 지시 10/20: 좁아서 숨긴 열 ([columnHideLevel]) - 열을 그리는 창 (목록) 의 폭으로 정하고 열 머리 · 줄이 같이 쓴다
|
||||
int colHide = 0;
|
||||
|
||||
/// 관리자 지시 10/21: 날짜 열이 좁아 짧은 꼴 ([columnDateText] short) 로 쓰는지
|
||||
bool colDateShort = false;
|
||||
|
||||
/// 지금 폴더. 75: 바뀔 때마다 뒤로 가기 내역에 (뒤로 · 앞으로로 움직일 때는 빼고)
|
||||
String get current => _current;
|
||||
set current(String v) {
|
||||
|
|
@ -4710,8 +4713,14 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
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)),
|
||||
// 아주 좁고 글자가 크면 (300 폭 · 1.3 × 150%) 제목도 줄임 - 넘쳐 그리기 오류가 나던 것 (날짜 열 312 조합 시험에서 찾음).
|
||||
// 보통은 제목 폭 그대로 (경로 막대에 80 남짓 남을 때까지)
|
||||
middle: LayoutBuilder(builder: (context, box) => Row(children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: math.max(40, box.maxWidth - 92)),
|
||||
child: Text(widget.rsync ? 'Rsync' : tr('파일 탐색기'),
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// X-plore 경로 막대 (4.4x): 경로를 누르면 위 폴더들 (창의 맨 위까지) 이 나와 골라 간다
|
||||
Expanded(
|
||||
|
|
@ -4765,7 +4774,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
])),
|
||||
);
|
||||
|
||||
/// 모양 · 배치가 바뀌면 (줄 높이 · 창 너비가 달라져) 보던 폴더가 밀려나므로 다시 그 폴더로
|
||||
|
|
@ -5122,6 +5131,20 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
|
||||
/// 트리 창 (저장 장치 고르기 + 트리)
|
||||
/// 관리자 지시 10/20 · 10/21: 좁은 창의 열 - 숨길 열 · 날짜를 짧게 쓸지. 줄의 흐린 글과 같은 모양으로 날짜 글 폭을 잰다
|
||||
/// (날짜가 "2026-10-2…" 로 잘리지 않게). 열 머리 · 줄이 같이 쓴다
|
||||
void _layoutColumns(BuildContext context, _Pane pane, ExplorerStyle look, double width) {
|
||||
final scale = MediaQuery.textScalerOf(context).scale(14) / 14 * _listTextScale;
|
||||
final style = DefaultTextStyle.of(context).style.merge(TextStyle(fontSize: look.fontSize - 2));
|
||||
double measure(String t) =>
|
||||
(TextPainter(text: TextSpan(text: t, style: style), textDirection: TextDirection.ltr, textScaler: TextScaler.linear(scale), maxLines: 1)
|
||||
..layout())
|
||||
.width;
|
||||
final lay = columnLayout(look, width, marks: _selecting, textScale: scale, measure: measure);
|
||||
pane.colHide = lay.$1;
|
||||
pane.colDateShort = lay.$2;
|
||||
}
|
||||
|
||||
Widget _paneView(int i, {bool foldersOnly = false}) {
|
||||
final pane = _panes[i];
|
||||
final rows = _rows(pane, foldersOnly: foldersOnly);
|
||||
|
|
@ -5129,10 +5152,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 38: 창 높이를 알아야 안내 상자의 높이를 제한할 수 있다
|
||||
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);
|
||||
}
|
||||
if (look.columns && !foldersOnly) _layoutColumns(context, pane, look, box.maxWidth);
|
||||
return _frame(i, [
|
||||
_paneHeader(pane),
|
||||
const Divider(height: 1),
|
||||
|
|
@ -5199,10 +5219,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
final look = _look;
|
||||
// 38: 창 높이를 알아야 안내 상자의 높이를 제한할 수 있다
|
||||
return LayoutBuilder(builder: (context, box) {
|
||||
if (look.columns) {
|
||||
pane.colHide = columnHideLevel(look, box.maxWidth,
|
||||
marks: _selecting, textScale: MediaQuery.textScalerOf(context).scale(14) / 14 * _listTextScale);
|
||||
}
|
||||
if (look.columns) _layoutColumns(context, pane, look, box.maxWidth);
|
||||
return _frame(0, [
|
||||
Container(
|
||||
height: 40,
|
||||
|
|
@ -5872,7 +5889,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
: look == ExplorerStyle.windows
|
||||
? [
|
||||
Expanded(flex: 6, child: nameCell()),
|
||||
if (hide < 2) cell(_date(e.modified), 3),
|
||||
if (hide < 2) cell(columnDateText(e.modified, short: pane.colDateShort), 3),
|
||||
if (hide < 1) cell(typeLabel(e), 2),
|
||||
cell(e.isDir ? (_dirSize(pane, e) ?? '') : formatSize(e.size), 2, align: TextAlign.end),
|
||||
]
|
||||
|
|
@ -5880,7 +5897,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Expanded(flex: 6, child: nameCell()),
|
||||
if (hide < 1) cell(e.isDir ? '' : e.ext, 1),
|
||||
cell(e.isDir ? (_dirSize(pane, e) ?? '<DIR>') : formatSize(e.size), 2, align: TextAlign.end),
|
||||
if (hide < 2) ...[const SizedBox(width: 8), cell(_date(e.modified), 3)],
|
||||
if (hide < 2) ...[const SizedBox(width: 8), cell(columnDateText(e.modified, short: pane.colDateShort), 3)],
|
||||
];
|
||||
|
||||
final paneIndex = _panes.indexOf(pane);
|
||||
|
|
|
|||
Loading…
Reference in New Issue