이동 경고 문구 (222) · 자동 두 쪽 나누기는 두 쪽 스캔일 때만 (223) · 보는 동안 지운 그림 (224)

- 222 (관리자 지시): 원본이 대상 안일 때 이동이면 "옮겨집니다 (원본 파일은 지워지고, 원본 폴더는 아래에서 고른 대로)" - 예전에는 이동일 때도 "복사됩니다 · 원본 폴더 그대로".
- 223 (관리자 지시): 보기 화면 "두 쪽 나눠 보기: 자동" 이 평범한 가로 사진도 반으로 잘랐다. ZIP · CBZ · PDF 묶음의 가로 그림은 예전처럼 나누고,
  폴더의 그림은 다른 그림 대부분 (3/4 이상, 2장 이상) 이 세로일 때만 나눈다. 나눠 볼 때 위 막대 맨 앞에 [두 쪽 나눔 끄기]. 설정 값 · 처음 값은 그대로.
- 224 (관리자 지시): 보기 화면이 앱으로 돌아올 때 폴더의 그림 중 없어진 것을 목록에서 빼고, 보던 그림이 없어졌으면 "파일이 없어졌습니다" 와 다음 그림, 다 없으면 닫는다.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix_mkv
RyuJongJin 2026-10-11 21:48:10 +09:00
parent 3ffaf23955
commit f59e2f75a1
8 changed files with 183 additions and 5 deletions

View File

@ -750,9 +750,11 @@
"되살리고 다시 시작": "Restore and restart",
"되살리기": "Restore",
"두 번 넣은 비밀번호가 다릅니다": "The two passwords don't match",
"두 쪽 나눔 끄기 (한 장으로 보기)": "Turn off two-page split (show as one)",
"두 쪽 나눠 보기": "Split two-page spreads",
"두 쪽 나눠 보기: 끄기 (누르면 자동)": "Split two-page spreads: off (tap for auto)",
"두 쪽 나눠 보기: 자동 - 가로로 긴 그림만 (누르면 켜기)": "Split two-page spreads: auto - wide images only (tap to turn on)",
"두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)": "Two-page split: Auto - only images that look like two-page scans (tap to turn on)",
"두 쪽 나눠 보기: 켜기 (누르면 끄기)": "Split two-page spreads: on (tap to turn off)",
"두 창": "Two panes",
"두 창 배치": "Two-pane arrangement",
@ -1368,6 +1370,7 @@
"원본이 대상 안에 있어도 맞추기 (안쪽 → 바깥)": "Sync even when the source is inside the target (inner → outer)",
"원본이 대상 안에 있으면 지우기 (--delete) 를 함께 쓸 수 없습니다 (대상의 다른 파일이 모두 지워집니다).": "When the source is inside the target, delete (--delete) can't be used (all other files in the target would be deleted).",
"원본이 대상 안에 있으면 지우기 포함으로 맞출 수 없습니다": "Can't sync with delete when the source is inside the target",
"원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래로 옮겨집니다 (원본 파일은 지워지고, 원본 폴더는 아래에서 고른 대로).": "The source is inside the destination folder: its contents are moved directly into the destination folder (source files are removed; the source folder is handled as chosen below).",
"원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (원본 폴더는 그대로 남음).": "The source is inside the target folder: its contents are copied directly under the target folder (the source folder stays).",
"원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (지우기는 쓸 수 없음).": "The source is inside the target folder: its contents are copied directly under the target folder (delete can't be used).",
"원본이 바뀌면 대상에 맞춥니다. 일정을 정하면 그 시간에만 맞추고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다.": "When the source changes, the target is synced. With a schedule, sync happens only in that time; changes outside it are collected and synced at the next start time.",
@ -1907,6 +1910,7 @@
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "Files are not shown: \"All files access\" is not allowed",
"파일이 손상되었습니다 (SHA-256 다름): {0}": "The file is damaged (SHA-256 mismatch): {0}",
"파일이 없습니다 (지워졌거나 옮겨짐)": "File not found (deleted or moved)",
"파일이 없어졌습니다": "The file is gone",
"팝업 끄기": "Close pop-up",
"팝업 보기": "Pop-up view",
"패키지 라이선스": "Package licenses",

View File

