From 53a1b47a2cb3bd1898074dbb2eca8d464a2507ca Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Sat, 17 Oct 2026 13:36:59 +0900 Subject: [PATCH] =?UTF-8?q?WIP=2085=20=ED=8C=8C=EC=9D=BC=20=ED=83=90?= =?UTF-8?q?=EC=83=89=EA=B8=B0=20=EC=A7=84=ED=96=89=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EC=88=A8=EA=B8=B0=EA=B8=B0=20=C2=B7=20=EC=9E=91=EC=9D=80=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=C2=B7=20Android=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EB=A5=A0=20=C2=B7=20=EB=81=9D=EB=82=98?= =?UTF-8?q?=EB=A9=B4=20[=ED=8F=B4=EB=8D=94=20=EC=97=B4=EA=B8=B0]=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 - 관리자 지시 (F14): 복사 진행 패널을 숨길 수 있게 (Android 는 알림줄에 진행), 끝나면 [폴더 열기]. - 패널 오른쪽 위 [숨기기] → 오른쪽 아래 작은 표시 "복사 · 이동 n개 · 45%" (누르면 다시 펼침). 숨김은 설정으로 기억 (처음 값: 보임). 관리자 판단: 숨겨도 묻는 창 (확인 · 같은 이름) 과 실패 알림은 늘 뜬다 (패널과 따로). 멈춘 (실패한) 것이 있으면 작은 표시가 빨갛게 "복사 · 이동 멈춤 - 눌러서 보기". - Android 작업 알림: "복사 · 이동 n개 · 45%" (CopyCenter.activeProgress), 다운로드가 없으면 진행 막대도 복사 · 이동 진행률. - 끝난 알림에 [폴더 열기] - 앱 탐색기에서 넣은 폴더로 (그 폴더를 보는 창, 없으면 지금 창). - 시험: keep_alive_test (복사 진행률 글 · 막대 · 다운로드가 먼저), explorer_test 85 ([폴더 열기] · [숨기기] → 작은 표시 → 다시). Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 5 ++ assets/l10n/ja.json | 5 ++ assets/l10n/zh-Hans.json | 5 ++ lib/app/copy_center.dart | 7 ++ lib/app/settings.dart | 5 ++ lib/platform/android/android_keep_alive.dart | 24 ++++-- lib/ui/explorer_page.dart | 85 +++++++++++++++++++- test/explorer_test.dart | 26 ++++++ test/keep_alive_test.dart | 5 ++ 9 files changed, 158 insertions(+), 9 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index cb081e0..ca146e3 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -1209,7 +1209,10 @@ "복사 · rsync 목록으로 옮겼습니다.": "Moved to the copy · rsync list.", "복사 · 동기화가 중간에 끊겨 남은 임시 파일입니다 (.jjpart). 완성된 파일이 아니므로 지워도 됩니다. 같은 파일을 다시 복사하면 새로 만듭니다.": "Temporary files left behind when a copy or sync was interrupted (.jjpart). They are not complete files, so you can delete them. Copying the same file again creates it anew.", "복사 · 이동 {0}개": "{0} copy · move", + "복사 · 이동 {0}개 · {1}%": "Copy · move: {0} · {1}%", "복사 · 이동 {0}개가 아직 끝나지 않았습니다. 끝내면 멈추고, 복사하던 파일은 완성되지 않은 채로 남지 않게 지웁니다.": "{0} copy/move tasks have not finished. Exiting stops them, and the file being copied is removed so no incomplete file is left.", + "복사 · 이동 끝": "Copy · move finished", + "복사 · 이동 멈춤 - 눌러서 보기": "Copy · move stopped - tap to see", "복사 · 이동 중입니다": "Copying · moving in progress", "복사 · 이동: 받는 쪽 파일이 더 새것이면 건너뜁니다 (예전에는 예전 원본으로 덮어쓰고, 이동이면 원본도 지웠습니다). 건너뛴 파일은 원본에 남고 끝 알림에 그 수가 나옵니다. 되돌리기: 환경 설정 > 파일 탐색기 (또는 Rsync) > 받는 쪽이 더 새것이면 건너뛰기": "Copy · move: files are skipped when the receiving side is newer (before, the older source overwrote them, and a move also deleted the source). Skipped files stay in the source and their number appears in the finish notice. To change back: Settings > File explorer (or Rsync) > Skip when the receiving side is newer", "복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "Copy · move works with two panes (Pane layout > Two panes).", @@ -1377,6 +1380,7 @@ "수정 · 이동": "Edit · move", "수정한 날짜": "Date modified", "숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "Even when hidden, you can change the pane layout · buttons from the ⋮ menu at the top.", + "숨기기": "Hide", "숨긴 항목 (. 으로 시작) 보이기": "Show hidden items (starting with .)", "숨긴 항목 숨기기": "Hide hidden items", "숨김": "Hidden", @@ -2115,6 +2119,7 @@ "직접 지정 (기본)": "Manual (default)", "진행 중인 작업 없음": "No jobs in progress", "진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "A job is in progress: {0}\nStop it and quit?", + "진행 패널 보기": "Show progress panel", "집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 \"인증서 확인 안 함\" 을 켜세요.": "If it uses a self-signed certificate (like a home NAS), turn on \"Don't verify certificate\" in [Edit server settings].", "짧을수록 빨리 맞추지만 배터리를 더 씁니다.": "Shorter means faster syncing but more battery use.", "차례 기다림": "Waiting in queue", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 6ece6ac..35491df 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -1209,7 +1209,10 @@ "복사 · rsync 목록으로 옮겼습니다.": "コピー · rsync の一覧へ移しました。", "복사 · 동기화가 중간에 끊겨 남은 임시 파일입니다 (.jjpart). 완성된 파일이 아니므로 지워도 됩니다. 같은 파일을 다시 복사하면 새로 만듭니다.": "コピー・同期が途中で中断されて残った一時ファイルです (.jjpart)。完成したファイルではないので削除しても構いません。同じファイルをもう一度コピーすると新しく作ります。", "복사 · 이동 {0}개": "コピー・移動 {0} 件", + "복사 · 이동 {0}개 · {1}%": "コピー・移動 {0} 件 · {1}%", "복사 · 이동 {0}개가 아직 끝나지 않았습니다. 끝내면 멈추고, 복사하던 파일은 완성되지 않은 채로 남지 않게 지웁니다.": "コピー・移動 {0} 件がまだ終わっていません。終了すると止まり、コピー中のファイルは未完成のまま残らないよう削除します。", + "복사 · 이동 끝": "コピー・移動 完了", + "복사 · 이동 멈춤 - 눌러서 보기": "コピー・移動が停止 - タップして表示", "복사 · 이동 중입니다": "コピー・移動中です", "복사 · 이동: 받는 쪽 파일이 더 새것이면 건너뜁니다 (예전에는 예전 원본으로 덮어쓰고, 이동이면 원본도 지웠습니다). 건너뛴 파일은 원본에 남고 끝 알림에 그 수가 나옵니다. 되돌리기: 환경 설정 > 파일 탐색기 (또는 Rsync) > 받는 쪽이 더 새것이면 건너뛰기": "コピー · 移動: 受け取る側のファイルが新しければスキップします (以前は古い元で上書きし、移動なら元も削除していました)。スキップしたファイルは元に残り、終了の通知にその数が出ます。戻すには: 環境設定 > ファイルエクスプローラー (または Rsync) > 受け取る側が新しければスキップ", "복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "コピー · 移動は 2 ペインで使います (ペインの配置 > 2 ペイン)。", @@ -1377,6 +1380,7 @@ "수정 · 이동": "編集・移動", "수정한 날짜": "更新日時", "숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "非表示にしても、上の ⋮ メニューからペインの配置 · ボタン構成を変えられます。", + "숨기기": "隠す", "숨긴 항목 (. 으로 시작) 보이기": "隠し項目 (. で始まる) を表示", "숨긴 항목 숨기기": "隠し項目を隠す", "숨김": "非表示", @@ -2115,6 +2119,7 @@ "직접 지정 (기본)": "手動指定 (既定)", "진행 중인 작업 없음": "進行中の作業なし", "진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "進行中の作業があります: {0}\n中止して終了しますか?", + "진행 패널 보기": "進行パネルを表示", "집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 \"인증서 확인 안 함\" 을 켜세요.": "自宅 NAS のように自己署名証明書なら、[サーバー設定を修正] で「証明書を検証しない」をオンにしてください。", "짧을수록 빨리 맞추지만 배터리를 더 씁니다.": "短いほど早く同期しますが、バッテリーをより使います。", "차례 기다림": "順番待ち", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index f835b57..c2fefaf 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -1209,7 +1209,10 @@ "복사 · rsync 목록으로 옮겼습니다.": "已移到 复制 · rsync 列表。", "복사 · 동기화가 중간에 끊겨 남은 임시 파일입니다 (.jjpart). 완성된 파일이 아니므로 지워도 됩니다. 같은 파일을 다시 복사하면 새로 만듭니다.": "这是复制 · 同步中途中断后留下的临时文件 (.jjpart)。它们不是完整的文件,可以删除。再次复制同一文件会重新创建。", "복사 · 이동 {0}개": "复制 · 移动 {0} 个", + "복사 · 이동 {0}개 · {1}%": "复制 · 移动 {0} 个 · {1}%", "복사 · 이동 {0}개가 아직 끝나지 않았습니다. 끝내면 멈추고, 복사하던 파일은 완성되지 않은 채로 남지 않게 지웁니다.": "{0} 个复制 · 移动尚未完成。退出会停止它们,并删除正在复制的文件,以免留下不完整的文件。", + "복사 · 이동 끝": "复制 · 移动 已完成", + "복사 · 이동 멈춤 - 눌러서 보기": "复制 · 移动已停止 - 点按查看", "복사 · 이동 중입니다": "正在复制 · 移动", "복사 · 이동: 받는 쪽 파일이 더 새것이면 건너뜁니다 (예전에는 예전 원본으로 덮어쓰고, 이동이면 원본도 지웠습니다). 건너뛴 파일은 원본에 남고 끝 알림에 그 수가 나옵니다. 되돌리기: 환경 설정 > 파일 탐색기 (또는 Rsync) > 받는 쪽이 더 새것이면 건너뛰기": "复制 · 移动:接收端文件较新时跳过(以前会用旧的源文件覆盖,移动时还会删除源)。跳过的文件留在源中,完成通知中会显示数量。恢复:设置 > 文件浏览器(或 Rsync)> 接收端较新时跳过", "복사 · 이동은 두 창에서 씁니다 (창 배치 > 두 창).": "复制 · 移动需在双窗格中使用 (窗格布局 > 双窗格)。", @@ -1377,6 +1380,7 @@ "수정 · 이동": "编辑 · 移动", "수정한 날짜": "修改日期", "숨겨도 위쪽 ⋮ 메뉴에서 창 배치 · 버튼 구성을 바꿀 수 있습니다.": "即使隐藏,也可以在顶部 ⋮ 菜单中更改窗格布局 · 按钮配置。", + "숨기기": "隐藏", "숨긴 항목 (. 으로 시작) 보이기": "显示隐藏项目 (以 . 开头)", "숨긴 항목 숨기기": "隐藏隐藏项目", "숨김": "隐藏", @@ -2115,6 +2119,7 @@ "직접 지정 (기본)": "手动指定 (默认)", "진행 중인 작업 없음": "没有进行中的任务", "진행 중인 작업이 있습니다: {0}\n중지하고 끝낼까요?": "有正在进行的任务: {0}\n要停止并退出吗?", + "진행 패널 보기": "显示进度面板", "집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 \"인증서 확인 안 함\" 을 켜세요.": "如果是自签名证书 (如家用 NAS),请在 [修改服务器设置] 中开启“不验证证书”。", "짧을수록 빨리 맞추지만 배터리를 더 씁니다.": "间隔越短同步越快,但更耗电。", "차례 기다림": "排队等待", diff --git a/lib/app/copy_center.dart b/lib/app/copy_center.dart index 1d6b322..34a7307 100644 --- a/lib/app/copy_center.dart +++ b/lib/app/copy_center.dart @@ -35,6 +35,13 @@ class CopyCenter extends ChangeNotifier { /// 지금 도는 복사 · 이동 수 (끝낼 때 확인 · Android 작업 알림에) int get activeCount => jobs.values.where((j) => !j.finished).length; + /// 85: 지금 도는 복사 · 이동 전체의 진행률 (0~1, 도는 것이 없거나 아직 세는 중이면 null) - Android 알림 · 숨긴 패널의 작은 표시 + double? get activeProgress { + final run = [for (final j in jobs.values) if (!j.finished) j]; + if (run.isEmpty || run.any((j) => j.counting)) return null; + return run.fold(0, (a, j) => a + j.overall) / run.length; + } + /// 이미 만들어진 것 (없으면 null - 새로 만들지 않음) static CopyCenter? peekOf(AppController c) => _instance != null && _instance!.c == c ? _instance : null; bool get anyRunning => jobs.values.any((j) => !j.finished); diff --git a/lib/app/settings.dart b/lib/app/settings.dart index 904c289..8d3dd6f 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -425,6 +425,9 @@ class AppSettings { /// 82: 창 사이 끌어 놓기 - 'ask' 묻기 (처음 값, 관리자 판단) · 'copy' 복사 · 'move' 이동 String explorerDropAction = 'ask'; + + /// 85: 탐색기의 복사 · 이동 진행 패널을 숨김 (아래 작은 표시만). 처음 값: 보임 + bool explorerProgressHidden = false; bool explorerShowHidden = false; // ── 복사 · 이동 (파일 탐색기) · rsync · lsync (Rsync 화면) ── @@ -745,6 +748,7 @@ class AppSettings { 'explorerSortDesc': explorerSortDesc, 'explorerFolderSize': explorerFolderSize, 'explorerDropAction': explorerDropAction, + 'explorerProgressHidden': explorerProgressHidden, 'explorerSortPerFolder': explorerSortPerFolder, 'explorerFolderSorts': explorerFolderSorts, 'explorerShowHidden': explorerShowHidden, @@ -962,6 +966,7 @@ class AppSettings { ..explorerSortDesc = j['explorerSortDesc'] as bool? ?? false ..explorerFolderSize = const ['sort', 'always', 'off'].contains(j['explorerFolderSize']) ? j['explorerFolderSize'] as String : 'sort' ..explorerSortPerFolder = j['explorerSortPerFolder'] == true + ..explorerProgressHidden = j['explorerProgressHidden'] == true ..explorerDropAction = const ['ask', 'copy', 'move'].contains(j['explorerDropAction']) ? j['explorerDropAction'] as String : 'ask' ..explorerFolderSorts = { if (j['explorerFolderSorts'] is Map) diff --git a/lib/platform/android/android_keep_alive.dart b/lib/platform/android/android_keep_alive.dart index 145a885..3af25c4 100644 --- a/lib/platform/android/android_keep_alive.dart +++ b/lib/platform/android/android_keep_alive.dart @@ -95,9 +95,12 @@ class AndroidKeepAlive { _ai = store?..addListener(_changed); } final ai = [AiJobs.instance.statusLine, store?.statusLine].nonNulls.join(' · ').trim(); - var (text, progress) = - status(c.currentJob, c.busy, running, downloads.overallProgress, - syncs: syncs, syncing: live?.anyRunning ?? false, copies: copying, stopped: live?.problems.length ?? 0); + var (text, progress) = status(c.currentJob, c.busy, running, downloads.overallProgress, + syncs: syncs, + syncing: live?.anyRunning ?? false, + copies: copying, + copyProgress: copies?.activeProgress, + stopped: live?.problems.length ?? 0); if (ai.isNotEmpty) { // 진행 알림에 남은 장 수와 예상 시간 (발열 · 배터리 때문에 쉬어 가지는 않는다 - 사용자 결정) text = text == null ? ai : '$ai · $text'; @@ -124,18 +127,25 @@ class AndroidKeepAlive { /// 알림 글과 진행률 (0~100, 모르면 -1). 진행 중인 일이 없으면 글이 null. /// [syncs] 백그라운드로 지켜보는 실시간 동기화 수 ([syncing] 지금 맞추는 중) static (String?, int) status(String? job, bool busy, int downloads, double? downloadProgress, - {int syncs = 0, bool syncing = false, int copies = 0, int stopped = 0}) { + {int syncs = 0, bool syncing = false, int copies = 0, double? copyProgress, int stopped = 0}) { final parts = [ // 68: 원본을 읽지 못해 멈춘 동기화 (화면을 보지 않아도 알 수 있게 맨 앞에) if (stopped > 0) trf('⚠ 동기화 멈춤 {0}개 · 원본 확인 필요', [stopped]), if (busy) job ?? tr('작업 중'), - if (copies > 0) trf('복사 · 이동 {0}개', [copies]), + // 85: 복사 · 이동 진행률도 알림에 (탐색기의 진행 패널을 숨겨도 보이게) + if (copies > 0) + copyProgress == null ? trf('복사 · 이동 {0}개', [copies]) : trf('복사 · 이동 {0}개 · {1}%', [copies, (copyProgress * 100).round()]), if (downloads > 0) trf('다운로드 {0}개{1}', [downloads, downloadProgress == null ? '' : ' · ${(downloadProgress * 100).round()}%']), if (syncs > 0) syncing ? trf('동기화 {0}개 · 맞추는 중', [syncs]) : trf('동기화 {0}개 지켜보는 중', [syncs]), ]; if (parts.isEmpty) return (null, -1); - // 진행률 막대는 다운로드만 있을 때 (작업 진행률은 동영상마다 따로라 하나로 합치기 어렵다) - final p = !busy && downloadProgress != null ? (downloadProgress * 100).round().clamp(0, 100) : -1; + // 진행률 막대는 다운로드만 있을 때 (작업 진행률은 동영상마다 따로라 하나로 합치기 어렵다). + // 85: 다운로드가 없으면 복사 · 이동 진행률 + final p = !busy && downloadProgress != null + ? (downloadProgress * 100).round().clamp(0, 100) + : !busy && downloads == 0 && copyProgress != null + ? (copyProgress * 100).round().clamp(0, 100) + : -1; return (parts.join(' · '), p); } } diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index af287bc..0cfd560 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -2320,7 +2320,7 @@ class _ExplorerPageState extends State { } x.changed(); } - _snack(error is FileOpCancelled + final doneText = error is FileOpCancelled ? tr('취소했습니다.') : error != null ? trf('끝나지 못했습니다: {0}', [error]) @@ -2331,7 +2331,23 @@ class _ExplorerPageState extends State { if (!move && job.keptNewer > 0) trf('받는 쪽이 더 새것 {0}개 건너뜀', [job.keptNewer]), if (job.skipped.isNotEmpty) trf('같은 이름이라 건너뜀: {0}', [_namesOf(job.skipped)]), if (skippedHere.isNotEmpty) trf('이미 이 폴더에 있어 건너뜀: {0}', [_namesOf(skippedHere)]), - ].join(' · ')); + ].join(' · '); + // 85: 끝나면 [폴더 열기] - 앱 탐색기에서 넣은 폴더로 (그 폴더를 보고 있는 창, 없으면 지금 창) + if (error == null && mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(doneText), + action: SnackBarAction( + label: tr('폴더 열기'), + onPressed: () { + final show = _panes.where((x) => samePath(x.current, dest)).firstOrNull ?? _pane; + setState(() => _active = _panes.indexOf(show)); + unawaited(_goTo(show, dest, fresh: true)); + }, + ), + )); + } else { + _snack(doneText); + } // 82: 끌어 놓기의 이동 되돌리기 (끝난 알림 바로 뒤에 - 진행 막대가 내려가기를 기다리지 않게) if (error == null && mounted) onFinished?.call(job); // 다 되면 진행 막대가 잠깐 100% 를 보인 뒤 내려간다 @@ -2624,6 +2640,58 @@ class _ExplorerPageState extends State { /// 아래에서 올라오는 진행 막대 (위: 전체 항목 중 · 아래: 지금 폴더의 파일 중) Widget _transferPanel() { final jobs = _jobs; + // 85: 숨겼으면 오른쪽 아래 작은 표시 "복사 · 이동 n개 · 45%" (누르면 다시 펼침) + if (jobs.isNotEmpty && c.settings.explorerProgressHidden) { + return Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: ListenableBuilder( + listenable: Listenable.merge(jobs), + builder: (context, _) { + final run = [for (final j in jobs) if (!j.finished) j]; + // 관리자 판단: 숨겨도 멈춘 (실패한) 것은 분명히 - 빨간 표시 (끝난 알림 · 묻는 창은 숨김과 상관없이 늘 뜬다) + final failed = jobs.any((j) => j.finished && j.error != null && j.error is! FileOpCancelled); + final pct = run.isEmpty || run.any((j) => j.counting) + ? null + : (run.fold(0, (a, j) => a + j.overall) / run.length * 100).round(); + return Material( + elevation: 6, + color: failed ? JjColors.danger.withValues(alpha: 0.25) : JjColors.panelHigh, + borderRadius: BorderRadius.circular(18), + child: InkWell( + borderRadius: BorderRadius.circular(18), + onTap: () => c.updateSettings((x) => x.explorerProgressHidden = false), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2, value: run.isEmpty ? 1 : (pct == null ? null : pct / 100)), + ), + const SizedBox(width: 8), + Text( + failed + ? tr('복사 · 이동 멈춤 - 눌러서 보기') + : run.isEmpty + ? tr('복사 · 이동 끝') + : pct == null + ? trf('복사 · 이동 {0}개', [run.length]) + : trf('복사 · 이동 {0}개 · {1}%', [run.length, pct]), + style: const TextStyle(fontSize: 12.5), + ), + const SizedBox(width: 4), + Tooltip(message: tr('진행 패널 보기'), child: const Icon(Icons.expand_less, size: 18)), + ]), + ), + ), + ); + }, + ), + ), + ); + } return AnimatedSlide( offset: jobs.isEmpty ? const Offset(0, 1.2) : Offset.zero, duration: const Duration(milliseconds: 250), @@ -2641,6 +2709,19 @@ class _ExplorerPageState extends State { constraints: BoxConstraints(maxHeight: MediaQuery.sizeOf(context).height * 0.45), child: SingleChildScrollView( child: Column(mainAxisSize: MainAxisSize.min, children: [ + // 85: 숨기기 (아래 작은 표시로) - 다른 파일을 보면서 기다릴 수 있게. Android 는 알림줄에도 진행률 + Align( + alignment: Alignment.centerRight, + child: SizedBox( + height: 28, + child: TextButton.icon( + style: TextButton.styleFrom(visualDensity: VisualDensity.compact), + onPressed: () => c.updateSettings((x) => x.explorerProgressHidden = true), + icon: const Icon(Icons.expand_more, size: 18), + label: Text(tr('숨기기'), style: const TextStyle(fontSize: 12)), + ), + ), + ), for (final (k, job) in jobs.indexed) ...[ if (k > 0) const Divider(height: 18), ListenableBuilder(listenable: job, builder: (context, _) => _jobView(job)), diff --git a/test/explorer_test.dart b/test/explorer_test.dart index aedb224..bb9b82b 100644 --- a/test/explorer_test.dart +++ b/test/explorer_test.dart @@ -1102,6 +1102,32 @@ void main() { }); }); + testWidgets('85: 진행 패널 [숨기기] → 아래 작은 표시 (누르면 다시) · 끝난 알림의 [폴더 열기]', (tester) async { + await open(tester); + await act(tester, () => tester.tap(find.text('doc.txt'))); + await settle(tester, () => false, rounds: 15); + await act(tester, () => tester.tap(find.text('복사').first)); + await calm(tester); + await act(tester, () => tester.tap(find.widgetWithText(FilledButton, '복사'))); + await settle(tester, () => File(p.join(right, 'doc.txt')).existsSync() && find.text('폴더 열기').evaluate().isNotEmpty); + expect(find.text('폴더 열기'), findsOneWidget, reason: '끝난 알림에 [폴더 열기]'); + // 패널은 끝난 뒤 잠깐 남는다 - [숨기기] + expect(find.text('숨기기'), findsOneWidget); + await act(tester, () => tester.tap(find.text('숨기기'))); + await calm(tester); + expect(c.settings.explorerProgressHidden, isTrue, reason: '숨김은 기억'); + expect(find.text('숨기기'), findsNothing); + expect(find.text('복사 · 이동 끝'), findsOneWidget, reason: '아래 작은 표시'); + // (아래 알림이 잠깐 작은 표시를 덮는다 - 알림을 닫고 누름) + ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); + await calm(tester); + await act(tester, () => tester.tap(find.text('복사 · 이동 끝'))); + await calm(tester); + expect(c.settings.explorerProgressHidden, isFalse, reason: '누르면 다시 펼침'); + ScaffoldMessenger.of(tester.element(find.byType(ExplorerPage))).clearSnackBars(); + await calm(tester); + }); + testWidgets('81: Space 로 빠른 보기 (글 파일 앞부분) · 필터에 치는 중에는 그냥 빈칸 · TC 는 F3', (tester) async { c.settings.explorerStyle = 'windows'; await open(tester); diff --git a/test/keep_alive_test.dart b/test/keep_alive_test.dart index 9f70153..ca8014c 100644 --- a/test/keep_alive_test.dart +++ b/test/keep_alive_test.dart @@ -14,5 +14,10 @@ void main() { // 백그라운드로 실행: 실시간 동기화를 지켜보는 동안에도 expect(AndroidKeepAlive.status(null, false, 0, null, syncs: 2), ('동기화 2개 지켜보는 중', -1)); expect(AndroidKeepAlive.status('MKV 만들기', true, 0, null, syncs: 1, syncing: true), ('MKV 만들기 · 동기화 1개 · 맞추는 중', -1)); + // 85: 복사 · 이동 진행률 - 다운로드가 없으면 막대도 + expect(AndroidKeepAlive.status(null, false, 0, null, copies: 1, copyProgress: 0.4), ('복사 · 이동 1개 · 40%', 40)); + expect(AndroidKeepAlive.status(null, false, 0, null, copies: 2), ('복사 · 이동 2개', -1), reason: '세는 중 (모름)'); + expect(AndroidKeepAlive.status(null, false, 1, 0.5, copies: 1, copyProgress: 0.1), ('복사 · 이동 1개 · 10% · 다운로드 1개 · 50%', 50), + reason: '막대는 다운로드가 먼저'); }); }