WIP X-plore 숨기기 후속: 지우기 · 옮기기 · 이름 바꾸기를 따라감 · 설정의 [목록에서 숨긴 항목] · 위 막대 ⋮ 에도 보이기 (검토 대기)

관리자 결정 (10/18, 9f758ac 의 판단에 대해):
- 이름 바꾸기 (하나 · 일괄) · 옮기기 (다른 창으로 · 붙여넣기 · 끌어 놓기 · 되돌리기 - 작업이 끝나면) 를 하면 숨긴 목록의 경로도 새 경로로 (그 안의 것도).
  옮기기는 원래 자리에 없고 새 자리에 있는 것만 (같은 이름이라 바꾼 것 · 실패는 그대로).
- 영구로 지운 것 (그 안 포함, 실제로 없어진 것만) 은 목록에서 뺀다. 내 판단: 휴지통 (Windows · 앱 휴지통) 으로 보낸 것은 남긴다 - [되돌리기] 로 되살리면 그대로 숨김이 되게.
- 환경 설정 > 파일 탐색기 [목록에서 숨긴 항목 (n개)] → 목록 (이름 · 폴더) 과 하나씩 [숨기기 풀기] · [모두 풀기] (lib/ui/hidden_items_dialog.dart). 풀면 열려 있는 탐색기에 바로 보인다 (설정 바뀜을 듣는다).
- 위 막대 ⋮ 메뉴에도 [목록에서 숨긴 항목 보이기 (n개)] · [다시 숨기기] (큰 폴더를 펼쳐 목록 맨 아래가 멀 때).

시험: explorer_test (새 3: 이름 바꾸기 → 새 경로 · 영구 삭제 → 뺌 / 다른 창으로 옮기기 → 새 자리 / ⋮ 보이기 · 다시 숨기기 · 설정 창에서 풀기 → 바로 보임) 포함 68 통과, analyze 문제 없음, l10n 빠짐 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
xplore_hide
RyuJongJin 2026-10-18 02:57:16 +09:00
parent 9f758ac386
commit 546b915b55
7 changed files with 247 additions and 11 deletions

View File

@ -1214,6 +1214,9 @@
"목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.": "Hidden from the list ({0}). The files are unchanged - use [Show hidden items] at the bottom of the list to see them again.",
"목록에서 숨겼습니다 ({0}개). 파일은 그대로입니다.": "Hidden from the list ({0}). The files are unchanged.",
"목록에서 숨기기": "Hide from list",
"목록에서 숨긴 항목 ({0}개)": "Items hidden from the list ({0})",
"목록에서 숨긴 항목 다시 숨기기": "Hide the hidden items again",
"목록에서 숨긴 항목 보이기 ({0}개)": "Show items hidden from the list ({0})",
"목록에서 제거": "Remove from list",
"목록에서 지우기 (파일은 그대로)": "Remove from list (files stay)",
"목록을 모두 비울까요?": "Clear the whole list?",
@ -1591,6 +1594,7 @@
"숨긴 항목 {0}개 다시 숨기기": "Hide {0} hidden items again",
"숨긴 항목 {0}개 보이기": "Show {0} hidden items",
"숨긴 항목 숨기기": "Hide hidden items",
"숨긴 항목이 없습니다": "No hidden items",
"숨김": "Hidden",
"숨은 다른 창으로": "To the hidden other pane",
"숨은 다른 창으로: X-plore 처럼 위에 \"갈 곳\" 을 늘 보이고 그 폴더로 바로 복사 · 이동합니다": "To the hidden other pane: like X-plore, the destination is always shown at the top and items are copied or moved straight there",
@ -2564,6 +2568,7 @@
"파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)": "You can also change it from the ⋮ menu at the top of the file explorer (side by side · top and bottom · button bar position · buttons)",
"파일 탐색기 창: 두 창 → 화면 크기 따라 (폰 세로는 한 창). 되돌리기: 환경 설정 > 파일 탐색기 > 창 배치": "File explorer panes: two panes → by screen size (one pane on a portrait phone). To revert: Settings > File explorer > Pane layout",
"파일 탐색기: 파일을 한 번 누르면 바로 엽니다 (고르기 · 메뉴는 길게 누르기). 바꾸기: 환경 설정 > 파일 탐색기 > 누르기": "File explorer: a single tap now opens a file right away (long-press to select or for the menu). To change: Settings > File explorer > Tap",
"파일 탐색기의 길게 누르기 [목록에서 숨기기] 로 숨긴 것. 파일은 그대로이고 목록에서만 빠집니다": "Hidden with long-press [Hide from list] in the file explorer. The files are unchanged; they are only left out of the list",
"파일 폴더": "File folder",
"파일 확인 중…": "Checking files…",
"파일만 골랐을 때": "When only files are selected",
@ -2649,6 +2654,7 @@
"풀 수 없는 이름 {0}개 (건너뜀): {1}": "{0} names can't be used on this device (skipped): {1}",
"풀기": "Extract",
"풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "The extracted size ({0}) is larger than the free space ({1}). Extract anyway?",
"풀면 파일 탐색기 목록에 다시 보입니다. 파일 자체는 바꾸지 않습니다.": "Unhidden items show in the file explorer list again. The files themselves are not changed.",
"풀지 못했습니다: {0}": "Could not extract: {0}",
"품질 올리기": "Improve quality",
"품질 올리기 (이 장)": "Improve quality (this page)",