@ -750,9 +750,11 @@
"되살리고 다시 시작": "復元して再起動",
"되살리기": "元に戻す",
"두 번 넣은 비밀번호가 다릅니다": "2 回入力したパスワードが違います",
"두 쪽 나눔 끄기 (한 장으로 보기)": "見開き分割をオフ (1 枚で表示)",
"두 쪽 나눠 보기": "見開きを分けて表示",
"두 쪽 나눠 보기: 끄기 (누르면 자동)": "見開きを分けて表示: オフ (押すと自動)",
"두 쪽 나눠 보기: 자동 - 가로로 긴 그림만 (누르면 켜기)": "見開きを分けて表示: 自動 - 横長の画像のみ (押すとオン)",
"두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)": "見開き分割: 自動 - 見開きスキャンらしい画像だけ (押すとオン)",
"두 쪽 나눠 보기: 켜기 (누르면 끄기)": "見開きを分けて表示: オン (押すとオフ)",
"두 창": "2 ペイン",
"두 창 배치": "2 ペインの配置",
@ -1368,6 +1370,7 @@
"원본이 대상 안에 있어도 맞추기 (안쪽 → 바깥)": "元が対象の中にあっても同期 (内側 → 外側)",
"원본이 대상 안에 있으면 지우기 (--delete) 를 함께 쓸 수 없습니다 (대상의 다른 파일이 모두 지워집니다).": "元が対象の中にある場合、削除 (--delete) は使えません (対象の他のファイルがすべて削除されます)。",
"원본이 대상 안에 있으면 지우기 포함으로 맞출 수 없습니다": "元が対象の中にある場合、削除を含めて同期できません",
"원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래로 옮겨집니다 (원본 파일은 지워지고, 원본 폴더는 아래에서 고른 대로).": "元のフォルダーが移動先の中にあります: 中身は移動先フォルダーの直下に移動します (元のファイルは削除され、元のフォルダーは下で選んだとおり)。",
"원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (원본 폴더는 그대로 남음).": "元が対象フォルダーの中にあります: 元の内容が対象フォルダーの直下にコピーされます (元フォルダーはそのまま)。",
"원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (지우기는 쓸 수 없음).": "元が対象フォルダーの中にあります: 元の内容が対象フォルダーの直下にコピーされます (削除は使えません)。",
"원본이 바뀌면 대상에 맞춥니다. 일정을 정하면 그 시간에만 맞추고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다.": "元が変わると対象に合わせます。予定を決めるとその時間だけ同期し、それ以外は変更を貯めて次の開始時刻に同期します。",
@ -1907,6 +1910,7 @@
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "ファイルが表示されません: 「すべてのファイルへのアクセス」権限がありません",
"파일이 손상되었습니다 (SHA-256 다름): {0}": "ファイルが破損しています (SHA-256 不一致): {0}",
"파일이 없습니다 (지워졌거나 옮겨짐)": "ファイルがありません (削除または移動)",
"파일이 없어졌습니다": "ファイルがなくなりました",
"팝업 끄기": "ポップアップを閉じる",
"팝업 보기": "ポップアップ表示",
"패키지 라이선스": "パッケージのライセンス",

View File

