WIP 보기 화면 [다시 시도] (28 지적, 관리자 지시 10/20): 그림 · ZIP 원본도 다시 읽음 - 그 장의 화면 키를 올리고 (같은 키 · 같은 그림이면 Image 가 다시 읽지 않았다) 그림 캐시에서도 비움 · 그림 디코드 오류는 "그림 파일을 읽을 수 없습니다 - 깨졌거나 지원하지 않는 형식", 원문은 [자세히] · 시험: 깨진 그림 → 고친 뒤 [다시 시도] 하면 열림 (키 올리기를 빼면 실패함을 확인) (검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>hwp3
parent
ae4e29620e
commit
6b4743c9e9
|
|
@ -800,6 +800,7 @@
|
||||||
"그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기": "Images · comic reading, PDF view · page editing, ZIP list · comic reading",
|
"그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기": "Images · comic reading, PDF view · page editing, ZIP list · comic reading",
|
||||||
"그림 · 프롬프트가 기기 밖으로 나갑니다 ({0})": "Images and prompts leave this device ({0})",
|
"그림 · 프롬프트가 기기 밖으로 나갑니다 ({0})": "Images and prompts leave this device ({0})",
|
||||||
"그림 보기 대상": "Image viewer file types",
|
"그림 보기 대상": "Image viewer file types",
|
||||||
|
"그림 파일을 읽을 수 없습니다 - 깨졌거나 지원하지 않는 형식": "Can't read the image file - damaged or unsupported format",
|
||||||
"그림으로 바꾸는 중": "Turning into an image",
|
"그림으로 바꾸는 중": "Turning into an image",
|
||||||
"그림을 만들지 못했습니다: {0}": "Couldn't make the image: {0}",
|
"그림을 만들지 못했습니다: {0}": "Couldn't make the image: {0}",
|
||||||
"그림을 바꿔 만들기": "Make from this picture",
|
"그림을 바꿔 만들기": "Make from this picture",
|
||||||
|
|
|
||||||
|
|
@ -800,6 +800,7 @@
|
||||||
"그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기": "画像・マンガ表示、PDF 表示・ページ編集、ZIP 一覧・マンガ表示",
|
"그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기": "画像・マンガ表示、PDF 表示・ページ編集、ZIP 一覧・マンガ表示",
|
||||||
"그림 · 프롬프트가 기기 밖으로 나갑니다 ({0})": "画像・プロンプトが端末の外に送られます ({0})",
|
"그림 · 프롬프트가 기기 밖으로 나갑니다 ({0})": "画像・プロンプトが端末の外に送られます ({0})",
|
||||||
"그림 보기 대상": "画像表示の対象",
|
"그림 보기 대상": "画像表示の対象",
|
||||||
|
"그림 파일을 읽을 수 없습니다 - 깨졌거나 지원하지 않는 형식": "画像ファイルを読み込めません - 破損しているか未対応の形式",
|
||||||
"그림으로 바꾸는 중": "画像に変換中",
|
"그림으로 바꾸는 중": "画像に変換中",
|
||||||
"그림을 만들지 못했습니다: {0}": "画像を作成できませんでした: {0}",
|
"그림을 만들지 못했습니다: {0}": "画像を作成できませんでした: {0}",
|
||||||
"그림을 바꿔 만들기": "画像を変えて作成",
|
"그림을 바꿔 만들기": "画像を変えて作成",
|
||||||
|
|
|
||||||
|
|
@ -800,6 +800,7 @@
|
||||||
"그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기": "图片 · 漫画阅读,PDF 查看 · 页面编辑,ZIP 列表 · 漫画阅读",
|
"그림 · 만화 보기, PDF 보기 · 페이지 편집, ZIP 목록 · 만화 보기": "图片 · 漫画阅读,PDF 查看 · 页面编辑,ZIP 列表 · 漫画阅读",
|
||||||
"그림 · 프롬프트가 기기 밖으로 나갑니다 ({0})": "图片和提示词将离开本设备 ({0})",
|
"그림 · 프롬프트가 기기 밖으로 나갑니다 ({0})": "图片和提示词将离开本设备 ({0})",
|
||||||
"그림 보기 대상": "图片查看的文件类型",
|
"그림 보기 대상": "图片查看的文件类型",
|
||||||
|
"그림 파일을 읽을 수 없습니다 - 깨졌거나 지원하지 않는 형식": "无法读取图片文件 - 已损坏或格式不受支持",
|
||||||
"그림으로 바꾸는 중": "正在生成图像",
|
"그림으로 바꾸는 중": "正在生成图像",
|
||||||
"그림을 만들지 못했습니다: {0}": "无法生成图片: {0}",
|
"그림을 만들지 못했습니다: {0}": "无法生成图片: {0}",
|
||||||
"그림을 바꿔 만들기": "基于图片生成",
|
"그림을 바꿔 만들기": "基于图片生成",
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,9 @@ class _ReaderPageState extends State<ReaderPage> {
|
||||||
/// PDF 는 페이지를 지우거나 넣으면 다시 그린다
|
/// PDF 는 페이지를 지우거나 넣으면 다시 그린다
|
||||||
int _version = 0;
|
int _version = 0;
|
||||||
|
|
||||||
|
/// [다시 시도] 를 누른 횟수 (장마다) - 그 장의 화면을 새로 만든다 (28 지적: 같은 키면 실패한 그림을 그대로 다시 썼다)
|
||||||
|
final _retry = <int, int>{};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -442,37 +445,51 @@ class _ReaderPageState extends State<ReaderPage> {
|
||||||
final slot = _v[i];
|
final slot = _v[i];
|
||||||
final half = slot.part >= 0;
|
final half = slot.part >= 0;
|
||||||
// 관리자 지시 (10/20, HWP 2단계): 이유와 [다시 시도] - 그 장의 기억을 지우고 다시 부른다 (시간 제한을 넘긴 HWP 묶음 등)
|
// 관리자 지시 (10/20, HWP 2단계): 이유와 [다시 시도] - 그 장의 기억을 지우고 다시 부른다 (시간 제한을 넘긴 HWP 묶음 등)
|
||||||
Widget failed(Object? e) => SizedBox(
|
// [provider]: 그림 파일 · ZIP 의 그림을 읽다 (디코드) 실패한 것 - 화면 글은 종류에 맞게, 원문은 [자세히] (28 지적)
|
||||||
width: 320,
|
Widget failed(Object? e, {ImageProvider? provider}) {
|
||||||
height: 160,
|
final text = e is HwpPdfException
|
||||||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
? trf('열 수 없습니다: {0}', [e.message])
|
||||||
Text(trf('열 수 없습니다: {0}', [e]), textAlign: TextAlign.center, style: const TextStyle(color: Colors.white70)),
|
: provider != null
|
||||||
const SizedBox(height: 8),
|
? tr('그림 파일을 읽을 수 없습니다 - 깨졌거나 지원하지 않는 형식')
|
||||||
// 관리자 지시 (10/20, 28 지적): 변환기 원문은 화면 글 대신 [자세히] 에
|
: trf('열 수 없습니다: {0}', [e]);
|
||||||
if (e is HwpPdfException && e.detail.isNotEmpty)
|
final detail = e is HwpPdfException ? e.detail : (provider != null ? '$e' : '');
|
||||||
TextButton(
|
return SizedBox(
|
||||||
onPressed: () => showDialog<void>(
|
width: 320,
|
||||||
context: context,
|
height: 160,
|
||||||
builder: (ctx) => AlertDialog(
|
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
content: SelectableText(e.detail, style: const TextStyle(fontSize: 12)),
|
Text(text, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white70)),
|
||||||
actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(tr('닫기')))],
|
const SizedBox(height: 8),
|
||||||
|
// 관리자 지시 (10/20, 28 지적): 변환기 · 그림 디코드 원문은 화면 글 대신 [자세히] 에
|
||||||
|
if (detail.isNotEmpty)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
content: SelectableText(detail, style: const TextStyle(fontSize: 12)),
|
||||||
|
actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(tr('닫기')))],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
child: Text(tr('자세히')),
|
||||||
),
|
),
|
||||||
child: Text(tr('자세히')),
|
OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
// 그림은 Flutter 의 그림 캐시에 실패가 남아 같은 키로 다시 쓰인다 - 비우고 (ResizeImage 의 키째) 다시 읽는다
|
||||||
|
if (provider != null) unawaited(provider.evict());
|
||||||
|
setState(() {
|
||||||
|
_cache.remove(slot.page);
|
||||||
|
_cacheHalf.remove(slot.page);
|
||||||
|
_sharp.remove(i);
|
||||||
|
_sharpWidth.remove(i);
|
||||||
|
_retry[slot.page] = (_retry[slot.page] ?? 0) + 1;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(tr('다시 시도')),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
]));
|
||||||
onPressed: () => setState(() {
|
}
|
||||||
_cache.remove(slot.page);
|
|
||||||
_cacheHalf.remove(slot.page);
|
|
||||||
_sharp.remove(i);
|
|
||||||
_sharpWidth.remove(i);
|
|
||||||
}),
|
|
||||||
child: Text(tr('다시 시도')),
|
|
||||||
),
|
|
||||||
]));
|
|
||||||
// 확대해 다시 그린 것이 있으면 그것 (다 그릴 때까지는 앞의 그림을 그대로 보인다)
|
// 확대해 다시 그린 것이 있으면 그것 (다 그릴 때까지는 앞의 그림을 그대로 보인다)
|
||||||
final page = FutureBuilder<ReaderImage>(
|
final page = FutureBuilder<ReaderImage>(
|
||||||
key: ValueKey('$_version:${slot.page}:${slot.part}'),
|
key: ValueKey('$_version:${slot.page}:${slot.part}:${_retry[slot.page] ?? 0}'),
|
||||||
future: _sharp[i] ?? (half ? _loadHalf(slot.page) : _load(slot.page)),
|
future: _sharp[i] ?? (half ? _loadHalf(slot.page) : _load(slot.page)),
|
||||||
builder: (context, snap) {
|
builder: (context, snap) {
|
||||||
if (snap.hasError) return Center(child: failed(snap.error));
|
if (snap.hasError) return Center(child: failed(snap.error));
|
||||||
|
|
@ -481,7 +498,7 @@ class _ReaderPageState extends State<ReaderPage> {
|
||||||
Widget picture = r.image != null
|
Widget picture = r.image != null
|
||||||
? RawImage(image: r.image, filterQuality: FilterQuality.medium)
|
? RawImage(image: r.image, filterQuality: FilterQuality.medium)
|
||||||
: Image(image: r.provider!, gaplessPlayback: true, filterQuality: FilterQuality.medium,
|
: Image(image: r.provider!, gaplessPlayback: true, filterQuality: FilterQuality.medium,
|
||||||
errorBuilder: (_, e, _) => failed(e));
|
errorBuilder: (_, e, _) => failed(e, provider: r.provider));
|
||||||
// 120: 회전 · 상하 반전 (보기만, 파일은 그대로)
|
// 120: 회전 · 상하 반전 (보기만, 파일은 그대로)
|
||||||
if (_turns != 0) picture = RotatedBox(quarterTurns: _turns, child: picture);
|
if (_turns != 0) picture = RotatedBox(quarterTurns: _turns, child: picture);
|
||||||
if (_flipV) picture = Transform.flip(flipY: true, child: picture);
|
if (_flipV) picture = Transform.flip(flipY: true, child: picture);
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,33 @@ void main() {
|
||||||
expect(c.settings.readerSystemBars, isFalse);
|
expect(c.settings.readerSystemBars, isFalse);
|
||||||
expect(AppSettings.fromJson(c.settings.toJson()).readerSystemBars, isFalse);
|
expect(AppSettings.fromJson(c.settings.toJson()).readerSystemBars, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('그림이 깨졌으면 화면 글 + [자세히], 파일을 고친 뒤 [다시 시도] 하면 열린다 (28 지적: 같은 키 · 같은 그림이라 Image 가 다시 읽지 않았다 - 장의 키를 올리고 그림 캐시도 비움)', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(900, 1200);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
final broken = File(p.join(tmp.path, 'broken.png'))..writeAsBytesSync([1, 2, 3, 4, 5]);
|
||||||
|
final c = AppController(PlatformServices(mediaTool: ProcessMediaTool('x', 'y'), storage: DesktopStorageService()));
|
||||||
|
final src = ImageFilesSource([broken.path], tempDir: tmp.path, title: 'imgs');
|
||||||
|
Future<void> settle() async {
|
||||||
|
for (var k = 0; k < 20; k++) {
|
||||||
|
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 50)));
|
||||||
|
await tester.pump();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(MaterialApp(home: ReaderPage(c: c, source: src)));
|
||||||
|
await settle();
|
||||||
|
expect(find.text('그림 파일을 읽을 수 없습니다 - 깨졌거나 지원하지 않는 형식'), findsOneWidget, reason: '원문 대신 화면 글');
|
||||||
|
expect(find.text('자세히'), findsOneWidget, reason: '원문은 [자세히] 에');
|
||||||
|
// 파일을 고치고 [다시 시도]
|
||||||
|
broken.writeAsBytesSync(_png(0, 200, 0));
|
||||||
|
await tester.tap(find.text('다시 시도'));
|
||||||
|
await settle();
|
||||||
|
expect(find.text('그림 파일을 읽을 수 없습니다 - 깨졌거나 지원하지 않는 형식'), findsNothing);
|
||||||
|
expect(find.byType(RawImage), findsWidgets, reason: '고친 그림이 보임');
|
||||||
|
await tester.pumpWidget(const SizedBox());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 읽은 너비를 기록 (확대하면 더 크게 다시 읽는지)
|
/// 읽은 너비를 기록 (확대하면 더 크게 다시 읽는지)
|
||||||
|
|
@ -209,4 +236,5 @@ class _CountingSource implements ReaderSource {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() => inner.dispose();
|
Future<void> dispose() => inner.dispose();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue