// 보기 화면 조작 모듈: 계속 보기 시계 (117) · 확대 상태 (30) · 볼륨 키 · 누른 자리 // // 약속 // - 입력: 간격 (초, 부를 때마다 읽음) · 다 보인 장의 이름 (예: '판:번호') · 손가락 수 변화 · 키 이벤트 · 누른 x 와 화면 너비 // - 출력: [ReaderSlideshow] 는 간격이 지나면 [ReaderSlideshow.onTick] 을 부른다 (장이 다 보인 뒤부터 잼). // [ReaderZoom] 은 쪽마다 확대 컨트롤러와 "확대 중 · 두 손가락" 상태. [volumeKeyStep] · [tapZone] 은 넘길 방향 // - 오류: 던지지 않는다. 화면 갱신 (setState) 은 부른 쪽이 돌려받은 값을 보고 한다 // 다른 보기 화면 (동영상 사진 슬라이드 등) 도 다시 쓸 수 있다. import 'dart:async'; import 'dart:math' as math; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; /// 계속 보기 (117). 관리자 지시 10/20 (28 검토): 시계는 지금 장이 다 보인 뒤부터 잰다 - /// WebDAV 처럼 느린 원본에서 받는 동안 시간이 흘러 보지도 못한 장을 넘겼다 class ReaderSlideshow { ReaderSlideshow({required this.seconds, required this.onTick}); /// 간격 (초) - 시계를 걸 때마다 읽는다 final int Function() seconds; /// 간격이 지났다 (계속 보는 중일 때만) final void Function() onTick; bool _playing = false; bool get playing => _playing; Timer? _timer; /// 다 보인 장 ('판:번호') String? shownKey; void _start() { _timer?.cancel(); _timer = Timer(Duration(seconds: seconds()), _fire); } void _fire() { _timer = null; if (_playing) onTick(); } /// [key] 장이 화면에 다 그려졌다 (그림 첫 장면 · 실패 글). 계속 보는 중이면 시계를 시작 void markShown(String key) { if (shownKey == key) return; shownKey = key; if (_playing && _timer == null) _start(); } /// 켜기 · 끄기 (켤 때 [current] 장이 이미 보였으면 바로 재기 시작, 아니면 보인 뒤에). 켬 · 끔이 바뀌었으면 true /// ([current] null = 볼 장 없음) bool set(bool on, String? current) { _timer?.cancel(); _timer = null; if (on && current != null && shownKey == current) _start(); if (on == _playing) return false; _playing = on; return true; } /// 넘긴 뒤 [current] 장이 곧바로 보였는데 (캐시) 알림을 놓쳤으면 시계를 다시 건다 void rearmIfShown(String current) { if (_playing && _timer == null && shownKey == current) _start(); } void dispose() => _timer?.cancel(); } /// 확대 (30): 쪽마다 확대 상태 (보는 쪽만 쓰고, 쪽을 넘기면 [reset]) · 두 손가락 class ReaderZoom { final _zoom = {}; /// [slot] 쪽의 확대 컨트롤러 TransformationController of(int slot) => _zoom.putIfAbsent(slot, TransformationController.new); /// [slot] 쪽의 지금 배율 double scaleOf(int slot) => of(slot).value.getMaxScaleOnAxis(); /// 확대해 보고 있는지 (그동안은 밀어도 장이 넘어가지 않고 그림을 옮긴다) bool zoomed = false; /// 화면에 닿은 손가락 수: 둘이면 (벌리고 오므리기) 옆으로 움직여도 장을 넘기지 않고 확대 · 축소 int _fingers = 0; bool get pinching => _fingers >= 2; /// 확대 중이거나 두 손가락이면 장을 넘기지 않는다 bool get holdPage => zoomed || pinching; /// 손가락 수 변화 ([d] +1 · -1). 두 손가락 상태가 바뀌었으면 true bool finger(int d) { final was = pinching; _fingers = math.max(0, _fingers + d); return was != pinching; } /// 모든 쪽의 확대를 원래대로 (컨트롤러를 버림) void reset() { for (final z in _zoom.values) { z.dispose(); } _zoom.clear(); zoomed = false; } } /// X-plore 맞추기 (관리자 지시 10/20): 볼륨 키로 넘기기. [enabled] (설정 · 기기) 가 아니거나 볼륨 키가 아니면 null (처리 안 함). /// 볼륨 키면 넘길 방향 (아래 +1 · 위 -1, 키를 뗄 때는 0) - 처리한 키는 시스템 볼륨을 바꾸지 않는다 int? volumeKeyStep(KeyEvent e, {required bool enabled}) { if (!enabled) return null; final k = e.logicalKey; if (k != LogicalKeyboardKey.audioVolumeDown && k != LogicalKeyboardKey.audioVolumeUp) return null; if (e is KeyDownEvent || e is KeyRepeatEvent) return k == LogicalKeyboardKey.audioVolumeDown ? 1 : -1; return 0; } /// 화면을 누른 자리 ([x], 너비 [width]): 오른쪽 1/3 은 다음 (+1), 왼쪽 1/3 은 이전 (-1), 가운데는 0 (막대 보이기 · 숨기기). /// [rtl] (오른쪽 → 왼쪽으로 넘기기) 이면 좌우 반대 int tapZone(double x, double width, {required bool rtl}) { if (x > width * 2 / 3) return rtl ? -1 : 1; if (x < width / 3) return rtl ? 1 : -1; return 0; }