@ -750,9 +750,11 @@
"되살리고 다시 시작": "恢复并重新启动",
"되살리기": "恢复",
"두 번 넣은 비밀번호가 다릅니다": "两次输入的密码不一致",
"두 쪽 나눔 끄기 (한 장으로 보기)": "关闭双页拆分(整张显示)",
"두 쪽 나눠 보기": "拆分双页",
"두 쪽 나눠 보기: 끄기 (누르면 자동)": "拆分双页: 关闭 (点按切换为自动)",
"두 쪽 나눠 보기: 자동 - 가로로 긴 그림만 (누르면 켜기)": "拆分双页: 自动 - 仅横向图片 (点按开启)",
"두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)": "双页拆分:自动 - 仅拆分看起来像双页扫描的图片(点按开启)",
"두 쪽 나눠 보기: 켜기 (누르면 끄기)": "拆分双页: 开启 (点按关闭)",
"두 창": "双窗格",
"두 창 배치": "双窗格排列",
@ -1368,6 +1370,7 @@
"원본이 대상 안에 있어도 맞추기 (안쪽 → 바깥)": "源位于目标内时也同步 (内 → 外)",
"원본이 대상 안에 있으면 지우기 (--delete) 를 함께 쓸 수 없습니다 (대상의 다른 파일이 모두 지워집니다).": "源位于目标内时不能使用删除 (--delete) (会删除目标中的其他所有文件)。",
"원본이 대상 안에 있으면 지우기 포함으로 맞출 수 없습니다": "源位于目标内时无法包含删除同步",
"원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래로 옮겨집니다 (원본 파일은 지워지고, 원본 폴더는 아래에서 고른 대로).": "源文件夹位于目标文件夹内:其内容将移到目标文件夹下(源文件会被删除,源文件夹按下方的选择处理)。",
"원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (원본 폴더는 그대로 남음).": "源位于目标文件夹内: 源的内容会复制到目标文件夹下 (源文件夹保留)。",
"원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (지우기는 쓸 수 없음).": "源位于目标文件夹内: 源的内容会复制到目标文件夹下 (不能使用删除)。",
"원본이 바뀌면 대상에 맞춥니다. 일정을 정하면 그 시간에만 맞추고, 그 밖의 시간에는 바뀐 것을 모아 두었다가 다음 시작 시간에 맞춥니다.": "源变化时同步到目标。设定计划后只在该时间同步,其他时间的变化会累积到下次开始时间再同步。",
@ -1907,6 +1910,7 @@
"파일이 보이지 않습니다: \"모든 파일에 대한 접근\" 권한이 없습니다": "文件不可见: 没有“所有文件访问”权限",
"파일이 손상되었습니다 (SHA-256 다름): {0}": "文件已损坏(SHA-256 不一致): {0}",
"파일이 없습니다 (지워졌거나 옮겨짐)": "找不到文件 (已删除或移动)",
"파일이 없어졌습니다": "文件已不存在",
"팝업 끄기": "关闭弹出窗口",
"팝업 보기": "弹出窗口观看",
"패키지 라이선스": "软件包许可",

View File

