66-2 화면 옮기기: 지금 화면 아이콘 파랗게 · 민 쪽으로 미끄러지는 전환 · 처음으로 / 끝 알림 · 브라우저 안내 한 줄 (한 번)

- 지금 화면 아이콘: 넓은 화면도 파란 글자 + 옅은 바탕 (좁은 화면은 바탕 더함)
- 좌우로 밀기 · 이전 · 다음으로 옮길 때만 민 쪽에서 미끄러져 들어옴 (NavSlideTransitionsBuilder, 그 밖은 기본 전환)
- 끝에서 처음으로 넘어가면 "처음으로: …", 처음에서 끝이면 "마지막으로: …", 돌기를 끈 채 끝이면 "마지막 화면입니다" / "첫 화면입니다"
- 웹 브라우저 처음 열 때 위쪽 막대 아래 한 줄 안내 (✕ 로 닫음, browserSwipeHinted) - 알림이면 다른 알림을 뒤로 밀어서 한 줄로
- 캡처 시험: 154 · 155 를 새 화면에서 알림을 지운 뒤 찍음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
묶음3
RyuJongJin 2026-10-09 20:14:57 +09:00
parent 1450b51316
commit 01600ec69a
10 changed files with 189 additions and 6 deletions

View File

@ -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)",

View File

@ -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)",

View File

@ -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)",

View File

@ -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 {

View File

@ -412,6 +412,9 @@ class AppSettings {
/// 66: 좌우로 밀 때 끝에서 처음으로 (끄면 끝에서 멈춤)
bool swipeWrap = true;
/// 66-2: 웹 브라우저에서 화면 옮기기 (위쪽 막대를 좌우로) 안내를 한 번 보였는지
bool browserSwipeHinted = false;
/// 저장 공간 정리 창에서 미리 체크할 묶음 (작업 임시 파일 · 받다 만 다운로드)
List<String> 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']

View File

@ -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<void>.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,
),

View File

@ -284,7 +284,37 @@ class _BrowserPageState extends State<BrowserPage> 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() {
@ -475,6 +505,7 @@ class _BrowserPageState extends State<BrowserPage> with RouteAware {
listenable: Listenable.merge([bm, widget.c]),
builder: (context, _) => Column(children: [
_toolbar(),
if (_swipeHint) _swipeHintBar(),
Expanded(child: Row(children: [
if (_panel)
SizedBox(

View File

@ -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<int>('navSlide');
}
class NavSlideTransitionsBuilder extends PageTransitionsBuilder {
const NavSlideTransitionsBuilder();
@override
Widget buildTransitions<T>(PageRoute<T> route, BuildContext context, Animation<double> animation,
Animation<double> 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,
);
}
}

View File

@ -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);
});
}

View File

@ -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<IconButton>(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);
});
}