JJ_MKVMaker/lib/ui/reader/reader_dialogs.dart

147 lines
7.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 보기 화면 창 모듈: 사진 정보 (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<void> afterFrame() => WidgetsBinding.instance.endOfFrame;
/// X-plore 맞추기 (관리자 지시 10/20): 폴더 그림 [src] 의 지금 장 ([page] - 창을 띄울 때 읽음) 사진 정보 창.
/// 지금 장면이 끝난 뒤에 띄운다 (Windows 통합 시험에서 재현 4번 → 0번). 폴더 그림이 아니거나 볼 장이 없으면 아무것도 안 함
Future<void> 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<void> showPhotoInfoDialog(BuildContext context,
{required String path, required PhotoInfo? info, required int? size, required Object? error}) async {
var showLocation = false;
await showDialog<void>(
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<bool?> confirmPdfPageDelete(BuildContext context, int page) => showDialog<bool>(
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<String?> askSaveChanges(BuildContext context) => showDialog<String>(
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<List<String>> pickPdfImages(AppController c) =>
c.services.storage.pickFiles(title: tr('PDF 에 넣을 그림 선택'), extensions: c.settings.imageExts);