124 lines
4.8 KiB
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;
|
|
}
|