rc2: 경로 줄은 좁으면 앞을 "…" 로 - 지금 폴더가 늘 보이게 (보관함 · 탐색기 위 경로 막대) · 보관함 정렬 상자는 다른 줄로 (28 지적, 관리자 지시 10/21)

- 보관함 깊은 폴더 (360 · 글자 1.3) 에서 "보관함 › 가족 › 2026 › 여름 휴가…" 로 끝이 잘려 지금 폴더가 안 보였고 정렬 상자와 붙어 두 줄로 눌렸다
- TailText (lib/ui/tail_text.dart): 들어가면 그대로, 모자라면 구분 ( › · \ · / ) 에서 앞을 잘라 "… › 여름 휴가 › 제주도 둘째 날",
  마지막 조각도 길면 그 끝을 글자 단위로. 지금 글꼴 · 글자 배율로 잼
- 보관함: 폴더 안에서는 경로 줄 (TailText) 과 정렬 상자를 다른 줄로. 맨 위 (안내 글 + 정렬) 는 예전 그대로
- 파일 탐색기 위 경로 막대도 같은 문제 (끝이 "…" 로 잘림) 라 TailText 로 - 다 들어가는 경로는 예전 그대로
- 시험 tail_text_test (자르는 자리 · 1.3 배 좁은 칸에서 지금 폴더가 보임), vault_page · explorer 시험 그대로 통과

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc2
RyuJongJin 2026-10-21 19:48:25 +09:00
parent c65f4851f7
commit e785ba96a6
4 changed files with 115 additions and 23 deletions

View File

@ -58,6 +58,7 @@ import '../l10n/tr.dart';
import 'explorer_bookmarks.dart';
import 'explorer_search.dart';
import 'remote_thumb_view.dart';
import 'tail_text.dart';
import '../core/thumb_cache.dart' show thumbMemoryForgetters;
import '../core/shizuku.dart';
import 'vault_page.dart' show openVault;
@ -4738,8 +4739,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
child: Text(_displayPath(d), maxLines: 1, overflow: TextOverflow.ellipsis),
),
],
child: Text(_displayPath(_pane.current),
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(color: JjColors.textDim)),
// 관리자 지시 (10/21, 28 지적): 좁으면 앞을 "…" 로 - 지금 폴더 (끝) 가 늘 보이게
child: TailText(_displayPath(_pane.current), style: TextStyle(color: JjColors.textDim)),
),
),
PopupMenuButton<Object>(

49
lib/ui/tail_text.dart Normal file
View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
/// 경로처럼 끝 (지금 폴더) 이 중요한 글 (관리자 지시 10/21, 28 지적): 다 들어가면 그대로, 모자라면 앞을 "…" 로 줄인다.
/// 구분 ([separators] - " › " · "\" · "/") 에서 자르는 것을 먼저 (가장 긴 뒤쪽), 마지막 조각도 안 들어가면 글자 단위로.
/// 지금 글꼴 · 글자 배율로 잰다
class TailText extends StatelessWidget {
final String text;
final TextStyle? style;
final List<String> separators;
const TailText(this.text, {super.key, this.style, this.separators = const [' › ', '\\', '/']});
/// [width] 에 들어가는 꼴 (시험에서 씀)
static String fit(String text, double width, double Function(String) measure, {List<String> separators = const [' › ', '\\', '/']}) {
if (measure(text) <= width) return text;
// 구분에서 시작하는 뒤쪽들 (긴 것부터)
final cuts = <int>[];
for (final s in separators) {
for (var i = text.indexOf(s); i >= 0; i = text.indexOf(s, i + 1)) {
cuts.add(i); // 구분 기호는 남김 ("… › 여름 휴가")
}
}
cuts.sort();
for (final c in cuts) {
final t = '…${text.substring(c)}';
if (measure(t) <= width) return t;
}
// 마지막 조각도 길면 글자 단위로 (뒤쪽 가장 길게)
var lo = 0, hi = text.length;
while (lo < hi) {
final mid = (lo + hi) ~/ 2;
if (measure('…${text.substring(mid)}') <= width) {
hi = mid;
} else {
lo = mid + 1;
}
}
return '…${text.substring(lo)}';
}
@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;
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);
});
}

View File

