diff --git a/assets/l10n/en.json b/assets/l10n/en.json index de42852..9c006b7 100644 --- a/assets/l10n/en.json +++ b/assets/l10n/en.json @@ -2452,6 +2452,7 @@ "지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "The last run did not end normally (last check {0}:{1}). It has been noted in the job log.", "지난 업데이트가 남긴 임시 파일을 지움: {0}": "Removed temporary files left by the last update: {0}", "지난 작업 기록": "Old job logs", + "지난번에 남은 문서 보기 임시 폴더를 지움: {0}개": "Removed {0} leftover document-view temp folders", "지문 확인하고 연결": "Check fingerprint and connect", "지우고 맞추기": "Delete and sync", "지우고 초기화": "Delete and reset", diff --git a/assets/l10n/ja.json b/assets/l10n/ja.json index 09393b3..53ff40b 100644 --- a/assets/l10n/ja.json +++ b/assets/l10n/ja.json @@ -2452,6 +2452,7 @@ "지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "前回の実行が正常に終了しませんでした(最終確認 {0}:{1})。作業記録に残しました。", "지난 업데이트가 남긴 임시 파일을 지움: {0}": "前回のアップデートが残した一時ファイルを削除: {0}", "지난 작업 기록": "過去の作業記録", + "지난번에 남은 문서 보기 임시 폴더를 지움: {0}개": "前回残った文書表示の一時フォルダーを削除: {0} 個", "지문 확인하고 연결": "フィンガープリントを確認して接続", "지우고 맞추기": "削除して同期", "지우고 초기화": "削除して初期化", diff --git a/assets/l10n/zh-Hans.json b/assets/l10n/zh-Hans.json index bd1f8ff..33e369f 100644 --- a/assets/l10n/zh-Hans.json +++ b/assets/l10n/zh-Hans.json @@ -2452,6 +2452,7 @@ "지난 실행이 정상적으로 끝나지 않았습니다 (마지막 확인 {0}:{1}). 작업 기록에 남겨 두었습니다.": "上次运行未正常结束(最后检查 {0}:{1})。已记录在任务记录中。", "지난 업데이트가 남긴 임시 파일을 지움: {0}": "已删除上次更新留下的临时文件: {0}", "지난 작업 기록": "旧任务记录", + "지난번에 남은 문서 보기 임시 폴더를 지움: {0}개": "已删除上次留下的文档查看临时文件夹: {0} 个", "지문 확인하고 연결": "确认指纹并连接", "지우고 맞추기": "删除并同步", "지우고 초기화": "删除并重置", diff --git a/lib/app/app_controller.dart b/lib/app/app_controller.dart index ccf8268..89a9bcb 100644 --- a/lib/app/app_controller.dart +++ b/lib/app/app_controller.dart @@ -32,6 +32,7 @@ import '../services/platform_services.dart'; import '../services/system_usage.dart'; import 'settings.dart'; import 'cleanup.dart' show removeUpdateLeftovers; +import '../core/hwp_source.dart' show removeStaleHwpWork; import '../l10n/tr.dart'; import '../core/remote_idle.dart'; @@ -213,6 +214,11 @@ class AppController extends ChangeNotifier { final gone = removeUpdateLeftovers(Directory.systemTemp.path); if (gone.isNotEmpty) _log(trf('지난 업데이트가 남긴 임시 파일을 지움: {0}', [gone.join(', ')])); } + // 관리자 지시 (10/20): 강제로 끝나 남은 HWP 보기 작업 폴더 (바꾼 PDF 들) + try { + final hwp = removeStaleHwpWork(await services.storage.tempDirectory()); + if (hwp.isNotEmpty) _log(trf('지난번에 남은 문서 보기 임시 폴더를 지움: {0}개', [hwp.length])); + } catch (_) {} ffmpegVersion = await _tool.version(); if (ffmpegVersion == null) { _log(tr('⚠ FFmpeg 를 찾을 수 없습니다. MKV 만들기를 사용할 수 없습니다.')); diff --git a/lib/core/hwp_source.dart b/lib/core/hwp_source.dart index f623a7e..680dd57 100644 --- a/lib/core/hwp_source.dart +++ b/lib/core/hwp_source.dart @@ -12,6 +12,25 @@ import '../l10n/tr.dart'; /// HWP · HWPX 확장자 (앱 안에서 PDF 로 바꿔 보기 - Android, rhwp) const hwpExtensions = ['hwp', 'hwpx']; +/// 관리자 지시 (10/20): 앱이 강제로 끝나 남은 HWP 작업 폴더 (앱 임시 폴더의 jj_hwp_…, 바꾼 PDF 들) 를 앱이 켜질 때 지운다. +/// 막 생긴 것 ([minAge] 안) 은 남긴다. 지운 이름을 돌려준다 +List removeStaleHwpWork(String tempDir, {Duration minAge = const Duration(minutes: 1), DateTime? now}) { + final before = (now ?? DateTime.now()).subtract(minAge); + final removed = []; + try { + for (final e in Directory(tempDir).listSync(followLinks: false)) { + final name = p.basename(e.path); + if (e is! Directory || !name.startsWith('jj_hwp_')) continue; + try { + if (!e.statSync().modified.isBefore(before)) continue; + e.deleteSync(recursive: true); + removed.add(name); + } catch (_) {} + } + } catch (_) {} + return removed; +} + /// HWP · HWPX → PDF 엔진 (Android: APK 의 libjj_rhwp.so - lib/platform/android/hwp_pdf_ffi.dart, 시험: 가짜) abstract class HwpPdfEngine { /// 문서 열기 (암호가 있으면 [password]). 실패하면 [HwpPdfException] diff --git a/lib/ui/explorer_page.dart b/lib/ui/explorer_page.dart index 9e616d2..b6d59f9 100644 --- a/lib/ui/explorer_page.dart +++ b/lib/ui/explorer_page.dart @@ -41,6 +41,7 @@ import '../platform/windows/recycle_bin.dart'; import '../core/rsync_filter.dart'; import '../core/sync_preview.dart'; import 'path_label.dart'; +import 'progress_dialog.dart'; import 'sync_preview_view.dart'; import 'sftp_settings.dart'; import 'ftp_settings.dart'; @@ -1768,17 +1769,7 @@ class _ExplorerPageState extends State { Future _viewDoc(String path) async { final temp = await c.services.storage.tempDirectory(); if (!mounted) return; - final dialog = showDialog( - context: context, - barrierDismissible: false, - builder: (_) => AlertDialog( - content: Row(children: [ - const CircularProgressIndicator(), - const SizedBox(width: 16), - Expanded(child: Text(trf('PDF 로 바꾸는 중: {0}', [vBasename(path)]))), - ]), - ), - ); + final dialog = showProgressDialog(context, trf('PDF 로 바꾸는 중: {0}', [vBasename(path)])); String? pdf; Object? error; try { @@ -1787,8 +1778,10 @@ class _ExplorerPageState extends State { } catch (e) { error = e; } - if (mounted) Navigator.of(context).pop(); - await dialog; + // 사용자가 [취소] · ← 로 그만두었으면 결과를 보이지 않는다 (바꾸기는 끝까지 돌았음) + if (dialog.cancelled()) return; + dialog.close(); + await dialog.done; if (pdf == null) { _snack(trf('미리 볼 수 없습니다: {0}', [error])); return; @@ -1808,17 +1801,7 @@ class _ExplorerPageState extends State { Future _viewHwp(String path) async { final temp = await c.services.storage.tempDirectory(); if (!mounted) return true; - final dialog = showDialog( - context: context, - barrierDismissible: false, - builder: (_) => AlertDialog( - content: Row(children: [ - const CircularProgressIndicator(), - const SizedBox(width: 16), - Expanded(child: Text(trf('여는 중: {0}', [vBasename(path)]))), - ]), - ), - ); + final dialog = showProgressDialog(context, trf('여는 중: {0}', [vBasename(path)])); HwpSource? src; Object? error; try { @@ -1826,8 +1809,13 @@ class _ExplorerPageState extends State { } catch (e) { error = e; } - if (mounted) Navigator.of(context).pop(); - await dialog; + // 사용자가 [취소] · ← 로 그만두었으면 연 문서를 닫고 끝 (다른 앱으로도 열지 않음) + if (dialog.cancelled()) { + await src?.dispose(); + return true; + } + dialog.close(); + await dialog.done; if (src == null) { _snack(trf('앱 안에서 열지 못해 다른 앱으로 엽니다: {0}', [error])); return false; diff --git a/lib/ui/progress_dialog.dart b/lib/ui/progress_dialog.dart new file mode 100644 index 0000000..e16337e --- /dev/null +++ b/lib/ui/progress_dialog.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; + +import '../l10n/tr.dart'; + +/// 바꾸는 · 여는 동안의 진행 창 ([취소] 있음). 관리자 지시 (10/20, 28 지적): Android ← · [취소] 로 창이 먼저 닫힐 수 있어, +/// 일이 끝난 뒤에는 그 창이 아직 열려 있을 때만 닫는다 (예전에는 끝난 뒤의 pop() 이 다른 화면을 닫을 수 있었다). +/// - [ProgressDialog.cancelled]: 사용자가 [취소] · ← 로 그만두었는지 (그러면 결과를 보이지 않는다) +/// - [ProgressDialog.close]: 일이 끝나 창을 닫는다 (이미 닫혔으면 아무것도 하지 않는다) +class ProgressDialog { + final Future done; + final bool Function() cancelled; + final void Function() close; + const ProgressDialog._(this.done, this.cancelled, this.close); +} + +ProgressDialog showProgressDialog(BuildContext context, String text) { + BuildContext? dctx; + var open = true; + var byUser = true; + final done = showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) { + dctx = ctx; + return AlertDialog( + content: Row(children: [ + const CircularProgressIndicator(), + const SizedBox(width: 16), + Expanded(child: Text(text)), + ]), + actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(tr('취소')))], + ); + }, + ).whenComplete(() => open = false); + return ProgressDialog._(done, () => !open && byUser, () { + if (!open) return; + byUser = false; + final x = dctx; + if (x != null && x.mounted) Navigator.of(x).pop(); + }); +} diff --git a/test/hwp_source_unit_test.dart b/test/hwp_source_unit_test.dart index b137ecf..ccfbe83 100644 --- a/test/hwp_source_unit_test.dart +++ b/test/hwp_source_unit_test.dart @@ -166,4 +166,19 @@ void main() { expect(r, isA(), reason: '닫힌 문서'); expect(o.opened, isEmpty, reason: '닫은 뒤에는 PDF 를 열지 않음'); }); + + test('앱이 켜질 때 남은 HWP 작업 폴더 (jj_hwp_) 만 지운다 - 막 생긴 것 · 다른 이름은 남김', () { + final old = Directory(p.join(dir.path, 'jj_hwp_1'))..createSync(); + File(p.join(old.path, 'p0.pdf')).writeAsStringSync('x'); + final fresh = Directory(p.join(dir.path, 'jj_hwp_2'))..createSync(); + final other = Directory(p.join(dir.path, 'jj_hwpx_keep'))..createSync(); + final file = File(p.join(dir.path, 'jj_hwp_file'))..writeAsStringSync('x'); + // 5분 뒤로 보면 old · fresh 둘 다 1분이 지났다 - fresh 는 지금 기준으로는 남는다 + expect(removeStaleHwpWork(dir.path), isEmpty, reason: '모두 막 생김'); + final gone = removeStaleHwpWork(dir.path, now: DateTime.now().add(const Duration(minutes: 5))); + expect(gone..sort(), ['jj_hwp_1', 'jj_hwp_2']); + expect(old.existsSync() || fresh.existsSync(), isFalse); + expect(other.existsSync(), isTrue, reason: '이름이 jj_hwp_ 로 시작하지 않음 (jj_hwpx_…)'); + expect(file.existsSync(), isTrue, reason: '폴더만'); + }); } diff --git a/test/progress_dialog_test.dart b/test/progress_dialog_test.dart new file mode 100644 index 0000000..0d9b338 --- /dev/null +++ b/test/progress_dialog_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jj_mkvmaker/ui/progress_dialog.dart'; + +/// 관리자 지시 (10/20, 28 지적): 진행 창이 ← · [취소] 로 먼저 닫혀도, 일이 끝난 뒤 close() 가 아래 화면을 닫지 않는다 +void main() { + Future pushPage(WidgetTester tester) async { + late BuildContext page; + await tester.pumpWidget(MaterialApp(home: Builder(builder: (ctx) { + return Scaffold( + body: TextButton( + onPressed: () => Navigator.of(ctx).push(MaterialPageRoute(builder: (c) { + page = c; + return const Scaffold(body: Text('아래 화면')); + })), + child: const Text('열기'), + ), + ); + }))); + await tester.tap(find.text('열기')); + await tester.pumpAndSettle(); + return page; + } + + testWidgets('← 로 닫으면 그만둔 것 - 끝난 뒤 close() 는 아래 화면을 닫지 않는다', (tester) async { + final page = await pushPage(tester); + final d = showProgressDialog(page, '여는 중: a.hwp'); + await tester.pump(); + expect(find.text('여는 중: a.hwp'), findsOneWidget); + expect(d.cancelled(), isFalse); + // Android ← (창을 닫음) + await tester.binding.handlePopRoute(); + await tester.pumpAndSettle(); + expect(find.text('여는 중: a.hwp'), findsNothing); + expect(d.cancelled(), isTrue); + d.close(); // 일이 끝남 - 이미 닫혔으니 아무것도 닫지 않는다 + await tester.pumpAndSettle(); + expect(find.text('아래 화면'), findsOneWidget, reason: '예전에는 이 pop() 이 아래 화면을 닫았다'); + }); + + testWidgets('[취소] 도 그만둔 것, 일이 끝나 close() 하면 창만 닫히고 그만둔 것이 아니다', (tester) async { + final page = await pushPage(tester); + final a = showProgressDialog(page, '바꾸는 중'); + await tester.pump(); + await tester.tap(find.text('취소')); + await tester.pumpAndSettle(); + expect(a.cancelled(), isTrue); + expect(find.text('아래 화면'), findsOneWidget); + + final b = showProgressDialog(page, '바꾸는 중'); + await tester.pump(); + b.close(); + await tester.pumpAndSettle(); + await b.done; + expect(b.cancelled(), isFalse); + expect(find.text('바꾸는 중'), findsNothing); + expect(find.text('아래 화면'), findsOneWidget); + }); +}