JJ_MKVMaker/lib/ui/reader/reader_slots.dart

111 lines
5.4 KiB
Dart

// 보기 화면 쪽 계산 모듈 (순수 계산 - 위젯 · 상태 없음)
//
// 약속
// - 입력: 원본 장 수 · 장마다 나눌지 판단 · 보이는 쪽 목록 · 그림 크기 · 회전 수 · 밝기 값
// - 출력: 보이는 쪽 목록 ([ReaderSlot]) · 옮길 자리 번호 · 지운 뒤 남은 장 번호 · 쪽 이름 · 밝기 필터
// ([removeMissingPaths] 만 받은 목록에서 없어진 경로를 뺀다)
// - 오류: 던지지 않는다. 찾을 수 없으면 -1 (또는 범위 안으로 맞춘 번호) 을 돌려준다
// 다른 보기 화면도 그대로 다시 쓸 수 있다.
import 'dart:math' as math;
import 'package:flutter/painting.dart';
/// 보이는 한 쪽: 원본 [page] 장의 통째 (part -1) 또는 읽는 순서로 첫째 (0) · 둘째 (1) 반쪽 (119)
typedef ReaderSlot = ({int page, int part});
/// 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;
}
/// [turns] (시계 방향 90° 회전 수) 를 반영한 크기
Size turnedSize(Size s, int turns) => turns.isOdd ? Size(s.height, s.width) : s;
/// 이 장 ([page]) 을 반씩 나눠 볼지. [mode] 는 설정 값 ('on' · 'off' · 'auto'), [dims] 는 크기를 아는 장들.
/// 자동은 크기를 알 때만 [autoSplitsPage] 로 판단한다
bool splitsPage(String mode, int page, Map<int, Size> dims, {required int turns, required bool bundle}) {
switch (mode) {
case 'on':
return true;
case 'off':
return false;
}
final d = dims[page];
if (d == null) return false;
return autoSplitsPage(turnedSize(d, turns), [for (final e in dims.entries) if (e.key != page) turnedSize(e.value, turns)],
bundle: bundle);
}
/// 원본 [length] 장으로 보이는 쪽 목록을 만든다 ([splits] 가 참인 장은 반쪽 둘)
List<ReaderSlot> buildSlots(int length, bool Function(int page) splits) => [
for (var i = 0; i < length; i++)
if (splits(i)) ...[(page: i, part: 0), (page: i, part: 1)] else (page: i, part: -1),
];
/// [v] 에서 [index] 번 쪽 (범위 밖이면 끝으로 맞춤). 비었으면 첫 장 통째
ReaderSlot slotAt(List<ReaderSlot> v, int index) => v.isEmpty ? (page: 0, part: -1) : v[index.clamp(0, v.length - 1)];
/// [page] 장의 첫 쪽 번호 (없으면 -1)
int indexOfPage(List<ReaderSlot> v, int page) => v.indexWhere((e) => e.page == page);
/// 쪽 목록을 새로 만든 뒤 보던 쪽 [cur] 의 새 자리. 같은 반쪽 → 같은 장의 다른 모양 (통째 ↔ 첫 반쪽) →
/// 옛 번호 [index] (범위 안으로) 차례로 찾는다
int relocateSlot(List<ReaderSlot> v, ReaderSlot cur, int index) {
int find(int part) => v.indexWhere((e) => e.page == cur.page && e.part == part);
var to = find(cur.part);
if (to < 0) to = find(cur.part < 0 ? 0 : -1);
if (to < 0) to = index.clamp(0, math.max(0, v.length - 1));
return to;
}
/// 224: [count] 장 중 [gone] 을 뺀 뒤 보던 장 [curPage] (없어졌으면 그 뒤, 뒤에 없으면 앞의 남은 장) 의 새 번호.
/// 남은 장이 없으면 -1
int pageAfterRemoval(int curPage, Set<int> gone, int count) {
var keep = curPage;
while (keep < count && gone.contains(keep)) {
keep++;
}
if (keep >= count) {
keep = curPage;
while (keep >= 0 && gone.contains(keep)) {
keep--;
}
}
return keep < 0 ? -1 : keep - gone.where((g) => g < keep).length;
}
/// 224: [paths] 에서 없어진 것 ([missing]) 을 뺀다 (목록을 바꿈). 없어진 것이 없으면 null, 있으면 보던 장 [curPage] 의
/// 새 번호 ([pageAfterRemoval]) 와 보던 장도 없어졌는지
({int newPage, bool curGone})? removeMissingPaths(List<String> paths, int curPage, bool Function(String path) missing) {
final gone = <int>{
for (var i = 0; i < paths.length; i++)
if (missing(paths[i])) i,
};
if (gone.isEmpty) return null;
final newPage = pageAfterRemoval(curPage, gone, paths.length);
for (final g in gone.toList()..sort((a, b) => b - a)) {
paths.removeAt(g);
}
return (newPage: newPage, curGone: gone.contains(curPage));
}
/// 쪽 번호: 통째면 "12", 반쪽이면 "12-1" · "12-2"
String slotLabel(ReaderSlot s) => s.part < 0 ? '${s.page + 1}' : '${s.page + 1}-${s.part + 1}';
/// 반쪽은 두 배 너비로 읽는다 (반쪽이 화면 가득이어도 원본 해상도로). [width] 화면 너비 (기기 픽셀)
int slotBaseWidth(ReaderSlot s, int width) => s.part >= 0 ? math.min(width * 2, 8192) : width;
/// 어둡게 · 밝게 ([b] -0.7 ~ 0.7): 음수는 색을 줄이고 양수는 더한다
ColorFilter brightnessFilter(double b) {
final double s = 1 + math.min(b, 0.0);
final double o = math.max(b, 0.0) * 160;
return ColorFilter.matrix(<double>[s, 0, 0, 0, o, 0, s, 0, 0, o, 0, 0, s, 0, o, 0, 0, 0, 1, 0]);
}