From 01600ec69a7b1d5b0eb3ae91285dff5c09de7258 Mon Sep 17 00:00:00 2001 From: RyuJongJin <266630912+RyuJongJin@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:14:57 +0900 Subject: [PATCH] =?UTF-8?q?66-2=20=ED=99=94=EB=A9=B4=20=EC=98=AE=EA=B8=B0?= =?UTF-8?q?=EA=B8=B0:=20=EC=A7=80=EA=B8=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=95=84=EC=9D=B4=EC=BD=98=20=ED=8C=8C=EB=9E=97=EA=B2=8C=20?= =?UTF-8?q?=C2=B7=20=EB=AF=BC=20=EC=AA=BD=EC=9C=BC=EB=A1=9C=20=EB=AF=B8?= =?UTF-8?q?=EB=81=84=EB=9F=AC=EC=A7=80=EB=8A=94=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=C2=B7=20=EC=B2=98=EC=9D=8C=EC=9C=BC=EB=A1=9C=20/=20=EB=81=9D?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=20=C2=B7=20=EB=B8=8C=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=EC=A0=80=20=EC=95=88=EB=82=B4=20=ED=95=9C=20=EC=A4=84=20(?= =?UTF-8?q?=ED=95=9C=20=EB=B2=88)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 지금 화면 아이콘: 넓은 화면도 파란 글자 + 옅은 바탕 (좁은 화면은 바탕 더함) - 좌우로 밀기 · 이전 · 다음으로 옮길 때만 민 쪽에서 미끄러져 들어옴 (NavSlideTransitionsBuilder, 그 밖은 기본 전환) - 끝에서 처음으로 넘어가면 "처음으로: …", 처음에서 끝이면 "마지막으로: …", 돌기를 끈 채 끝이면 "마지막 화면입니다" / "첫 화면입니다" - 웹 브라우저 처음 열 때 위쪽 막대 아래 한 줄 안내 (✕ 로 닫음, browserSwipeHinted) - 알림이면 다른 알림을 뒤로 밀어서 한 줄로 - 캡처 시험: 154 · 155 를 새 화면에서 알림을 지운 뒤 찍음 Co-Authored-By: Claude Opus 5.5 --- assets/l10n/en.json | 6 ++++ assets/l10n/ja.json | 6 ++++ assets/l10n/zh-Hans.json | 6 ++++ integration_test/windows_b3_shot_test.dart | 18 ++++++++-- lib/app/settings.dart | 5 +++ lib/ui/app_actions.dart | 33 +++++++++++++++++-- lib/ui/browser_page.dart | 31 ++++++++++++++++++ lib/ui/theme.dart | 38 ++++++++++++++++++++++ test/browser_test.dart | 34 +++++++++++++++++++ test/components_test.dart | 18 +++++++++- 10 files changed, 189 insertions(+), 6 deletions(-) diff --git a/assets/l10n/en.json b/assets/l10n/en.json index e074cbb..d488c8c 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -726,6 +726,8 @@ "마지막 장입니다.": "This is the last page.", "마지막 한 페이지는 지울 수 없습니다.": "The last remaining page cannot be deleted.", "마지막 화면에서 더 밀면 첫 화면으로 (끄면 끝에서 멈춤)": "Swiping past the last screen goes to the first (off: stops at the end)", + "마지막 화면입니다": "This is the last screen", + "마지막으로: {0}": "To the last: {0}", "만든 그림 · 저장: {0}": "Made images · saved in: {0}", "만들 자막 언어 (파일명_언어코드.srt)": "Subtitle languages to make (filename_languagecode.srt)", "만들기": "Make", @@ -1103,6 +1105,7 @@ "아직 없음 (rsync 로 복사할 때 · 아래 [내려받기] 로 받습니다)": "Not yet (downloaded when you copy with rsync · or press [Download])", "아직 저장하지 못했습니다. 잠시 뒤 다시 시도하세요": "Still couldn't save. Please try again in a moment", "아직 지원하지 않는 플랫폼입니다: {0}": "This platform is not supported yet: {0}", + "안내 닫기": "Close tip", "알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "Notifications show as JJ_MKVMaker and open the app when clicked (current user only, no admin rights)", "암호화 안 됨 (http)": "Not encrypted (http)", "압축 파일 안에 없습니다": "Not in the archive", @@ -1262,6 +1265,7 @@ "웹 브라우저 (지금 여기)": "Web browser (you are here)", "웹 브라우저 (홈 주소)": "Web browser (home page)", "웹 브라우저(Edge WebView2)를 시작할 수 없습니다.\n{0}": "Cannot start the web browser (Edge WebView2).\n{0}", + "웹 브라우저에서는 위쪽 막대를 좌우로 밀면 다른 화면으로 옮깁니다 (가운데는 페이지가 밀립니다).": "In the web browser, swipe the top bar left or right to switch screens (swiping the middle scrolls the page).", "웹 브라우저의 시작 · 🏠 주소": "Start · 🏠 page of the web browser", "웹 페이지 자동 번역": "Translate web pages automatically", "웹 페이지를 번역하지 못했습니다: {0}": "Could not translate the web page: {0}", @@ -1651,8 +1655,10 @@ "처음 쓸 때 내려받기 (기본)": "Download on first use (default)", "처음 아이콘 (파랑)": "Original icon (blue)", "처음대로": "Reset to default", + "처음으로: {0}": "Back to the first: {0}", "첫 장입니다.": "This is the first page.", "첫 줄 추가": "Add first line", + "첫 화면입니다": "This is the first screen", "청각장애인용": "Hearing impaired", "체코어": "Czech", "체크포인트 파일 이름 (예: sd_xl_turbo_1.0_fp16.safetensors)": "Checkpoint file name (e.g. sd_xl_turbo_1.0_fp16.safetensors)", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 4bcf58c..dc62649 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -726,6 +726,8 @@ "마지막 장입니다.": "最後のページです。", "마지막 한 페이지는 지울 수 없습니다.": "最後の 1 ページは削除できません。", "마지막 화면에서 더 밀면 첫 화면으로 (끄면 끝에서 멈춤)": "最後の画面からさらにスワイプすると最初の画面へ (オフ: 端で止まる)", + "마지막 화면입니다": "最後の画面です", + "마지막으로: {0}": "最後へ: {0}", "만든 그림 · 저장: {0}": "作成した画像・保存先: {0}", "만들 자막 언어 (파일명_언어코드.srt)": "作成する字幕の言語(ファイル名_言語コード.srt)", "만들기": "作成", @@ -1103,6 +1105,7 @@ "아직 없음 (rsync 로 복사할 때 · 아래 [내려받기] 로 받습니다)": "まだありません (rsync でコピーするとき · [ダウンロード] で入手)", "아직 저장하지 못했습니다. 잠시 뒤 다시 시도하세요": "まだ保存できませんでした。しばらくしてからもう一度お試しください", "아직 지원하지 않는 플랫폼입니다: {0}": "まだ対応していないプラットフォームです: {0}", + "안내 닫기": "案内を閉じる", "알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知が JJ_MKVMaker の名前で表示され、クリックするとアプリが開きます (管理者権限なしで現在のユーザーのみ)", "암호화 안 됨 (http)": "暗号化なし (http)", "압축 파일 안에 없습니다": "圧縮ファイル内にありません", @@ -1262,6 +1265,7 @@ "웹 브라우저 (지금 여기)": "ウェブブラウザー(現在地)", "웹 브라우저 (홈 주소)": "ウェブブラウザー(ホームページ)", "웹 브라우저(Edge WebView2)를 시작할 수 없습니다.\n{0}": "ウェブブラウザー(Edge WebView2)を起動できません。\n{0}", + "웹 브라우저에서는 위쪽 막대를 좌우로 밀면 다른 화면으로 옮깁니다 (가운데는 페이지가 밀립니다).": "ウェブブラウザーでは上部のバーを左右にスワイプすると別の画面に移ります (中央はページがスクロールします)。", "웹 브라우저의 시작 · 🏠 주소": "ウェブブラウザーの開始・🏠 ページ", "웹 페이지 자동 번역": "ウェブページを自動翻訳", "웹 페이지를 번역하지 못했습니다: {0}": "ウェブページを翻訳できませんでした: {0}", @@ -1651,8 +1655,10 @@ "처음 쓸 때 내려받기 (기본)": "初めて使うときにダウンロード (既定)", "처음 아이콘 (파랑)": "最初のアイコン(青)", "처음대로": "既定に戻す", + "처음으로: {0}": "最初へ: {0}", "첫 장입니다.": "最初のページです。", "첫 줄 추가": "最初の行を追加", + "첫 화면입니다": "最初の画面です", "청각장애인용": "聴覚障害者用", "체코어": "チェコ語", "체크포인트 파일 이름 (예: sd_xl_turbo_1.0_fp16.safetensors)": "チェックポイントのファイル名 (例: sd_xl_turbo_1.0_fp16.safetensors)", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index ba07acd..3344213 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -726,6 +726,8 @@ "마지막 장입니다.": "已是最后一页。", "마지막 한 페이지는 지울 수 없습니다.": "无法删除仅剩的一页。", "마지막 화면에서 더 밀면 첫 화면으로 (끄면 끝에서 멈춤)": "在最后一个界面继续滑动会回到第一个界面 (关闭: 停在末尾)", + "마지막 화면입니다": "这是最后一个画面", + "마지막으로: {0}": "到最后: {0}", "만든 그림 · 저장: {0}": "生成的图片 · 保存位置: {0}", "만들 자막 언어 (파일명_언어코드.srt)": "要生成的字幕语言(文件名_语言代码.srt)", "만들기": "生成", @@ -1103,6 +1105,7 @@ "아직 없음 (rsync 로 복사할 때 · 아래 [내려받기] 로 받습니다)": "尚未安装 (用 rsync 复制时 · 或按 [下载] 获取)", "아직 저장하지 못했습니다. 잠시 뒤 다시 시도하세요": "仍未能保存。请稍后再试", "아직 지원하지 않는 플랫폼입니다: {0}": "尚不支持此平台: {0}", + "안내 닫기": "关闭提示", "알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知以 JJ_MKVMaker 名义显示,点击即可打开应用 (仅当前用户,无需管理员权限)", "암호화 안 됨 (http)": "未加密 (http)", "압축 파일 안에 없습니다": "压缩文件中没有", @@ -1262,6 +1265,7 @@ "웹 브라우저 (지금 여기)": "网页浏览器(当前位置)", "웹 브라우저 (홈 주소)": "网页浏览器(主页)", "웹 브라우저(Edge WebView2)를 시작할 수 없습니다.\n{0}": "无法启动网页浏览器(Edge WebView2)。\n{0}", + "웹 브라우저에서는 위쪽 막대를 좌우로 밀면 다른 화면으로 옮깁니다 (가운데는 페이지가 밀립니다).": "在网页浏览器中,左右滑动顶部栏可切换画面 (中间滑动的是页面)。", "웹 브라우저의 시작 · 🏠 주소": "网页浏览器的起始 · 🏠 页面", "웹 페이지 자동 번역": "自动翻译网页", "웹 페이지를 번역하지 못했습니다: {0}": "无法翻译网页: {0}", @@ -1651,8 +1655,10 @@ "처음 쓸 때 내려받기 (기본)": "首次使用时下载 (默认)", "처음 아이콘 (파랑)": "最初的图标(蓝色)", "처음대로": "恢复默认", + "처음으로: {0}": "回到第一个: {0}", "첫 장입니다.": "已是第一页。", "첫 줄 추가": "添加第一行", + "첫 화면입니다": "这是第一个画面", "청각장애인용": "听障字幕", "체코어": "捷克语", "체크포인트 파일 이름 (예: sd_xl_turbo_1.0_fp16.safetensors)": "检查点文件名 (例: sd_xl_turbo_1.0_fp16.safetensors)", diff --git a/integration_test/windows_b3_shot_test.dart b/integration_test/windows_b3_shot_test.dart index e8bdf3e..d0bc981 100644 --- a/integration_test/windows_b3_shot_test.dart +++ b/integration_test/windows_b3_shot_test.dart @@ -121,12 +121,26 @@ void main() { await until(t, () => find.byType(SnackBar).evaluate().isNotEmpty); await shot(t, 'b3_48_done'); expect(File(p.join(right, 'doc.txt')).readAsStringSync(), 'new'); - // 처음 열고 아무것도 고르지 않고 [복사] + }); + + testWidgets('154 · 155: 처음 열고 아무것도 고르지 않고 [복사] · [이동] → "…할 항목을 고르세요"', (t) async { await show(t, ExplorerPage(c: c)); await until(t, () => find.text('doc.txt').evaluate().isNotEmpty); + final messenger = ScaffoldMessenger.of(t.element(find.byType(ExplorerPage))); + messenger.clearSnackBars(); // 앞 단계의 알림이 남지 않게 + await settle(t, 2); await t.tap(find.text('복사').first); - await settle(t, 3); + await until(t, () => find.textContaining('복사할 항목을 고르세요').evaluate().isNotEmpty); + expect(find.textContaining('복사할 항목을 고르세요'), findsOneWidget); await shot(t, 'b3_154_nothing_selected'); + messenger.clearSnackBars(); + await settle(t, 2); + await t.tap(find.text('이동').first); + await until(t, () => find.textContaining('옮길 항목을 고르세요').evaluate().isNotEmpty); + expect(find.textContaining('옮길 항목을 고르세요'), findsOneWidget); + await shot(t, 'b3_155_nothing_selected_move'); + messenger.clearSnackBars(); + await settle(t, 2); }); testWidgets('52: 복사 중에 다른 화면에 갔다 와도 진행 막대', (t) async { diff --git a/lib/app/settings.dart b/lib/app/settings.dart index ee15e41..c2447f7 100644 --- a/lib/app/settings.dart +++ b/lib/app/settings.dart @@ -412,6 +412,9 @@ class AppSettings { /// 66: 좌우로 밀 때 끝에서 처음으로 (끄면 끝에서 멈춤) bool swipeWrap = true; + /// 66-2: 웹 브라우저에서 화면 옮기기 (위쪽 막대를 좌우로) 안내를 한 번 보였는지 + bool browserSwipeHinted = false; + /// 저장 공간 정리 창에서 미리 체크할 묶음 (작업 임시 파일 · 받다 만 다운로드) List cleanupPrechecked = ['work', 'download']; @@ -648,6 +651,7 @@ class AppSettings { 'startMenuShortcut': startMenuShortcut, 'syncStopToast': syncStopToast, 'swipeWrap': swipeWrap, + 'browserSwipeHinted': browserSwipeHinted, 'cleanupPrechecked': cleanupPrechecked, 'migrationNotice': migrationNotice, 'allowInnerToOuter': allowInnerToOuter, @@ -839,6 +843,7 @@ class AppSettings { ..startMenuShortcut = j['startMenuShortcut'] != false ..syncStopToast = j['syncStopToast'] != false ..swipeWrap = j['swipeWrap'] != false + ..browserSwipeHinted = j['browserSwipeHinted'] == true ..cleanupPrechecked = j['cleanupPrechecked'] is List ? [for (final x in j['cleanupPrechecked'] as List) '$x'] : ['work', 'download'] diff --git a/lib/ui/app_actions.dart b/lib/ui/app_actions.dart index 44e7fd8..4fe3a6c 100644 --- a/lib/ui/app_actions.dart +++ b/lib/ui/app_actions.dart @@ -171,8 +171,26 @@ class AppNavButtons extends StatelessWidget { if (i < 0) i = 0; final j = i + step; // 102 · 66: 끝에서 처음으로 돌기를 끄면 끝에서 멈춘다 - if (!scope.controller.settings.swipeWrap && (j < 0 || j >= pages.length)) return; - open(context, pages[j % pages.length].id); + final wraps = j < 0 || j >= pages.length; + if (!scope.controller.settings.swipeWrap && wraps) { + _hint(context, step > 0 ? tr('마지막 화면입니다') : tr('첫 화면입니다')); + return; + } + final target = pages[j % pages.length]; + // 66-2: 민 방향으로 미끄러져 들어오게 (다음 = 오른쪽에서, 이전 = 왼쪽에서). 이미 열린 화면으로 돌아가면 그 화면의 전환대로 + NavSlide.pending = step > 0 ? 1 : -1; + Future.delayed(const Duration(milliseconds: 600), () => NavSlide.pending = 0); + // 66-2: 끝에서 처음으로 (처음에서 끝으로) 넘어갈 때 알린다 - 어디로 갔는지 몰라 헤매지 않게 + if (wraps) { + _hint(context, step > 0 ? trf('처음으로: {0}', [tr(target.name)]) : trf('마지막으로: {0}', [tr(target.name)])); + } + open(context, target.id); + } + + static void _hint(BuildContext context, String text) { + final m = ScaffoldMessenger.maybeOf(context); + m?.hideCurrentSnackBar(); + m?.showSnackBar(SnackBar(content: Text(text), duration: const Duration(milliseconds: 1500))); } @override @@ -224,7 +242,11 @@ class AppNavButtons extends StatelessWidget { child: InkWell( onTap: f, borderRadius: BorderRadius.circular(8), - child: SizedBox( + // 66-2: 지금 화면 아이콘은 파랗게 · 뒤에 옅은 바탕 + child: Container( + decoration: here + ? BoxDecoration(color: JjColors.accent.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(8)) + : null, width: 54, height: 46, child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -249,6 +271,11 @@ class AppNavButtons extends StatelessWidget { tooltip: tip, padding: EdgeInsets.zero, isSelected: here, + // 66-2: 지금 화면 아이콘은 파랗게 · 뒤에 옅은 바탕 (넓은 화면에서도 어디인지 보이게) + style: here + ? IconButton.styleFrom( + foregroundColor: JjColors.accent, backgroundColor: JjColors.accent.withValues(alpha: 0.16)) + : null, icon: icon, onPressed: f, ), diff --git a/lib/ui/browser_page.dart b/lib/ui/browser_page.dart index f95862c..d9cdc2f 100644 --- a/lib/ui/browser_page.dart +++ b/lib/ui/browser_page.dart @@ -284,8 +284,38 @@ class _BrowserPageState extends State with RouteAware { _applyCookieExport(); widget.c.addListener(_onSettings); if (_trOn) _translateNow(); + // 66-2: 브라우저 가운데는 페이지가 좌우로 밀리므로 화면 옮기기는 위쪽 막대에서 - 처음 한 번만 알린다. + // 알림 (SnackBar) 은 다른 알림 (다운로드 등) 을 뒤로 밀어 기다리게 하므로 위쪽 막대 아래의 한 줄로 + if (s.swipeNav && !s.browserSwipeHinted) { + _swipeHint = true; + WidgetsBinding.instance.addPostFrameCallback((_) => widget.c.updateSettings((x) => x.browserSwipeHinted = true)); + } } + /// 66-2: 화면 옮기기 안내 한 줄 (처음 한 번, ✕ 로 닫음) + bool _swipeHint = false; + + Widget _swipeHintBar() => Material( + color: JjColors.accent.withValues(alpha: 0.14), + child: Padding( + padding: const EdgeInsets.fromLTRB(12, 2, 4, 2), + child: Row(children: [ + const Icon(Icons.swipe, size: 16, color: JjColors.accent), + const SizedBox(width: 8), + Expanded( + child: Text(tr('웹 브라우저에서는 위쪽 막대를 좌우로 밀면 다른 화면으로 옮깁니다 (가운데는 페이지가 밀립니다).'), + style: const TextStyle(fontSize: 12))), + IconButton( + tooltip: tr('안내 닫기'), + visualDensity: VisualDensity.compact, + iconSize: 16, + icon: const Icon(Icons.close), + onPressed: () => setState(() => _swipeHint = false), + ), + ]), + ), + ); + /// 53: 쿠키를 쓰도록 정했을 때만 · 고른 사이트만 내보낸다 void _applyCookieExport() { final s = widget.c.settings; @@ -475,6 +505,7 @@ class _BrowserPageState extends State with RouteAware { listenable: Listenable.merge([bm, widget.c]), builder: (context, _) => Column(children: [ _toolbar(), + if (_swipeHint) _swipeHintBar(), Expanded(child: Row(children: [ if (_panel) SizedBox( diff --git a/lib/ui/theme.dart b/lib/ui/theme.dart index 9a26ce6..ba172f6 100644 --- a/lib/ui/theme.dart +++ b/lib/ui/theme.dart @@ -27,6 +27,14 @@ ThemeData buildTheme() { fontFamily: 'Malgun Gothic', ); return base.copyWith( + // 66-2: 좌우로 밀어 화면을 옮길 때는 그 방향으로 미끄러지게 (그 밖의 화면 열기는 기본 전환) + pageTransitionsTheme: const PageTransitionsTheme(builders: { + TargetPlatform.android: NavSlideTransitionsBuilder(), + TargetPlatform.windows: NavSlideTransitionsBuilder(), + TargetPlatform.linux: NavSlideTransitionsBuilder(), + TargetPlatform.macOS: NavSlideTransitionsBuilder(), + TargetPlatform.iOS: NavSlideTransitionsBuilder(), + }), dividerColor: JjColors.border, textTheme: base.textTheme.apply( bodyColor: JjColors.text, @@ -48,3 +56,33 @@ ThemeData buildTheme() { ), ); } + +/// 66-2: 다음 화면으로 열리는 화면이 어느 쪽에서 들어올지 (+1 오른쪽에서 = 다음, −1 왼쪽에서 = 이전, 0 기본 전환). +/// 좌우로 밀기 · 이전 · 다음으로 옮길 때만 정하고, 화면이 열리면 (그 화면에 기억한 뒤) 0 으로 돌린다. +class NavSlide { + static int pending = 0; + static final _dir = Expando('navSlide'); +} + +class NavSlideTransitionsBuilder extends PageTransitionsBuilder { + const NavSlideTransitionsBuilder(); + + @override + Widget buildTransitions(PageRoute route, BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + // 처음 그릴 때의 방향을 그 화면에 기억한다 (전환 중에 다시 불려도 같은 방향) + var dir = NavSlide._dir[route]; + if (dir == null) { + dir = NavSlide._dir[route] = NavSlide.pending; + NavSlide.pending = 0; + } + if (dir == 0) { + return const PageTransitionsTheme().buildTransitions(route, context, animation, secondaryAnimation, child); + } + return SlideTransition( + position: Tween(begin: Offset(dir.toDouble(), 0), end: Offset.zero) + .animate(CurvedAnimation(parent: animation, curve: Curves.easeOutCubic)), + child: child, + ); + } +} diff --git a/test/browser_test.dart b/test/browser_test.dart index 6b5241c..24a2419 100644 --- a/test/browser_test.dart +++ b/test/browser_test.dart @@ -476,4 +476,38 @@ void main() { expect(b.tree.bar.children!.first.title, 'B'); expect(b.tree.bar.children!.any((n) => n.isFolder && n.title == '영상'), isTrue); }); + + testWidgets('66-2: 처음 열면 위쪽 막대 아래에 화면 옮기기 안내 한 줄 (알림을 막지 않음) · ✕ 로 닫고 다시 열면 없음', (tester) async { + tester.view.physicalSize = const Size(1500, 900); + tester.view.devicePixelRatio = 1; + addTearDown(tester.view.reset); + final dir = Directory.systemTemp.createTempSync('jj_hint_'); + addTearDown(() => dir.deleteSync(recursive: true)); + final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); + final bm = BookmarksController(p.join(dir.path, 'bookmarks.json')); + Widget page() => MaterialApp( + home: BrowserPage( + c: c, + bookmarks: bm, + initialUrl: 'https://example.com/', + viewBuilder: (h, url) { + h.attach(_Nav()); + return const ColoredBox(color: Colors.white); + }, + ), + ); + await tester.pumpWidget(page()); + await tester.pumpAndSettle(); + expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsOneWidget); + expect(find.byType(SnackBar), findsNothing, reason: '알림 (다운로드 등) 을 뒤로 밀지 않게 알림이 아니라 한 줄'); + expect(c.settings.browserSwipeHinted, isTrue); + await tester.tap(find.byTooltip('안내 닫기')); + await tester.pump(); + expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsNothing); + // 다시 열면 없음 (한 번만) + await tester.pumpWidget(const SizedBox()); + await tester.pumpWidget(page()); + await tester.pumpAndSettle(); + expect(find.textContaining('위쪽 막대를 좌우로 밀면'), findsNothing); + }); } diff --git a/test/components_test.dart b/test/components_test.dart index e7fb2d9..bb31c08 100644 --- a/test/components_test.dart +++ b/test/components_test.dart @@ -9,6 +9,7 @@ import 'package:jj_mkvmaker/services/platform_services.dart'; import 'package:jj_mkvmaker/ui/app_actions.dart'; import 'package:jj_mkvmaker/ui/component_settings.dart'; import 'package:jj_mkvmaker/ui/home_page.dart'; +import 'package:jj_mkvmaker/ui/theme.dart'; AppController _plain() => AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService())); @@ -97,12 +98,21 @@ void main() { await tester.pump(const Duration(milliseconds: 500)); } - await swipe(-600); // 왼쪽으로 → 다음 (파일 탐색기) + // 66-2: 민 쪽으로 미끄러져 들어온다 (오른쪽에서) · 지금 화면 아이콘은 파랗게 + await tester.flingFrom(const Offset(750, 450), const Offset(-600, 0), 2000); + await tester.pump(); + await tester.pump(const Duration(milliseconds: 80)); + expect(find.byType(SlideTransition), findsWidgets); + await tester.pump(const Duration(milliseconds: 500)); expect(find.byTooltip('파일 탐색기 (지금 여기)'), findsOneWidget); + final here = tester.widget(find.ancestor(of: find.byTooltip('파일 탐색기 (지금 여기)'), matching: find.byType(IconButton)).first); + expect(here.style?.foregroundColor?.resolve({}), JjColors.accent); await swipe(-600); expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget); await swipe(-600); // 마지막 다음은 처음 expect(find.byTooltip('MKV 화면 (지금 여기)'), findsOneWidget); + // 66-2: 끝에서 처음으로 넘어가면 알린다 + expect(find.textContaining('처음으로: MKV 만들기'), findsOneWidget); await swipe(600); // 오른쪽으로 → 이전 (끝으로) expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget); // 102: 끝에서 처음으로 돌기를 끄면 끝에서 멈춘다 @@ -110,6 +120,7 @@ void main() { await tester.pump(); await swipe(-600); expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget); + expect(find.textContaining('마지막 화면입니다'), findsOneWidget, reason: '끝에서 멈출 때도 왜 안 넘어가는지 알림'); await swipe(600); expect(find.byTooltip('파일 탐색기 (지금 여기)'), findsOneWidget); await swipe(-600); @@ -121,4 +132,9 @@ void main() { await swipe(600); expect(find.byTooltip('Rsync (지금 여기)'), findsOneWidget); }); + + test('66-2: 웹 브라우저 화면 옮기기 안내는 한 번만 (설정에 남음)', () { + expect(AppSettings().browserSwipeHinted, isFalse); + expect(AppSettings.fromJson((AppSettings()..browserSwipeHinted = true).toJson()).browserSwipeHinted, isTrue); + }); }