View File

@ -1214,6 +1214,9 @@
"목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.": "リストから隠しました ({0} 件)。ファイルはそのままで、リストの一番下の [隠した項目を表示] で再び見られます。",
"목록에서 숨겼습니다 ({0}개). 파일은 그대로입니다.": "リストから隠しました ({0} 件)。ファイルはそのままです。",
"목록에서 숨기기": "リストから隠す",
"목록에서 숨긴 항목 ({0}개)": "リストから隠した項目 ({0} 件)",
"목록에서 숨긴 항목 다시 숨기기": "リストから隠した項目を再び隠す",
"목록에서 숨긴 항목 보이기 ({0}개)": "リストから隠した項目を表示 ({0} 件)",
"목록에서 제거": "リストから削除",
"목록에서 지우기 (파일은 그대로)": "リストから削除 (ファイルはそのまま)",
"목록을 모두 비울까요?": "一覧をすべて空にしますか?",
@ -1591,6 +1594,7 @@
"숨긴 항목 {0}개 다시 숨기기": "隠した項目 {0} 件を再び隠す",
"숨긴 항목 {0}개 보이기": "隠した項目 {0} 件を表示",
"숨긴 항목 숨기기": "隠し項目を隠す",
"숨긴 항목이 없습니다": "隠した項目はありません",
"숨김": "非表示",
"숨은 다른 창으로": "隠れた別ペインへ",
"숨은 다른 창으로: X-plore 처럼 위에 \"갈 곳\" 을 늘 보이고 그 폴더로 바로 복사 · 이동합니다": "隠れた別ペインへ: X-plore のように上部に「移動先」を常に表示し、そのフォルダーへ直接コピー・移動します",
@ -2564,6 +2568,7 @@
"파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)": "ファイルエクスプローラー上部の ⋮ メニューからも変えられます (左右 · 上下 · ボタン列の位置 · ボタン構成)",
"파일 탐색기 창: 두 창 → 화면 크기 따라 (폰 세로는 한 창). 되돌리기: 환경 설정 > 파일 탐색기 > 창 배치": "ファイルエクスプローラーの画面: 2 画面 → 画面サイズに合わせる (縦向きのスマホは 1 画面)。元に戻す: 環境設定 > ファイルエクスプローラー > 画面配置",
"파일 탐색기: 파일을 한 번 누르면 바로 엽니다 (고르기 · 메뉴는 길게 누르기). 바꾸기: 환경 설정 > 파일 탐색기 > 누르기": "ファイルエクスプローラー: ファイルを 1 回タップするとすぐ開きます (選択 · メニューは長押し)。変更: 環境設定 > ファイルエクスプローラー > タップ",
"파일 탐색기의 길게 누르기 [목록에서 숨기기] 로 숨긴 것. 파일은 그대로이고 목록에서만 빠집니다": "ファイルエクスプローラーの長押し [リストから隠す] で隠したもの。ファイルはそのままで、リストから外れるだけです",
"파일 폴더": "ファイル フォルダー",
"파일 확인 중…": "ファイルを確認中…",
"파일만 골랐을 때": "ファイルだけを選んだとき",
@ -2649,6 +2654,7 @@
"풀 수 없는 이름 {0}개 (건너뜀): {1}": "展開できない名前 {0} 件 (スキップ): {1}",
"풀기": "展開",
"풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "展開後のサイズ ({0}) が空き容量 ({1}) より大きいです。それでも展開しますか?",
"풀면 파일 탐색기 목록에 다시 보입니다. 파일 자체는 바꾸지 않습니다.": "隠すのをやめるとファイルエクスプローラーのリストに再び表示されます。ファイル自体は変更しません。",
"풀지 못했습니다: {0}": "展開できませんでした: {0}",
"품질 올리기": "画質を上げる",
"품질 올리기 (이 장)": "画質を上げる (このページ)",

