// 보기 화면 위 막대의 버튼 목록 모듈 // // 약속 // - 입력: 앱 ([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 Function(void Function(dynamic s) f) updateSettings; final VoidCallback applySystemBars; final VoidCallback upscalePage; final VoidCallback qualityPage; } /// 위 막대의 버튼들 (앞의 것일수록 먼저 보이고, 못 들어가면 ⋮ 메뉴로). [context] 는 AI 창을 띄울 화면 List 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.components.contains('aiimage'); final playing = st.playing; final secs = s.readerAutoSeconds; ReaderBarItem one(ReaderAct a) => ReaderBarItem(48, [a]); final split = switch (s.readerSplit) { '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.readerGallery ? tr('아래 갤러리 줄 숨기기') : tr('아래 갤러리 줄 보이기'), on.toggleGallery, on: s.readerGallery)), // 119 · 120 one(ReaderAct(const Icon(Icons.vertical_split_outlined), split, on.cycleSplit, on: s.readerSplit != '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.readerFit = 'page'), on: s.readerFit == 'page')), one(ReaderAct(const Icon(Icons.width_full_outlined), tr('좌우 맞추기 (세로로 밀어 봄)'), () => on.updateSettings((x) => x.readerFit = 'width'), on: s.readerFit == 'width')), one(ReaderAct(const Icon(Icons.swap_horiz), s.readerRtl ? tr('넘기는 방향: 오른쪽 → 왼쪽 (만화)') : tr('넘기는 방향: 왼쪽 → 오른쪽'), () => on.updateSettings((x) => x.readerRtl = !x.readerRtl), on: s.readerRtl)), one(ReaderAct(const Icon(Icons.brightness_low), tr('어둡게'), () => on.updateSettings((x) => x.readerBrightness = (x.readerBrightness - 0.1).clamp(-0.7, 0.7)))), one(ReaderAct(const Icon(Icons.brightness_high), tr('밝게'), () => on.updateSettings((x) => x.readerBrightness = (x.readerBrightness + 0.1).clamp(-0.7, 0.7)))), // 30: 시계 · 알림 · 뒤로 막대를 그대로 / 화면 가득 one(ReaderAct(Icon(s.readerSystemBars ? Icons.fullscreen : Icons.fullscreen_exit), s.readerSystemBars ? tr('화면 가득 (시스템 막대 숨기기)') : tr('시스템 막대 보이기 (시계 · 알림 · 뒤로)'), () async { await on.updateSettings((x) => x.readerSystemBars = !x.readerSystemBars); on.applySystemBars(); }, on: s.readerSystemBars)), // 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.aiQualityButton) 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))), ]; }