WIP X-plore c: X-plore 스타일 트리 맨 위에 저장 장치 · 서버 · 즐겨찾기 (위 줄 탭 · ★ 는 그대로) (검토 대기)
15 비교표 P1 c. X-plore 는 저장 장치 · 서버 · 즐겨찾기가 한 트리의 맨 위 항목이다. - X-plore 스타일에서 지금 저장 장치를 그 자리에 펼친 채, 다른 저장 장치 · WebDAV 서버를 접힌 줄로 앞뒤에 (탭 순서대로), 맨 끝에 "★ 즐겨찾기" (누르면 펼침 · 즐겨찾기를 누르면 그 폴더로). 다른 저장 장치 줄을 누르면 그 저장 장치로. - 위 줄 탭 · ★ 메뉴는 그대로 (관리자 결정 "함께"). 다른 스타일 · Rsync 화면은 그대로. - 설정 "저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에" (처음 값 켬, X-plore 스타일에서만 보임). - 줄 위젯은 따로 (_extraRow) - 파일 줄의 고르기 · 끌기 · 메뉴는 이 줄에 없음. 폴더로 스크롤 (_reveal) 은 실제 트리 줄만. - 시험: explorer_test "X-plore c" + 탐색기 시험 15개 파일 200개 통과. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_p3
parent
0ce26e9124
commit
23d3da8483
|
|
@ -351,6 +351,7 @@
|
||||||
"Windows 에서만 쓸 수 있습니다. 이 기기에서는 문서를 다른 앱으로 엽니다.": "Available on Windows only. On this device, documents open in another app.",
|
"Windows 에서만 쓸 수 있습니다. 이 기기에서는 문서를 다른 앱으로 엽니다.": "Available on Windows only. On this device, documents open in another app.",
|
||||||
"Windows 오류 0x{0}": "Windows error 0x{0}",
|
"Windows 오류 0x{0}": "Windows error 0x{0}",
|
||||||
"Windows 탐색기": "Windows Explorer",
|
"Windows 탐색기": "Windows Explorer",
|
||||||
|
"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 처럼. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)": "Like X-plore. When off, folders collapse and expand only with the › arrow (the list doesn't move on a double tap)",
|
"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 · 토렌트 다운로드 목록": "YouTube · torrent download list",
|
||||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp, magnet · .torrent → aria2 (no seeding after completion)",
|
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp, magnet · .torrent → aria2 (no seeding after completion)",
|
||||||
|
|
@ -2017,6 +2018,7 @@
|
||||||
"저장 실패: {0}": "Save failed: {0}",
|
"저장 실패: {0}": "Save failed: {0}",
|
||||||
"저장 안 함": "Don't save",
|
"저장 안 함": "Don't save",
|
||||||
"저장 위치": "Save locations",
|
"저장 위치": "Save locations",
|
||||||
|
"저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에": "Storage, servers and favorites at the top of the tree",
|
||||||
"저장 폴더": "Save folder",
|
"저장 폴더": "Save folder",
|
||||||
"저장 형식": "Save format",
|
"저장 형식": "Save format",
|
||||||
"저장: {0}": "Save: {0}",
|
"저장: {0}": "Save: {0}",
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,7 @@
|
||||||
"Windows 에서만 쓸 수 있습니다. 이 기기에서는 문서를 다른 앱으로 엽니다.": "Windows でのみ使えます。この端末では文書を他のアプリで開きます。",
|
"Windows 에서만 쓸 수 있습니다. 이 기기에서는 문서를 다른 앱으로 엽니다.": "Windows でのみ使えます。この端末では文書を他のアプリで開きます。",
|
||||||
"Windows 오류 0x{0}": "Windows エラー 0x{0}",
|
"Windows 오류 0x{0}": "Windows エラー 0x{0}",
|
||||||
"Windows 탐색기": "Windows エクスプローラー",
|
"Windows 탐색기": "Windows エクスプローラー",
|
||||||
|
"X-plore 스타일에서 X-plore 처럼 한 트리에 보입니다 (위 줄 탭 · ★ 는 그대로)": "X-plore スタイルで X-plore のように 1 つのツリーに表示します (上部のタブ・★ はそのまま)",
|
||||||
"X-plore 처럼. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)": "X-plore のように。オフのときは › 矢印でのみ開閉します (2 回押しても一覧は動きません)",
|
"X-plore 처럼. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)": "X-plore のように。オフのときは › 矢印でのみ開閉します (2 回押しても一覧は動きません)",
|
||||||
"YouTube · 토렌트 다운로드 목록": "YouTube・トレント ダウンロード一覧",
|
"YouTube · 토렌트 다운로드 목록": "YouTube・トレント ダウンロード一覧",
|
||||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp、マグネット・.torrent → aria2(完了後のアップロードなし)",
|
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp、マグネット・.torrent → aria2(完了後のアップロードなし)",
|
||||||
|
|
@ -2017,6 +2018,7 @@
|
||||||
"저장 실패: {0}": "保存失敗: {0}",
|
"저장 실패: {0}": "保存失敗: {0}",
|
||||||
"저장 안 함": "保存しない",
|
"저장 안 함": "保存しない",
|
||||||
"저장 위치": "保存先",
|
"저장 위치": "保存先",
|
||||||
|
"저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에": "ストレージ・サーバー・お気に入りをツリーの最上位に",
|
||||||
"저장 폴더": "保存フォルダー",
|
"저장 폴더": "保存フォルダー",
|
||||||
"저장 형식": "保存形式",
|
"저장 형식": "保存形式",
|
||||||
"저장: {0}": "保存: {0}",
|
"저장: {0}": "保存: {0}",
|
||||||
|
|
|
||||||
|
|
@ -351,6 +351,7 @@
|
||||||
"Windows 에서만 쓸 수 있습니다. 이 기기에서는 문서를 다른 앱으로 엽니다.": "仅限 Windows。在本设备上会用其他应用打开文档。",
|
"Windows 에서만 쓸 수 있습니다. 이 기기에서는 문서를 다른 앱으로 엽니다.": "仅限 Windows。在本设备上会用其他应用打开文档。",
|
||||||
"Windows 오류 0x{0}": "Windows 错误 0x{0}",
|
"Windows 오류 0x{0}": "Windows 错误 0x{0}",
|
||||||
"Windows 탐색기": "Windows 资源管理器",
|
"Windows 탐색기": "Windows 资源管理器",
|
||||||
|
"X-plore 스타일에서 X-plore 처럼 한 트리에 보입니다 (위 줄 탭 · ★ 는 그대로)": "在 X-plore 样式中像 X-plore 一样显示在一棵树中 (顶部的标签 · ★ 保持不变)",
|
||||||
"X-plore 처럼. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)": "像 X-plore 一样。关闭时仅用 › 箭头折叠 · 展开 (双击时列表不会移动)",
|
"X-plore 처럼. 끄면 접기 · 펼치기는 › 화살표로만 (두 번 눌러도 목록이 움직이지 않음)": "像 X-plore 一样。关闭时仅用 › 箭头折叠 · 展开 (双击时列表不会移动)",
|
||||||
"YouTube · 토렌트 다운로드 목록": "YouTube · 种子下载列表",
|
"YouTube · 토렌트 다운로드 목록": "YouTube · 种子下载列表",
|
||||||
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp,磁力链接 · .torrent → aria2(完成后不做种)",
|
"YouTube → yt-dlp, 마그넷 · .torrent → aria2 (완료 후 올려 주기 안 함)": "YouTube → yt-dlp,磁力链接 · .torrent → aria2(完成后不做种)",
|
||||||
|
|
@ -2017,6 +2018,7 @@
|
||||||
"저장 실패: {0}": "保存失败: {0}",
|
"저장 실패: {0}": "保存失败: {0}",
|
||||||
"저장 안 함": "不保存",
|
"저장 안 함": "不保存",
|
||||||
"저장 위치": "保存位置",
|
"저장 위치": "保存位置",
|
||||||
|
"저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에": "将存储 · 服务器 · 收藏放在树的最上层",
|
||||||
"저장 폴더": "保存文件夹",
|
"저장 폴더": "保存文件夹",
|
||||||
"저장 형식": "保存格式",
|
"저장 형식": "保存格式",
|
||||||
"저장: {0}": "保存: {0}",
|
"저장: {0}": "保存: {0}",
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,10 @@ class AppSettings {
|
||||||
/// 다시 · 두 번 눌러도 아래 줄이 움직이지 않게) 와 부딪혀 설정으로만. 켜도 두 번 누르기의 두 번째는 접지 않는다
|
/// 다시 · 두 번 눌러도 아래 줄이 움직이지 않게) 와 부딪혀 설정으로만. 켜도 두 번 누르기의 두 번째는 접지 않는다
|
||||||
bool explorerRetapCollapse = false;
|
bool explorerRetapCollapse = false;
|
||||||
|
|
||||||
|
/// X-plore 비교 c: X-plore 스타일에서 저장 장치 · 서버 · 즐겨찾기를 트리의 맨 위 항목으로 (위 줄 탭 · ★ 는 그대로 함께).
|
||||||
|
/// 처음 값 켬 (X-plore 스타일에서만 보임 - 다른 스타일은 그대로)
|
||||||
|
bool explorerRootsInTree = true;
|
||||||
|
|
||||||
/// 누르기: 'select' 한 번 = 선택 · 두 번 = 열기 (기본) / 'open' 한 번 = 바로 열기.
|
/// 누르기: 'select' 한 번 = 선택 · 두 번 = 열기 (기본) / 'open' 한 번 = 바로 열기.
|
||||||
/// 길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴.
|
/// 길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴.
|
||||||
String explorerClick = Platform.isAndroid ? 'open' : 'select';
|
String explorerClick = Platform.isAndroid ? 'open' : 'select';
|
||||||
|
|
@ -755,6 +759,7 @@ class AppSettings {
|
||||||
'explorerLayout': explorerLayout,
|
'explorerLayout': explorerLayout,
|
||||||
'explorerOnePaneCopy': explorerOnePaneCopy,
|
'explorerOnePaneCopy': explorerOnePaneCopy,
|
||||||
'explorerRetapCollapse': explorerRetapCollapse,
|
'explorerRetapCollapse': explorerRetapCollapse,
|
||||||
|
'explorerRootsInTree': explorerRootsInTree,
|
||||||
'explorerClick': explorerClick,
|
'explorerClick': explorerClick,
|
||||||
// 예전 기본값 (두 창 · 한 번 누르면 선택) 은 사용자가 고른 값이 아니므로 한 번 새 기본값으로 옮긴다
|
// 예전 기본값 (두 창 · 한 번 누르면 선택) 은 사용자가 고른 값이 아니므로 한 번 새 기본값으로 옮긴다
|
||||||
'explorerV2': true,
|
'explorerV2': true,
|
||||||
|
|
@ -966,6 +971,7 @@ class AppSettings {
|
||||||
// 'auto' = 넓은 화면은 두 창, 좁은 화면 (폰 세로) 은 한 창. 예전 기본 'dual' 은 한 번 'auto' 로
|
// 'auto' = 넓은 화면은 두 창, 좁은 화면 (폰 세로) 은 한 창. 예전 기본 'dual' 은 한 번 'auto' 로
|
||||||
..explorerOnePaneCopy = j['explorerOnePaneCopy'] == 'other' ? 'other' : 'clip'
|
..explorerOnePaneCopy = j['explorerOnePaneCopy'] == 'other' ? 'other' : 'clip'
|
||||||
..explorerRetapCollapse = j['explorerRetapCollapse'] == true
|
..explorerRetapCollapse = j['explorerRetapCollapse'] == true
|
||||||
|
..explorerRootsInTree = j['explorerRootsInTree'] != false
|
||||||
..explorerLayout = const ['single', 'split', 'auto'].contains(j['explorerLayout'])
|
..explorerLayout = const ['single', 'split', 'auto'].contains(j['explorerLayout'])
|
||||||
? j['explorerLayout'] as String
|
? j['explorerLayout'] as String
|
||||||
: j['explorerLayout'] == 'dual' && j['explorerV2'] == true
|
: j['explorerLayout'] == 'dual' && j['explorerV2'] == true
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,9 @@ class _Pane extends ChangeNotifier {
|
||||||
/// 74: 범위 고르기의 시작 (마지막으로 표시한 것)
|
/// 74: 범위 고르기의 시작 (마지막으로 표시한 것)
|
||||||
String? anchor;
|
String? anchor;
|
||||||
|
|
||||||
|
/// X-plore 비교 c: 트리의 "즐겨찾기" 묶음을 펼쳤는지
|
||||||
|
bool favoritesOpen = false;
|
||||||
|
|
||||||
/// 76: 빠른 필터 - 열려 있으면 이름에 [filter] 가 든 것만 보인다 (창마다, 닫으면 지움)
|
/// 76: 빠른 필터 - 열려 있으면 이름에 [filter] 가 든 것만 보인다 (창마다, 닫으면 지움)
|
||||||
bool filterOpen = false;
|
bool filterOpen = false;
|
||||||
final filterCtl = TextEditingController();
|
final filterCtl = TextEditingController();
|
||||||
|
|
@ -273,7 +276,11 @@ class _Row {
|
||||||
|
|
||||||
/// 파일 목록 맨 위의 ".." (상위 폴더로)
|
/// 파일 목록 맨 위의 ".." (상위 폴더로)
|
||||||
final bool isUp;
|
final bool isUp;
|
||||||
const _Row(this.entry, this.depth, {this.isRoot = false, this.isUp = false});
|
|
||||||
|
/// X-plore 비교 c: 트리 맨 위 항목 - [otherRoot] 다른 저장 장치 · 서버, [favorites] 즐겨찾기 묶음, [favorite] 즐겨찾기 하나
|
||||||
|
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});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ExplorerPageState extends State<ExplorerPage> {
|
class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
|
|
@ -894,7 +901,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!mounted || !pane.scroll.hasClients) return;
|
if (!mounted || !pane.scroll.hasClients) return;
|
||||||
// 화면의 트리와 같은 줄로 센다 (폴더 + 파일 목록 배치 · Rsync 화면은 폴더만)
|
// 화면의 트리와 같은 줄로 센다 (폴더 + 파일 목록 배치 · Rsync 화면은 폴더만)
|
||||||
final i = _rows(pane, foldersOnly: _split || widget.rsync).indexWhere((r) => samePath(r.entry.path, path));
|
final i = _rows(pane, foldersOnly: _split || widget.rsync).indexWhere((r) => r.kind == 0 && samePath(r.entry.path, path));
|
||||||
if (i < 0) return;
|
if (i < 0) return;
|
||||||
final pos = pane.scroll.position;
|
final pos = pane.scroll.position;
|
||||||
// 180: 끝 (maxScrollExtent) 으로 자르지 않는다 - 줄이 막 늘어난 때는 끝이 아직 앞의 것이라 보던 폴더로 못 간다.
|
// 180: 끝 (maxScrollExtent) 으로 자르지 않는다 - 줄이 막 늘어난 때는 끝이 아직 앞의 것이라 보던 폴더로 못 간다.
|
||||||
|
|
@ -972,9 +979,18 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
bool get _selectMode => c.settings.explorerClick != 'open';
|
bool get _selectMode => c.settings.explorerClick != 'open';
|
||||||
|
|
||||||
/// 트리 줄. [foldersOnly]: 폴더만 ("폴더 + 파일 목록" 배치의 왼쪽)
|
/// 트리 줄. [foldersOnly]: 폴더만 ("폴더 + 파일 목록" 배치의 왼쪽)
|
||||||
|
/// X-plore 비교 c: 트리 맨 위에 저장 장치 · 서버 · 즐겨찾기 (X-plore 스타일 · 설정)
|
||||||
|
bool get _rootsInTree => !widget.rsync && _look == ExplorerStyle.xplore && c.settings.explorerRootsInTree;
|
||||||
|
|
||||||
List<_Row> _rows(_Pane pane, {bool foldersOnly = false}) {
|
List<_Row> _rows(_Pane pane, {bool foldersOnly = false}) {
|
||||||
final out = <_Row>[];
|
final out = <_Row>[];
|
||||||
final rootEntry = FileEntry(pane.root, isDir: true, modified: DateTime(0));
|
final rootEntry = FileEntry(pane.root, isDir: true, modified: DateTime(0));
|
||||||
|
// X-plore 비교 c: 지금 저장 장치 앞뒤로 다른 저장 장치 · 서버 (접힌 줄), 맨 끝에 즐겨찾기
|
||||||
|
final roots = _rootsInTree ? _volumes : const <(String, String)>[];
|
||||||
|
final here = roots.indexWhere((v) => samePath(v.$1, pane.root));
|
||||||
|
for (final (path, _) in roots.take(here < 0 ? 0 : here)) {
|
||||||
|
out.add(_Row(FileEntry(path, isDir: true, modified: DateTime(0)), 0, kind: _Row.otherRoot));
|
||||||
|
}
|
||||||
out.add(_Row(rootEntry, 0, isRoot: true));
|
out.add(_Row(rootEntry, 0, isRoot: true));
|
||||||
// 76: 빠른 필터 - 지금 폴더 안만 거른다 (트리의 다른 곳은 그대로).
|
// 76: 빠른 필터 - 지금 폴더 안만 거른다 (트리의 다른 곳은 그대로).
|
||||||
// 이름이 맞지 않아도 안에 맞는 것이 보이는 폴더는 남긴다 (어디에 있는지 보이게)
|
// 이름이 맞지 않아도 안에 맞는 것이 보이는 폴더는 남긴다 (어디에 있는지 보이게)
|
||||||
|
|
@ -996,9 +1012,83 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
walk(pane.root, 1);
|
walk(pane.root, 1);
|
||||||
|
if (roots.isNotEmpty) {
|
||||||
|
for (final (path, _) in here < 0 ? roots : roots.skip(here + 1)) {
|
||||||
|
out.add(_Row(FileEntry(path, isDir: true, modified: DateTime(0)), 0, kind: _Row.otherRoot));
|
||||||
|
}
|
||||||
|
final favs = c.settings.folderBookmarks;
|
||||||
|
if (favs.isNotEmpty) {
|
||||||
|
out.add(_Row(FileEntry('\u0000favorites', isDir: true, modified: DateTime(0)), 0, kind: _Row.favorites));
|
||||||
|
if (pane.favoritesOpen) {
|
||||||
|
for (final b in favs) {
|
||||||
|
out.add(_Row(FileEntry(b.path, isDir: true, modified: DateTime(0)), 1, kind: _Row.favorite));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// X-plore 비교 c: 트리 맨 위 항목 줄 (다른 저장 장치 · 서버 · 즐겨찾기) - 누르면 그곳으로, 즐겨찾기 묶음은 펼치기 · 접기
|
||||||
|
Widget _extraRow(_Pane pane, _Row row) {
|
||||||
|
final look = _look;
|
||||||
|
final path = row.entry.path;
|
||||||
|
final volume = _volumes.firstWhere((v) => samePath(v.$1, path), orElse: () => (path, vBasename(path))).$2;
|
||||||
|
final fav = row.kind == _Row.favorite
|
||||||
|
? c.settings.folderBookmarks.firstWhere((b) => b.path == path, orElse: () => FolderBookmark(name: vBasename(path), path: path))
|
||||||
|
: null;
|
||||||
|
final (IconData icon, Color color, String label, bool? open) = switch (row.kind) {
|
||||||
|
_Row.otherRoot => (
|
||||||
|
isDav(path)
|
||||||
|
? Icons.cloud_outlined
|
||||||
|
: Platform.isWindows
|
||||||
|
? Icons.storage
|
||||||
|
: Icons.sd_storage_outlined,
|
||||||
|
JjColors.accent,
|
||||||
|
volume,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
_Row.favorites => (Icons.star, Colors.amber, tr('즐겨찾기'), pane.favoritesOpen),
|
||||||
|
_ => (Icons.star_outline, Colors.amber, fav!.name, null),
|
||||||
|
};
|
||||||
|
void tap() {
|
||||||
|
setState(() => _active = _panes.indexOf(pane));
|
||||||
|
if (row.kind == _Row.favorites) {
|
||||||
|
pane.favoritesOpen = !pane.favoritesOpen;
|
||||||
|
pane.changed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unawaited(_goTo(pane, path, fresh: true).then((_) => _reveal(pane, path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Semantics(
|
||||||
|
button: true,
|
||||||
|
label: label,
|
||||||
|
excludeSemantics: true,
|
||||||
|
onTap: tap,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: tap,
|
||||||
|
child: Row(children: [
|
||||||
|
SizedBox(width: look.indent * row.depth + 4),
|
||||||
|
SizedBox(
|
||||||
|
width: treeChevronWidth(defaultTargetPlatform),
|
||||||
|
child: open == null ? null : Icon(open ? Icons.expand_more : Icons.chevron_right, size: 18, color: JjColors.textDim),
|
||||||
|
),
|
||||||
|
Icon(icon, size: look.iconSize * 0.8, color: color),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
|
Text(label, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: look.fontSize)),
|
||||||
|
if (fav != null)
|
||||||
|
Text(_displayPath(path),
|
||||||
|
maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: look.fontSize - 3, color: JjColors.textDim)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ───────── 누르기 ─────────
|
// ───────── 누르기 ─────────
|
||||||
|
|
||||||
/// 선택 모드 (X-plore 처럼): 각 줄의 ✔ 동그라미가 보이고, 누르면 고르기 · 풀기
|
/// 선택 모드 (X-plore 처럼): 각 줄의 ✔ 동그라미가 보이고, 누르면 고르기 · 풀기
|
||||||
|
|
@ -3815,7 +3905,9 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
itemCount: rows.length,
|
itemCount: rows.length,
|
||||||
itemExtent: look.rowHeightOf(context),
|
itemExtent: look.rowHeightOf(context),
|
||||||
// 76: 줄 위젯은 항목을 따라간다 (아래 [_rowKey])
|
// 76: 줄 위젯은 항목을 따라간다 (아래 [_rowKey])
|
||||||
itemBuilder: (_, k) => KeyedSubtree(key: _rowKey(rows[k]), child: _rowView(pane, rows[k], compact: foldersOnly)),
|
itemBuilder: (_, k) => KeyedSubtree(
|
||||||
|
key: _rowKey(rows[k]),
|
||||||
|
child: rows[k].kind != 0 ? _extraRow(pane, rows[k]) : _rowView(pane, rows[k], compact: foldersOnly)),
|
||||||
findChildIndexCallback: _rowIndexOf(rows),
|
findChildIndexCallback: _rowIndexOf(rows),
|
||||||
),
|
),
|
||||||
// 읽는 중 (WebDAV 서버를 기다릴 때 등). 180: 목록 위에 겹쳐 그린다 (끼어들면 목록이 밀려 다음 것을 누르려다 엉뚱한 것을 누른다)
|
// 읽는 중 (WebDAV 서버를 기다릴 때 등). 180: 목록 위에 겹쳐 그린다 (끼어들면 목록이 밀려 다음 것을 누르려다 엉뚱한 것을 누른다)
|
||||||
|
|
@ -3829,7 +3921,8 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
||||||
/// 76: 목록 줄의 키 = 항목 경로. 키가 없으면 줄 목록이 바뀔 때 (빠른 필터 · 펼치기 · 새로 고침) 같은 자리의 줄 위젯을
|
/// 76: 목록 줄의 키 = 항목 경로. 키가 없으면 줄 목록이 바뀔 때 (빠른 필터 · 펼치기 · 새로 고침) 같은 자리의 줄 위젯을
|
||||||
/// 다른 항목에 다시 써서, 방금 누른 파일 줄이 폴더 줄로 바뀌면 두 번 누르기 기다림이 그리는 중에 버려지고
|
/// 다른 항목에 다시 써서, 방금 누른 파일 줄이 폴더 줄로 바뀌면 두 번 누르기 기다림이 그리는 중에 버려지고
|
||||||
/// 그 누르기가 다른 항목에 들어갔다 (디버그에서는 "setState() called during build")
|
/// 그 누르기가 다른 항목에 들어갔다 (디버그에서는 "setState() called during build")
|
||||||
static Key _rowKey(_Row r) => ValueKey<String>('${r.isRoot ? 'root:' : r.isUp ? 'up:' : ''}${r.entry.path}');
|
static Key _rowKey(_Row r) =>
|
||||||
|
ValueKey<String>('${r.kind != 0 ? 'k${r.kind}:' : r.isRoot ? 'root:' : r.isUp ? 'up:' : ''}${r.entry.path}');
|
||||||
|
|
||||||
/// 76: 줄이 옮겨 가도 같은 줄 위젯을 찾게 (키 → 지금 자리)
|
/// 76: 줄이 옮겨 가도 같은 줄 위젯을 찾게 (키 → 지금 자리)
|
||||||
static int? Function(Key) _rowIndexOf(List<_Row> rows) {
|
static int? Function(Key) _rowIndexOf(List<_Row> rows) {
|
||||||
|
|
|
||||||
|
|
@ -838,6 +838,13 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||||
onChanged: (v) => c.updateSettings((x) => x.explorerLayout = v!),
|
onChanged: (v) => c.updateSettings((x) => x.explorerLayout = v!),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// X-plore 비교 c: 저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에 (X-plore 스타일)
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text(tr('저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에')),
|
||||||
|
subtitle: Text(tr('X-plore 스타일에서 X-plore 처럼 한 트리에 보입니다 (위 줄 탭 · ★ 는 그대로)')),
|
||||||
|
value: s.explorerRootsInTree,
|
||||||
|
onChanged: (v) => c.updateSettings((x) => x.explorerRootsInTree = v),
|
||||||
|
),
|
||||||
// X-plore 비교 d: 지금 폴더를 다시 누르면 접기
|
// X-plore 비교 d: 지금 폴더를 다시 누르면 접기
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(tr('펼친 폴더를 다시 누르면 접기')),
|
title: Text(tr('펼친 폴더를 다시 누르면 접기')),
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,33 @@ void main() {
|
||||||
expect(find.text('inner.txt'), findsNothing);
|
expect(find.text('inner.txt'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('X-plore c: X-plore 스타일 트리 맨 위에 다른 저장 장치 · 즐겨찾기 - 누르면 그곳으로, 즐겨찾기는 펼침', (tester) async {
|
||||||
|
ExplorerPage.debugVolumes = () => [(left, 'L:'), (right, 'R:')];
|
||||||
|
addTearDown(() => ExplorerPage.debugVolumes = null);
|
||||||
|
c.settings
|
||||||
|
..explorerStyle = 'xplore'
|
||||||
|
..folderBookmarks = [FolderBookmark(name: '내 즐겨찾기', path: p.join(left, 'sub'))];
|
||||||
|
await open(tester);
|
||||||
|
final otherRoot = find.byKey(ValueKey<String>('k1:$right'));
|
||||||
|
expect(otherRoot, findsWidgets, reason: '왼쪽 창 트리에 R: (다른 저장 장치) 줄');
|
||||||
|
final favs = find.byKey(const ValueKey<String>('k2:\u0000favorites'));
|
||||||
|
expect(favs, findsWidgets);
|
||||||
|
expect(find.text('내 즐겨찾기'), findsNothing, reason: '접힌 즐겨찾기');
|
||||||
|
await act(tester, () => tester.tap(favs.first));
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.text('내 즐겨찾기'), findsOneWidget);
|
||||||
|
await act(tester, () => tester.tap(find.text('내 즐겨찾기')));
|
||||||
|
await settle(tester, () => c.settings.explorerPaths.first == p.join(left, 'sub'), rounds: 80);
|
||||||
|
expect(c.settings.explorerPaths.first, p.join(left, 'sub'));
|
||||||
|
await act(tester, () => tester.tap(find.byKey(ValueKey<String>('k1:$right')).first));
|
||||||
|
await settle(tester, () => c.settings.explorerPaths.first == right, rounds: 80);
|
||||||
|
expect(c.settings.explorerPaths.first, right, reason: '다른 저장 장치 줄 = 그 저장 장치로');
|
||||||
|
// 다른 스타일은 그대로 (맨 위 항목 없음)
|
||||||
|
await tester.runAsync(() => c.updateSettings((x) => x.explorerStyle = 'windows'));
|
||||||
|
await calm(tester);
|
||||||
|
expect(find.byKey(const ValueKey<String>('k2:\u0000favorites')), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('178 보충: 설정의 원본 지우기 옵션은 복사 창에서 빠진 까닭을 알린다 (이동 창에는 없음)', (tester) async {
|
testWidgets('178 보충: 설정의 원본 지우기 옵션은 복사 창에서 빠진 까닭을 알린다 (이동 창에는 없음)', (tester) async {
|
||||||
c.settings
|
c.settings
|
||||||
..copyMethodFile = 'robocopy'
|
..copyMethodFile = 'robocopy'
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue