diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index cd7159e..c693bfe 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -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 { 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( diff --git a/lib/ui/tail_text.dart b/lib/ui/tail_text.dart new file mode 100644 index 0000000..2d599da --- /dev/null +++ b/lib/ui/tail_text.dart @@ -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 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 separators = const [' › ', '\\', '/']}) { + if (measure(text) <= width) return text; + // 구분에서 시작하는 뒤쪽들 (긴 것부터) + final cuts = []; + 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); + }); +} diff --git a/lib/ui/vault_page.dart b/lib/ui/vault_page.dart index bc56d79..9c0146c 100644 --- a/lib/ui/vault_page.dart +++ b/lib/ui/vault_page.dart @@ -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 with WidgetsBindingObserver { String _day(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; + Widget _sortBox() => DropdownButton( + 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,29 +955,28 @@ class _VaultPageState extends State with WidgetsBindingObserver { list.sort((a, b) => b.added.compareTo(a.added)); } return Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 16, 0), - child: Row(children: [ - if (_cwd.isNotEmpty) + // 관리자 지시 (10/21, 28 지적): 폴더 안에서는 경로 줄과 정렬 상자를 다른 줄로, 경로는 좁으면 앞을 "…" 로 (지금 폴더가 늘 보이게) + if (_cwd.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 16, 0), + child: Row(children: [ IconButton( tooltip: tr('위 폴더로'), icon: const Icon(Icons.arrow_upward), onPressed: () => setState(() => _cwd = _parentOf(_cwd))), - if (_cwd.isEmpty) 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( - value: _sort, - items: [ - DropdownMenuItem(value: 'added', child: Text(tr('넣은 날'))), - DropdownMenuItem(value: 'name', child: Text(tr('이름'))), - ], - onChanged: (v) => setState(() => _sort = v!), - ), - ]), - ), + 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: Text(tr('넣기는 파일 탐색기에서 파일 · 폴더를 길게 눌러 [보관함에 넣기]'), style: TextStyle(fontSize: 12, color: JjColors.textDim)), + ), + _sortBox(), + ]), + ), if (list.isEmpty && dirs.isEmpty) Expanded(child: Center(child: Text(_cwd.isEmpty ? tr('보관함이 비어 있습니다') : tr('이 폴더는 비어 있습니다')))), if (list.isNotEmpty || dirs.isNotEmpty) diff --git a/test/tail_text_test.dart b/test/tail_text_test.dart new file mode 100644 index 0000000..0c2ed78 --- /dev/null +++ b/test/tail_text_test.dart @@ -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(find.byType(Text)).data!; + expect(shown, startsWith('…')); + expect(shown, endsWith('제주도 둘째 날')); + expect(t.takeException(), isNull); + }); +}