WIP 계속 보기 시계는 그 장이 다 보인 뒤부터 (관리자 지시 10/20, 28 검토): WebDAV 처럼 느린 원본에서 받는 동안 시간이 흘러 보지도 못한 장을 넘겼다 - 장마다 한 번 타이머 (그림 첫 장면 · 실패 글이 그려진 뒤 시작, 받는 중에는 재지 않음) · 시험: 느린 원본에서 10초 동안 넘기지 않고 다 받은 뒤 3초에 넘김, 기존 시험은 넘긴 뒤 한 장면을 더 지나게 (FutureBuilder 가 그림을 받아 그리는 장면) (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>xplore_view
parent
9ff8343835
commit
4ec505db13
|
|
@ -269,32 +269,57 @@ class _ReaderPageState extends State<ReaderPage> {
|
||||||
/// 계속 보기를 멈추게 한 누름은 장 넘기기 · 막대 숨기기로 쓰지 않는다
|
/// 계속 보기를 멈추게 한 누름은 장 넘기기 · 막대 숨기기로 쓰지 않는다
|
||||||
bool _swallowTap = false;
|
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) {
|
void _play(bool on) {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = null;
|
_timer = null;
|
||||||
if (on && _v.isNotEmpty) {
|
// 지금 장이 이미 보이면 바로 재기 시작, 아직 받는 중이면 보인 뒤에 ([_markShown])
|
||||||
_timer = Timer.periodic(Duration(seconds: c.settings.readerAutoSeconds), (_) => _tick());
|
if (on && _v.isNotEmpty && _shownKey == '$_version:$_index') _startTimer();
|
||||||
}
|
|
||||||
if (on == _playing) return;
|
if (on == _playing) return;
|
||||||
setState(() => _playing = on);
|
setState(() => _playing = on);
|
||||||
unawaited(ReaderPage.keepScreenOn(on).catchError((_) {}));
|
unawaited(ReaderPage.keepScreenOn(on).catchError((_) {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _tick() {
|
void _tick() {
|
||||||
if (!mounted) return;
|
_timer = null;
|
||||||
|
if (!mounted || !_playing) return;
|
||||||
if (_index >= _v.length - 1) {
|
if (_index >= _v.length - 1) {
|
||||||
_play(false);
|
_play(false);
|
||||||
_snack(tr('마지막 장이라 계속 보기를 멈췄습니다.'));
|
_snack(tr('마지막 장이라 계속 보기를 멈췄습니다.'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_pages.jumpToPage(_index + 1);
|
_pages.jumpToPage(_index + 1);
|
||||||
|
// 다음 장이 이미 받아 둔 것이라 다시 그리지 않으면 그 자리에서 알린다
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _rearmIfShown());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 넘긴 뒤 그 장이 곧바로 보였는데 (캐시) 알림을 놓쳤으면 시계를 다시 건다
|
||||||
|
void _rearmIfShown() {
|
||||||
|
if (mounted && _playing && _timer == null && _shownKey == '$_version:$_index') _startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _setSeconds(int d) async {
|
Future<void> _setSeconds(int d) async {
|
||||||
await c.updateSettings((x) => x.readerAutoSeconds = (x.readerAutoSeconds + d).clamp(1, 60));
|
await c.updateSettings((x) => x.readerAutoSeconds = (x.readerAutoSeconds + d).clamp(1, 60));
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
if (_playing) _play(true); // 새 간격으로
|
if (_playing) _play(true); // 새 간격으로 (지금 장이 보이면 처음부터 다시)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────── 확대 (30) ─────────
|
// ───────── 확대 (30) ─────────
|
||||||
|
|
@ -521,13 +546,26 @@ class _ReaderPageState extends State<ReaderPage> {
|
||||||
key: ValueKey('$_version:${slot.page}:${slot.part}:${_retry[slot.page] ?? 0}'),
|
key: ValueKey('$_version:${slot.page}:${slot.part}:${_retry[slot.page] ?? 0}'),
|
||||||
future: _sharp[i] ?? (half ? _loadHalf(slot.page) : _load(slot.page)),
|
future: _sharp[i] ?? (half ? _loadHalf(slot.page) : _load(slot.page)),
|
||||||
builder: (context, snap) {
|
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;
|
final r = snap.data;
|
||||||
if (r == null) return const Center(child: CircularProgressIndicator());
|
if (r == null) return const Center(child: CircularProgressIndicator());
|
||||||
|
if (r.image != null) shown();
|
||||||
Widget picture = r.image != null
|
Widget picture = r.image != null
|
||||||
? RawImage(image: r.image, filterQuality: FilterQuality.medium)
|
? RawImage(image: r.image, filterQuality: FilterQuality.medium)
|
||||||
: Image(image: r.provider!, gaplessPlayback: true, 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: 회전 · 상하 반전 (보기만, 파일은 그대로)
|
// 120: 회전 · 상하 반전 (보기만, 파일은 그대로)
|
||||||
if (_turns != 0) picture = RotatedBox(quarterTurns: _turns, child: picture);
|
if (_turns != 0) picture = RotatedBox(quarterTurns: _turns, child: picture);
|
||||||
if (_flipV) picture = Transform.flip(flipY: true, child: picture);
|
if (_flipV) picture = Transform.flip(flipY: true, child: picture);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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<void>();
|
||||||
|
_Slow(super.images, this.hold);
|
||||||
|
@override
|
||||||
|
Future<ReaderImage> load(int i, {required int maxWidth}) async {
|
||||||
|
if (i == hold) await release.future;
|
||||||
|
return super.load(i, maxWidth: maxWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 223: 폴더의 그림들처럼 (자동 나누기는 다른 그림과 견주어 판단)
|
/// 223: 폴더의 그림들처럼 (자동 나누기는 다른 그림과 견주어 판단)
|
||||||
class _FolderSized extends ImageFilesSource {
|
class _FolderSized extends ImageFilesSource {
|
||||||
final List<ui.Image> images;
|
final List<ui.Image> images;
|
||||||
|
|
@ -201,15 +214,49 @@ void main() {
|
||||||
expect(find.text('1 / 4'), findsOneWidget);
|
expect(find.text('1 / 4'), findsOneWidget);
|
||||||
await t.pump(const Duration(milliseconds: 200));
|
await t.pump(const Duration(milliseconds: 200));
|
||||||
expect(find.text('2 / 4'), findsOneWidget);
|
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));
|
await t.pump(const Duration(seconds: 2));
|
||||||
expect(find.text('4 / 4'), findsOneWidget);
|
expect(find.text('4 / 4'), findsOneWidget);
|
||||||
|
await t.pump();
|
||||||
await t.pump(const Duration(seconds: 2));
|
await t.pump(const Duration(seconds: 2));
|
||||||
expect(find.byTooltip('계속 보기 (2초마다 다음 장)'), findsOneWidget, reason: '마지막 장에서 멈춤');
|
expect(find.byTooltip('계속 보기 (2초마다 다음 장)'), findsOneWidget, reason: '마지막 장에서 멈춤');
|
||||||
expect(screenOn, [true, false]);
|
expect(screenOn, [true, false]);
|
||||||
await t.pumpAndSettle(const Duration(seconds: 3));
|
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 {
|
testWidgets('117: 화면을 누르면 멈추고 그 누름으로는 넘기지 않음 · 손으로 밀어도 멈춤', (t) async {
|
||||||
final c = controller();
|
final c = controller();
|
||||||
final src = await source(t, [(100, 200), (100, 200), (100, 200), (100, 200)]);
|
final src = await source(t, [(100, 200), (100, 200), (100, 200), (100, 200)]);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue