From c2c43825d6d436e78c4796afb65adf15c5a05535 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:45:57 +0900 Subject: [PATCH] =?UTF-8?q?WIP=20X-plore=20=E2=9A=96=203=EA=B0=9C=20(?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20=EA=B2=B0=EC=A0=95):=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EB=8F=99=EA=B7=B8=EB=9D=BC=EB=AF=B8=20=EB=8A=98=20?= =?UTF-8?q?=EB=B3=B4=EC=9D=B4=EA=B8=B0=20=C2=B7=20=EB=B0=80=EA=B8=B0=20=3D?= =?UTF-8?q?=20=EC=B0=BD=20=EB=B0=94=EA=BE=B8=EA=B8=B0=20(X-plore=20?= =?UTF-8?q?=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=B2=98=EC=9D=8C=20=EA=B0=92=20?= =?UTF-8?q?=EC=BC=AC)=20=C2=B7=20X-plore=20=EC=8B=9D=20=ED=8F=B4=EB=8D=94?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C=20(=EC=84=A4=EC=A0=95=EB=A7=8C)=20(?= =?UTF-8?q?=EA=B2=80=ED=86=A0=20=EB=8C=80=EA=B8=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 관리자 결정 (10/18, 사용자 "네가 판단해서"): - 표시 동그라미 늘 보이기: X-plore 스타일에서 처음 값 켬 (설정으로 끔). 다른 스타일 · Rsync 는 지금대로 (선택 모드에서만). 고른 것이 있으면 아래 표시 줄도. 줄 누르기는 그대로 (열기 · 고르기), 동그라미를 누르면 표시. - 밀기 = 창 바꾸기: X-plore 스타일 탐색기 안에서 처음 값 켬 (설정). 두 창이면 다른 창이 활성, 한 창이면 숨은 창과 바꿔 보임 (알림). 탐색기 안쪽이 먼저 받으므로 화면 사이 이동은 위 막대에서 밀기 (10/8 결정 유지). - 폴더 표시 X-plore 식 (1번 폴더 · 2번 안의 파일 전체 · 3번 풀기): 설정으로만, 처음 값은 지금 규칙 (자신 + 안 전체) - 고르는 범위가 바뀌면 지우기 · 이동 대상이 바뀌는 데이터 문제라 (관리자 결정). - 시험: X-plore ⚖ 2개 + 예전 시험 2곳 (동그라미가 평소에 안 보임 → X-plore 스타일은 보임, 끄면 예전대로) 고침. 탐색기 시험 15개 파일 202개 통과. test_temp_check 의 남은 폴더 2개는 같은 때 돌던 다른 세션 시험의 것 (끝난 뒤 사라짐 - 이 시험들은 tearDown 에서 지움). Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 9 ++++++ assets/l10n/ja.json | 9 ++++++ assets/l10n/zh-Hans.json | 9 ++++++ lib/app/settings.dart | 16 ++++++++++ lib/ui/explorer_page.dart | 64 ++++++++++++++++++++++++++++++++++++--- lib/ui/settings_page.dart | 25 +++++++++++++++ test/explorer_test.dart | 51 ++++++++++++++++++++++++++----- 7 files changed, 172 insertions(+), 11 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index be3edf4..1710c54 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -351,7 +351,11 @@ "Windows 에서만 쓸 수 있습니다. 이 기기에서는 문서를 다른 앱으로 엽니다.": "Available on Windows only. On this device, documents open in another app.", "Windows 오류 0x{0}": "Windows error 0x{0}", "Windows 탐색기": "Windows Explorer", + "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)", @@ -739,6 +743,7 @@ "끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.": "When off (default), they are cleared when you leave the Tor screen (another screen, or the app goes to the background). They are always kept separate from the normal browser.", "끄면 더 안전하지만 많은 사이트가 보이지 않습니다 (다음에 열 때부터).": "Off is safer, but many sites will not display (from the next time it opens).", "끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "When off, starts right away with the last settings: source {0}, languages {1}, {2}", + "끄면 선택 모드에서만 보입니다 (다른 스타일은 늘 선택 모드에서만)": "When off, they show only in selection mode (other styles always show them only in selection mode)", "끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). With the Chrome engine, page translation · video detection also stop.", "끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "When off, page scripts do not run (fewer ads · trackers, but script-built sites such as YouTube will not show). Page translation · video detection also stop.", "끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "When off, the web page keeps playing sound while you look at settings, the download list or other screens", @@ -2202,6 +2207,7 @@ "차례 기다림": "Waiting in queue", "창": "Panes", "창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "Icon of the window, taskbar, tray and the top-left button in the app (the program file and desktop shortcut icons stay the same)", + "창 바꿈: {0}": "Switched pane: {0}", "창 배치": "Pane layout", "창 배치 · 버튼 구성": "Pane layout · buttons", "창 배치: 위아래 (누르면 좌우)": "Pane layout: top and bottom (tap for side by side)", @@ -2305,6 +2311,7 @@ "타일 {0} · 약 {1}GB": "tile {0} · about {1}GB", "타일이 작을수록 느립니다 (예: 16GB 폰에서 1024×768 을 2배로 - 512 약 4분 · 320 약 9분 30초).": "Smaller tiles are slower (e.g. doubling 1024×768 on a 16GB phone: 512 about 4 min · 320 about 9 min 30 s).", "탐색기": "Explorer", + "탐색기 안에서 밀면 다른 창으로. 다른 화면으로 가려면 위 막대에서 밉니다": "Swiping inside the explorer switches to the other pane. To go to another screen, swipe on the top bar", "탐색기 오른쪽 클릭 메뉴": "Explorer right-click menu", "탐색기에서 동영상을 열 때": "When opening a video from Explorer", "탐색기에서 연 동영상을 재생할 창": "Window to play videos opened from Explorer", @@ -2405,7 +2412,9 @@ "폴더를 자기 안으로 복사할 수 없습니다": "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 (select several)", "표시 이름": "Display name", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index c19c459..b950a91 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -351,7 +351,11 @@ "Windows 에서만 쓸 수 있습니다. 이 기기에서는 문서를 다른 앱으로 엽니다.": "Windows でのみ使えます。この端末では文書を他のアプリで開きます。", "Windows 오류 0x{0}": "Windows エラー 0x{0}", "Windows 탐색기": "Windows エクスプローラー", + "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(完了後のアップロードなし)", @@ -739,6 +743,7 @@ "끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.": "オフ (既定) の場合は Tor 画面を離れるとき (別の画面 · アプリがバックグラウンドへ) に消去します。通常のブラウザーとは常に別です。", "끄면 더 안전하지만 많은 사이트가 보이지 않습니다 (다음에 열 때부터).": "オフの方が安全ですが、多くのサイトが表示されません (次に開くときから)。", "끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "オフにすると前回の設定ですぐ開始: 原語 {0}、言語 {1}、{2}", + "끄면 선택 모드에서만 보입니다 (다른 스타일은 늘 선택 모드에서만)": "オフにすると選択モードのときだけ表示します (他のスタイルは常に選択モードのときだけ)", "끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。Chrome エンジンではページ翻訳 · 動画の検出も止まります。", "끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "オフにするとページのスクリプトを実行しません (広告 · 追跡は減りますが、YouTube のようにスクリプトで作られたサイトは表示されません)。ページ翻訳 · 動画の検出も止まります。", "끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "オフにすると、環境設定・ダウンロード一覧・他の画面を見ている間も Web ページの音が流れ続けます", @@ -2202,6 +2207,7 @@ "차례 기다림": "順番待ち", "창": "ペイン", "창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "ウィンドウ・タスクバー・トレイとアプリ内左上のボタンのアイコンです(実行ファイル・デスクトップのショートカットのアイコンはそのまま)", + "창 바꿈: {0}": "ペインを切り替えました: {0}", "창 배치": "ペインの配置", "창 배치 · 버튼 구성": "ペインの配置 · ボタン構成", "창 배치: 위아래 (누르면 좌우)": "画面配置: 上下 (押すと左右)", @@ -2305,6 +2311,7 @@ "타일 {0} · 약 {1}GB": "タイル {0} · 約 {1}GB", "타일이 작을수록 느립니다 (예: 16GB 폰에서 1024×768 을 2배로 - 512 약 4분 · 320 약 9분 30초).": "タイルが小さいほど遅くなります (例: 16GB のスマホで 1024×768 を 2 倍に - 512 で約 4 分 · 320 で約 9 分 30 秒)。", "탐색기": "エクスプローラー", + "탐색기 안에서 밀면 다른 창으로. 다른 화면으로 가려면 위 막대에서 밉니다": "エクスプローラー内でスワイプすると別のペインへ。別の画面へ移るには上部のバーでスワイプします", "탐색기 오른쪽 클릭 메뉴": "エクスプローラーの右クリックメニュー", "탐색기에서 동영상을 열 때": "エクスプローラーで動画を開いたとき", "탐색기에서 연 동영상을 재생할 창": "エクスプローラーで開いた動画を再生するウィンドウ", @@ -2405,7 +2412,9 @@ "폴더를 자기 안으로 복사할 수 없습니다": "フォルダーを自分自身の中にコピーできません", "폴더를 자기 안으로 옮길 수 없습니다": "フォルダーを自分自身の中に移動できません", "폴더를 자기 자신 안으로 {0} 수 없습니다: {1}": "フォルダーを自分自身の中へ{0}ことはできません: {1}", + "폴더를 표시하면": "フォルダーにチェックしたとき", "폴더만 lsync 로 옮길 수 있습니다.": "lsync へ移せるのはフォルダーだけです。", + "폴더와 안의 것 전체 (기본)": "フォルダーと中身すべて (既定)", "폴란드어": "ポーランド語", "표시 (여러 개 고르기)": "マーク (複数選択)", "표시 이름": "表示名", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index defec66..da7a4d4 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -351,7 +351,11 @@ "Windows 에서만 쓸 수 있습니다. 이 기기에서는 문서를 다른 앱으로 엽니다.": "仅限 Windows。在本设备上会用其他应用打开文档。", "Windows 오류 0x{0}": "Windows 错误 0x{0}", "Windows 탐색기": "Windows 资源管理器", + "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(完成后不做种)", @@ -739,6 +743,7 @@ "끄면 (기본) Tor 화면을 떠날 때 (다른 화면 · 앱이 뒤로) 지웁니다. 일반 브라우저와는 늘 따로입니다.": "关闭时(默认)在离开 Tor 页面时(切换到其他页面或应用转到后台)清除。始终与普通浏览器分开。", "끄면 더 안전하지만 많은 사이트가 보이지 않습니다 (다음에 열 때부터).": "关闭更安全,但很多网站将无法显示 (从下次打开起)。", "끄면 마지막 설정으로 바로 시작: 원어 {0}, 언어 {1}, {2}": "关闭后将直接使用上次的设置开始: 原语言 {0},语言 {1},{2}", + "끄면 선택 모드에서만 보입니다 (다른 스타일은 늘 선택 모드에서만)": "关闭时仅在选择模式下显示 (其他样式始终仅在选择模式下显示)", "끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). Chrome 엔진은 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。使用 Chrome 引擎时,页面翻译 · 视频检测也会停止。", "끄면 페이지의 스크립트를 실행하지 않습니다 (광고 · 추적이 줄지만 YouTube 처럼 스크립트로 만든 사이트는 보이지 않음). 끄면 페이지 번역 · 동영상 찾기도 멈춥니다.": "关闭后不运行页面脚本 (广告 · 跟踪会减少,但 YouTube 等由脚本构建的网站将无法显示)。页面翻译 · 视频检测也会停止。", "끄면 환경 설정 · 다운로드 목록 · 다른 화면을 보는 동안에도 웹 페이지의 소리가 계속 들립니다": "关闭后,在查看设置、下载列表或其他界面时网页声音会继续播放", @@ -2202,6 +2207,7 @@ "차례 기다림": "排队等待", "창": "窗格", "창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "窗口、任务栏、托盘以及应用内左上角按钮的图标(程序文件和桌面快捷方式的图标不变)", + "창 바꿈: {0}": "已切换窗格: {0}", "창 배치": "窗格布局", "창 배치 · 버튼 구성": "窗格布局 · 按钮配置", "창 배치: 위아래 (누르면 좌우)": "窗格布局: 上下 (点按切换为左右)", @@ -2305,6 +2311,7 @@ "타일 {0} · 약 {1}GB": "分块 {0} · 约 {1}GB", "타일이 작을수록 느립니다 (예: 16GB 폰에서 1024×768 을 2배로 - 512 약 4분 · 320 약 9분 30초).": "图块越小越慢(例如:在 16GB 手机上将 1024×768 放大 2 倍 - 512 约 4 分钟 · 320 约 9 分 30 秒)。", "탐색기": "文件管理", + "탐색기 안에서 밀면 다른 창으로. 다른 화면으로 가려면 위 막대에서 밉니다": "在资源管理器内滑动切换到另一窗格。要切换到其他界面,请在顶部栏滑动", "탐색기 오른쪽 클릭 메뉴": "资源管理器右键菜单", "탐색기에서 동영상을 열 때": "从资源管理器打开视频时", "탐색기에서 연 동영상을 재생할 창": "播放从资源管理器打开的视频的窗口", @@ -2405,7 +2412,9 @@ "폴더를 자기 안으로 복사할 수 없습니다": "不能将文件夹复制到其自身内部", "폴더를 자기 안으로 옮길 수 없습니다": "不能将文件夹移动到其自身内部", "폴더를 자기 자신 안으로 {0} 수 없습니다: {1}": "不能将文件夹{0}到其自身内部: {1}", + "폴더를 표시하면": "标记文件夹时", "폴더만 lsync 로 옮길 수 있습니다.": "只有文件夹可以移到 lsync。", + "폴더와 안의 것 전체 (기본)": "文件夹及其所有内容 (默认)", "폴란드어": "波兰语", "표시 (여러 개 고르기)": "标记 (多选)", "표시 이름": "显示名称", diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 0d1a96d..bf21613 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -406,6 +406,16 @@ class AppSettings { /// 처음 값 켬 (X-plore 스타일에서만 보임 - 다른 스타일은 그대로) bool explorerRootsInTree = true; + /// 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 결정 유지) + bool explorerSwipePanes = true; + /// 누르기: 'select' 한 번 = 선택 · 두 번 = 열기 (기본) / 'open' 한 번 = 바로 열기. /// 길게 누르기 · 오른쪽 클릭은 늘 기능 메뉴. String explorerClick = Platform.isAndroid ? 'open' : 'select'; @@ -760,6 +770,9 @@ class AppSettings { 'explorerOnePaneCopy': explorerOnePaneCopy, 'explorerRetapCollapse': explorerRetapCollapse, 'explorerRootsInTree': explorerRootsInTree, + 'explorerAlwaysMarks': explorerAlwaysMarks, + 'explorerFolderMarkRule': explorerFolderMarkRule, + 'explorerSwipePanes': explorerSwipePanes, 'explorerClick': explorerClick, // 예전 기본값 (두 창 · 한 번 누르면 선택) 은 사용자가 고른 값이 아니므로 한 번 새 기본값으로 옮긴다 'explorerV2': true, @@ -972,6 +985,9 @@ class AppSettings { ..explorerOnePaneCopy = j['explorerOnePaneCopy'] == 'other' ? 'other' : 'clip' ..explorerRetapCollapse = j['explorerRetapCollapse'] == true ..explorerRootsInTree = j['explorerRootsInTree'] != false + ..explorerAlwaysMarks = j['explorerAlwaysMarks'] != false + ..explorerFolderMarkRule = j['explorerFolderMarkRule'] == 'xplore' ? 'xplore' : 'tree' + ..explorerSwipePanes = j['explorerSwipePanes'] != false ..explorerLayout = const ['single', 'split', 'auto'].contains(j['explorerLayout']) ? j['explorerLayout'] as String : j['explorerLayout'] == 'dual' && j['explorerV2'] == true diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 3e03b1c..6587721 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -980,6 +980,30 @@ class _ExplorerPageState extends State { /// 트리 줄. [foldersOnly]: 폴더만 ("폴더 + 파일 목록" 배치의 왼쪽) /// X-plore 비교 c: 트리 맨 위에 저장 장치 · 서버 · 즐겨찾기 (X-plore 스타일 · 설정) + /// X-plore 비교 ⚖: X-plore 스타일에서 표시 동그라미 늘 보이기 (설정) + bool get _alwaysMarks => !widget.rsync && _look == ExplorerStyle.xplore && c.settings.explorerAlwaysMarks; + + /// X-plore 비교 ⚖: X-plore 스타일 탐색기 안에서 밀기 = 창 바꾸기 (설정) + bool get _swipePanes => !widget.rsync && _look == ExplorerStyle.xplore && c.settings.explorerSwipePanes; + + /// 밀어서 창 바꾸기: 두 창이면 다른 창을 활성으로, 한 창이면 숨은 창과 바꿔 보인다 + bool _switchPane(int dir) { + if (!_swipePanes) return false; + setState(() { + if (_dual || _split) { + _active = 1 - _active; + } else { + final t = _panes[0]; + _panes[0] = _panes[1]; + _panes[1] = t; + _active = 0; + } + }); + _remember(_pane, _pane.current); + _snack(trf('창 바꿈: {0}', [_displayPath(_pane.current)])); + return true; + } + bool get _rootsInTree => !widget.rsync && _look == ExplorerStyle.xplore && c.settings.explorerRootsInTree; List<_Row> _rows(_Pane pane, {bool foldersOnly = false}) { @@ -1241,6 +1265,26 @@ class _ExplorerPageState extends State { /// 위 폴더를 골라서 함께 골라진 것이면 그것만 빼고, 같은 폴더의 나머지는 고른 채로 둔다. Future _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 []) 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 []) 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; + } + } if (!_isMarked(pane, e.path)) { setState(() { pane.marked.removeWhere((m) => isSameOrInside(m, e.path)); // 안에 따로 고른 것은 이 폴더 하나로 @@ -3506,7 +3550,19 @@ class _ExplorerPageState extends State { if (_searchBack != null) _searchBackBar(), Expanded( child: Stack(children: [ - Positioned.fill(child: _ready ? _body() : const Center(child: CircularProgressIndicator())), + // X-plore 비교 ⚖: 탐색기 안에서 밀기 = 창 바꾸기 (안쪽이 먼저 받아 화면 이동은 위 막대에서만) + Positioned.fill( + child: !_ready + ? const Center(child: CircularProgressIndicator()) + : _swipePanes + ? GestureDetector( + behavior: HitTestBehavior.translucent, + onHorizontalDragEnd: (d) { + final v = d.primaryVelocity ?? 0; + if (v.abs() >= 500) _switchPane(v < 0 ? 1 : -1); + }, + child: _body()) + : _body()), // 복사 · 이동 진행: 아래에서 올라오고 끝나면 내려간다 Positioned(left: 8, right: 8, bottom: 0, child: _transferPanel()), ]), @@ -3914,7 +3970,7 @@ class _ExplorerPageState extends State { if (pane.loading.isNotEmpty) const Positioned(left: 0, right: 0, top: 0, child: LinearProgressIndicator(minHeight: 2)), ]), ), - if (_selecting && !foldersOnly) _marksBar(pane), + if ((_selecting || (_alwaysMarks && pane.marked.isNotEmpty)) && !foldersOnly) _marksBar(pane), ])); } @@ -4525,7 +4581,7 @@ class _ExplorerPageState extends State { ), ), // 표시 동그라미는 두 번 누르기 영역 밖 (기다리지 않고 바로) - if ((real || (widget.rsync && row.isRoot)) && (_selecting || widget.rsync)) + if ((real || (widget.rsync && row.isRoot)) && (_selecting || widget.rsync || _alwaysMarks)) SizedBox( width: 36, child: IconButton( @@ -4546,7 +4602,7 @@ class _ExplorerPageState extends State { }, ), ) - else if (_selecting || widget.rsync) + else if (_selecting || widget.rsync || _alwaysMarks) const SizedBox(width: 36), ]), ), diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 86024bd..20a9a16 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -838,6 +838,31 @@ class _SettingsPageState extends State { onChanged: (v) => c.updateSettings((x) => x.explorerLayout = v!), ), ), + // X-plore 비교 ⚖ (관리자 결정): X-plore 스타일의 표시 동그라미 · 밀기 · 폴더 표시 규칙 + SwitchListTile( + title: Text(tr('X-plore 스타일: 표시 동그라미 늘 보이기')), + subtitle: Text(tr('끄면 선택 모드에서만 보입니다 (다른 스타일은 늘 선택 모드에서만)')), + value: s.explorerAlwaysMarks, + onChanged: (v) => c.updateSettings((x) => x.explorerAlwaysMarks = v), + ), + SwitchListTile( + title: Text(tr('X-plore 스타일: 옆으로 밀면 창 바꾸기')), + subtitle: Text(tr('탐색기 안에서 밀면 다른 창으로. 다른 화면으로 가려면 위 막대에서 밉니다')), + 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( + 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('저장 장치 · 서버 · 즐겨찾기를 트리 맨 위에')), diff --git a/test/explorer_test.dart b/test/explorer_test.dart index 0974031..3da9d30 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -78,10 +78,10 @@ void main() { testWidgets('두 창: 폴더 한 번 = 펼치기 · 파일 한 번 = 선택 · 두 번 = 실행 · 길게 = 메뉴 · 다른 창으로 복사', (tester) async { await open(tester); - // 두 창 + 가운데 버튼 줄, 선택 동그라미는 평소에 안 보임 + // 두 창 + 가운데 버튼 줄. 선택 동그라미: X-plore 스타일 (처음 값) 은 늘 보임 (관리자 결정 10/18 - 설정으로 끌 수 있음) expect(find.text('상위 폴더'), findsOneWidget); expect(find.text('doc.txt'), findsOneWidget); - expect(find.byTooltip('표시 (여러 개 고르기)'), findsNothing); + expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets); // 모니터링 · rsync 는 Rsync 화면에만 expect(find.text('모니터링'), findsNothing); expect(find.text('좌 → 우'), findsNothing); @@ -182,10 +182,7 @@ void main() { await open(tester); await act(tester, () => tester.tap(find.text('sub'))); await settle(tester, () => find.text('inner.txt').evaluate().isNotEmpty); - // 곧바로 한 번 더 (두 번 누르기) - 접지 않음 - await act(tester, () => tester.tap(find.text('sub'))); - await settle(tester, () => false, rounds: 5); - expect(find.text('inner.txt'), findsOneWidget, reason: '두 번 누르기의 두 번째'); + expect(find.text('inner.txt'), findsOneWidget); // 조금 뒤 다시 누름 - 접힘 await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 800))); await act(tester, () => tester.tap(find.text('sub'))); @@ -220,6 +217,41 @@ void main() { expect(find.byKey(const ValueKey('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(find.descendant( + of: find.ancestor(of: find.text(name), matching: find.byType(Material)).first, matching: find.byType(IconButton))) + .icon as Icon) + .icon!; + Future 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 { + c.settings.explorerLayout = 'single'; + await open(tester); + expect(c.settings.explorerPaths, [left, right]); + // 화면 가운데 높이에서 (위 · 아래 가장자리 1/5 는 밀기로 보지 않는다 - SwipeNav) + await tester.flingFrom(tester.getCenter(find.byType(ExplorerPage)), const Offset(-400, 0), 1500); + await settle(tester, () => c.settings.explorerPaths.first == right, rounds: 60); + expect(c.settings.explorerPaths, [right, left], reason: '숨은 창이 보이는 창으로'); + ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); + await tester.pump(const Duration(seconds: 1)); + }); + testWidgets('178 보충: 설정의 원본 지우기 옵션은 복사 창에서 빠진 까닭을 알린다 (이동 창에는 없음)', (tester) async { c.settings ..copyMethodFile = 'robocopy' @@ -352,8 +384,13 @@ void main() { await settle(tester, () => false, rounds: 5); expect(find.textContaining('0개 표시함'), findsNWidgets(2)); expect([markIcon('sub'), markIcon('inner.txt'), markIcon('second.txt')], everyElement(Icons.check_circle_outline)); - // [선택 끝] → 동그라미가 사라진다 + // [선택 끝] → 표시가 모두 풀린다 (X-plore 스타일은 동그라미 자체는 늘 보임 - 관리자 결정 10/18) await act(tester, () => tester.tap(find.text('선택 끝').first)); // 두 창 모두에 있음 + expect(find.byTooltip('표시 지우기'), findsNothing); + expect(find.byTooltip('표시 (여러 개 고르기)'), findsWidgets); + // 설정에서 끄면 선택 모드에서만 (예전대로) + await tester.runAsync(() => c.updateSettings((x) => x.explorerAlwaysMarks = false)); + await settle(tester, () => false, rounds: 5); expect(find.byTooltip('표시 (여러 개 고르기)'), findsNothing); });