JJ_MKVMaker/lib/ui/reader/reader_controls.dart

124 lines
4.8 KiB
Dart

// 보기 화면 조작 모듈: 계속 보기 시계 (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 = <int, TransformationController>{};
/// [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;
}