// 보기 화면 그림 기억 모듈 // // 약속 // - 입력: 그림 원본 ([ReaderSource]) · 장 번호 · 읽을 너비 (기기 픽셀) // - 출력: 장마다 한 번만 읽은 그림 ([ReaderImage] 의 Future) - 통째 · 반쪽 (두 배 너비) · 확대해 다시 그린 것 · 작은 그림. // [imageSize] 는 그림의 원래 크기 (모르면 null) // - 오류: 읽기 실패는 Future 의 오류로 그대로 돌려준다 (기억에 남아 같은 실패를 다시 보인다 - 지우려면 [forget]). // 버릴 때 dispose 실패는 삼킨다 // 이 모듈이 그림을 버리는 (dispose) 유일한 곳이다. 다른 보기 화면도 다시 쓸 수 있다. import 'dart:async'; import 'dart:math' as math; import 'package:flutter/widgets.dart'; import '../../core/reader_sources.dart'; class ReaderImageCache { ReaderImageCache(this.source); /// 읽을 원본 (폴더 그림 목록처럼 장이 바뀌어도 같은 객체) final ReaderSource source; final _full = >{}; /// 반쪽으로 보는 장 (두 배 너비) final _half = >{}; /// 확대한 배율에 맞춰 더 크게 다시 그린 쪽 (쪽 번호 - 흐릿하지 않게) final _sharp = >{}; final _sharpWidth = {}; /// 아래 갤러리 줄의 작은 그림 (장 번호) final _thumbs = >{}; static void _drop(Future f) => unawaited(f.then((x) => x.dispose(), onError: (_) {})); /// [page] 장 통째 ([width] 너비). 처음 읽을 때만 [onLoaded] 를 부른다 (크기 알아 두기 등) Future full(int page, int width, {void Function(ReaderImage r)? onLoaded}) => _full.putIfAbsent(page, () { final f = source.load(page, maxWidth: width); f.then((r) => onLoaded?.call(r), onError: (_) {}); return f; }); /// [page] 장을 반쪽으로 볼 때 (화면 너비 [width] 의 두 배, 8192 까지) Future half(int page, int width) => _half.putIfAbsent(page, () => source.load(page, maxWidth: math.min(width * 2, 8192))); /// [slot] 쪽을 확대해 다시 그린 것 (없으면 null) Future? sharp(int slot) => _sharp[slot]; /// 작은 그림 (160 너비) Future thumb(int page) => _thumbs.putIfAbsent(page, () => source.load(page, maxWidth: 160)); /// 확대 배율 [scale] 에 맞는 너비. 더 크게 다시 그릴 만큼이 아니면 null ([base] 쪽 기본 너비) int? sharpWidthFor(int slot, double scale, int base) { if (scale < 1.3) return null; final want = (base * scale).round().clamp(base, 8192); if (want <= (_sharpWidth[slot] ?? base) * 1.2) return null; return want; } /// [slot] 쪽 ([page] 장) 을 [width] 너비로 다시 그린다. 앞의 것은 새것을 건 뒤에 버린다 void sharpen(int slot, int page, int width) { _sharpWidth[slot] = width; final old = _sharp[slot]; _sharp[slot] = source.load(page, maxWidth: width); if (old != null) _drop(old); } /// 확대해 다시 그린 것을 모두 버린다 void clearSharp() { for (final f in _sharp.values) { _drop(f); } _sharp.clear(); _sharpWidth.clear(); } /// 통째 · 반쪽을 모두 버린다 (장이 바뀌었을 때) void clearPages() { for (final f in [..._full.values, ..._half.values]) { _drop(f); } _full.clear(); _half.clear(); } /// [다시 시도]: [page] 장과 [slot] 쪽의 기억을 잊는다 (버리지 않음 - 아직 화면에 있을 수 있다) void forget(int page, int slot) { _full.remove(page); _half.remove(page); _sharp.remove(slot); _sharpWidth.remove(slot); } /// [page] 가까이 (앞뒤 3장) 만 남기고 버린다 (메모리) void trim(int page) { for (final m in [_full, _half]) { for (final k in m.keys.toList()) { if ((k - page).abs() > 3) _drop(m.remove(k)!); } } } /// 모두 버린다 (작은 그림 → 통째 · 반쪽 → 확대한 것 차례) void dispose() { for (final f in _thumbs.values) { _drop(f); } clearPages(); clearSharp(); } /// 그림의 원래 크기 (그려 둔 그림이면 바로, 아니면 한 번 풀어 보고). 못 읽으면 null static Future imageSize(ReaderImage r) { final img = r.image; if (img != null) return Future.value(Size(img.width.toDouble(), img.height.toDouble())); final provider = r.provider; if (provider == null) return Future.value(null); final done = Completer(); final stream = provider.resolve(ImageConfiguration.empty); late final ImageStreamListener l; l = ImageStreamListener((info, _) { if (!done.isCompleted) done.complete(Size(info.image.width.toDouble(), info.image.height.toDouble())); info.dispose(); stream.removeListener(l); }, onError: (_, _) { if (!done.isCompleted) done.complete(null); stream.removeListener(l); }); stream.addListener(l); return done.future; } }