View File

@ -1214,6 +1214,9 @@
"목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.": "已从列表中隐藏 ({0} 项)。文件本身未改变,可用列表底部的 [显示已隐藏项] 再次查看。",
"목록에서 숨겼습니다 ({0}개). 파일은 그대로입니다.": "已从列表中隐藏 ({0} 项)。文件本身未改变。",
"목록에서 숨기기": "从列表中隐藏",
"목록에서 숨긴 항목 ({0}개)": "从列表中隐藏的项 ({0} 项)",
"목록에서 숨긴 항목 다시 숨기기": "再次隐藏已从列表中隐藏的项",
"목록에서 숨긴 항목 보이기 ({0}개)": "显示从列表中隐藏的项 ({0} 项)",
"목록에서 제거": "从列表中移除",
"목록에서 지우기 (파일은 그대로)": "从列表中删除 (文件保留)",
"목록을 모두 비울까요?": "清空整个列表吗?",
@ -1591,6 +1594,7 @@
"숨긴 항목 {0}개 다시 숨기기": "再次隐藏 {0} 个已隐藏项",
"숨긴 항목 {0}개 보이기": "显示 {0} 个已隐藏项",
"숨긴 항목 숨기기": "隐藏隐藏项目",
"숨긴 항목이 없습니다": "没有已隐藏的项",
"숨김": "隐藏",
"숨은 다른 창으로": "到隐藏的另一窗格",
"숨은 다른 창으로: X-plore 처럼 위에 \"갈 곳\" 을 늘 보이고 그 폴더로 바로 복사 · 이동합니다": "到隐藏的另一窗格: 像 X-plore 一样在顶部始终显示\"目标\",并直接复制 · 移动到该文件夹",
@ -2564,6 +2568,7 @@
"파일 탐색기 위쪽 ⋮ 메뉴에서도 바꿀 수 있습니다 (좌우 · 위아래 · 버튼 줄 위치 · 버튼 구성)": "也可以在文件浏览器顶部的 ⋮ 菜单中更改 (左右 · 上下 · 按钮栏位置 · 按钮配置)",
"파일 탐색기 창: 두 창 → 화면 크기 따라 (폰 세로는 한 창). 되돌리기: 환경 설정 > 파일 탐색기 > 창 배치": "文件浏览器窗格: 双窗格 → 按屏幕大小 (竖屏手机为单窗格)。恢复: 设置 > 文件浏览器 > 窗格布局",
"파일 탐색기: 파일을 한 번 누르면 바로 엽니다 (고르기 · 메뉴는 길게 누르기). 바꾸기: 환경 설정 > 파일 탐색기 > 누르기": "文件浏览器:单击文件会立即打开(选择 · 菜单请长按)。更改:设置 > 文件浏览器 > 点按",
"파일 탐색기의 길게 누르기 [목록에서 숨기기] 로 숨긴 것. 파일은 그대로이고 목록에서만 빠집니다": "在文件浏览器中长按 [从列表中隐藏] 隐藏的项。文件本身不变,只是不在列表中显示",
"파일 폴더": "文件夹",
"파일 확인 중…": "正在检查文件…",
"파일만 골랐을 때": "只选择文件时",
@ -2649,6 +2654,7 @@
"풀 수 없는 이름 {0}개 (건너뜀): {1}": "{0} 个名称无法在本设备使用 (跳过): {1}",
"풀기": "解压",
"풀린 크기 ({0}) 가 남은 공간 ({1}) 보다 큽니다. 그래도 풀까요?": "解压后大小 ({0}) 大于剩余空间 ({1})。仍要解压吗?",
"풀면 파일 탐색기 목록에 다시 보입니다. 파일 자체는 바꾸지 않습니다.": "取消隐藏后会重新显示在文件浏览器列表中。不会更改文件本身。",
"풀지 못했습니다: {0}": "无法解压: {0}",
"품질 올리기": "提升画质",
"품질 올리기 (이 장)": "提升画质 (本页)",