@ -2112,8 +2112,12 @@ class _ExplorerPageState extends State<ExplorerPage> {
],
Text(tr('폴더 안의 것을 맞춥니다 (대상에 같은 이름의 폴더를 새로 만들지 않음).'),
style: const TextStyle(fontSize: 12, color: JjColors.textDim)),
// 222: 이동이면 동작대로 (예전에는 이동일 때도 "복사 · 원본 폴더는 그대로" 라고 썼다)
if (nested)
Text(tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (원본 폴더는 그대로 남음).'),
Text(
removeSource && !both
? tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래로 옮겨집니다 (원본 파일은 지워지고, 원본 폴더는 아래에서 고른 대로).')
: tr('원본이 대상 폴더 안에 있습니다: 원본의 내용이 대상 폴더 바로 아래에 복사됩니다 (원본 폴더는 그대로 남음).'),
style: const TextStyle(fontSize: 12, color: Colors.orangeAccent)),
// 1280×720 등 낮은 화면에서도 보이게: 원본 지우기 체크 · 경고는 비교 목록보다 위에 (178)
if (both) ...[

View File

@ -29,6 +29,17 @@ typedef ReaderSlot = ({int page, int part});
/// (오른쪽에서 왼쪽으로 넘기기를 켜면 반대). 좌우로 밀어도 넘어간다.
/// - 위 막대: 계속 보기 (117) · 두 쪽 나누기 (119) · 회전 · 상하 반전 (120) · 한 쪽 맞추기 · 좌우 맞추기, 넘기는 방향,
/// 어둡게 · 밝게, (PDF) 페이지 지우기 · 그림 넣기 · 저장. 좁으면 못 들어간 버튼은 ⋮ 메뉴로.
/// 223: "두 쪽 나눠 보기: 자동" 이 이 장 ([size], 회전 반영) 을 나눌지 - 두 쪽 스캔일 가능성이 높을 때만.
/// [bundle] (ZIP · CBZ · PDF 로 연 묶음): 가로로 긴 그림은 나눈다 (만화 두 쪽 스캔이 흔함).
/// 폴더의 그림들: 같은 폴더의 다른 그림 ([others], 크기를 아는 것) 대부분 (3/4 이상, 2장 이상) 이 세로인데 이것만 가로일 때만 -
/// 평범한 가로 사진은 나누지 않는다 (예전에는 모두 반으로 잘라 "그림이 잘렸다" 고 보였다)
bool autoSplitsPage(Size size, List<Size> others, {required bool bundle}) {
if (size.width <= size.height) return false;
if (bundle) return true;
final tall = others.where((s) => s.height > s.width).length;
return others.length >= 2 && tall * 4 >= others.length * 3;
}
class ReaderPage extends StatefulWidget {
final AppController c;
final ReaderSource source;
@ -64,6 +75,59 @@ class _ReaderPageState extends State<ReaderPage> {
_index = i < 0 ? 0 : i;
_pages = PageController(initialPage: _index);
_applySystemBars();
// 224: 앱으로 돌아올 때 그사이 밖에서 지운 그림을 목록에서 뺀다
_life = AppLifecycleListener(onResume: () => unawaited(dropMissingFiles()));
}
late final AppLifecycleListener _life;
/// 224: 폴더의 그림 중 없어진 것 (앱 밖에서 지움) 을 목록에서 뺀다. 보던 그림이 없어졌으면 알리고 다음 그림으로,
/// 남은 것이 없으면 닫는다 (예전에는 지운 그림이 그대로 보였다). 시험에서도 부른다
@visibleForTesting
Future<void> dropMissingFiles() async {
final s = src;
if (s is! ImageFilesSource) return;
final gone = <int>{
for (var i = 0; i < s.paths.length; i++)
if (vMissingSync(s.paths[i])) i,
};
if (gone.isEmpty || !mounted) return;
final curPage = _cur.page;
final curGone = gone.contains(curPage);
// 보던 그림 (없어졌으면 그 뒤의 남은 그림) 의 새 번호
var keep = curPage;
while (keep < s.paths.length && gone.contains(keep)) {
keep++;
}
if (keep >= s.paths.length) {
keep = curPage;
while (keep >= 0 && gone.contains(keep)) {
keep--;
}
}
final newPage = keep < 0 ? -1 : keep - gone.where((g) => g < keep).length;
for (final g in gone.toList()..sort((a, b) => b - a)) {
s.paths.removeAt(g);
}
for (final f in [..._cache.values, ..._cacheHalf.values]) {
unawaited(f.then((x) => x.dispose(), onError: (_) {}));
}
_cache.clear();
_cacheHalf.clear();
_dims.clear();
if (s.paths.isEmpty) {
_snack(tr('파일이 없어졌습니다'));
Navigator.of(context).maybePop();
return;
}
setState(() {
_v = const [];
_rebuildPages();
final i = _v.indexWhere((e) => e.page == newPage);
_index = i < 0 ? 0 : i;
});
_jumpLater();
if (curGone) _snack(tr('파일이 없어졌습니다'));
}
/// 보기 화면은 위 · 아래 시스템 막대까지 그림으로 (30: 설정으로 막대를 그대로 둘 수 있다)
@ -83,7 +147,7 @@ class _ReaderPageState extends State<ReaderPage> {
int _turns = 0;
bool _flipV = false;
/// 이 장을 반씩 나눠 볼지 (자동: 회전한 뒤 가로가 세로보다 긴 그림만)
/// 이 장을 반씩 나눠 볼지. 자동은 두 쪽 스캔일 가능성이 높을 때만 ([autoSplitsPage])
bool _splits(int page) {
switch (c.settings.readerSplit) {
case 'on':
@ -93,7 +157,19 @@ class _ReaderPageState extends State<ReaderPage> {
}
final d = _dims[page];
if (d == null) return false;
return _turns.isOdd ? d.height > d.width : d.width > d.height;
Size turned(Size s) => _turns.isOdd ? Size(s.height, s.width) : s;
return autoSplitsPage(turned(d), [for (final e in _dims.entries) if (e.key != page) turned(e.value)],
bundle: src is! ImageFilesSource);
}
/// 223: 나눠 본 장에서 바로 한 장으로 (설정을 "끄기" 로)
Future<void> _splitOff() async {
await c.updateSettings((x) => x.readerSplit = 'off');
if (!mounted) return;
setState(() {
_resetZoom();
_rebuildPages();
});
}
/// 보이는 쪽 목록을 다시 만든다. 보던 장은 그대로 보이게 (쪽 번호가 밀리면 그 자리로 옮긴다)
@ -259,6 +335,7 @@ class _ReaderPageState extends State<ReaderPage> {
@override
void dispose() {
_life.dispose();
_timer?.cancel();
if (_playing) unawaited(ReaderPage.keepScreenOn(false).catchError((_) {}));
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
@ -508,9 +585,11 @@ class _ReaderPageState extends State<ReaderPage> {
final split = switch (s.readerSplit) {
'on' => tr('두 쪽 나눠 보기: 켜기 (누르면 끄기)'),
'off' => tr('두 쪽 나눠 보기: 끄기 (누르면 자동)'),
_ => tr('두 쪽 나눠 보기: 자동 - 가로로 긴 그림만 (누르면 켜기)'),
_ => tr('두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)'),
};
return [
// 223: 나눠 보고 있으면 바로 한 장으로 (자동이 사진을 잘라 보인다고 여기지 않게 맨 앞에)
if (_cur.part >= 0) one(_Act(const Icon(Icons.crop_original), tr('두 쪽 나눔 끄기 (한 장으로 보기)'), _splitOff)),
// 117: 계속 보기 · 간격 (▲ 늘리기 · ▼ 줄이기)
one(_Act(Icon(_playing ? Icons.pause : Icons.play_arrow),
_playing ? tr('계속 보기 멈추기') : trf('계속 보기 ({0}초마다 다음 장)', [secs]), () => _play(!_playing),

View File

@ -218,8 +218,12 @@ void main() {
await settle(tester, () => find.text('rsync').evaluate().isNotEmpty && find.textContaining('비교하는 중').evaluate().isEmpty,
rounds: 300);
expect(find.text('rsync'), findsWidgets);
// 222: 원본이 대상 안 - 복사면 "복사됩니다 · 원본 폴더 그대로", 이동이면 "옮겨집니다 · 원본 파일은 지워짐"
expect(find.textContaining('바로 아래에 복사됩니다'), findsOneWidget);
await tester.tap(find.text('원본 파일 지우기 (--remove-source-files)'));
await tester.pump();
expect(find.textContaining('바로 아래로 옮겨집니다 (원본 파일은 지워지고'), findsOneWidget);
expect(find.textContaining('바로 아래에 복사됩니다'), findsNothing);
expect(find.text('원본이 대상 안에 있으면 지우기 (--delete) 를 함께 쓸 수 없습니다 (대상의 다른 파일이 모두 지워집니다).'), findsOneWidget);
expect(tester.widget<FilledButton>(find.widgetWithText(FilledButton, '이동')).onPressed, isNull);
await tester.tap(find.text('취소').last);

View File

@ -28,6 +28,14 @@ class _Sized extends ReaderSource {
}
}
/// 223: 폴더의 그림들처럼 (자동 나누기는 다른 그림과 견주어 판단)
class _FolderSized extends ImageFilesSource {
final List<ui.Image> images;
_FolderSized(this.images) : super([for (var i = 0; i < images.length; i++) 'p$i.png'], tempDir: '.', title: 'photos');
@override
Future<ReaderImage> load(int i, {required int maxWidth}) async => ReaderImage(image: images[i].clone());
}
void main() {
late List<bool> screenOn;
setUp(() {
@ -90,7 +98,7 @@ void main() {
expect(find.text('2-2 / 3장'), findsOneWidget);
// 버튼: 자동 → 켜기 (모든 장) → 끄기 → 자동
await t.tap(find.byTooltip('두 쪽 나눠 보기: 자동 - 가로로 긴 그림만 (누르면 켜기)'));
await t.tap(find.byTooltip('두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)'));
await t.pumpAndSettle();
expect(c.settings.readerSplit, 'on');
expect(find.text('2-2 / 3장'), findsOneWidget, reason: '보던 쪽 그대로');
@ -105,6 +113,38 @@ void main() {
expect(c.settings.readerSplit, 'auto');
});
test('223: 자동 나누기 판단 - 묶음 (ZIP · CBZ · PDF) 의 가로 그림은 나눔, 폴더는 다른 그림 대부분이 세로일 때만', () {
const wide = Size(480, 320), tall = Size(320, 480);
expect(autoSplitsPage(wide, const [], bundle: true), isTrue);
expect(autoSplitsPage(tall, const [], bundle: true), isFalse);
// 사진 폴더: 가로 사진 한 장 · 가로 사진들 사이 → 나누지 않음
expect(autoSplitsPage(wide, const [], bundle: false), isFalse);
expect(autoSplitsPage(wide, const [wide, wide, tall], bundle: false), isFalse);
// 만화 폴더 (낱장 세로들 사이의 펼침 한 장) → 나눔
expect(autoSplitsPage(wide, const [tall, tall, tall, tall], bundle: false), isTrue);
expect(autoSplitsPage(wide, const [tall], bundle: false), isFalse, reason: '비교할 그림이 너무 적으면 나누지 않음');
});
testWidgets('223: 폴더의 평범한 가로 사진 (480×320) 은 자동이어도 반으로 자르지 않는다', (t) async {
final c = controller();
final images = <ui.Image>[];
await t.runAsync(() async => images.add(await createTestImage(width: 480, height: 320)));
await open(t, c, _FolderSized(images));
expect(find.text('1 / 1'), findsOneWidget, reason: '"1-1 / 1장" 처럼 나뉘지 않음');
expect(find.byTooltip('두 쪽 나눔 끄기 (한 장으로 보기)'), findsNothing);
});
testWidgets('223: 나눠 본 장에서는 위 막대의 [두 쪽 나눔 끄기] 로 바로 한 장으로', (t) async {
final c = controller();
final src = await source(t, [(200, 100)]);
await open(t, c, src);
expect(find.text('1-1 / 1장'), findsOneWidget);
await t.tap(find.byTooltip('두 쪽 나눔 끄기 (한 장으로 보기)'));
await t.pumpAndSettle();
expect(c.settings.readerSplit, 'off');
expect(find.text('1 / 1'), findsOneWidget);
});
testWidgets('119: 오른쪽 → 왼쪽으로 읽으면 오른쪽 반부터', (t) async {
final c = controller()..settings.readerRtl = true;
final src = await source(t, [(200, 100)]);

View File

@ -69,6 +69,45 @@ void main() {
expect(AppSettings.fromJson({}).imageExts, defaultImageExtensions);
});
testWidgets('224: 보는 동안 밖에서 지운 그림 - 앱으로 돌아오면 "파일이 없어졌습니다" 와 다음 그림, 다 없어지면 닫는다', (tester) async {
tester.view.physicalSize = const Size(900, 1200);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final files = [
for (final (i, n) in ['a.png', 'b.png', 'c.png'].indexed)
(File(p.join(tmp.path, n))..writeAsBytesSync(_png(i * 80, 0, 0))).path,
];
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
final src = ImageFilesSource([...files], tempDir: tmp.path, title: 'imgs');
await tester.pumpWidget(MaterialApp(
home: Builder(
builder: (ctx) => TextButton(
onPressed: () => Navigator.of(ctx).push(MaterialPageRoute<void>(builder: (_) => ReaderPage(c: c, source: src, start: 1))),
child: const Text('open')))));
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
expect(find.text('2 / 3'), findsOneWidget);
expect(find.text('b.png'), findsOneWidget);
// 보던 b 를 밖에서 지움 → 돌아오면 c 로
await tester.runAsync(() => File(files[1]).delete());
await ((tester.state(find.byType(ReaderPage)) as dynamic).dropMissingFiles() as Future<void>);
await tester.pumpAndSettle();
expect(find.text('파일이 없어졌습니다'), findsOneWidget);
expect(find.text('c.png'), findsOneWidget);
expect(find.text('2 / 2'), findsOneWidget);
// 나머지도 다 지우면 보기 화면을 닫는다
await tester.runAsync(() async {
await File(files[0]).delete();
await File(files[2]).delete();
});
await ((tester.state(find.byType(ReaderPage)) as dynamic).dropMissingFiles() as Future<void>);
await tester.pumpAndSettle();
expect(find.byType(ReaderPage), findsNothing);
expect(find.text('open'), findsOneWidget);
ScaffoldMessenger.of(tester.element(find.text('open'))).clearSnackBars();
await tester.pumpAndSettle();
});
testWidgets('그림 보기: 오른쪽을 누르면 다음, 왼쪽을 누르면 이전, 가운데는 막대 숨기기 · 오른쪽→왼쪽 넘기기', (tester) async {
tester.view.physicalSize = const Size(900, 1200);
tester.view.devicePixelRatio = 1;