diff --git a/lib/ui/reader_page.dart b/lib/ui/reader_page.dart index 7d0d24e..05d158e 100644 --- a/lib/ui/reader_page.dart +++ b/lib/ui/reader_page.dart @@ -269,32 +269,57 @@ class _ReaderPageState extends State { /// 계속 보기를 멈추게 한 누름은 장 넘기기 · 막대 숨기기로 쓰지 않는다 bool _swallowTap = false; + /// 관리자 지시 10/20 (28 검토): 다 보인 장 ('$_version:$_index'). 계속 보기 시계는 그 장이 보인 뒤부터 잰다 - + /// WebDAV 처럼 느린 원본에서 받는 동안 시간이 흘러 보지도 못한 장을 넘겼다 + String? _shownKey; + + /// [i] 장이 화면에 다 그려졌다 (그림 첫 장면 · 실패 글). 지금 장이면 계속 보기 시계를 시작 + void _markShown(int i) { + if (!mounted || i != _index) return; + final key = '$_version:$i'; + if (_shownKey == key) return; + _shownKey = key; + if (_playing && _timer == null) _startTimer(); + } + + void _startTimer() { + _timer?.cancel(); + _timer = Timer(Duration(seconds: c.settings.readerAutoSeconds), _tick); + } + void _play(bool on) { _timer?.cancel(); _timer = null; - if (on && _v.isNotEmpty) { - _timer = Timer.periodic(Duration(seconds: c.settings.readerAutoSeconds), (_) => _tick()); - } + // 지금 장이 이미 보이면 바로 재기 시작, 아직 받는 중이면 보인 뒤에 ([_markShown]) + if (on && _v.isNotEmpty && _shownKey == '$_version:$_index') _startTimer(); if (on == _playing) return; setState(() => _playing = on); unawaited(ReaderPage.keepScreenOn(on).catchError((_) {})); } void _tick() { - if (!mounted) return; + _timer = null; + if (!mounted || !_playing) return; if (_index >= _v.length - 1) { _play(false); _snack(tr('마지막 장이라 계속 보기를 멈췄습니다.')); return; } _pages.jumpToPage(_index + 1); + // 다음 장이 이미 받아 둔 것이라 다시 그리지 않으면 그 자리에서 알린다 + WidgetsBinding.instance.addPostFrameCallback((_) => _rearmIfShown()); + } + + /// 넘긴 뒤 그 장이 곧바로 보였는데 (캐시) 알림을 놓쳤으면 시계를 다시 건다 + void _rearmIfShown() { + if (mounted && _playing && _timer == null && _shownKey == '$_version:$_index') _startTimer(); } Future _setSeconds(int d) async { await c.updateSettings((x) => x.readerAutoSeconds = (x.readerAutoSeconds + d).clamp(1, 60)); if (!mounted) return; setState(() {}); - if (_playing) _play(true); // 새 간격으로 + if (_playing) _play(true); // 새 간격으로 (지금 장이 보이면 처음부터 다시) } // ───────── 확대 (30) ───────── @@ -521,13 +546,26 @@ class _ReaderPageState extends State { key: ValueKey('$_version:${slot.page}:${slot.part}:${_retry[slot.page] ?? 0}'), future: _sharp[i] ?? (half ? _loadHalf(slot.page) : _load(slot.page)), builder: (context, snap) { - if (snap.hasError) return Center(child: failed(snap.error)); + // 계속 보기: 받는 동안은 시계를 재지 않고, 보인 (또는 실패 글이 보인) 뒤부터 + void shown() => WidgetsBinding.instance.addPostFrameCallback((_) => _markShown(i)); + if (snap.hasError) { + shown(); + return Center(child: failed(snap.error)); + } final r = snap.data; if (r == null) return const Center(child: CircularProgressIndicator()); + if (r.image != null) shown(); Widget picture = r.image != null ? RawImage(image: r.image, filterQuality: FilterQuality.medium) : Image(image: r.provider!, gaplessPlayback: true, filterQuality: FilterQuality.medium, - errorBuilder: (_, e, _) => failed(e, provider: r.provider)); + frameBuilder: (_, child, frame, sync) { + if (frame != null || sync) shown(); + return child; + }, + errorBuilder: (_, e, _) { + shown(); + return failed(e, provider: r.provider); + }); // 120: 회전 · 상하 반전 (보기만, 파일은 그대로) if (_turns != 0) picture = RotatedBox(quarterTurns: _turns, child: picture); if (_flipV) picture = Transform.flip(flipY: true, child: picture); diff --git a/test/reader_more_test.dart b/test/reader_more_test.dart index d7c7c02..1228b58 100644 --- a/test/reader_more_test.dart +++ b/test/reader_more_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -28,6 +29,18 @@ class _Sized extends ReaderSource { } } +/// 관리자 지시 10/20: WebDAV 처럼 느린 원본 - [hold] 의 장은 [release] 할 때까지 받는 중 +class _Slow extends _Sized { + final int hold; + final release = Completer(); + _Slow(super.images, this.hold); + @override + Future load(int i, {required int maxWidth}) async { + if (i == hold) await release.future; + return super.load(i, maxWidth: maxWidth); + } +} + /// 223: 폴더의 그림들처럼 (자동 나누기는 다른 그림과 견주어 판단) class _FolderSized extends ImageFilesSource { final List images; @@ -201,15 +214,49 @@ void main() { expect(find.text('1 / 4'), findsOneWidget); await t.pump(const Duration(milliseconds: 200)); expect(find.text('2 / 4'), findsOneWidget); - await t.pump(const Duration(seconds: 2)); + // 관리자 지시 10/20: 시계는 그 장이 다 보인 뒤부터 - 넘긴 뒤 한 장면 (FutureBuilder 가 그림을 받아 그림) 이 지나야 잰다 + await t.pump(); + await t.pump(const Duration(milliseconds: 1900)); + expect(find.text('2 / 4'), findsOneWidget); + await t.pump(const Duration(milliseconds: 200)); + expect(find.text('3 / 4'), findsOneWidget); + await t.pump(); await t.pump(const Duration(seconds: 2)); expect(find.text('4 / 4'), findsOneWidget); + await t.pump(); await t.pump(const Duration(seconds: 2)); expect(find.byTooltip('계속 보기 (2초마다 다음 장)'), findsOneWidget, reason: '마지막 장에서 멈춤'); expect(screenOn, [true, false]); await t.pumpAndSettle(const Duration(seconds: 3)); }); + testWidgets('117 (관리자 지시 10/20): 느린 원본 - 받는 동안은 넘기지 않고, 다 보인 뒤부터 정한 초를 잰다', (t) async { + final c = controller(); + final sized = await source(t, [(100, 200), (100, 200), (100, 200)]); + final src = _Slow(sized.images, 1); + await open(t, c, src); + await t.tap(find.byTooltip('계속 보기 (3초마다 다음 장)')); + await t.pump(); + await t.pump(const Duration(seconds: 3)); + expect(find.text('2 / 3'), findsOneWidget); + // 2 장은 받는 중 - 한참 지나도 넘기지 않는다 + for (var k = 0; k < 10; k++) { + await t.pump(const Duration(seconds: 1)); + } + expect(find.text('2 / 3'), findsOneWidget, reason: '받는 동안 시계가 흐르지 않음'); + expect(find.byType(CircularProgressIndicator), findsOneWidget); + // 다 받으면 그때부터 3초 + src.release.complete(); + await t.pump(); + await t.pump(); + await t.pump(const Duration(milliseconds: 2900)); + expect(find.text('2 / 3'), findsOneWidget); + await t.pump(const Duration(milliseconds: 200)); + expect(find.text('3 / 3'), findsOneWidget); + await t.tap(find.byTooltip('계속 보기 멈추기')); + await t.pumpAndSettle(); + }); + testWidgets('117: 화면을 누르면 멈추고 그 누름으로는 넘기지 않음 · 손으로 밀어도 멈춤', (t) async { final c = controller(); final src = await source(t, [(100, 200), (100, 200), (100, 200), (100, 200)]);