@ -22,6 +22,7 @@ import '../core/vfs.dart' show isDav, vBasename, vDisplay, vIsDir, vList, vLocal
import '../l10n/tr.dart';
import 'player_page.dart';
import 'reader_page.dart' show openReader;
import 'tail_text.dart';
import 'theme.dart';
/// X-plore 비교 5-2 암호 보관함 화면 (사용자 결정 10/21, 관리자 결정 - 설계안 위키 프로젝트/암호 보관함 · 루트 설계안 (10-21))
@ -931,6 +932,15 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
String _day(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}';
Widget _sortBox() => DropdownButton<String>(
value: _sort,
items: [
DropdownMenuItem(value: 'added', child: Text(tr('넣은 날'))),
DropdownMenuItem(value: 'name', child: Text(tr('이름'))),
],
onChanged: (v) => setState(() => _sort = v!),
);
Widget _list() {
final v = _vault!;
// 지운 · 옮긴 폴더 안에 있었으면 남은 가장 가까운 위 폴더로
@ -945,27 +955,26 @@ class _VaultPageState extends State<VaultPage> with WidgetsBindingObserver {
list.sort((a, b) => b.added.compareTo(a.added));
}
return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
// 관리자 지시 (10/21, 28 지적): 폴더 안에서는 경로 줄과 정렬 상자를 다른 줄로, 경로는 좁으면 앞을 "…" 로 (지금 폴더가 늘 보이게)
if (_cwd.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 16, 0),
child: Row(children: [
if (_cwd.isNotEmpty)
IconButton(
tooltip: tr('위 폴더로'), icon: const Icon(Icons.arrow_upward), onPressed: () => setState(() => _cwd = _parentOf(_cwd))),
if (_cwd.isEmpty) const SizedBox(width: 8),
Expanded(child: TailText(_cwdLabel(), style: const TextStyle(fontWeight: FontWeight.w600))),
]),
),
Padding(padding: const EdgeInsets.only(right: 16), child: Align(alignment: Alignment.centerRight, child: _sortBox())),
] else
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 16, 0),
child: Row(children: [
const SizedBox(width: 8),
Expanded(
child: _cwd.isEmpty
? Text(tr('넣기는 파일 탐색기에서 파일 · 폴더를 길게 눌러 [보관함에 넣기]'),
style: TextStyle(fontSize: 12, color: JjColors.textDim))
: Text(_cwdLabel(), maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.w600)),
),
DropdownButton<String>(
value: _sort,
items: [
DropdownMenuItem(value: 'added', child: Text(tr('넣은 날'))),
DropdownMenuItem(value: 'name', child: Text(tr('이름'))),
],
onChanged: (v) => setState(() => _sort = v!),
child: Text(tr('넣기는 파일 탐색기에서 파일 · 폴더를 길게 눌러 [보관함에 넣기]'), style: TextStyle(fontSize: 12, color: JjColors.textDim)),
),
_sortBox(),
]),
),
if (list.isEmpty && dirs.isEmpty)

33
test/tail_text_test.dart Normal file
View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:jj_mkvmaker/ui/tail_text.dart';
/// 관리자 지시 (10/21, 28 지적): 경로 줄은 좁으면 앞을 "…" 로 - 지금 폴더 (끝) 가 늘 보이게
void main() {
double m(String t) => t.length * 10.0;
test('들어가면 그대로 · 모자라면 구분에서 앞을 줄임 (가장 긴 뒤쪽) · 마지막 조각도 길면 글자 단위', () {
const path = '보관함 › 가족 › 2026 › 여름 휴가 › 제주도 둘째 날';
expect(TailText.fit(path, 1000, m), path);
expect(TailText.fit(path, 200, m), '… › 여름 휴가 › 제주도 둘째 날');
expect(TailText.fit(path, 120, m), '… › 제주도 둘째 날');
expect(TailText.fit(path, 60, m), '… 둘째 날', reason: '마지막 조각도 길면 그 끝을');
expect(TailText.fit(r'C:\Users\haram\AppData\Local\Temp', 170, m), r'…\Local\Temp');
expect(TailText.fit('dav://nas/사진/2026', 80, m), '…/2026');
});
testWidgets('좁은 칸 (글자 1.3 배) 에서도 지금 폴더가 보임', (t) async {
await t.pumpWidget(MaterialApp(
home: MediaQuery(
data: const MediaQueryData(textScaler: TextScaler.linear(1.3)),
child: Scaffold(
body: Center(child: SizedBox(width: 220, child: TailText('보관함 › 가족 › 2026 › 여름 휴가 › 제주도 둘째 날'))),
),
),
));
final shown = t.widget<Text>(find.byType(Text)).data!;
expect(shown, startsWith('…'));
expect(shown, endsWith('제주도 둘째 날'));
expect(t.takeException(), isNull);
});
}