diff --git a/assets/l10n/en.json b/assets/l10n/en.json index 37f23be..81f4bef 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -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)", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 5ee1839..db58b73 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -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}", "품질 올리기": "画質を上げる", "품질 올리기 (이 장)": "画質を上げる (このページ)", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index b81bae5..8469522 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -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}", "품질 올리기": "提升画质", "품질 올리기 (이 장)": "提升画质 (本页)", diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index d3c390a..5c30a27 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -415,6 +415,8 @@ class _ExplorerPageState extends State { 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 { @override void dispose() { + c.removeListener(_onSettings); _keys.dispose(); _sizes.dispose(); if (widget.rsync) { @@ -1229,26 +1232,67 @@ class _ExplorerPageState extends State { return Platform.isWindows ? n.toLowerCase() : n; } + /// X-plore 숨기기: 옮기거나 이름을 바꾼 것 ([moved] 원래 → 새 경로, 그 안의 것도) 은 숨긴 목록도 새 경로로, + /// 지운 것 ([gone], 그 안의 것도 - 실제로 없어진 것만) 은 목록에서 뺀다 (관리자 결정 10/18) + Future _followHidden({Iterable<(String, String)> moved = const [], Iterable gone = const []}) async { + final list = c.settings.explorerHiddenItems; + if (list.isEmpty || (moved.isEmpty && gone.isEmpty)) return; + final out = []; + 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; + void _toggleAppHidden() { + setState(() => _showAppHidden = !_showAppHidden); + for (final q in _panes) { + q.changed(); + } + } + + /// 설정 화면에서 숨긴 목록을 바꾸면 (풀기) 바로 목록에 보이게 + List? _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]); - void tap() { - setState(() => _showAppHidden = !_showAppHidden); - for (final q in _panes) { - q.changed(); - } - } - 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 { 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 { /// 바꾼 뒤: 목록을 다시 읽고, 표시 · 초점을 새 이름으로 Future _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 { // X-plore 비교 e: 앱 휴지통으로 보낸 것 · 옮기지 못한 것 (영구 삭제할지 묻는다) final trashed = []; final notTrashed = <(String, String)>[]; + // X-plore 숨기기: 휴지통에서 [되돌리기] 할 수 있는 것 (숨긴 목록에 남겨 되살리면 그대로 숨김) + final undoable = []; 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 { : 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 { } /// 52: 이 화면을 떠난 사이에 시작된 (또는 앞의 화면이 시작한) 작업을 다시 보이고, 끝나면 새로 읽고 막대를 내린다 + /// X-plore 숨기기: 옮긴 것은 숨긴 목록도 새 자리로 (원래 자리에 없고 새 자리에 있는 것만 - 같은 이름 바꾸기 · 실패는 그대로) + Future _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 { 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 { child: Text(_ready ? _displayPath(_pane.current) : '', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: JjColors.textDim)), ), - PopupMenuButton( + PopupMenuButton( 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 { 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])), + ), + ]), + ), ], ), ]), diff --git a/lib/ui/hidden_items_dialog.dart b/lib/ui/hidden_items_dialog.dart new file mode 100644 index 0000000..c47352e --- /dev/null +++ b/lib/ui/hidden_items_dialog.dart @@ -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 showHiddenItems(BuildContext context, AppController c) => showDialog( + context: context, + builder: (ctx) => StatefulBuilder(builder: (ctx, set) { + final list = c.settings.explorerHiddenItems; + Future unhide(List 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('닫기'))), + ], + ); + }), + ); diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 0d7f8ae..fbb1725 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -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 { 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('한 창일 때 복사 · 이동')), diff --git a/test/explorer_test.dart b/test/explorer_test.dart index d81c0a8..27bd387 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -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