View File

@ -415,6 +415,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
pane.addListener(_onPane);
}
_resumeReload = AppLifecycleListener(onResume: () => unawaited(_reloadOpen(localOnly: true)));
_hiddenSeen = c.settings.explorerHiddenItems;
c.addListener(_onSettings);
_init();
}
@ -446,6 +448,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
@override
void dispose() {
c.removeListener(_onSettings);
_keys.dispose();
_sizes.dispose();
if (widget.rsync) {
@ -1229,26 +1232,67 @@ class _ExplorerPageState extends State<ExplorerPage> {
return Platform.isWindows ? n.toLowerCase() : n;
}
/// X-plore 숨기기: 옮기거나 이름을 바꾼 것 ([moved] 원래 → 새 경로, 그 안의 것도) 은 숨긴 목록도 새 경로로,
/// 지운 것 ([gone], 그 안의 것도 - 실제로 없어진 것만) 은 목록에서 뺀다 (관리자 결정 10/18)
Future<void> _followHidden({Iterable<(String, String)> moved = const [], Iterable<String> gone = const []}) async {
final list = c.settings.explorerHiddenItems;
if (list.isEmpty || (moved.isEmpty && gone.isEmpty)) return;
final out = <String>[];
var changed = false;
for (final h in list) {
if (gone.any((g) => isSameOrInside(h, g)) && !await vExists(h)) {
changed = true;
continue;
}
var now = h;
for (final (from, to) in moved) {
if (samePath(h, from)) {
now = to;
break;
}
if (isSameOrInside(h, from)) {
now = vJoin(to, isDav(h) ? h.substring(from.length).replaceAll('\\', '/').replaceFirst(RegExp('^/+'), '') : p.relative(h, from: from));
break;
}
}
if (now != h) changed = true;
if (!out.any((x) => _hideKey(x) == _hideKey(now))) out.add(now);
}
if (changed) await c.updateSettings((s) => s.explorerHiddenItems = out);
}
/// X-plore 숨기기: 목록 맨 아래의 [숨긴 항목 n개 보이기] · [다시 숨기기] 를 누르면 (이번 실행 동안, 모든 창에)
bool _showAppHidden = false;
Widget _hiddenToggleRow(_Row row) {
final look = _look;
final label = _showAppHidden ? trf('숨긴 항목 {0}개 다시 숨기기', [row.count]) : trf('숨긴 항목 {0}개 보이기', [row.count]);
void tap() {
void _toggleAppHidden() {
setState(() => _showAppHidden = !_showAppHidden);
for (final q in _panes) {
q.changed();
}
}
/// 설정 화면에서 숨긴 목록을 바꾸면 (풀기) 바로 목록에 보이게
List<String>? _hiddenSeen;
void _onSettings() {
final now = c.settings.explorerHiddenItems;
if (identical(now, _hiddenSeen) || !mounted) return;
_hiddenSeen = now;
setState(() {});
for (final q in _panes) {
q.changed();
}
}
Widget _hiddenToggleRow(_Row row) {
final look = _look;
final label = _showAppHidden ? trf('숨긴 항목 {0}개 다시 숨기기', [row.count]) : trf('숨긴 항목 {0}개 보이기', [row.count]);
return Semantics(
button: true,
label: label,
excludeSemantics: true,
onTap: tap,
onTap: _toggleAppHidden,
child: InkWell(
onTap: tap,
onTap: _toggleAppHidden,
child: Row(children: [
SizedBox(width: treeChevronWidth(defaultTargetPlatform) + 4),
Icon(_showAppHidden ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: look.iconSize * 0.8, color: JjColors.textDim),
@ -2498,6 +2542,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
if (name == null || name == vBasename(path)) return;
try {
final now = await FileOps.rename(path, name);
await _followHidden(moved: [(path, now)]);
await _refreshAll([vDirname(path)]);
for (final x in _panes) {
if (x.marked.remove(path)) x.marked.add(now);
@ -2547,6 +2592,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
/// 바꾼 뒤: 목록을 다시 읽고, 표시 · 초점을 새 이름으로
Future<void> _afterBatchRename(_Pane pane, List<(String, String)> moved) async {
await _followHidden(moved: moved);
await _refreshAll({for (final (a, _) in moved) vDirname(a)});
for (final x in _panes) {
for (final (from, to) in moved) {
@ -2658,10 +2704,13 @@ class _ExplorerPageState extends State<ExplorerPage> {
// X-plore 비교 e: 앱 휴지통으로 보낸 것 · 옮기지 못한 것 (영구 삭제할지 묻는다)
final trashed = <TrashItem>[];
final notTrashed = <(String, String)>[];
// X-plore 숨기기: 휴지통에서 [되돌리기] 할 수 있는 것 (숨긴 목록에 남겨 되살리면 그대로 숨김)
final undoable = <String>[];
for (final x in paths) {
if (!permanent && canTrash) {
try {
trashed.add(await trash.moveToTrash(x));
undoable.add(x);
recycled++;
} on TrashMoveException catch (e) {
notTrashed.add((x, '${e.cause}'));
@ -2794,6 +2843,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
: SnackBarAction(label: tr('되돌리기'), onPressed: () => _undoRecycle(sent, since)),
));
}
// X-plore 숨기기: 영구로 지운 것 (그 안 포함) 은 숨긴 목록에서 뺀다. 휴지통으로 보낸 것은 되돌리기 뒤에도 숨김이 되게 남긴다
await _followHidden(gone: [
for (final x in paths)
if (!sent.contains(x) && !undoable.contains(x) && !failed.any((f) => f.$1 == x)) x,
]);
for (final x in _panes) {
x.marked.removeAll(paths);
if (_selecting && x.marked.isEmpty) _selecting = false;
@ -2952,10 +3006,23 @@ class _ExplorerPageState extends State<ExplorerPage> {
}
/// 52: 이 화면을 떠난 사이에 시작된 (또는 앞의 화면이 시작한) 작업을 다시 보이고, 끝나면 새로 읽고 막대를 내린다
/// X-plore 숨기기: 옮긴 것은 숨긴 목록도 새 자리로 (원래 자리에 없고 새 자리에 있는 것만 - 같은 이름 바꾸기 · 실패는 그대로)
Future<void> _followMovedJob(TransferJob job) async {
if (!job.move || job.contents || c.settings.explorerHiddenItems.isEmpty) return;
final moved = <(String, String)>[];
for (final s in job.sources) {
final to = vJoin(job.dest, vBasename(s));
if (!await vExists(s) && await vExists(to)) moved.add((s, to));
}
await _followHidden(moved: moved);
}
void _adoptJob(TransferJob job) {
if (_jobs.contains(job)) return;
setState(() => _jobs = [..._jobs, job]);
unawaited(job.done.then((_) async {
if (!mounted) return;
await _followMovedJob(job);
if (!mounted) return;
await _refreshAll([job.dest, ...job.sources.map(vDirname)]);
for (final x in _panes) {
@ -3170,6 +3237,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
setState(() => _jobs = [..._jobs.where((j) => !j.finished), job]);
await job.done;
final error = job.error;
await _followMovedJob(job);
if (!mounted) return false;
// 180: 표시만 지우고 선택 모드는 그대로 (꺼지면 동그라미 칸 · 아래 막대가 사라져 자리가 바뀐다)
pane.marked.clear();
@ -4191,10 +4259,10 @@ class _ExplorerPageState extends State<ExplorerPage> {
child: Text(_ready ? _displayPath(_pane.current) : '',
maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)),
),
PopupMenuButton<ExplorerButton>(
PopupMenuButton<Object>(
tooltip: tr('창 배치 · 버튼 구성'),
icon: const Icon(Icons.more_vert),
onSelected: _button,
onSelected: (v) => v is ExplorerButton ? _button(v) : _toggleAppHidden(),
itemBuilder: (_) => [
for (final b in [
if (!widget.rsync) ...[ExplorerButton.layout, ExplorerButton.buttons],
@ -4207,6 +4275,20 @@ class _ExplorerPageState extends State<ExplorerPage> {
value: b,
child: Row(children: [Icon(b.icon, size: 18), const SizedBox(width: 12), Text(tr(b.label))]),
),
// X-plore 숨기기: 큰 폴더를 펼쳐 목록 맨 아래가 멀 때도 (관리자 결정 10/18)
if (!widget.rsync && c.settings.explorerHiddenItems.isNotEmpty)
PopupMenuItem(
value: 'appHidden',
child: Row(children: [
Icon(_showAppHidden ? Icons.visibility_off_outlined : Icons.visibility_outlined, size: 18),
const SizedBox(width: 12),
Flexible(
child: Text(_showAppHidden
? tr('목록에서 숨긴 항목 다시 숨기기')
: trf('목록에서 숨긴 항목 보이기 ({0}개)', [c.settings.explorerHiddenItems.length])),
),
]),
),
],
),
]),

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import '../app/app_controller.dart';
import '../core/file_ops.dart' show samePath;
import '../core/vfs.dart' show vBasename, vDirname;
import '../l10n/tr.dart';
import 'path_label.dart';
import 'theme.dart';
/// X-plore 숨기기 (관리자 결정 10/18): 환경 설정 > 파일 탐색기 [목록에서 숨긴 항목] - 숨긴 것을 보고 하나씩 · 모두 풀기.
/// 파일은 건드리지 않는다 (목록에서만 다시 보이게)
Future<void> showHiddenItems(BuildContext context, AppController c) => showDialog<void>(
context: context,
builder: (ctx) => StatefulBuilder(builder: (ctx, set) {
final list = c.settings.explorerHiddenItems;
Future<void> unhide(List<String> which) async {
await c.updateSettings((s) => s.explorerHiddenItems = [
for (final x in s.explorerHiddenItems)
if (!which.any((w) => samePath(w, x))) x,
]);
set(() {});
}
return AlertDialog(
scrollable: true,
title: Text(trf('목록에서 숨긴 항목 ({0}개)', [list.length])),
content: SizedBox(
width: 520,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(tr('풀면 파일 탐색기 목록에 다시 보입니다. 파일 자체는 바꾸지 않습니다.'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
const SizedBox(height: 8),
if (list.isEmpty) Text(tr('숨긴 항목이 없습니다')),
for (final x in list)
ListTile(
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(vBasename(x), maxLines: 1, overflow: TextOverflow.ellipsis),
subtitle: Text(friendlyPath(vDirname(x)), maxLines: 2, overflow: TextOverflow.ellipsis),
trailing: IconButton(
tooltip: tr('숨기기 풀기'),
icon: const Icon(Icons.visibility_outlined),
onPressed: () => unhide([x]),
),
),
]),
),
actions: [
if (list.length > 1) TextButton(onPressed: () => unhide([...list]), child: Text(tr('모두 풀기'))),
FilledButton(onPressed: () => Navigator.pop(ctx), child: Text(tr('닫기'))),
],
);
}),
);

View File

@ -44,6 +44,7 @@ import '../app/master_lock.dart';
import 'toast_status.dart';
import 'license_texts.dart';
import 'subtitle_language_picker.dart';
import 'hidden_items_dialog.dart';
/// 동시 작업 수 고르기: 1 · 5 · 10 · 무한(0) · 직접 입력
class CountSelector extends StatelessWidget {
@ -894,6 +895,14 @@ class _SettingsPageState extends State<SettingsPage> {
value: s.explorerRetapCollapse,
onChanged: (v) => c.updateSettings((x) => x.explorerRetapCollapse = v),
),
// X-plore 숨기기: 목록에서 숨긴 것 보기 · 풀기 (관리자 결정 10/18)
ListTile(
title: Text(trf('목록에서 숨긴 항목 ({0}개)', [s.explorerHiddenItems.length])),
subtitle: Text(tr('파일 탐색기의 길게 누르기 [목록에서 숨기기] 로 숨긴 것. 파일은 그대로이고 목록에서만 빠집니다')),
trailing: const Icon(Icons.chevron_right),
enabled: s.explorerHiddenItems.isNotEmpty,
onTap: () => showHiddenItems(context, c),
),
// X-plore 비교 a: 한 창일 때 복사 · 이동이 갈 곳
SettingTile(
title: Text(tr('한 창일 때 복사 · 이동')),

View File

@ -15,6 +15,7 @@ import 'package:jj_mkvmaker/services/app_shell.dart';
import 'package:jj_mkvmaker/services/platform_services.dart';
import 'package:jj_mkvmaker/core/file_ops.dart' show formatSize;
import 'package:jj_mkvmaker/ui/explorer_page.dart';
import 'package:jj_mkvmaker/ui/hidden_items_dialog.dart';
import 'package:jj_mkvmaker/ui/explorer_search.dart';
import 'package:jj_mkvmaker/ui/file_tools.dart' show HexViewerPage;
import 'package:jj_mkvmaker/ui/quick_look.dart';
@ -1612,6 +1613,78 @@ void main() {
await calm(tester);
});
testWidgets('X-plore 숨기기: 이름을 바꾸면 숨긴 목록도 새 경로로 · 영구로 지우면 목록에서 뺀다 (관리자 결정)', (tester) async {
c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')];
await open(tester);
await tester.longPress(find.text('sub'));
await calm(tester);
await act(tester, () => tester.tap(find.byTooltip('이름 변경').last));
await calm(tester);
await act(tester, () => tester.enterText(find.byType(TextField), 'sub2'));
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '확인')));
await settle(tester, () => find.text('sub2').evaluate().isNotEmpty);
expect(c.settings.explorerHiddenItems, [p.join(left, 'sub2', 'inner.txt')]);
await idle(tester);
await tester.longPress(find.text('sub2'));
await calm(tester);
await act(tester, () => tester.tap(find.byTooltip('삭제').last));
await calm(tester);
await tester.tap(find.text('휴지통을 거치지 않고 영구 삭제 (Shift+Delete)'));
await calm(tester);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '영구 삭제')));
await settle(tester, () => find.text('sub2').evaluate().isEmpty);
await settle(tester, () => c.settings.explorerHiddenItems.isEmpty);
expect(c.settings.explorerHiddenItems, isEmpty);
});
testWidgets('X-plore 숨기기: 다른 창으로 옮기면 숨긴 목록도 새 자리로 (그 안의 것도)', (tester) async {
c.settings.explorerHiddenItems = [p.join(left, 'sub', 'inner.txt')];
await open(tester);
await tester.longPress(find.text('sub'));
await calm(tester);
await act(tester, () => tester.tap(find.byTooltip('다른 창으로 이동').last));
await settle(tester, () => find.widgetWithText(FilledButton, '이동').evaluate().isNotEmpty);
await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '이동')));
await settle(tester, () => c.settings.explorerHiddenItems.first != p.join(left, 'sub', 'inner.txt'));
expect(File(p.join(right, 'sub', 'inner.txt')).existsSync(), isTrue);
expect(c.settings.explorerHiddenItems, [p.join(right, 'sub', 'inner.txt')]);
await idle(tester);
});
testWidgets('X-plore 숨기기: 위 막대 ⋮ [목록에서 숨긴 항목 보이기] · 환경 설정의 [목록에서 숨긴 항목] 에서 풀기', (tester) async {
final doc = p.join(left, 'doc.txt');
c.settings
..explorerLayout = 'split'
..explorerHiddenItems = [doc];
tester.view.physicalSize = const Size(1600, 900);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
await tester.runAsync(() => tester.pumpWidget(MaterialApp(home: ExplorerPage(c: c))));
await settle(tester, () => find.text('숨긴 항목 1개 보이기').evaluate().isNotEmpty);
expect(find.text('doc.txt'), findsNothing);
await act(tester, () => tester.tap(find.byTooltip('창 배치 · 버튼 구성')));
await calm(tester);
await act(tester, () => tester.tap(find.text('목록에서 숨긴 항목 보이기 (1개)')));
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
expect(find.text('숨긴 항목 1개 다시 숨기기'), findsOneWidget);
await act(tester, () => tester.tap(find.byTooltip('창 배치 · 버튼 구성')));
await calm(tester);
await act(tester, () => tester.tap(find.text('목록에서 숨긴 항목 다시 숨기기')));
await settle(tester, () => find.text('doc.txt').evaluate().isEmpty);
// 설정 화면의 목록에서 풀면 탐색기 목록에 바로 보인다
final ctx = tester.element(find.byType(ExplorerPage));
unawaited(showHiddenItems(ctx, c));
await calm(tester);
expect(find.text('목록에서 숨긴 항목 (1개)'), findsOneWidget);
await act(tester, () => tester.tap(find.byTooltip('숨기기 풀기')));
await calm(tester);
expect(c.settings.explorerHiddenItems, isEmpty);
expect(find.text('숨긴 항목이 없습니다'), findsOneWidget);
await act(tester, () => tester.tap(find.text('닫기')));
await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty);
expect(find.textContaining('숨긴 항목'), findsNothing);
});
testWidgets('80: 크기순 정렬이면 폴더 크기를 계산해 보이고, "안 함" 이어도 길게 누르기 [폴더 크기 계산]', (tester) async {
File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1));
c.settings