JJ_MKVMaker/lib/ui/reader/reader_bar_items.dart

135 lines
7.8 KiB
Dart

// 보기 화면 위 막대의 버튼 목록 모듈
//
// 약속
// - 입력: 앱 ([AppController] - 설정을 읽고 바꿈) · 그림 원본 · 지금 상태 ([ReaderBarState]) · 누를 때 부를 일 ([ReaderBarHandlers])
// - 출력: 위 막대 칸 목록 ([ReaderBarItem]) - 앞의 것일수록 먼저 보이고, 못 들어가면 ⋮ 메뉴로 ([ReaderTopBar])
// - 오류: 던지지 않는다. 쓸 수 없는 버튼은 onTap 이 null (흐리게)
// 버튼의 순서 · 글 · 아이콘은 이 모듈 한 곳에서 정한다. 다른 보기 화면도 다시 쓸 수 있다.
import 'package:flutter/material.dart';
import '../../app/app_controller.dart';
import '../../core/reader_sources.dart';
import '../../l10n/tr.dart';
import '../ai_image_page.dart';
import '../ai_upscale_ui.dart';
import 'reader_bars.dart';
/// 버튼 모양을 정하는 지금 상태
typedef ReaderBarState = ({bool halfShown, bool playing, int turns, bool flipV, String? aiSource});
/// 버튼을 누를 때 부를 일 (화면이 준다)
class ReaderBarHandlers {
const ReaderBarHandlers({
required this.splitOff,
required this.play,
required this.setSeconds,
required this.save,
required this.deletePage,
required this.insertImages,
required this.photoInfo,
required this.toggleGallery,
required this.cycleSplit,
required this.rotate,
required this.toggleFlip,
required this.updateSettings,
required this.applySystemBars,
required this.upscalePage,
required this.qualityPage,
});
final VoidCallback splitOff;
final void Function(bool on) play;
final void Function(int d) setSeconds;
final VoidCallback save;
final VoidCallback deletePage;
final VoidCallback insertImages;
final VoidCallback photoInfo;
final VoidCallback toggleGallery;
final VoidCallback cycleSplit;
final void Function(int d) rotate;
final VoidCallback toggleFlip;
/// 설정을 바꾸고 화면을 다시 그린다
final Future<void> Function(void Function(dynamic s) f) updateSettings;
final VoidCallback applySystemBars;
final VoidCallback upscalePage;
final VoidCallback qualityPage;
}
/// 위 막대의 버튼들 (앞의 것일수록 먼저 보이고, 못 들어가면 ⋮ 메뉴로). [context] 는 AI 창을 띄울 화면
List<ReaderBarItem> readerBarItems(BuildContext context, AppController c, ReaderSource src, ReaderBarState st,
ReaderBarHandlers on) {
final s = c.settings;
final pdf = src is PdfSource ? src : null;
final pictures = src is ImageFilesSource || src is ZipImagesSource;
final aiOn = c.settings.display.components.contains('aiimage');
final playing = st.playing;
final secs = s.reader.autoSeconds;
ReaderBarItem one(ReaderAct a) => ReaderBarItem(48, [a]);
final split = switch (s.reader.split) {
'on' => tr('두 쪽 나눠 보기: 켜기 (누르면 끄기)'),
'off' => tr('두 쪽 나눠 보기: 끄기 (누르면 자동)'),
_ => tr('두 쪽 나눠 보기: 자동 - 두 쪽 스캔으로 보이는 그림만 (누르면 켜기)'),
};
return [
// 223: 나눠 보고 있으면 바로 한 장으로 (자동이 사진을 잘라 보인다고 여기지 않게 맨 앞에)
if (st.halfShown) one(ReaderAct(const Icon(Icons.crop_original), tr('두 쪽 나눔 끄기 (한 장으로 보기)'), on.splitOff)),
// 117: 계속 보기 · 간격 (▲ 늘리기 · ▼ 줄이기)
one(ReaderAct(Icon(playing ? Icons.pause : Icons.play_arrow),
playing ? tr('계속 보기 멈추기') : trf('계속 보기 ({0}초마다 다음 장)', [secs]), () => on.play(!playing),
on: playing)),
ReaderBarItem(112, [
ReaderAct(const Icon(Icons.keyboard_arrow_up), trf('계속 보기 간격 늘리기 (지금 {0}초)', [secs]),
secs < 60 ? () => on.setSeconds(1) : null),
ReaderAct(const Icon(Icons.keyboard_arrow_down), trf('계속 보기 간격 줄이기 (지금 {0}초)', [secs]),
secs > 1 ? () => on.setSeconds(-1) : null),
], seconds: secs),
if (pdf != null) ...[
one(ReaderAct(const Icon(Icons.save_outlined), tr('저장'), pdf.dirty ? on.save : null)),
one(ReaderAct(const Icon(Icons.delete_outline), tr('이 페이지 지우기'), on.deletePage)),
one(ReaderAct(const Icon(Icons.add_photo_alternate_outlined), tr('그림을 페이지로 넣기 (이 페이지 뒤)'), on.insertImages)),
],
// X-plore 맞추기 (관리자 지시 10/20): 사진 정보 (Exif) - 폴더의 그림만
if (src is ImageFilesSource) one(ReaderAct(const Icon(Icons.info_outline), tr('사진 정보 (Exif)'), on.photoInfo)),
// X-plore 맞추기 (관리자 지시 10/20): 아래 갤러리 줄 켜고 끄기 (그림 원본만)
if (pictures)
one(ReaderAct(const Icon(Icons.view_carousel_outlined), s.reader.gallery ? tr('아래 갤러리 줄 숨기기') : tr('아래 갤러리 줄 보이기'),
on.toggleGallery, on: s.reader.gallery)),
// 119 · 120
one(ReaderAct(const Icon(Icons.vertical_split_outlined), split, on.cycleSplit, on: s.reader.split != 'off')),
one(ReaderAct(const Icon(Icons.rotate_right), tr('90° 회전 (시계 방향)'), () => on.rotate(1), on: st.turns != 0)),
one(ReaderAct(const Icon(Icons.rotate_left), tr('반대로 90° 회전'), () => on.rotate(3), on: st.turns != 0)),
one(ReaderAct(const RotatedBox(quarterTurns: 1, child: Icon(Icons.flip)), tr('상하 반전'), on.toggleFlip, on: st.flipV)),
one(ReaderAct(const Icon(Icons.fit_screen_outlined), tr('한 쪽 맞추기'), () => on.updateSettings((x) => x.reader.fit = 'page'),
on: s.reader.fit == 'page')),
one(ReaderAct(const Icon(Icons.width_full_outlined), tr('좌우 맞추기 (세로로 밀어 봄)'),
() => on.updateSettings((x) => x.reader.fit = 'width'), on: s.reader.fit == 'width')),
one(ReaderAct(const Icon(Icons.swap_horiz),
s.reader.rtl ? tr('넘기는 방향: 오른쪽 → 왼쪽 (만화)') : tr('넘기는 방향: 왼쪽 → 오른쪽'),
() => on.updateSettings((x) => x.reader.rtl = !x.reader.rtl), on: s.reader.rtl)),
one(ReaderAct(const Icon(Icons.brightness_low), tr('어둡게'),
() => on.updateSettings((x) => x.reader.brightness = (x.reader.brightness - 0.1).clamp(-0.7, 0.7)))),
one(ReaderAct(const Icon(Icons.brightness_high), tr('밝게'),
() => on.updateSettings((x) => x.reader.brightness = (x.reader.brightness + 0.1).clamp(-0.7, 0.7)))),
// 30: 시계 · 알림 · 뒤로 막대를 그대로 / 화면 가득
one(ReaderAct(Icon(s.reader.systemBars ? Icons.fullscreen : Icons.fullscreen_exit),
s.reader.systemBars ? tr('화면 가득 (시스템 막대 숨기기)') : tr('시스템 막대 보이기 (시계 · 알림 · 뒤로)'), () async {
await on.updateSettings((x) => x.reader.systemBars = !x.reader.systemBars);
on.applySystemBars();
}, on: s.reader.systemBars)),
// AI 는 막대가 좁으면 먼저 ⋮ 로 (늘 쓰는 버튼이 막대에 남게)
// 121: AI 해상도 올리기 (이 장 · 모든 장) - 새 파일로 저장, 원본 · ZIP 은 그대로
if (aiOn && src is! PdfSource) ...[
one(ReaderAct(const Icon(Icons.hd_outlined), tr('AI 해상도 올리기 (이 장)'), on.upscalePage)),
one(ReaderAct(const Icon(Icons.burst_mode_outlined), tr('AI 해상도 올리기 (모든 장)'), () => upscaleAll(context, c, src))),
// 관리자 판단 (10/17): [품질 올리기] - 설정으로 켰을 때만
if (c.settings.aiImage.qualityButton) one(ReaderAct(const Icon(Icons.auto_fix_high), tr('품질 올리기 (이 장)'), on.qualityPage)),
],
// 123: 이 그림을 바탕으로 AI 그림 (그림 → 그림, 원본은 그대로)
if (st.aiSource case final path?)
one(ReaderAct(const Icon(Icons.auto_awesome_outlined), tr('AI 그림으로 (이 그림을 바탕으로)'),
() => AiImagePage.open(Navigator.of(context), c: c, initImage: path))),
];
}