// 보기 화면 쪽 계산 모듈 (순수 계산 - 위젯 · 상태 없음) // // 약속 // - 입력: 원본 장 수 · 장마다 나눌지 판단 · 보이는 쪽 목록 · 그림 크기 · 회전 수 · 밝기 값 // - 출력: 보이는 쪽 목록 ([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 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 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 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 v, int index) => v.isEmpty ? (page: 0, part: -1) : v[index.clamp(0, v.length - 1)]; /// [page] 장의 첫 쪽 번호 (없으면 -1) int indexOfPage(List v, int page) => v.indexWhere((e) => e.page == page); /// 쪽 목록을 새로 만든 뒤 보던 쪽 [cur] 의 새 자리. 같은 반쪽 → 같은 장의 다른 모양 (통째 ↔ 첫 반쪽) → /// 옛 번호 [index] (범위 안으로) 차례로 찾는다 int relocateSlot(List 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 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 paths, int curPage, bool Function(String path) missing) { final gone = { 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([s, 0, 0, 0, o, 0, s, 0, 0, o, 0, 0, s, 0, o, 0, 0, 0, 1, 0]); }