JJ_MKVMaker/lib/ui/reader/reader_images.dart

136 lines
5.0 KiB
Dart

// 보기 화면 그림 기억 모듈
//
// 약속
// - 입력: 그림 원본 ([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 = <int, Future<ReaderImage>>{};
/// 반쪽으로 보는 장 (두 배 너비)
final _half = <int, Future<ReaderImage>>{};
/// 확대한 배율에 맞춰 더 크게 다시 그린 쪽 (쪽 번호 - 흐릿하지 않게)
final _sharp = <int, Future<ReaderImage>>{};
final _sharpWidth = <int, int>{};
/// 아래 갤러리 줄의 작은 그림 (장 번호)
final _thumbs = <int, Future<ReaderImage>>{};
static void _drop(Future<ReaderImage> f) => unawaited(f.then((x) => x.dispose(), onError: (_) {}));
/// [page] 장 통째 ([width] 너비). 처음 읽을 때만 [onLoaded] 를 부른다 (크기 알아 두기 등)
Future<ReaderImage> 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<ReaderImage> half(int page, int width) =>
_half.putIfAbsent(page, () => source.load(page, maxWidth: math.min(width * 2, 8192)));
/// [slot] 쪽을 확대해 다시 그린 것 (없으면 null)
Future<ReaderImage>? sharp(int slot) => _sharp[slot];
/// 작은 그림 (160 너비)
Future<ReaderImage> 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<Size?> 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<Size?>();
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;
}
}