// 보기 화면 창 모듈: 사진 정보 (Exif) · PDF 페이지 지우기 확인 · 저장 묻기 · PDF 에 넣을 그림 고르기 // // 약속 // - 입력: 그림 경로 · 임시 폴더 · 페이지 번호 (0 부터) · 앱 ([AppController] - 파일 고르기) // - 출력: [readPhotoFile] 은 사진 정보 · 파일 크기 · 오류 (셋 중 읽은 것). 창들은 고른 답 (닫으면 null) // - 오류: [readPhotoFile] 은 던지지 않고 오류를 돌려준다. 창은 던지지 않는다 // 창은 부른 쪽이 지금 장면이 끝난 뒤 (endOfFrame) 띄운다 (28 지적: 누른 이벤트 안에서 띄우면 의미 트리 assertion). // 다른 화면도 다시 쓸 수 있다. import 'dart:io' show File; import 'package:flutter/material.dart'; import '../../app/app_controller.dart'; import '../../core/file_ops.dart' show formatSize; import '../../core/photo_info.dart'; import '../../core/reader_sources.dart'; import '../../core/vfs.dart'; import '../../l10n/tr.dart'; import '../theme.dart'; /// 지금 장면이 끝난 뒤 (창 띄우기 전 - 28 지적 (10/20): 위 막대 단추를 누른 그 이벤트 안에서 창을 띄우면 (장을 손으로 넘긴 뒤) /// 창을 닫을 때 디버그 판에서 의미 트리 assertion ('node.built') 이 났다. 부른 쪽은 뒤에 mounted 확인) Future afterFrame() => WidgetsBinding.instance.endOfFrame; /// X-plore 맞추기 (관리자 지시 10/20): 폴더 그림 [src] 의 지금 장 ([page] - 창을 띄울 때 읽음) 사진 정보 창. /// 지금 장면이 끝난 뒤에 띄운다 (Windows 통합 시험에서 재현 4번 → 0번). 폴더 그림이 아니거나 볼 장이 없으면 아무것도 안 함 Future showReaderPhotoInfo(BuildContext context, ReaderSource src, int Function() page, {required bool Function() hasPages}) async { await afterFrame(); if (!context.mounted) return; final s = src; if (s is! ImageFilesSource || !hasPages()) return; final path = s.paths[page()]; final r = await readPhotoFile(path, s.tempDir); if (!context.mounted) return; await showPhotoInfoDialog(context, path: path, info: r.info, size: r.size, error: r.error); } /// [path] 그림 (WebDAV 면 [tempDir] 에 받아서) 의 사진 정보와 파일 크기. 못 읽으면 [error] Future<({PhotoInfo? info, int? size, Object? error})> readPhotoFile(String path, String tempDir) async { PhotoInfo? info; int? size; Object? error; try { final local = await vLocalCopy(path, tempDir); final f = File(local); size = await f.length(); final bytes = await f.readAsBytes(); info = readPhotoInfo(bytes); } catch (e) { error = e; } return (info: info, size: size, error: error); } /// X-plore 맞추기 (관리자 지시 10/20): 사진 정보 (찍은 날 · 기기 · 크기 · 위치). 위치는 [위치 보기] 를 누를 때만 /// 보이고, 지도 · 인터넷으로 보내지 않는다 (링크 없음) Future showPhotoInfoDialog(BuildContext context, {required String path, required PhotoInfo? info, required int? size, required Object? error}) async { var showLocation = false; await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, set) { final i = info; Widget row(String k, Widget v) => Padding( padding: const EdgeInsets.symmetric(vertical: 3), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: 90, child: Text(k, style: TextStyle(color: JjColors.textDim))), Expanded(child: v), ]), ); return AlertDialog( scrollable: true, title: Text(tr('사진 정보 (Exif)')), content: SizedBox( width: 420, child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ row(tr('파일'), SelectableText(vBasename(path))), if (i == null) Text(trf('읽을 수 없습니다: {0}', [error])) else ...[ row(tr('크기'), Text([ if (i.width != null) trf('{0} × {1} 픽셀', [i.width!, i.height!]), if (size != null) formatSize(size), ].join(' · '))), row(tr('찍은 날'), Text(i.taken ?? '-')), row(tr('기기'), Text(i.device ?? '-')), row( tr('위치'), !i.hasLocation ? const Text('-') : showLocation ? SelectableText(i.locationText) : Align( alignment: Alignment.centerLeft, child: TextButton(onPressed: () => set(() => showLocation = true), child: Text(tr('위치 보기'))))), if (i.hasLocation) Text(tr('위치는 이 기기에서만 보이며 지도 · 인터넷으로 보내지 않습니다'), style: TextStyle(fontSize: 12, color: JjColors.textDim)), if (!i.hasExif) Padding( padding: const EdgeInsets.only(top: 6), child: Text(tr('이 그림에는 Exif 정보가 없습니다 (JPEG 사진만 읽음)'), style: TextStyle(fontSize: 12, color: JjColors.textDim)), ), ], ]), ), actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(tr('닫기')))], ); }, ), ); } /// PDF [page] (0 부터) 페이지를 지울지 묻는다. 지우기 = true Future confirmPdfPageDelete(BuildContext context, int page) => showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(tr('페이지 지우기')), content: Text(trf('{0} 페이지를 지울까요? (저장해야 파일에 반영됩니다)', [page + 1])), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, false), child: Text(tr('취소'))), FilledButton(onPressed: () => Navigator.pop(ctx, true), child: Text(tr('지우기'))), ], ), ); /// 저장하지 않은 변경을 어떻게 할지: 'save' · 'discard' · 'cancel' (닫으면 null) Future askSaveChanges(BuildContext context) => showDialog( context: context, builder: (ctx) => AlertDialog( title: Text(tr('저장하지 않은 변경')), content: Text(tr('페이지를 바꾼 것을 저장할까요?')), actions: [ TextButton(onPressed: () => Navigator.pop(ctx, 'cancel'), child: Text(tr('취소'))), TextButton(onPressed: () => Navigator.pop(ctx, 'discard'), child: Text(tr('저장 안 함'))), FilledButton(onPressed: () => Navigator.pop(ctx, 'save'), child: Text(tr('저장'))), ], ), ); /// PDF 에 넣을 그림을 고른다 (설정의 그림 확장자). 고르지 않으면 빈 목록 Future> pickPdfImages(AppController c) => c.services.storage.pickFiles(title: tr('PDF 에 넣을 그림 선택'), extensions: c.settings.imageExts);