From 9f758ac38633c172f12356d2a902ec59ea97bf54 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sun, 18 Oct 2026 02:21:26 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20X-plore=20=EC=88=A8=EA=B8=B0=EA=B8=B0:=20?= =?UTF-8?q?=EA=B3=A0=EB=A5=B8=20=ED=8C=8C=EC=9D=BC=20=C2=B7=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=EB=A5=BC=20=EB=AA=A9=EB=A1=9D=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=A7=8C=20=EC=88=A8=EA=B8=B0=EA=B8=B0=20=C2=B7=20=EB=A7=A8=20?= =?UTF-8?q?=EC=95=84=EB=9E=98=20[=EC=88=A8=EA=B8=B4=20=ED=95=AD=EB=AA=A9?= =?UTF-8?q?=20=EB=B3=B4=EC=9D=B4=EA=B8=B0]=20(=EA=B2=80=ED=86=A0=20?= =?UTF-8?q?=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관리자 지시 (10/18, X-plore 비교표 P3): - 길게 누르기 메뉴 [목록에서 숨기기] (표시한 것이 여럿이면 "n개 목록에서 숨기기"). 앱 설정 explorerHiddenItems 에 경로 목록으로만 둔다 - 파일 · 폴더 자체 (속성 · 이름) 는 바꾸지 않는다. - 펼친 폴더 안에 숨긴 것이 있으면 목록 맨 아래 (X-plore 자리) 에 [숨긴 항목 n개 보이기] → 흐리게 (불투명도 0.5) 보이고 [숨긴 항목 n개 다시 숨기기]. 보이기는 이번 실행 동안 · 모든 창 (내 판단: 다시 켜면 숨김). 트리 (두 창 · 한 창) · 폴더 + 파일 목록 배치의 오른쪽 목록 모두. - 보이는 숨긴 것의 메뉴는 [숨기기 풀기]. - 숨긴 것은 지우기 · 이동 대상에서 빠지지 않는다 (폴더를 지우거나 옮기면 안의 숨긴 것도 함께). 지우는 폴더 안에 숨긴 것이 있으면 확인 창에 "지우는 폴더 안에 목록에서 숨긴 항목 n개가 있습니다 - 함께 지워집니다". - 숨긴 것은 모두 고르기 · 빠른 필터 결과에서 빠진다 (보이는 것만 - 76 과 같게). - 내 판단: 지우거나 이름이 바뀐 항목의 경로는 목록에 남는다 (같은 이름이 다시 생기면 숨김). 설정 화면에서 목록 보기 · 지우기는 아직 없음. 트리는 Temp 처럼 큰 폴더를 펼쳐 두면 맨 아래가 멀다 (X-plore 와 같은 자리) - 필요하면 가운데 버튼 줄에도 둘 수 있음. 시험: explorer_test (새 2: 숨기기 → 목록에서 빠짐 · 파일 그대로 · 설정에 경로 · 보이기 → 흐리게 · 풀기 / 숨긴 것이 든 폴더 지우기 확인 창 알림 · 다른 파일은 알리지 않음) 57 통과, settings_store_test 통과, analyze 문제 없음, l10n 빠짐 0. Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 9 +++ assets/l10n/ja.json | 9 +++ assets/l10n/zh-Hans.json | 9 +++ lib/app/settings.dart | 6 ++ lib/ui/explorer_page.dart | 124 ++++++++++++++++++++++++++++++++++++-- test/explorer_test.dart | 51 ++++++++++++++++ 6 files changed, 203 insertions(+), 5 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 0e9aac5..37f23be 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -543,9 +543,11 @@ "{0}개 고름": "{0} selected", "{0}개 더 보기": "Show {0} more", "{0}개 맞춤": "{0} synced", + "{0}개 목록에서 숨기기": "Hide {0} items from list", "{0}개 보임": "{0} shown", "{0}개 삭제": "Delete {0} items", "{0}개 선택": "{0} selected", + "{0}개 숨기기 풀기": "Unhide {0} items", "{0}개 이름 일괄 바꾸기": "Rename {0} items", "{0}개 중 {1}개를 지우지 못했습니다": "Couldn't delete {1} of {0}", "{0}개 지우고 맞추기": "Delete {0} and sync", @@ -1209,6 +1211,9 @@ "목록에서 동영상을 누르면 여기에 자세히 보입니다": "Tap a video in the list to see its details here", "목록에서 빼기": "Remove from list", "목록에서 빼기 (파일은 그대로)": "Remove from list (file is kept)", + "목록에서 숨겼습니다 ({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", "목록에서 제거": "Remove from list", "목록에서 지우기 (파일은 그대로)": "Remove from list (files stay)", "목록을 모두 비울까요?": "Clear the whole list?", @@ -1581,7 +1586,10 @@ "수정한 날짜": "Date modified", "숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "Even when hidden, you can change the pane layout · buttons from the ⋮ menu at the top.", "숨기기": "Hide", + "숨기기 풀기": "Unhide", "숨긴 항목 (. 으로 시작) 보이기": "Show hidden items (starting with .)", + "숨긴 항목 {0}개 다시 숨기기": "Hide {0} hidden items again", + "숨긴 항목 {0}개 보이기": "Show {0} hidden items", "숨긴 항목 숨기기": "Hide hidden items", "숨김": "Hidden", "숨은 다른 창으로": "To the hidden other pane", @@ -2369,6 +2377,7 @@ "지우기가 들어 있는 작업입니다": "This task deletes files", "지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "This run deletes files, so it can't run without a comparison. Check the source and target, then press [Compare again].", "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "This run deletes files, so you can run it after the list of deletions appears.", + "지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "The folders being deleted contain {0} items hidden from the list - they will be deleted too", "지우면 휴지통으로": "Delete to the Recycle Bin", "지우지 못한 파일: {0}": "Couldn't delete: {0}", "지우지 못함: {0}": "Could not delete: {0}", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index ce4dc2a..5ee1839 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -543,9 +543,11 @@ "{0}개 고름": "{0} 件選択", "{0}개 더 보기": "さらに {0} 件表示", "{0}개 맞춤": "{0} 件を同期", + "{0}개 목록에서 숨기기": "{0} 件をリストから隠す", "{0}개 보임": "{0} 件表示", "{0}개 삭제": "{0} 件を削除", "{0}개 선택": "{0}個選択", + "{0}개 숨기기 풀기": "{0} 件の非表示をやめる", "{0}개 이름 일괄 바꾸기": "{0} 件の名前を一括変更", "{0}개 중 {1}개를 지우지 못했습니다": "{0} 個中 {1} 個を削除できませんでした", "{0}개 지우고 맞추기": "{0} 個を削除して同期", @@ -1209,6 +1211,9 @@ "목록에서 동영상을 누르면 여기에 자세히 보입니다": "リストの動画をタップすると、ここに詳細が表示されます", "목록에서 빼기": "リストから外す", "목록에서 빼기 (파일은 그대로)": "リストから外す (ファイルはそのまま)", + "목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.": "リストから隠しました ({0} 件)。ファイルはそのままで、リストの一番下の [隠した項目を表示] で再び見られます。", + "목록에서 숨겼습니다 ({0}개). 파일은 그대로입니다.": "リストから隠しました ({0} 件)。ファイルはそのままです。", + "목록에서 숨기기": "リストから隠す", "목록에서 제거": "リストから削除", "목록에서 지우기 (파일은 그대로)": "リストから削除 (ファイルはそのまま)", "목록을 모두 비울까요?": "一覧をすべて空にしますか?", @@ -1581,7 +1586,10 @@ "수정한 날짜": "更新日時", "숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "非表示にしても、上の ⋮ メニューからペインの配置 · ボタン構成を変えられます。", "숨기기": "隠す", + "숨기기 풀기": "隠すのをやめる", "숨긴 항목 (. 으로 시작) 보이기": "隠し項目 (. で始まる) を表示", + "숨긴 항목 {0}개 다시 숨기기": "隠した項目 {0} 件を再び隠す", + "숨긴 항목 {0}개 보이기": "隠した項目 {0} 件を表示", "숨긴 항목 숨기기": "隠し項目を隠す", "숨김": "非表示", "숨은 다른 창으로": "隠れた別ペインへ", @@ -2369,6 +2377,7 @@ "지우기가 들어 있는 작업입니다": "削除を含む作業です", "지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "削除を含む実行のため、比較できないと実行できません。元・対象を確認してから [もう一度比較] を押してください。", "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "削除を含む実行のため、削除一覧が表示されてから実行できます。", + "지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "削除するフォルダーの中にリストから隠した項目が {0} 件あります - 一緒に削除されます", "지우면 휴지통으로": "削除するとごみ箱へ", "지우지 못한 파일: {0}": "削除できなかったファイル: {0}", "지우지 못함: {0}": "削除できません: {0}", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 9df00ae..b81bae5 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -543,9 +543,11 @@ "{0}개 고름": "已选 {0} 个", "{0}개 더 보기": "再显示 {0} 个", "{0}개 맞춤": "已同步 {0} 个", + "{0}개 목록에서 숨기기": "从列表中隐藏 {0} 项", "{0}개 보임": "显示 {0} 项", "{0}개 삭제": "删除 {0} 项", "{0}개 선택": "已选 {0} 个", + "{0}개 숨기기 풀기": "取消隐藏 {0} 项", "{0}개 이름 일괄 바꾸기": "批量重命名 {0} 项", "{0}개 중 {1}개를 지우지 못했습니다": "{0} 个中有 {1} 个无法删除", "{0}개 지우고 맞추기": "删除 {0} 个并同步", @@ -1209,6 +1211,9 @@ "목록에서 동영상을 누르면 여기에 자세히 보입니다": "点按列表中的视频即可在此查看详情", "목록에서 빼기": "从列表中移除", "목록에서 빼기 (파일은 그대로)": "从列表中移除 (保留文件)", + "목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.": "已从列表中隐藏 ({0} 项)。文件本身未改变,可用列表底部的 [显示已隐藏项] 再次查看。", + "목록에서 숨겼습니다 ({0}개). 파일은 그대로입니다.": "已从列表中隐藏 ({0} 项)。文件本身未改变。", + "목록에서 숨기기": "从列表中隐藏", "목록에서 제거": "从列表中移除", "목록에서 지우기 (파일은 그대로)": "从列表中删除 (文件保留)", "목록을 모두 비울까요?": "清空整个列表吗?", @@ -1581,7 +1586,10 @@ "수정한 날짜": "修改日期", "숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "即使隐藏,也可以在顶部 ⋮ 菜单中更改窗格布局 · 按钮配置。", "숨기기": "隐藏", + "숨기기 풀기": "取消隐藏", "숨긴 항목 (. 으로 시작) 보이기": "显示隐藏项目 (以 . 开头)", + "숨긴 항목 {0}개 다시 숨기기": "再次隐藏 {0} 个已隐藏项", + "숨긴 항목 {0}개 보이기": "显示 {0} 个已隐藏项", "숨긴 항목 숨기기": "隐藏隐藏项目", "숨김": "隐藏", "숨은 다른 창으로": "到隐藏的另一窗格", @@ -2369,6 +2377,7 @@ "지우기가 들어 있는 작업입니다": "此任务包含删除", "지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "此操作包含删除,无法比较时不能执行。请检查源和目标后按 [重新比较]。", "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "此操作包含删除,需在显示删除列表后才能执行。", + "지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "要删除的文件夹中有 {0} 个从列表中隐藏的项 - 将一并删除", "지우면 휴지통으로": "删除时移到回收站", "지우지 못한 파일: {0}": "无法删除的文件: {0}", "지우지 못함: {0}": "无法删除: {0}", diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 1ab1f5f..261d234 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -470,6 +470,10 @@ class AppSettings { int zipLevel = 6; bool explorerShowHidden = false; + /// X-plore 숨기기 (관리자 지시 10/18): 파일 탐색기 목록에서 숨긴 파일 · 폴더의 경로 (파일 자체는 바꾸지 않는다). + /// 지우기 · 이동 대상에서는 빠지지 않는다. 목록 맨 아래 [숨긴 항목 보이기] 로 다시 본다 + List explorerHiddenItems = []; + // ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ── /// 파일 탐색기의 복사 · 이동 방법: 'builtin' 현재 방식 (기본) · 'robocopy' (Windows). 파일만 고를 때 / 폴더가 들어 있을 때. /// rsync 는 Rsync 화면에서 따로 (파일 탐색기에서는 쓰지 않음) @@ -818,6 +822,7 @@ class AppSettings { 'explorerSortPerFolder': explorerSortPerFolder, 'explorerFolderSorts': explorerFolderSorts, 'explorerShowHidden': explorerShowHidden, + 'explorerHiddenItems': explorerHiddenItems, 'explorerPaths': explorerPaths, 'copyMethodFile': copyMethodFile, 'copyConflict': copyConflict, @@ -1057,6 +1062,7 @@ class AppSettings { if (e.key is String && e.value is String) e.key as String: e.value as String, } ..explorerShowHidden = j['explorerShowHidden'] as bool? ?? false + ..explorerHiddenItems = [for (final x in (j['explorerHiddenItems'] as List?) ?? const []) if (x is String && x.isNotEmpty) x] ..explorerPaths = [for (final x in (j['explorerPaths'] as List?) ?? const []) '$x'] ..copyMethodFile = _method(j['copyMethodFile']) ..copyConflict = const ['rename', 'overwrite', 'skip'].contains(j['copyConflict']) ? j['copyConflict'] as String : 'ask' diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 0cdf633..d3c390a 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -305,10 +305,12 @@ class _Row { /// 파일 목록 맨 위의 ".." (상위 폴더로) final bool isUp; - /// X-plore 비교 c: 트리 맨 위 항목 - [otherRoot] 다른 저장 장치 · 서버, [favorites] 즐겨찾기 묶음, [favorite] 즐겨찾기 하나 + /// X-plore 비교 c: 트리 맨 위 항목 - [otherRoot] 다른 저장 장치 · 서버, [favorites] 즐겨찾기 묶음, [favorite] 즐겨찾기 하나. + /// X-plore 숨기기: [hiddenToggle] 목록 맨 아래의 [숨긴 항목 보이기] (숨긴 수 [count]) final int kind; - static const otherRoot = 1, favorites = 2, favorite = 3; - const _Row(this.entry, this.depth, {this.isRoot = false, this.isUp = false, this.kind = 0}); + final int count; + static const otherRoot = 1, favorites = 2, favorite = 3, hiddenToggle = 4; + const _Row(this.entry, this.depth, {this.isRoot = false, this.isUp = false, this.kind = 0, this.count = 0}); } class _ExplorerPageState extends State { @@ -1162,11 +1164,17 @@ class _ExplorerPageState extends State { out.add(_Row(rootEntry, 0, isRoot: true)); // 76: 빠른 필터 - 지금 폴더 안만 거른다 (트리의 다른 곳은 그대로). // 이름이 맞지 않아도 안에 맞는 것이 보이는 폴더는 남긴다 (어디에 있는지 보이게) + var hidden = 0; bool walk(String dir, int depth) { if (!pane.expanded.contains(dir)) return false; var kept = false; for (final e in pane.cache[dir] ?? const []) { if (foldersOnly && !e.isDir) continue; + // X-plore 숨기기: 목록에서 숨긴 것 (그 안도) 은 [숨긴 항목 보이기] 를 켰을 때만 + if (_appHidden(e.path)) { + hidden++; + if (!_showAppHidden) continue; + } final at = out.length; out.add(_Row(e, depth)); // X-plore 비교 f: 펼친 압축 (ZIP · 7z · RAR) 도 안을 폴더처럼 @@ -1195,11 +1203,68 @@ class _ExplorerPageState extends State { } } } + // X-plore 숨기기: 펼친 폴더 안에 숨긴 것이 있으면 목록 맨 아래에 [숨긴 항목 n개 보이기] (X-plore 자리) + if (hidden > 0) out.add(_Row(FileEntry('\u0000hidden', isDir: false, modified: DateTime(0)), 0, kind: _Row.hiddenToggle, count: hidden)); return out; } + /// X-plore 숨기기: [path] 가 목록에서 숨긴 것인지 (앱 설정의 목록 - 파일은 그대로) + bool _appHidden(String path) { + final list = c.settings.explorerHiddenItems; + if (list.isEmpty) return false; + if (!identical(list, _hiddenFrom)) { + _hiddenFrom = list; + _hiddenKeys = {for (final x in list) _hideKey(x)}; + } + return _hiddenKeys.contains(_hideKey(path)); + } + + List? _hiddenFrom; + Set _hiddenKeys = const {}; + + /// 숨긴 목록에서 같은 경로를 찾는 열쇠 (Windows 는 대소문자 무시) + static String _hideKey(String x) { + if (isDav(x) || x.contains('://')) return x; + final n = p.normalize(x); + return Platform.isWindows ? n.toLowerCase() : n; + } + + /// 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() { + setState(() => _showAppHidden = !_showAppHidden); + for (final q in _panes) { + q.changed(); + } + } + + return Semantics( + button: true, + label: label, + excludeSemantics: true, + onTap: tap, + child: InkWell( + onTap: tap, + 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), + const SizedBox(width: 8), + Expanded( + child: Text(label, + maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: look.fontSize - 1, color: JjColors.textDim)), + ), + ]), + ), + ); + } + /// X-plore 비교 c: 트리 맨 위 항목 줄 (다른 저장 장치 · 서버 · 즐겨찾기) - 누르면 그곳으로, 즐겨찾기 묶음은 펼치기 · 접기 Widget _extraRow(_Pane pane, _Row row) { + if (row.kind == _Row.hiddenToggle) return _hiddenToggleRow(row); final look = _look; final path = row.entry.path; final volume = _volumes.firstWhere((v) => samePath(v.$1, path), orElse: () => (path, vBasename(path))).$2; @@ -1358,7 +1423,11 @@ class _ExplorerPageState extends State { quickFilterMatch(vBasename(e.path), pane.filter); /// 76: [dir] 에서 지금 보이는 것 (빠른 필터에 맞는 것) - List _shown(_Pane pane, String dir) => [for (final x in pane.cache[dir] ?? const []) if (_passes(pane, x)) x]; + /// X-plore 숨기기: 목록에서 숨긴 것은 [숨긴 항목 보이기] 를 켰을 때만 + List _shown(_Pane pane, String dir) => [ + for (final x in pane.cache[dir] ?? const []) + if (_passes(pane, x) && (_showAppHidden || !_appHidden(x.path))) x + ]; /// 74: 마지막으로 표시한 것부터 [to] 까지 (보이는 순서대로) 모두 표시 void _markRange(_Pane pane, FileEntry to) { @@ -1842,6 +1911,10 @@ class _ExplorerPageState extends State { ('toPdf', Icons.picture_as_pdf_outlined, images.length == 1 ? tr('PDF 로 만들기') : trf('그림 {0}장을 PDF 로 만들기', [images.length])), ('select', Icons.check_circle_outline, tr('선택')), + // X-plore 숨기기: 목록에서만 숨긴다 (파일은 그대로) · 숨긴 것을 보이고 있으면 풀기 + targets.every(_appHidden) + ? ('appUnhide', Icons.visibility_outlined, many ? trf('{0}개 숨기기 풀기', [targets.length]) : tr('숨기기 풀기')) + : ('appHide', Icons.visibility_off_outlined, many ? trf('{0}개 목록에서 숨기기', [targets.length]) : tr('목록에서 숨기기')), // 두 창이면 아이콘 줄의 복사 · 이동은 다른 창으로 - 담아 두었다가 원하는 폴더에 붙여넣기는 여기 if (dual) ('clipCopy', Icons.content_copy, tr('복사 (붙여넣기로)')), if (dual) ('clipMove', Icons.content_cut, tr('이동 (붙여넣기로)')), @@ -1937,6 +2010,30 @@ class _ExplorerPageState extends State { pane.focused = made; pane.changed(); } + case 'appHide': + await c.updateSettings((s) => s.explorerHiddenItems = [ + ...s.explorerHiddenItems, + for (final x in targets) + if (!_appHidden(x)) x, + ]); + for (final q in _panes) { + q.marked.removeAll(targets); + q.changed(); + } + if (mounted) setState(() {}); + _snack(_showAppHidden + ? trf('목록에서 숨겼습니다 ({0}개). 파일은 그대로입니다.', [targets.length]) + : trf('목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.', [targets.length])); + case 'appUnhide': + final keys = {for (final x in targets) _hideKey(x)}; + await c.updateSettings((s) => s.explorerHiddenItems = [ + for (final x in s.explorerHiddenItems) + if (!keys.contains(_hideKey(x))) x, + ]); + for (final q in _panes) { + q.changed(); + } + if (mounted) setState(() {}); case 'props': await showFolderProperties(context, e.path); case 'hash': @@ -2481,6 +2578,11 @@ class _ExplorerPageState extends State { final canTrash = trash != null && paths.every((x) => !isDav(x) && trash.trashDirFor(x) != null && !trash.isInTrash(x)); final canRecycle = (local && c.settings.recycleOnDelete && paths.every(hasRecycleBin)) || canTrash; var permanent = !canRecycle || shift; + // X-plore 숨기기: 지우는 폴더 안에 목록에서 숨긴 것이 있으면 알린다 (숨긴 것도 함께 지워진다) + final hiddenInside = [ + for (final h in c.settings.explorerHiddenItems) + if (paths.any((x) => !samePath(h, x) && isSameOrInside(h, x))) h + ].length; final ok = await showDialog( context: context, builder: (ctx) => StatefulBuilder( @@ -2507,6 +2609,12 @@ class _ExplorerPageState extends State { child: Text(trf('그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께', [paths.where(vIsDirSync).length]), style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), ), + if (hiddenInside > 0) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text(trf('지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다', [hiddenInside]), + style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)), + ), if (local && !canRecycle) Padding( padding: const EdgeInsets.only(top: 8), @@ -4498,9 +4606,12 @@ class _ExplorerPageState extends State { final entries = pane.cache[dir] ?? const []; final up = !samePath(dir, pane.root); final shown = _shown(pane, dir); + final hidden = entries.where((e) => _appHidden(e.path)).length; final rows = [ if (up) _Row(FileEntry(vDirname(dir), isDir: true, modified: DateTime(0)), 0, isUp: true), for (final e in shown) _Row(e, 0), + // X-plore 숨기기: 맨 아래 [숨긴 항목 n개 보이기] + if (hidden > 0) _Row(FileEntry('\u0000hidden', isDir: false, modified: DateTime(0)), 0, kind: _Row.hiddenToggle, count: hidden), ]; final look = _look; // 38: 창 높이를 알아야 안내 상자의 높이를 제한할 수 있다 @@ -4532,7 +4643,8 @@ class _ExplorerPageState extends State { controller: pane.listScroll, itemCount: rows.length, itemExtent: look.rowHeightOf(context), - itemBuilder: (_, k) => KeyedSubtree(key: _rowKey(rows[k]), child: _rowView(pane, rows[k], list: true)), + itemBuilder: (_, k) => KeyedSubtree( + key: _rowKey(rows[k]), child: rows[k].kind != 0 ? _extraRow(pane, rows[k]) : _rowView(pane, rows[k], list: true)), findChildIndexCallback: _rowIndexOf(rows), ), ), @@ -5195,6 +5307,8 @@ class _ExplorerPageState extends State { ]), ), ); + // X-plore 숨기기: 숨긴 것을 보이고 있을 때 숨긴 줄은 흐리게 + if (real && _showAppHidden && _appHidden(e.path)) line = Opacity(opacity: 0.5, child: line); if (!drag) return line; // 82: 폴더 줄 위에 놓으면 그 폴더 안으로 (놓을 수 있으면 강조) if (e.isDir) { diff --git a/test/explorer_test.dart b/test/explorer_test.dart index 7d05a16..d81c0a8 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -1561,6 +1561,57 @@ void main() { expect(AppSettings.fromJson({'explorerFolderSorts': 'broken'}).explorerFolderSorts, isEmpty, reason: '틀린 값은 비움'); }); + testWidgets('X-plore 숨기기: 길게 누르기 [목록에서 숨기기] → 목록에서만 빠짐 (파일 그대로) · 맨 아래 [숨긴 항목 보이기] · [숨기기 풀기]', (tester) async { + // 폴더 + 파일 목록 배치: 오른쪽 목록은 그 폴더의 것만이라 짧다 (트리는 Temp 폴더 줄이 많아 맨 아래가 멀다) + c.settings.explorerLayout = 'split'; + await open(tester); + final doc = p.join(left, 'doc.txt'); + await tester.longPress(find.text('doc.txt')); + await calm(tester); + await act(tester, () => tester.tap(find.text('목록에서 숨기기'))); + await settle(tester, () => find.text('doc.txt').evaluate().isEmpty); + expect(find.text('doc.txt'), findsNothing); + expect(File(doc).existsSync(), isTrue, reason: '파일은 바꾸지 않는다'); + expect(c.settings.explorerHiddenItems, [doc]); + // 목록 맨 아래에 [숨긴 항목 1개 보이기] + expect(find.text('숨긴 항목 1개 보이기'), findsOneWidget); + await idle(tester); + // 보이기 → 흐리게 보이고, 메뉴는 [숨기기 풀기] + await act(tester, () => tester.tap(find.text('숨긴 항목 1개 보이기'))); + await settle(tester, () => find.text('doc.txt').evaluate().isNotEmpty); + expect(find.text('숨긴 항목 1개 다시 숨기기'), findsOneWidget); + expect(find.ancestor(of: find.text('doc.txt'), matching: find.byType(Opacity)), findsOneWidget); + await tester.longPress(find.text('doc.txt')); + await calm(tester); + expect(find.text('목록에서 숨기기'), findsNothing); + await act(tester, () => tester.tap(find.text('숨기기 풀기'))); + await settle(tester, () => find.textContaining('숨긴 항목').evaluate().isEmpty); + expect(c.settings.explorerHiddenItems, isEmpty); + expect(find.text('doc.txt'), findsOneWidget); + expect(find.ancestor(of: find.text('doc.txt'), matching: find.byType(Opacity)), findsNothing); + }); + + 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); + expect(find.text('지우는 폴더 안에 목록에서 숨긴 항목 1개가 있습니다 - 함께 지워집니다'), findsOneWidget); + await act(tester, () => tester.tap(find.text('취소').last)); + await calm(tester); + expect(File(p.join(left, 'sub', 'inner.txt')).existsSync(), isTrue); + // 다른 파일만 지울 때는 알리지 않는다 + await tester.longPress(find.text('doc.txt')); + await calm(tester); + await act(tester, () => tester.tap(find.byTooltip('삭제').last)); + await calm(tester); + expect(find.textContaining('목록에서 숨긴 항목'), findsNothing); + await act(tester, () => tester.tap(find.text('취소').last)); + await calm(tester); + }); + testWidgets('80: 크기순 정렬이면 폴더 크기를 계산해 보이고, "안 함" 이어도 길게 누르기 [폴더 크기 계산]', (tester) async { File(p.join(left, 'sub', 'big.bin')).writeAsBytesSync(List.filled(3000, 1)); c.settings