E2: 탐색기 화면은 Selection · ClickPolicy 만 부름 + X-plore 식 폴더 표시 설정 뺌 (결정 5)
- explorer_page: 누르기 (한 번 · 두 번 · 길게 · 오른쪽) 는 ClickPolicy.decide 로만, 표시 (고르기 · 범위 · 뒤집기 ·
이름 바꾸기 · 지우기 따라가기 · Rsync 폴더 하나) 는 _Pane.sel (Selection) 로만. 화면 안의 규칙 코드 (_normalizeMarks ·
_toggleMark 의 나누기 · _pick 의 하나만) 를 지움
- Selection 에 rename · removeWhere · has, ClickPolicy 에 사용자 설정 옵션 tapOpens (바로 열기) · retapCollapses
(다시 누르면 접기) - 처음 값은 E3 에서 규칙대로
- 관리자 결정 5 (10/22): explorerFolderMarkRule ('xplore' 1 · 2 · 3번) 설정 · 설정 화면 항목 · 번역 4개 · 시험 1개를 뺌.
사용자가 요청하지 않고 넣은 것이라 규칙 6 하나로 합침. 저장된 값은 읽지 않음
- test/explorer_test.dart "선택 모드: 파일 누르기…" 기대값을 바꿈: 하나를 뺀 폴더를 다시 누르면 예전 "전체 다시 고름" →
관리자 결정 2 (10/22) "그 폴더와 안의 것 전체 취소" (규칙 6). 앱 동작이 결정대로 바뀐 것
- 약속 시험 25개 (지문 selection.dart d438fac0 · 시험 007d2a59) 같은 판 2회 통과
- 전체 단위 1,298 통과 · 건너뜀 17 · 실패 0 (third_party 연결 뒤), 점검 통합 시험 (1920 · 960 · 412) 2회 파일 결과가 1b31609 와 같음
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
split_pages
parent
88790819be
commit
797126b297
|
|
@ -427,8 +427,6 @@
|
|||
"X-plore 스타일: 옆으로 밀면 창 바꾸기": "X-plore style: swipe sideways to switch panes",
|
||||
"X-plore 스타일: 표시 동그라미 늘 보이기": "X-plore style: always show the mark circles",
|
||||
"X-plore 스타일에서 X-plore 처럼 한 트리에 보입니다 (위 줄 탭 · ★ 는 그대로)": "In the X-plore style they appear in one tree like X-plore (the tabs and ★ at the top stay)",
|
||||
"X-plore 식 (1 · 2 · 3번)": "X-plore way (1st · 2nd · 3rd)",
|
||||
"X-plore 식: 1번 = 폴더, 2번 = 안의 파일 전체, 3번 = 풀기": "X-plore way: 1st = the folder, 2nd = all files inside, 3rd = clear",
|
||||
"X-plore 처럼. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)": "Like X-plore. When off, folders collapse and expand only with the › arrow (the list doesn't move on a double tap)",
|
||||
"YouTube · 토렌트 다운로드 목록": "YouTube · torrent download list",
|
||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp, magnet · .torrent → aria2 (no seeding after completion)",
|
||||
|
|
@ -2966,9 +2964,7 @@
|
|||
"폴더를 자기 안으로 복사할 수 없습니다": "Cannot copy a folder into itself",
|
||||
"폴더를 자기 안으로 옮길 수 없습니다": "Cannot move a folder into itself",
|
||||
"폴더를 자기 자신 안으로 {0} 수 없습니다: {1}": "Cannot {0} a folder into itself: {1}",
|
||||
"폴더를 표시하면": "When marking a folder",
|
||||
"폴더만 lsync 로 옮길 수 있습니다.": "Only folders can be moved to lsync.",
|
||||
"폴더와 안의 것 전체 (기본)": "The folder and everything inside (default)",
|
||||
"폴란드어": "Polish",
|
||||
"표시": "Mark",
|
||||
"표시 (여러 개 고르기)": "Mark (select several)",
|
||||
|
|
|
|||
|
|
@ -427,8 +427,6 @@
|
|||
"X-plore 스타일: 옆으로 밀면 창 바꾸기": "X-plore スタイル: 横にスワイプでペイン切り替え",
|
||||
"X-plore 스타일: 표시 동그라미 늘 보이기": "X-plore スタイル: 選択の丸を常に表示",
|
||||
"X-plore 스타일에서 X-plore 처럼 한 트리에 보입니다 (위 줄 탭 · ★ 는 그대로)": "X-plore スタイルで X-plore のように 1 つのツリーに表示します (上部のタブ・★ はそのまま)",
|
||||
"X-plore 식 (1 · 2 · 3번)": "X-plore 式 (1・2・3 回目)",
|
||||
"X-plore 식: 1번 = 폴더, 2번 = 안의 파일 전체, 3번 = 풀기": "X-plore 式: 1 回目 = フォルダー、2 回目 = 中のファイル全部、3 回目 = 解除",
|
||||
"X-plore 처럼. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)": "X-plore のように。オフのときは › 矢印でのみ開閉します (2 回押しても一覧は動きません)",
|
||||
"YouTube · 토렌트 다운로드 목록": "YouTube・トレント ダウンロード一覧",
|
||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp、マグネット・.torrent → aria2(完了後のアップロードなし)",
|
||||
|
|
@ -2966,9 +2964,7 @@
|
|||
"폴더를 자기 안으로 복사할 수 없습니다": "フォルダーを自分自身の中にコピーできません",
|
||||
"폴더를 자기 안으로 옮길 수 없습니다": "フォルダーを自分自身の中に移動できません",
|
||||
"폴더를 자기 자신 안으로 {0} 수 없습니다: {1}": "フォルダーを自分自身の中へ{0}ことはできません: {1}",
|
||||
"폴더를 표시하면": "フォルダーにチェックしたとき",
|
||||
"폴더만 lsync 로 옮길 수 있습니다.": "lsync へ移せるのはフォルダーだけです。",
|
||||
"폴더와 안의 것 전체 (기본)": "フォルダーと中身すべて (既定)",
|
||||
"폴란드어": "ポーランド語",
|
||||
"표시": "選択",
|
||||
"표시 (여러 개 고르기)": "マーク (複数選択)",
|
||||
|
|
|
|||
|
|
@ -427,8 +427,6 @@
|
|||
"X-plore 스타일: 옆으로 밀면 창 바꾸기": "X-plore 样式: 左右滑动切换窗格",
|
||||
"X-plore 스타일: 표시 동그라미 늘 보이기": "X-plore 样式: 始终显示标记圆圈",
|
||||
"X-plore 스타일에서 X-plore 처럼 한 트리에 보입니다 (위 줄 탭 · ★ 는 그대로)": "在 X-plore 样式中像 X-plore 一样显示在一棵树中 (顶部的标签 · ★ 保持不变)",
|
||||
"X-plore 식 (1 · 2 · 3번)": "X-plore 方式 (第 1 · 2 · 3 次)",
|
||||
"X-plore 식: 1번 = 폴더, 2번 = 안의 파일 전체, 3번 = 풀기": "X-plore 方式: 第 1 次 = 文件夹,第 2 次 = 其中所有文件,第 3 次 = 取消",
|
||||
"X-plore 처럼. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)": "像 X-plore 一样。关闭时仅用 › 箭头折叠 · 展开 (双击时列表不会移动)",
|
||||
"YouTube · 토렌트 다운로드 목록": "YouTube · 种子下载列表",
|
||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp,磁力链接 · .torrent → aria2(完成后不做种)",
|
||||
|
|
@ -2966,9 +2964,7 @@
|
|||
"폴더를 자기 안으로 복사할 수 없습니다": "不能将文件夹复制到其自身内部",
|
||||
"폴더를 자기 안으로 옮길 수 없습니다": "不能将文件夹移动到其自身内部",
|
||||
"폴더를 자기 자신 안으로 {0} 수 없습니다: {1}": "不能将文件夹{0}到其自身内部: {1}",
|
||||
"폴더를 표시하면": "标记文件夹时",
|
||||
"폴더만 lsync 로 옮길 수 있습니다.": "只有文件夹可以移到 lsync。",
|
||||
"폴더와 안의 것 전체 (기본)": "文件夹及其所有内容 (默认)",
|
||||
"폴란드어": "波兰语",
|
||||
"표시": "标记",
|
||||
"표시 (여러 개 고르기)": "标记 (多选)",
|
||||
|
|
|
|||
|
|
@ -441,9 +441,6 @@ class AppSettings {
|
|||
/// X-plore 비교 ⚖ (관리자 결정 10/18): X-plore 스타일에서 줄 끝 표시 동그라미를 늘 보이기 (처음 값 켬). 다른 스타일은 선택 모드에서만 (10/5)
|
||||
bool explorerAlwaysMarks = true;
|
||||
|
||||
/// X-plore 비교 ⚖: 폴더 표시 규칙 - 'tree' 자신 + 안의 것 전체 (지금, 처음 값 - 고르는 범위가 바뀌면 지우기 · 이동 대상이 바뀌어 그대로) /
|
||||
/// 'xplore' 1번 = 폴더 자신, 2번 = 안의 파일 전체, 3번 = 풀기
|
||||
String explorerFolderMarkRule = 'tree';
|
||||
|
||||
/// X-plore 비교 ⚖: X-plore 스타일 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (설정으로만, 처음 값 끔 - 10/8 사용자 결정 "화면 가운데를 밀면 화면 이동" 을 지킴, 관리자 판단 10/18)
|
||||
bool explorerSwipePanes = false;
|
||||
|
|
@ -883,7 +880,6 @@ class AppSettings {
|
|||
'batchRenameLast': batchRenameLast,
|
||||
'explorerRootsInTree': explorerRootsInTree,
|
||||
'explorerAlwaysMarks': explorerAlwaysMarks,
|
||||
'explorerFolderMarkRule': explorerFolderMarkRule,
|
||||
'explorerSwipePanes': explorerSwipePanes,
|
||||
'androidTrashDays': androidTrashDays,
|
||||
'explorerClick': explorerClick,
|
||||
|
|
@ -1129,7 +1125,6 @@ class AppSettings {
|
|||
..batchRenameLast = j['batchRenameLast'] is Map ? Map<String, dynamic>.from(j['batchRenameLast'] as Map) : {}
|
||||
..explorerRootsInTree = j['explorerRootsInTree'] != false
|
||||
..explorerAlwaysMarks = j['explorerAlwaysMarks'] != false
|
||||
..explorerFolderMarkRule = j['explorerFolderMarkRule'] == 'xplore' ? 'xplore' : 'tree'
|
||||
..explorerSwipePanes = j['explorerSwipePanes'] == true
|
||||
..androidTrashDays = switch (j['androidTrashDays']) { final int d when d >= 0 && d <= 365 => d, _ => 14 }
|
||||
..explorerLayout = const ['single', 'split', 'auto'].contains(j['explorerLayout'])
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import 'vfs.dart' show vDirname;
|
|||
/// 약속
|
||||
/// - [ClickPolicy.decide]: 누르기 (한 번 · 두 번 · 길게 · 오른쪽) · 항목 종류 · 선택 모드 · 펼침 → 할 일 하나 ([ClickAction]).
|
||||
/// 모르는 조합은 [ClickAction.none] (아무것도 하지 않음). 화면 · 기기 · 시각을 모른다 (같은 입력 = 같은 결과).
|
||||
/// - [Selection]: 창 하나의 표시 (고른 경로 집합).
|
||||
/// - [Selection]: 창 하나의 표시 (고른 경로 집합). 이름 바꾸기 · 지우기를 따라간다 ([rename] · [forget] · [removeWhere]).
|
||||
/// · 파일: 누르면 고름, 다시 누르면 취소 (규칙 5)
|
||||
/// · 폴더: 누르면 자기 + 안의 것 전체, 다시 누르면 자기와 안의 것 전체 취소 (규칙 6)
|
||||
/// · 고른 폴더 안의 항목 하나를 누르면 그것만 빠짐 (나머지는 고른 채)
|
||||
|
|
@ -62,23 +62,31 @@ class ClickPolicy {
|
|||
/// [selectMode] 선택 모드 (가운데 [선택]) · [expanded] 그 폴더가 펼쳐져 있는지 ·
|
||||
/// [list] 트리가 아니라 목록 (폴더 + 목록 배치의 오른쪽) 의 줄인지 ·
|
||||
/// [repeat] 같은 줄을 두 번 누르기 시간 안에 다시 누른 것인지 (폴더는 두 번 누르기를 기다리지 않으므로,
|
||||
/// 두 번째 누르기로 방금 펼친 것을 접지 않게)
|
||||
/// 두 번째 누르기로 방금 펼친 것을 접지 않게).
|
||||
/// 사용자 설정 (처음 값은 규칙대로):
|
||||
/// [tapOpens] 파일 한 번 누르기 = 바로 실행 (설정 "바로 열기", 이때 두 번 누르기는 받지 않음) ·
|
||||
/// [retapCollapses] 펼친 폴더를 다시 누르면 접기 (끄면 [ClickAction.expand] - 화면은 펼친 폴더를 지금 폴더로만)
|
||||
static ClickAction decide(Press press, ItemKind kind,
|
||||
{required bool selectMode, bool expanded = false, bool list = false, bool repeat = false}) {
|
||||
{required bool selectMode,
|
||||
bool expanded = false,
|
||||
bool list = false,
|
||||
bool repeat = false,
|
||||
bool tapOpens = false,
|
||||
bool retapCollapses = true}) {
|
||||
final real = kind == ItemKind.file || kind == ItemKind.folder;
|
||||
switch (press) {
|
||||
case Press.longPress:
|
||||
case Press.secondary:
|
||||
return real ? ClickAction.menu : ClickAction.none;
|
||||
case Press.doubleTap:
|
||||
// 두 번 = 실행 (파일만, 선택 모드 밖). 폴더 · 선택 모드는 한 번 누르기로 이미 처리됨
|
||||
return kind == ItemKind.file && !selectMode ? ClickAction.open : ClickAction.none;
|
||||
// 두 번 = 실행 (파일만, 선택 모드 밖). 폴더 · 선택 모드 · 바로 열기는 한 번 누르기로 이미 처리됨
|
||||
return kind == ItemKind.file && !selectMode && !tapOpens ? ClickAction.open : ClickAction.none;
|
||||
case Press.tap:
|
||||
if (kind == ItemKind.up) return ClickAction.open;
|
||||
if (selectMode && real) return ClickAction.toggleMark;
|
||||
if (kind == ItemKind.file) return ClickAction.focus;
|
||||
if (kind == ItemKind.file) return tapOpens ? ClickAction.open : ClickAction.focus;
|
||||
if (list && kind == ItemKind.folder) return ClickAction.open;
|
||||
if (!expanded) return ClickAction.expand;
|
||||
if (!expanded || !retapCollapses) return ClickAction.expand;
|
||||
return repeat ? ClickAction.none : ClickAction.collapse;
|
||||
}
|
||||
}
|
||||
|
|
@ -199,6 +207,26 @@ class Selection {
|
|||
_split.removeWhere((s) => isSameOrInside(s, path));
|
||||
}
|
||||
|
||||
/// [test] 에 맞는 표시를 뺀다 (예: 다른 곳에서 지워져 없는 것)
|
||||
void removeWhere(bool Function(String path) test) {
|
||||
_marked.removeWhere(test);
|
||||
_split.removeWhere(test);
|
||||
}
|
||||
|
||||
/// 이름이 바뀐 것: 표시돼 있었으면 새 이름으로 (안의 표시도 새 경로로)
|
||||
void rename(String from, String to) {
|
||||
String moved(String x) => samePath(x, from) ? to : to + x.substring(from.length);
|
||||
for (final list in [_marked, _split]) {
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
if (isSameOrInside(list[i], from)) list[i] = moved(list[i]);
|
||||
}
|
||||
}
|
||||
if (anchor != null && isSameOrInside(anchor!, from)) anchor = moved(anchor!);
|
||||
}
|
||||
|
||||
/// [path] 자신이 표시돼 있는지 (위 폴더로 함께 골라진 것은 아님)
|
||||
bool has(String path) => _marked.any((m) => samePath(m, path));
|
||||
|
||||
void clear() {
|
||||
_marked.clear();
|
||||
_split.clear();
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import '../app/copy_center.dart';
|
|||
import '../app/live_sync.dart';
|
||||
import '../app/settings.dart' show CopyTask, FolderBookmark;
|
||||
import '../app/transfer_job.dart';
|
||||
import '../core/selection.dart';
|
||||
import '../core/secret_gate.dart';
|
||||
import '../core/sync_tools.dart';
|
||||
import '../core/file_ops.dart';
|
||||
|
|
@ -293,10 +294,8 @@ class _Pane extends ChangeNotifier {
|
|||
|
||||
/// l: 처음 보는 (바뀐) FTPS 자체 서명 인증서 - 폴더마다
|
||||
final certErrors = <String, FtpCertException>{};
|
||||
final marked = <String>{};
|
||||
|
||||
/// 74: 범위 고르기의 시작 (마지막으로 표시한 것)
|
||||
String? anchor;
|
||||
/// 표시 (고른 것) · 범위 고르기의 시작 - 규칙은 [Selection] 한 곳 (E1)
|
||||
final Selection sel;
|
||||
|
||||
/// X-plore 비교 c: 트리의 "즐겨찾기" 묶음을 펼쳤는지
|
||||
bool favoritesOpen = false;
|
||||
|
|
@ -316,7 +315,9 @@ class _Pane extends ChangeNotifier {
|
|||
/// 202: 창 위 저장 장치 탭 줄 (넘치면 옆으로 - 휠 · 끌기)
|
||||
final tabScroll = ScrollController();
|
||||
|
||||
_Pane(this.root) : _current = root;
|
||||
_Pane(this.root, {bool rsync = false})
|
||||
: _current = root,
|
||||
sel = Selection(rsync: rsync);
|
||||
|
||||
/// 화면을 닫은 뒤 끝난 파일 작업 · 읽기가 알리지 않게
|
||||
bool _disposed = false;
|
||||
|
|
@ -408,7 +409,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// X-plore 비교 5-2: 루트 (su, 읽기 전용) - 환경 설정에서 켰을 때 (Android)
|
||||
if (c.settings.rootEnabled && rootSupported()) (rootHome, tr('루트 (읽기 전용)')),
|
||||
];
|
||||
late final List<_Pane> _panes = [_Pane(''), _Pane('')];
|
||||
late final List<_Pane> _panes = [_Pane('', rsync: widget.rsync), _Pane('', rsync: widget.rsync)];
|
||||
int _active = 0;
|
||||
bool _ready = false;
|
||||
|
||||
|
|
@ -1005,7 +1006,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (!_ready) return;
|
||||
for (final pane in _panes) {
|
||||
if (isDav(pane.root) && !DavPath.parse(pane.root).serverKnown) {
|
||||
pane.marked.clear();
|
||||
pane.sel.clear();
|
||||
pane.cache.removeWhere((k, _) => isDav(k));
|
||||
pane.expanded.removeWhere(isDav);
|
||||
pane.root = _local.first.$1; // 다시 그릴 때 또 하지 않게
|
||||
|
|
@ -1593,7 +1594,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
_selecting = on;
|
||||
if (!on) {
|
||||
for (final x in _panes) {
|
||||
x.marked.clear();
|
||||
x.sel.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -1609,57 +1610,96 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Future<void> _onTap(_Pane pane, _Row row, {bool list = false}) async {
|
||||
final e = row.entry;
|
||||
setState(() => _active = _panes.indexOf(pane));
|
||||
if (row.isUp) {
|
||||
await _goTo(pane, e.path);
|
||||
return;
|
||||
}
|
||||
if (widget.rsync) {
|
||||
if (widget.rsync && !row.isUp) {
|
||||
await _pick(pane, e);
|
||||
return;
|
||||
}
|
||||
final real = !row.isRoot && !row.isUp;
|
||||
// 74: Shift + 누르기 = 마지막으로 표시한 것부터 여기까지 (PC)
|
||||
if (!row.isRoot && HardwareKeyboard.instance.isShiftPressed && _canRange(pane, e)) {
|
||||
if (real && HardwareKeyboard.instance.isShiftPressed && _canRange(pane, e)) {
|
||||
if (!_selecting) setState(() => _selecting = true);
|
||||
_markRange(pane, e);
|
||||
return;
|
||||
}
|
||||
if (!row.isRoot && (_selecting || HardwareKeyboard.instance.isControlPressed)) {
|
||||
// PC: Ctrl + 누르기 = 선택 모드로 고르기
|
||||
if (real && HardwareKeyboard.instance.isControlPressed) {
|
||||
if (!_selecting) setState(() => _selecting = true);
|
||||
await _toggleMark(pane, e);
|
||||
return;
|
||||
}
|
||||
// X-plore 비교 d: 펼쳐 둔 지금 폴더를 다시 누르면 접기 (설정). 두 번 누르기의 두 번째 (짧은 사이) 는 접지 않는다
|
||||
if (e.isDir && !list && c.settings.explorerRetapCollapse) {
|
||||
final now = DateTime.now();
|
||||
final last = _lastFolderTap;
|
||||
_lastFolderTap = (e.path, now);
|
||||
final again = last != null && samePath(last.$1, e.path) && now.difference(last.$2) < kDoubleTapTimeout * 2;
|
||||
if (!again && pane.expanded.contains(e.path) && samePath(pane.current, e.path)) {
|
||||
await _toggle(pane, e);
|
||||
// 폴더는 두 번 누르기를 기다리지 않는다 - 짧은 사이의 두 번째 누르기로 방금 펼친 것을 접지 않게
|
||||
final now = DateTime.now();
|
||||
final last = _lastFolderTap;
|
||||
_lastFolderTap = (e.path, now);
|
||||
final repeat = last != null && samePath(last.$1, e.path) && now.difference(last.$2) < kDoubleTapTimeout * 2;
|
||||
final act = ClickPolicy.decide(Press.tap, _kindOf(row),
|
||||
selectMode: _selecting,
|
||||
expanded: pane.expanded.contains(e.path),
|
||||
list: list,
|
||||
repeat: repeat,
|
||||
tapOpens: !_selectMode,
|
||||
retapCollapses: c.settings.explorerRetapCollapse);
|
||||
await _act(pane, row, act, list: list);
|
||||
}
|
||||
|
||||
/// 줄의 종류 ([ClickPolicy] 에 넘김)
|
||||
ItemKind _kindOf(_Row row) => row.isUp
|
||||
? ItemKind.up
|
||||
: row.isRoot
|
||||
? ItemKind.root
|
||||
: row.entry.isDir
|
||||
? ItemKind.folder
|
||||
: ItemKind.file;
|
||||
|
||||
/// [ClickPolicy] 가 정한 일을 한다 (화면 쪽 동작만 - 규칙은 [ClickPolicy] · [Selection])
|
||||
Future<void> _act(_Pane pane, _Row row, ClickAction act, {bool list = false}) async {
|
||||
final e = row.entry;
|
||||
switch (act) {
|
||||
case ClickAction.none:
|
||||
case ClickAction.menu:
|
||||
return;
|
||||
}
|
||||
case ClickAction.focus:
|
||||
pane.focused = e.path;
|
||||
if (!list) pane.current = vDirname(e.path);
|
||||
pane.changed();
|
||||
case ClickAction.open:
|
||||
await _onOpen(pane, row, list: list);
|
||||
case ClickAction.collapse:
|
||||
await _toggle(pane, e);
|
||||
case ClickAction.expand:
|
||||
if (!pane.expanded.contains(e.path)) {
|
||||
await _toggle(pane, e);
|
||||
} else if (!samePath(pane.current, e.path)) {
|
||||
// 펼쳐 둔 폴더 (다시 눌러 접기를 끈 설정): 그 폴더를 지금 폴더로
|
||||
pane
|
||||
..current = e.path
|
||||
..focused = e.path
|
||||
..changed();
|
||||
_remember(pane, e.path);
|
||||
}
|
||||
case ClickAction.toggleMark:
|
||||
if (!_selecting) setState(() => _selecting = true);
|
||||
await _toggleMark(pane, e);
|
||||
}
|
||||
if (e.isDir || !_selectMode) return _onOpen(pane, row, list: list);
|
||||
pane.focused = e.path;
|
||||
if (!list) pane.current = vDirname(e.path);
|
||||
pane.changed();
|
||||
}
|
||||
|
||||
/// 표시되었는지: 자기 자신, 또는 위 폴더가 표시되었으면 (폴더를 고르면 안의 것도 모두 고른 것).
|
||||
/// Rsync 화면은 고른 그 폴더만.
|
||||
bool _isMarked(_Pane pane, String path) =>
|
||||
pane.marked.any((m) => widget.rsync ? samePath(path, m) : isSameOrInside(path, m));
|
||||
bool _isMarked(_Pane pane, String path) => pane.sel.isMarked(path);
|
||||
|
||||
/// Rsync 화면: 창마다 폴더 하나만 고른다 (다시 누르면 취소). 고르면 펼쳐서 안의 폴더가 보이게.
|
||||
/// [Selection] 이 폴더를 나눌 때 안의 경로 (읽어 둔 목록, 없으면 읽음)
|
||||
Future<List<String>> _childPaths(_Pane pane, String dir) async {
|
||||
await _load(pane, dir);
|
||||
return [for (final x in pane.cache[dir] ?? const <FileEntry>[]) x.path];
|
||||
}
|
||||
|
||||
/// Rsync 화면: 창마다 폴더 하나만 고른다 (다시 누르면 취소, 규칙 8). 고르면 펼쳐서 안의 폴더가 보이게.
|
||||
Future<void> _pick(_Pane pane, FileEntry e) async {
|
||||
if (!e.isDir) return;
|
||||
if (pane.marked.length == 1 && samePath(pane.marked.first, e.path)) {
|
||||
setState(pane.marked.clear);
|
||||
return;
|
||||
}
|
||||
setState(() => pane.marked
|
||||
..clear()
|
||||
..add(e.path));
|
||||
await pane.sel.toggle(e.path, isDir: true, list: (d) => _childPaths(pane, d));
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
if (!pane.sel.has(e.path)) return;
|
||||
if (!pane.expanded.contains(e.path)) {
|
||||
await _toggle(pane, e);
|
||||
} else {
|
||||
|
|
@ -1672,7 +1712,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
|
||||
/// 74: 범위 고르기를 할 수 있는지 - 마지막으로 표시한 것이 같은 폴더에 있고 다른 항목
|
||||
bool _canRange(_Pane pane, FileEntry e) {
|
||||
final a = pane.anchor;
|
||||
final a = pane.sel.anchor;
|
||||
return a != null && !samePath(a, e.path) && samePath(vDirname(a), vDirname(e.path)) && _isMarked(pane, a);
|
||||
}
|
||||
|
||||
|
|
@ -1691,110 +1731,41 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
];
|
||||
|
||||
/// 74: 마지막으로 표시한 것부터 [to] 까지 (보이는 순서대로) 모두 표시
|
||||
void _markRange(_Pane pane, FileEntry to) {
|
||||
final list = _shown(pane, vDirname(to.path));
|
||||
final a = list.indexWhere((x) => samePath(x.path, pane.anchor!));
|
||||
final b = list.indexWhere((x) => samePath(x.path, to.path));
|
||||
if (a < 0 || b < 0) return;
|
||||
setState(() {
|
||||
for (var i = a < b ? a : b; i <= (a < b ? b : a); i++) {
|
||||
pane.marked.add(list[i].path);
|
||||
}
|
||||
_normalizeMarks(pane);
|
||||
pane.anchor = to.path;
|
||||
pane.focused = to.path;
|
||||
});
|
||||
}
|
||||
void _markRange(_Pane pane, FileEntry to) => setState(() {
|
||||
pane.sel.range([for (final x in _shown(pane, vDirname(to.path))) x.path], to.path);
|
||||
pane.focused = to.path;
|
||||
});
|
||||
|
||||
/// 74: 고르기 도구 - 이 폴더 ([dir]) 의 항목에서
|
||||
void _invertMarks(_Pane pane, String dir) => setState(() {
|
||||
for (final x in _shown(pane, dir)) {
|
||||
if (!pane.marked.remove(x.path)) pane.marked.add(x.path);
|
||||
}
|
||||
_normalizeMarks(pane);
|
||||
});
|
||||
void _invertMarks(_Pane pane, String dir) =>
|
||||
setState(() => pane.sel.invert([for (final x in _shown(pane, dir)) x.path]));
|
||||
|
||||
void _markWhere(_Pane pane, String dir, bool Function(FileEntry e) test) => setState(() {
|
||||
pane.marked.addAll([for (final x in _shown(pane, dir)) if (test(x)) x.path]);
|
||||
_normalizeMarks(pane);
|
||||
});
|
||||
void _markWhere(_Pane pane, String dir, bool Function(FileEntry e) test) =>
|
||||
setState(() => pane.sel.addAll([for (final x in _shown(pane, dir)) if (test(x)) x.path]));
|
||||
|
||||
Future<void> _markPattern(_Pane pane, String dir) async {
|
||||
final pattern = await showDialog<String>(context: context, builder: (_) => const _PatternDialog());
|
||||
if (pattern == null || pattern.trim().isEmpty) return;
|
||||
final re = globToRegExp(pattern);
|
||||
final before = pane.marked.length;
|
||||
final before = pane.sel.marked.length;
|
||||
_markWhere(pane, dir, (x) => re.hasMatch(vBasename(x.path)));
|
||||
if (pane.marked.length == before) _snack(trf('"{0}" 에 맞는 항목이 없습니다', [pattern.trim()]));
|
||||
if (pane.sel.marked.length == before) _snack(trf('"{0}" 에 맞는 항목이 없습니다', [pattern.trim()]));
|
||||
}
|
||||
|
||||
/// 다른 표시 안에 든 표시는 뺀다 (폴더 하나로 안의 것까지 고른 것이므로)
|
||||
void _normalizeMarks(_Pane pane) {
|
||||
final all = pane.marked.toList();
|
||||
pane.marked.removeWhere((m) => all.any((o) => !identical(o, m) && !samePath(o, m) && isSameOrInside(m, o)));
|
||||
}
|
||||
|
||||
/// 선택 모드에서 누르기. 폴더는 자기 자신과 안의 파일 · 폴더 전체가 한 묶음이다.
|
||||
/// - 고르지 않은 것을 누르면 고른다 (폴더는 펼쳐서 안의 것도 모두 고른 모양으로 보여 줌)
|
||||
/// - 고른 것을 다시 누르면 자기 자신과 안의 것을 모두 푼다.
|
||||
/// 위 폴더를 골라서 함께 골라진 것이면 그것만 빼고, 같은 폴더의 나머지는 고른 채로 둔다.
|
||||
/// 선택 모드에서 누르기 (규칙 5 · 6 은 [Selection.toggle]). 고른 폴더는 펼쳐서 안의 것도 고른 모양으로 보여 준다.
|
||||
Future<void> _toggleMark(_Pane pane, FileEntry e) async {
|
||||
pane.focused = e.path;
|
||||
// X-plore 비교 ⚖ (설정 'xplore'): 폴더 1번 = 폴더 자신 · 2번 = 안의 파일 전체 · 3번 = 풀기
|
||||
if (e.isDir && c.settings.explorerFolderMarkRule == 'xplore' && !widget.rsync) {
|
||||
final self = pane.marked.any((m) => samePath(m, e.path));
|
||||
if (self) {
|
||||
await _load(pane, e.path);
|
||||
final kids = [for (final x in pane.cache[e.path] ?? const <FileEntry>[]) if (!x.isDir) x.path];
|
||||
setState(() {
|
||||
pane.marked.removeWhere((m) => samePath(m, e.path));
|
||||
pane.marked.addAll(kids);
|
||||
pane.expanded.add(e.path);
|
||||
});
|
||||
return;
|
||||
}
|
||||
final kids = [for (final x in pane.cache[e.path] ?? const <FileEntry>[]) if (!x.isDir) x.path];
|
||||
final inside = kids.isNotEmpty && kids.every((k) => pane.marked.any((m) => samePath(m, k)));
|
||||
if (inside) {
|
||||
setState(() => pane.marked.removeWhere((m) => kids.any((k) => samePath(k, m))));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await pane.sel.toggle(e.path, isDir: e.isDir, list: (d) => _childPaths(pane, d));
|
||||
} on FileSystemException catch (err) {
|
||||
_snack(explainFileError('$err', dav: isDav(e.path)).$1);
|
||||
}
|
||||
if (!_isMarked(pane, e.path)) {
|
||||
setState(() {
|
||||
pane.marked.removeWhere((m) => isSameOrInside(m, e.path)); // 안에 따로 고른 것은 이 폴더 하나로
|
||||
pane.marked.add(e.path);
|
||||
pane.anchor = e.path;
|
||||
});
|
||||
if (e.isDir) {
|
||||
await _load(pane, e.path);
|
||||
setState(() => pane.expanded.add(e.path));
|
||||
}
|
||||
return;
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
if (e.isDir && pane.sel.has(e.path) && !pane.expanded.contains(e.path)) {
|
||||
await _load(pane, e.path);
|
||||
if (mounted) setState(() => pane.expanded.add(e.path));
|
||||
}
|
||||
final owner = pane.marked.firstWhere((m) => isSameOrInside(e.path, m));
|
||||
setState(() => pane.marked.removeWhere((m) => isSameOrInside(m, e.path)));
|
||||
if (samePath(owner, e.path)) return;
|
||||
// 위 폴더 [owner] 를 골라 둔 상태: owner 표시를 풀고, e 까지 내려가는 길의 다른 항목은 고른 채로
|
||||
final keep = <String>[];
|
||||
var dir = owner;
|
||||
while (!samePath(dir, e.path)) {
|
||||
await _load(pane, dir);
|
||||
String? next;
|
||||
for (final x in pane.cache[dir] ?? const <FileEntry>[]) {
|
||||
if (isSameOrInside(e.path, x.path)) {
|
||||
next = x.path;
|
||||
} else {
|
||||
keep.add(x.path);
|
||||
}
|
||||
}
|
||||
if (next == null) break;
|
||||
dir = next;
|
||||
}
|
||||
setState(() {
|
||||
pane.marked.remove(owner);
|
||||
pane.marked.addAll(keep);
|
||||
});
|
||||
}
|
||||
|
||||
/// 열기 (파일: 선택 설정이면 두 번 누르기, 바로 실행이면 한 번 누르기 / 폴더: 한 번 누르기):
|
||||
|
|
@ -2171,14 +2142,14 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
final dav = isDav(e.path);
|
||||
// 그림 → PDF: 표시한 것 중 그림 (누른 것이 표시에 없으면 누른 것만)
|
||||
final viewer = c.settings.components.contains('viewer');
|
||||
final picked = pane.marked.contains(e.path) ? pane.marked.toList() : [e.path];
|
||||
final picked = pane.sel.has(e.path) ? pane.sel.marked : [e.path];
|
||||
final images = [for (final x in picked) if (c.settings.imageExts.contains(extOf(x))) x];
|
||||
final ext = extOf(e.path);
|
||||
final viewable = viewer && !e.isDir &&
|
||||
(c.settings.imageExts.contains(ext) || ext == 'pdf' || zipExtensions.contains(ext));
|
||||
final dual = _dual;
|
||||
// 77: 누른 것이 표시한 것 중 하나면 표시한 것 모두에 (복사 · 이동 · 삭제 · 공유 · 경로 복사)
|
||||
final targets = _isMarked(pane, e.path) && pane.marked.length > 1 ? pane.marked.toList() : [e.path];
|
||||
final targets = _isMarked(pane, e.path) && pane.sel.marked.length > 1 ? pane.sel.marked : [e.path];
|
||||
final many = targets.length > 1;
|
||||
// 데이터 검토 (10/18): 펼친 압축 안의 것은 실제 파일이 아니다 - 그것으로 새 파일을 만들거나 내놓는 항목 (ZIP 묶기 · Wi-Fi 공유 · PDF) 은 숨긴다
|
||||
final inArchive = targets.any(_insideArchive) || _insideArchive(e.path);
|
||||
|
|
@ -2342,7 +2313,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
case 'wifiShare':
|
||||
await _wifiShare(e.path);
|
||||
case 'zip':
|
||||
final items = outermostPaths(_isMarked(pane, e.path) && pane.marked.length > 1 ? pane.marked.toList() : [e.path]);
|
||||
final items = outermostPaths(_isMarked(pane, e.path) && pane.sel.marked.length > 1 ? pane.sel.marked : [e.path]);
|
||||
final other = _dual ? _panes[1 - _panes.indexOf(pane)].current : null;
|
||||
final made = await createZipFlow(context, c, items, dir: vDirname(items.first), otherDir: other);
|
||||
if (made != null && mounted) {
|
||||
|
|
@ -2358,7 +2329,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (!_appHidden(x)) x,
|
||||
]);
|
||||
for (final q in _panes) {
|
||||
q.marked.removeAll(targets);
|
||||
targets.forEach(q.sel.forget);
|
||||
q.changed();
|
||||
}
|
||||
if (mounted) setState(() {});
|
||||
|
|
@ -2399,19 +2370,19 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
));
|
||||
}
|
||||
case 'rename':
|
||||
if (_isMarked(pane, e.path) && pane.marked.length > 1) {
|
||||
await _batchRename(pane, pane.marked.toList());
|
||||
if (_isMarked(pane, e.path) && pane.sel.marked.length > 1) {
|
||||
await _batchRename(pane, pane.sel.marked);
|
||||
} else {
|
||||
await _rename(pane, e.path);
|
||||
}
|
||||
case 'copy':
|
||||
await _transfer(pane, _isMarked(pane, e.path) ? pane.marked.toList() : [e.path], move: false);
|
||||
await _transfer(pane, _isMarked(pane, e.path) ? pane.sel.marked : [e.path], move: false);
|
||||
case 'move':
|
||||
await _transfer(pane, _isMarked(pane, e.path) ? pane.marked.toList() : [e.path], move: true);
|
||||
await _transfer(pane, _isMarked(pane, e.path) ? pane.sel.marked : [e.path], move: true);
|
||||
case 'clipCopy':
|
||||
_toClipboard(_isMarked(pane, e.path) ? pane.marked.toList() : [e.path], move: false);
|
||||
_toClipboard(_isMarked(pane, e.path) ? pane.sel.marked : [e.path], move: false);
|
||||
case 'clipMove':
|
||||
_toClipboard(_isMarked(pane, e.path) ? pane.marked.toList() : [e.path], move: true);
|
||||
_toClipboard(_isMarked(pane, e.path) ? pane.sel.marked : [e.path], move: true);
|
||||
case 'paste':
|
||||
await _paste(pane, e.isDir ? e.path : vDirname(e.path));
|
||||
case 'delete':
|
||||
|
|
@ -2673,7 +2644,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
|
||||
/// 기능의 대상: 표시한 항목, 없으면 마지막으로 누른 항목 (저장 장치 맨 위는 빼고)
|
||||
List<String> _targets(_Pane pane) {
|
||||
if (pane.marked.isNotEmpty) return pane.marked.toList();
|
||||
if (!pane.sel.isEmpty) return pane.sel.marked;
|
||||
final f = pane.focused;
|
||||
if (f == null || samePath(f, pane.root)) return const [];
|
||||
return [f];
|
||||
|
|
@ -2762,7 +2733,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 181: 강조한 것이 mp3 등 소리 파일이어도 그것을 (예전에는 동영상이 아니라고 빼고 폴더의 동영상을 틀었다)
|
||||
final videos = t.where((x) => !vIsDirSync(x) && (isVideoFile(x) || isAudioFile(x))).toList();
|
||||
if (videos.isNotEmpty) {
|
||||
await _play(videos, keepOrder: pane.marked.isNotEmpty);
|
||||
await _play(videos, keepOrder: !pane.sel.isEmpty);
|
||||
} else {
|
||||
await _playFolder(t.length == 1 && vIsDirSync(t.first) ? t.first : pane.current);
|
||||
}
|
||||
|
|
@ -2783,9 +2754,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
await _refreshAll([parent]);
|
||||
// Rsync 화면: 만든 폴더를 이 창의 원본 · 대상으로 바로 고른다
|
||||
if (widget.rsync) {
|
||||
pane.marked
|
||||
..clear()
|
||||
..add(made);
|
||||
pane.sel.clear();
|
||||
await pane.sel.toggle(made, isDir: true, list: (_) async => const []);
|
||||
}
|
||||
pane.focused = made;
|
||||
pane.changed();
|
||||
|
|
@ -2902,7 +2872,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
await _followHidden(moved: [(path, now)]);
|
||||
await _refreshAll([vDirname(path)]);
|
||||
for (final x in _panes) {
|
||||
if (x.marked.remove(path)) x.marked.add(now);
|
||||
x.sel.rename(path, now);
|
||||
}
|
||||
pane.focused = now;
|
||||
pane.changed();
|
||||
|
|
@ -2953,7 +2923,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
await _refreshAll({for (final (a, _) in moved) vDirname(a)});
|
||||
for (final x in _panes) {
|
||||
for (final (from, to) in moved) {
|
||||
if (x.marked.remove(from)) x.marked.add(to);
|
||||
x.sel.rename(from, to);
|
||||
if (x.focused != null && samePath(x.focused!, from)) x.focused = to;
|
||||
}
|
||||
x.changed();
|
||||
|
|
@ -3206,8 +3176,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
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;
|
||||
paths.forEach(x.sel.forget);
|
||||
if (_selecting && x.sel.isEmpty) _selecting = false;
|
||||
x.expanded.removeWhere((d) => paths.any((s) => isSameOrInside(d, s)));
|
||||
if (paths.any((s) => isSameOrInside(x.current, s))) x.current = vDirname(paths.first);
|
||||
}
|
||||
|
|
@ -3462,7 +3432,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
final problem = transferProblem(sources, dest, move: move);
|
||||
if (problem != null) {
|
||||
_snack(problem);
|
||||
pane.marked.removeWhere((m) => vMissingSync(m));
|
||||
pane.sel.removeWhere(vMissingSync);
|
||||
await _refreshAll([dest, ...sources.map(p.dirname)]);
|
||||
for (final x in _panes) {
|
||||
x.changed();
|
||||
|
|
@ -3600,7 +3570,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
await _followMovedJob(job);
|
||||
if (!mounted) return false;
|
||||
// 180: 표시만 지우고 선택 모드는 그대로 (꺼지면 동그라미 칸 · 아래 막대가 사라져 자리가 바뀐다)
|
||||
pane.marked.clear();
|
||||
pane.sel.clear();
|
||||
await _refreshAfterTransfer(sources, dest);
|
||||
// 넣은 폴더가 창의 지금 폴더면 펼쳐서 새 항목이 보이게
|
||||
for (final x in _panes) {
|
||||
|
|
@ -3666,7 +3636,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
}
|
||||
|
||||
Future<void> _runRsyncInner({required bool right, required bool left}) async {
|
||||
final l = _panes[0].marked.firstOrNull, r = _panes[1].marked.firstOrNull;
|
||||
final l = _panes[0].sel.marked.firstOrNull, r = _panes[1].sel.marked.firstOrNull;
|
||||
if (l == null || r == null) {
|
||||
_snack(tr('왼쪽 · 오른쪽 창에서 폴더를 하나씩 고르세요.'));
|
||||
return;
|
||||
|
|
@ -3696,7 +3666,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (problem != null) {
|
||||
_snack(problem);
|
||||
for (final x in _panes) {
|
||||
x.marked.removeWhere((m) => vMissingSync(m));
|
||||
x.sel.removeWhere(vMissingSync);
|
||||
}
|
||||
await _refreshAll([vDirname(l), vDirname(r)]);
|
||||
return;
|
||||
|
|
@ -3930,7 +3900,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (jobs.isEmpty || !mounted) return;
|
||||
// 원본 폴더까지 지웠으면 고른 표시도 뺀다
|
||||
for (final x in _panes) {
|
||||
x.marked.removeWhere((m) => vMissingSync(m));
|
||||
x.sel.removeWhere(vMissingSync);
|
||||
}
|
||||
await _refreshAll([l, r, vDirname(l), vDirname(r)]);
|
||||
for (final x in _panes) {
|
||||
|
|
@ -4596,11 +4566,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
|
||||
/// 49: 뒤로 키가 화면을 닫기 전에 할 일이 있는지 (고른 것이 있거나 맨 위 폴더가 아님)
|
||||
bool get _backHasWork =>
|
||||
_selecting || _panes.any((x) => x.marked.isNotEmpty) || (_ready && !samePath(_pane.current, _pane.root));
|
||||
_selecting || _panes.any((x) => !x.sel.isEmpty) || (_ready && !samePath(_pane.current, _pane.root));
|
||||
|
||||
/// 49: 뒤로 키 - 먼저 고른 것 풀기, 그다음 상위 폴더, 맨 위면 화면 닫기 (위쪽 ← 는 늘 닫기)
|
||||
Future<void> _onBack() async {
|
||||
if (_selecting || _panes.any((x) => x.marked.isNotEmpty)) {
|
||||
if (_selecting || _panes.any((x) => !x.sel.isEmpty)) {
|
||||
_setSelecting(false);
|
||||
return;
|
||||
}
|
||||
|
|
@ -4633,7 +4603,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 76: 열린 빠른 필터부터 닫는다
|
||||
if (_panes[_active].filterOpen) {
|
||||
_closeFilter(_panes[_active]);
|
||||
} else if (_selecting || _panes.any((x) => x.marked.isNotEmpty)) {
|
||||
} else if (_selecting || _panes.any((x) => !x.sel.isEmpty)) {
|
||||
_setSelecting(false);
|
||||
}
|
||||
},
|
||||
|
|
@ -4894,7 +4864,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
label: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
'$side · ${vBasename(_panes[k].marked.isNotEmpty ? _panes[k].marked.first : _panes[k].current)}',
|
||||
'$side · ${vBasename(!_panes[k].sel.isEmpty ? _panes[k].sel.marked.first : _panes[k].current)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
|
@ -4959,7 +4929,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
|
||||
/// 82: 끌 항목 - 누른 것이 표시한 것 중 하나면 표시한 것 모두 (바깥 것만), 아니면 그것 하나
|
||||
List<String> _dragPaths(_Pane pane, FileEntry e) =>
|
||||
_isMarked(pane, e.path) && pane.marked.isNotEmpty ? outermostPaths(pane.marked) : [e.path];
|
||||
_isMarked(pane, e.path) && !pane.sel.isEmpty ? outermostPaths(pane.sel.marked) : [e.path];
|
||||
|
||||
/// 82: 놓음 - 설정 "끌어 놓기 기본" (묻기 · 복사 · 이동) 뒤 지금의 복사 · 이동 (확인 창 · 같은 이름 · 진행 패널).
|
||||
/// 이동이 끝나면 아래 알림에 [되돌리기] (10초)
|
||||
|
|
@ -5060,7 +5030,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
// 74: 고른 개수와 합계 크기 (읽어 둔 목록 기준, 폴더 크기는 빼고)
|
||||
var size = 0, folders = 0;
|
||||
final byPath = {for (final l in pane.cache.values) for (final x in l) x.path: x};
|
||||
for (final m in pane.marked) {
|
||||
for (final m in pane.sel.marked) {
|
||||
final x = byPath[m];
|
||||
if (x == null) continue;
|
||||
if (x.isDir) {
|
||||
|
|
@ -5079,7 +5049,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Flexible(
|
||||
child: Text(
|
||||
[
|
||||
trf('{0}개 표시함', [pane.marked.length]),
|
||||
trf('{0}개 표시함', [pane.sel.marked.length]),
|
||||
if (size > 0) formatSize(size),
|
||||
if (folders > 0) trf('폴더 {0}개 (크기 빼고)', [folders]),
|
||||
].join(' · '),
|
||||
|
|
@ -5120,12 +5090,11 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
TextButton(
|
||||
onPressed: () => setState(() {
|
||||
final dir = listDir ?? pane.current;
|
||||
pane.marked.addAll([for (final x in _shown(pane, dir)) x.path]);
|
||||
_normalizeMarks(pane);
|
||||
pane.sel.addAll([for (final x in _shown(pane, dir)) x.path]);
|
||||
}),
|
||||
child: Text(tr('모두 선택')),
|
||||
),
|
||||
TextButton(onPressed: () => setState(pane.marked.clear), child: Text(tr('표시 지우기'))),
|
||||
TextButton(onPressed: () => setState(pane.sel.clear), child: Text(tr('표시 지우기'))),
|
||||
TextButton(onPressed: () => _setSelecting(false), child: Text(tr('선택 끝'))),
|
||||
]),
|
||||
);
|
||||
|
|
@ -5187,7 +5156,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
if (pane.loading.isNotEmpty) const Positioned(left: 0, right: 0, top: 0, child: LinearProgressIndicator(minHeight: 2)),
|
||||
]),
|
||||
),
|
||||
if ((_selecting || (_alwaysMarks && pane.marked.isNotEmpty)) && !foldersOnly) _marksBar(pane),
|
||||
if ((_selecting || (_alwaysMarks && !pane.sel.isEmpty)) && !foldersOnly) _marksBar(pane),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
|
@ -5879,6 +5848,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
});
|
||||
|
||||
final real = !row.isRoot && !row.isUp;
|
||||
// 길게 · 오른쪽 = 메뉴 (규칙 2, [ClickPolicy])
|
||||
final menu = ClickPolicy.decide(Press.longPress, _kindOf(row), selectMode: _selecting) == ClickAction.menu;
|
||||
Widget cell(String t, int flex, {TextAlign align = TextAlign.start}) => Expanded(
|
||||
flex: flex,
|
||||
child: Text(t, style: dim, textAlign: align, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
|
|
@ -5906,8 +5877,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
Widget line = GestureDetector(
|
||||
// Rsync 화면은 고르기만 (파일 기능 메뉴 없음)
|
||||
// 25: Rsync 화면에서도 (폴더 메뉴)
|
||||
onSecondaryTapUp: real ? (d) => _menu(pane, e, d.globalPosition) : null,
|
||||
onLongPressStart: real ? (d) => _menu(pane, e, d.globalPosition) : null,
|
||||
onSecondaryTapUp: menu ? (d) => _menu(pane, e, d.globalPosition) : null,
|
||||
onLongPressStart: menu ? (d) => _menu(pane, e, d.globalPosition) : null,
|
||||
child: Material(
|
||||
color: marked
|
||||
? JjColors.accent.withValues(alpha: 0.18)
|
||||
|
|
@ -5920,7 +5891,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
onTap: () => _onTap(pane, row, list: list),
|
||||
// 선택 모드: 두 번 누르면 열기 (바로 열기 모드는 두 번 누르기를 받지 않음 - 한 번 누르기가 늦어지지 않게)
|
||||
// 폴더는 한 번 누르기로 바로 펼치므로 두 번 누르기를 받지 않는다 (기다리지 않게)
|
||||
onDoubleTap: _selectMode && !_selecting && !e.isDir && !row.isUp
|
||||
onDoubleTap: ClickPolicy.decide(Press.doubleTap, _kindOf(row), selectMode: _selecting, tapOpens: !_selectMode) ==
|
||||
ClickAction.open
|
||||
? () => _onOpen(pane, row, list: list)
|
||||
: null,
|
||||
child: Padding(
|
||||
|
|
|
|||
|
|
@ -1022,18 +1022,6 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||
value: s.explorerSwipePanes,
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerSwipePanes = v),
|
||||
),
|
||||
SettingTile(
|
||||
title: Text(tr('폴더를 표시하면')),
|
||||
subtitle: Text(tr('X-plore 식: 1번 = 폴더, 2번 = 안의 파일 전체, 3번 = 풀기')),
|
||||
trailing: DropdownButton<String>(
|
||||
value: s.explorerFolderMarkRule,
|
||||
items: [
|
||||
DropdownMenuItem(value: 'tree', child: Text(tr('폴더와 안의 것 전체 (기본)'))),
|
||||
DropdownMenuItem(value: 'xplore', child: Text(tr('X-plore 식 (1 · 2 · 3번)'))),
|
||||
],
|
||||
onChanged: (v) => c.updateSettings((x) => x.explorerFolderMarkRule = v!),
|
||||
),
|
||||
),
|
||||
// X-plore 비교 c: 저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에 (X-plore 스타일)
|
||||
SwitchListTile(
|
||||
title: Text(tr('저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에')),
|
||||
|
|
|
|||
|
|
@ -306,29 +306,6 @@ void main() {
|
|||
expect(find.byKey(const ValueKey<String>('k2:\u0000favorites')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('X-plore ⚖: 폴더 표시 X-plore 식 (1번 폴더 · 2번 안의 파일 · 3번 풀기) - 처음 값은 지금 규칙', (tester) async {
|
||||
expect(AppSettings().explorerFolderMarkRule, 'tree', reason: '고르는 범위가 바뀌면 지우기 · 이동 대상이 바뀌어 처음 값은 그대로');
|
||||
c.settings.explorerFolderMarkRule = 'xplore';
|
||||
await open(tester);
|
||||
IconData markIcon(String name) => (tester
|
||||
.widget<IconButton>(find.descendant(
|
||||
of: find.ancestor(of: find.text(name), matching: find.byType(Material)).first, matching: find.byType(IconButton)))
|
||||
.icon as Icon)
|
||||
.icon!;
|
||||
Future<void> mark() async {
|
||||
// 표시 동그라미 (풍선 도움말이 표시 여부에 따라 바뀌므로 줄 안의 단추로 찾는다)
|
||||
await act(tester, () => tester.tap(find.descendant(of: find.ancestor(of: find.text('sub'), matching: find.byType(Material)).first, matching: find.byType(IconButton)).first));
|
||||
await settle(tester, () => false, rounds: 10);
|
||||
}
|
||||
|
||||
await mark();
|
||||
expect((markIcon('sub'), markIcon('inner.txt')), (Icons.check_circle, Icons.check_circle), reason: '1번 = 폴더 자신 (안의 것도 포함)');
|
||||
await mark();
|
||||
expect((markIcon('sub'), markIcon('inner.txt')), (Icons.check_circle_outline, Icons.check_circle), reason: '2번 = 안의 파일 전체');
|
||||
await mark();
|
||||
expect((markIcon('sub'), markIcon('inner.txt')), (Icons.check_circle_outline, Icons.check_circle_outline), reason: '3번 = 풀기');
|
||||
});
|
||||
|
||||
testWidgets('X-plore ⚖: 탐색기 안에서 옆으로 밀기 = 창 바꾸기 (한 창이면 숨은 창과 바꿈)', (tester) async {
|
||||
expect(AppSettings().explorerSwipePanes, isFalse, reason: '처음 값은 끔 - 화면 가운데를 밀면 화면 이동 (10/8 사용자 결정)');
|
||||
c.settings.explorerSwipePanes = true;
|
||||
|
|
@ -608,11 +585,7 @@ void main() {
|
|||
expect(markIcon('inner.txt'), Icons.check_circle_outline);
|
||||
expect(markIcon('second.txt'), Icons.check_circle);
|
||||
expect(markIcon('sub'), Icons.check_circle_outline);
|
||||
// 폴더를 다시 누르면 다시 전체 선택, 한 번 더 누르면 자기 자신과 안의 것 모두 취소
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => false, rounds: 5);
|
||||
expect(find.textContaining('1개 표시함'), findsOneWidget);
|
||||
expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle));
|
||||
// 하나를 뺀 폴더를 다시 누르면 그 폴더와 안의 것 모두 취소 (관리자 결정 2, 10/22 - 규칙 6 "다시 누르면 전체 취소")
|
||||
await act(tester, () => tester.tap(find.text('sub')));
|
||||
await settle(tester, () => false, rounds: 5);
|
||||
expect(find.textContaining('0개 표시함'), findsNWidgets(2));
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@ void main() {
|
|||
expect(ClickPolicy.decide(press, kind, selectMode: sel, expanded: exp, list: list, repeat: rep), want);
|
||||
});
|
||||
}
|
||||
test('사용자 설정: 바로 열기 · 다시 눌러 접지 않기', () {
|
||||
expect(ClickPolicy.decide(Press.tap, ItemKind.file, selectMode: false, tapOpens: true), ClickAction.open);
|
||||
expect(ClickPolicy.decide(Press.doubleTap, ItemKind.file, selectMode: false, tapOpens: true), ClickAction.none);
|
||||
expect(ClickPolicy.decide(Press.tap, ItemKind.file, selectMode: true, tapOpens: true), ClickAction.toggleMark);
|
||||
expect(ClickPolicy.decide(Press.tap, ItemKind.folder, selectMode: false, expanded: true, retapCollapses: false),
|
||||
ClickAction.expand);
|
||||
});
|
||||
});
|
||||
|
||||
group('Selection (규칙 5 · 6 · 8)', () {
|
||||
|
|
@ -140,6 +147,25 @@ void main() {
|
|||
expect(s.targets(null), isEmpty);
|
||||
});
|
||||
|
||||
test('이름 바꾸기 · 없어진 것 따라가기', () async {
|
||||
final s = Selection();
|
||||
await s.toggle(show, isDir: true, list: lister);
|
||||
await s.toggle(e1, isDir: false, list: lister); // Show 를 나눔: e2 · e3 · Extras 표시
|
||||
await s.toggle(notes, isDir: false, list: lister);
|
||||
final moved = p.join(tmp.path, 'Season');
|
||||
s.rename(show, moved);
|
||||
expect(s.has(p.join(moved, 'ep02.mkv')), isTrue);
|
||||
expect(s.isMarked(p.join(moved, 'Extras', 'bonus.mkv')), isTrue);
|
||||
expect(s.has(e2), isFalse);
|
||||
s.rename(notes, p.join(tmp.path, 'memo.txt'));
|
||||
expect(s.has(p.join(tmp.path, 'memo.txt')), isTrue);
|
||||
s.removeWhere((x) => x.endsWith('.txt'));
|
||||
expect(s.marked.any((x) => x.endsWith('.txt')), isFalse);
|
||||
// 나눈 폴더도 새 이름으로 - 다시 누르면 전체 취소
|
||||
await s.toggle(moved, isDir: true, list: lister);
|
||||
expect(s.isEmpty, isTrue);
|
||||
});
|
||||
|
||||
test('Rsync: 폴더만 · 하나만 · 다시 누르면 취소 (규칙 8)', () async {
|
||||
final s = Selection(rsync: true);
|
||||
await s.toggle(e1, isDir: false, list: lister);
|
||||
|
|
|
|||
Loading…
Reference in New Issue