diff --git a/assets/l10n/en.json b/assets/l10n/en.json index d0b6747..ed5dcda 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", @@ -1211,6 +1213,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?", @@ -1585,7 +1590,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", @@ -2376,6 +2384,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 c66e2be..abc1ea1 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} 個を削除して同期", @@ -1211,6 +1213,9 @@ "목록에서 동영상을 누르면 여기에 자세히 보입니다": "リストの動画をタップすると、ここに詳細が表示されます", "목록에서 빼기": "リストから外す", "목록에서 빼기 (파일은 그대로)": "リストから外す (ファイルはそのまま)", + "목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.": "リストから隠しました ({0} 件)。ファイルはそのままで、リストの一番下の [隠した項目を表示] で再び見られます。", + "목록에서 숨겼습니다 ({0}개). 파일은 그대로입니다.": "リストから隠しました ({0} 件)。ファイルはそのままです。", + "목록에서 숨기기": "リストから隠す", "목록에서 제거": "リストから削除", "목록에서 지우기 (파일은 그대로)": "リストから削除 (ファイルはそのまま)", "목록을 모두 비울까요?": "一覧をすべて空にしますか?", @@ -1585,7 +1590,10 @@ "수정한 날짜": "更新日時", "숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "非表示にしても、上の ⋮ メニューからペインの配置 · ボタン構成を変えられます。", "숨기기": "隠す", + "숨기기 풀기": "隠すのをやめる", "숨긴 항목 (. 으로 시작) 보이기": "隠し項目 (. で始まる) を表示", + "숨긴 항목 {0}개 다시 숨기기": "隠した項目 {0} 件を再び隠す", + "숨긴 항목 {0}개 보이기": "隠した項目 {0} 件を表示", "숨긴 항목 숨기기": "隠し項目を隠す", "숨김": "非表示", "숨은 다른 창으로": "隠れた別ペインへ", @@ -2376,6 +2384,7 @@ "지우기가 들어 있는 작업입니다": "削除を含む作業です", "지우기가 들어간 실행이라, 비교하지 못하면 실행할 수 없습니다. 원본 · 대상을 확인한 뒤 [다시 비교] 를 누르세요.": "削除を含む実行のため、比較できないと実行できません。元・対象を確認してから [もう一度比較] を押してください。", "지우기가 들어간 실행이라, 지울 목록이 나온 뒤에 실행할 수 있습니다.": "削除を含む実行のため、削除一覧が表示されてから実行できます。", + "지우는 폴더 안에 목록에서 숨긴 항목 {0}개가 있습니다 - 함께 지워집니다": "削除するフォルダーの中にリストから隠した項目が {0} 件あります - 一緒に削除されます", "지우면 휴지통으로": "削除するとごみ箱へ", "지우지 못한 파일: {0}": "削除できなかったファイル: {0}", "지우지 못함: {0}": "削除できません: {0}", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index 69a2e5e..a78d1cd 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} 个并同步", @@ -1211,6 +1213,9 @@ "목록에서 동영상을 누르면 여기에 자세히 보입니다": "点按列表中的视频即可在此查看详情", "목록에서 빼기": "从列表中移除", "목록에서 빼기 (파일은 그대로)": "从列表中移除 (保留文件)", + "목록에서 숨겼습니다 ({0}개). 파일은 그대로이고, 목록 맨 아래의 [숨긴 항목 보이기] 로 다시 볼 수 있습니다.": "已从列表中隐藏 ({0} 项)。文件本身未改变,可用列表底部的 [显示已隐藏项] 再次查看。", + "목록에서 숨겼습니다 ({0}개). 파일은 그대로입니다.": "已从列表中隐藏 ({0} 项)。文件本身未改变。", + "목록에서 숨기기": "从列表中隐藏", "목록에서 제거": "从列表中移除", "목록에서 지우기 (파일은 그대로)": "从列表中删除 (文件保留)", "목록을 모두 비울까요?": "清空整个列表吗?", @@ -1585,7 +1590,10 @@ "수정한 날짜": "修改日期", "숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "即使隐藏,也可以在顶部 ⋮ 菜单中更改窗格布局 · 按钮配置。", "숨기기": "隐藏", + "숨기기 풀기": "取消隐藏", "숨긴 항목 (. 으로 시작) 보이기": "显示隐藏项目 (以 . 开头)", + "숨긴 항목 {0}개 다시 숨기기": "再次隐藏 {0} 个已隐藏项", + "숨긴 항목 {0}개 보이기": "显示 {0} 个已隐藏项", "숨긴 항목 숨기기": "隐藏隐藏项目", "숨김": "隐藏", "숨은 다른 창으로": "到隐藏的另一窗格", @@ -2376,6 +2384,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 3c0d04e..107cff7 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -306,10 +306,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 { @@ -1163,11 +1165,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) 도 안을 폴더처럼 @@ -1196,11 +1204,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; @@ -1359,7 +1424,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) { @@ -1843,6 +1912,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('이동 (붙여넣기로)')), @@ -1938,6 +2011,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': @@ -2482,6 +2579,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( @@ -2508,6 +2610,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), @@ -4533,9 +4641,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: 창 높이를 알아야 안내 상자의 높이를 제한할 수 있다 @@ -4567,7 +4678,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), ), ), @@ -5230,6 +5342,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 8932d9f..bd14594 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -1580,6 +1580,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