From f089e046a413e466cc44cf559edb4b30c0cc42cf Mon Sep 17 00:00:00 2001 From: ehdnd_mac Date: Wed, 5 Nov 2025 02:51:14 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(canvas):=20=EC=A0=84=EC=B2=B4=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=8B=9C=20=ED=95=98=EB=8B=A8=20/=20=EC=83=81?= =?UTF-8?q?=EB=8B=A8=20UI=20=EB=B0=94=20=EC=82=AC=EB=9D=BC=EC=A7=80?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20(#51)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../canvas/pages/note_editor_screen.dart | 81 ++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/lib/features/canvas/pages/note_editor_screen.dart b/lib/features/canvas/pages/note_editor_screen.dart index 67e3bb0..4241409 100644 --- a/lib/features/canvas/pages/note_editor_screen.dart +++ b/lib/features/canvas/pages/note_editor_screen.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../design_system/components/atoms/app_fab_icon.dart'; @@ -51,7 +53,11 @@ class NoteEditorScreen extends ConsumerStatefulWidget { class _NoteEditorScreenState extends ConsumerState with RouteAware { + late final ProviderSubscription _uiSubscription; String? _lastLoggedNoteId; + bool _isRouteActive = false; + bool _lastRequestedFullscreen = false; + bool? _lastAppliedFullscreen; /// Sync the initial page index from per-route resume or lastKnown after /// route becomes current and note data is available. @@ -96,6 +102,24 @@ class _NoteEditorScreenState extends ConsumerState WidgetsBinding.instance.addPostFrameCallback((_) => attempt()); } + @override + void initState() { + super.initState(); + _uiSubscription = ref.listenManual( + noteEditorUiStateProvider(widget.noteId), + (previous, next) { + _lastRequestedFullscreen = next.isFullscreen; + if (!_isRouteActive) return; + if (previous?.isFullscreen == next.isFullscreen && + _lastAppliedFullscreen == next.isFullscreen) { + return; + } + _applySystemUiForEditor(fullscreen: next.isFullscreen); + }, + fireImmediately: true, + ); + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -103,6 +127,10 @@ class _NoteEditorScreenState extends ConsumerState if (route != null) { appRouteObserver.subscribe(this, route); debugPrint('๐Ÿงญ [RouteAware] subscribe noteId=${widget.noteId}'); + _isRouteActive = route.isCurrent; + if (_isRouteActive) { + _scheduleApplySystemUi(); + } } } @@ -110,11 +138,15 @@ class _NoteEditorScreenState extends ConsumerState void dispose() { appRouteObserver.unsubscribe(this); debugPrint('๐Ÿงญ [RouteAware] unsubscribe noteId=${widget.noteId}'); + _restoreSystemUiIfNeeded(); + _uiSubscription.close(); super.dispose(); } @override void didPush() { + _isRouteActive = true; + _scheduleApplySystemUi(); debugPrint( '๐Ÿงญ [RouteAware] didPush noteId=${widget.noteId} โ†’ schedule enter session', ); @@ -144,6 +176,8 @@ class _NoteEditorScreenState extends ConsumerState ); return; } + _isRouteActive = true; + _scheduleApplySystemUi(); // Ensure re-enter runs one frame AFTER didPop's exit to avoid final null. WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; @@ -175,6 +209,8 @@ class _NoteEditorScreenState extends ConsumerState @override void didPushNext() { + _isRouteActive = false; + _restoreSystemUiIfNeeded(); debugPrint( '๐Ÿงญ [RouteAware] didPushNext noteId=${widget.noteId} (save & no-op)', ); @@ -186,6 +222,8 @@ class _NoteEditorScreenState extends ConsumerState @override void didPop() { + _isRouteActive = false; + _restoreSystemUiIfNeeded(); debugPrint( '๐Ÿงญ [RouteAware] didPop noteId=${widget.noteId} โ†’ schedule exit session', ); @@ -219,6 +257,10 @@ class _NoteEditorScreenState extends ConsumerState if (!mounted) return; final route = ModalRoute.of(context); final isCurrent = route?.isCurrent ?? false; + if (isCurrent && !_isRouteActive) { + _isRouteActive = true; + _scheduleApplySystemUi(); + } final active = ref.read(noteSessionProvider); if (isCurrent && active != widget.noteId) { debugPrint( @@ -254,7 +296,9 @@ class _NoteEditorScreenState extends ConsumerState if (_lastLoggedNoteId != note.noteId) { _lastLoggedNoteId = note.noteId; unawaited( - ref.read(firebaseAnalyticsLoggerProvider).logNoteOpen( + ref + .read(firebaseAnalyticsLoggerProvider) + .logNoteOpen( noteId: note.noteId, source: 'route', ), @@ -373,4 +417,39 @@ class _NoteEditorScreenState extends ConsumerState ), ); } + + bool get _supportsSystemUiOverrides => + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + + void _applySystemUiForEditor({required bool fullscreen}) { + if (!_supportsSystemUiOverrides) return; + if (_lastAppliedFullscreen == fullscreen) return; + _lastAppliedFullscreen = fullscreen; + final future = fullscreen + ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky) + : SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: const [SystemUiOverlay.top], + ); + unawaited(future); + } + + void _restoreSystemUiIfNeeded() { + if (!_supportsSystemUiOverrides) return; + if (_lastAppliedFullscreen == null) return; + _lastAppliedFullscreen = null; + final future = SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + unawaited(future); + } + + void _scheduleApplySystemUi() { + if (!_isRouteActive) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || !_isRouteActive) return; + _applySystemUiForEditor(fullscreen: _lastRequestedFullscreen); + }); + } } From 3ba42f1807d3e87f3506e527d4c8f09150aeb145 Mon Sep 17 00:00:00 2001 From: ehdnd_mac Date: Wed, 5 Nov 2025 03:02:21 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(canvas):=20pdf=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20-=20file=20picker=20=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=B7=A8=EC=86=8C=20=EC=8B=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=B7=A8=EC=86=8C=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=EB=A1=9C=20=EC=88=98=EC=A0=95.=20(#?= =?UTF-8?q?48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/features/notes/providers/note_list_controller.dart | 3 +++ lib/shared/errors/pdf_import_cancelled_exception.dart | 8 ++++++++ lib/shared/services/file_picker_service.dart | 4 +++- lib/shared/services/note_service.dart | 6 +++++- lib/shared/services/pdf_processor.dart | 6 +++++- lib/shared/services/vault_notes_service.dart | 3 ++- 6 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 lib/shared/errors/pdf_import_cancelled_exception.dart diff --git a/lib/features/notes/providers/note_list_controller.dart b/lib/features/notes/providers/note_list_controller.dart index 0f4c88a..3295571 100644 --- a/lib/features/notes/providers/note_list_controller.dart +++ b/lib/features/notes/providers/note_list_controller.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../shared/errors/app_error_mapper.dart'; import '../../../shared/errors/app_error_spec.dart'; +import '../../../shared/errors/pdf_import_cancelled_exception.dart'; import '../../../shared/services/firebase_service_providers.dart'; import '../../../shared/services/vault_notes_service.dart'; import '../../vaults/data/derived_vault_providers.dart'; @@ -134,6 +135,8 @@ class NoteListController extends StateNotifier { parentFolderId: folderId, ); return AppErrorSpec.success('PDF ๋…ธํŠธ "${pdfNote.title}"๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + } on PdfImportCancelledException { + return AppErrorSpec.info('PDF ๊ฐ€์ ธ์˜ค๊ธฐ๋ฅผ ์ทจ์†Œํ–ˆ์–ด์š”.'); } catch (error) { return AppErrorMapper.toSpec(error); } finally { diff --git a/lib/shared/errors/pdf_import_cancelled_exception.dart b/lib/shared/errors/pdf_import_cancelled_exception.dart new file mode 100644 index 0000000..f0af9e5 --- /dev/null +++ b/lib/shared/errors/pdf_import_cancelled_exception.dart @@ -0,0 +1,8 @@ +/// Thrown when a user cancels the PDF import flow before completion. +class PdfImportCancelledException implements Exception { + /// Creates a new cancellation exception. + const PdfImportCancelledException(); + + @override + String toString() => 'PdfImportCancelledException'; +} diff --git a/lib/shared/services/file_picker_service.dart b/lib/shared/services/file_picker_service.dart index 259d99b..3152a03 100644 --- a/lib/shared/services/file_picker_service.dart +++ b/lib/shared/services/file_picker_service.dart @@ -1,6 +1,8 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/foundation.dart'; +import '../errors/pdf_import_cancelled_exception.dart'; + /// ๐Ÿ“ ํŒŒ์ผ ์„ ํƒ ์„œ๋น„์Šค (๋ชจ๋ฐ”์ผ ์•ฑ ์ „์šฉ) /// /// PDF ํŒŒ์ผ ์„ ํƒ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. @@ -34,7 +36,7 @@ class FilePickerService { } } else { debugPrint('โ„น๏ธ PDF ํŒŒ์ผ ์„ ํƒ ์ทจ์†Œ๋จ.'); - return null; + throw const PdfImportCancelledException(); } } catch (e) { debugPrint('โŒ ํŒŒ์ผ ์„ ํƒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: $e'); diff --git a/lib/shared/services/note_service.dart b/lib/shared/services/note_service.dart index 8decea4..bc38055 100644 --- a/lib/shared/services/note_service.dart +++ b/lib/shared/services/note_service.dart @@ -2,6 +2,7 @@ import 'package:uuid/uuid.dart'; import '../../features/notes/models/note_model.dart'; import '../../features/notes/models/note_page_model.dart'; +import '../errors/pdf_import_cancelled_exception.dart'; import 'pdf_processed_data.dart'; import 'pdf_processor.dart'; @@ -90,7 +91,7 @@ class NoteService { // 1. PDF ์ฒ˜๋ฆฌ (PdfProcessor์— ์œ„์ž„) final pdfData = await PdfProcessor.processFromSelection(); if (pdfData == null) { - print('โ„น๏ธ PDF ๋…ธํŠธ ์ƒ์„ฑ ์ทจ์†Œ'); + print('โŒ PDF ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'); return null; } @@ -115,6 +116,9 @@ class NoteService { print('โœ… PDF ๋…ธํŠธ ์ƒ์„ฑ ์™„๋ฃŒ: $noteTitle (${pages.length}ํŽ˜์ด์ง€)'); return note; + } on PdfImportCancelledException { + print('โ„น๏ธ PDF ๋…ธํŠธ ์ƒ์„ฑ ์ทจ์†Œ'); + rethrow; } catch (e) { print('โŒ PDF ๋…ธํŠธ ์ƒ์„ฑ ์‹คํŒจ: $e'); return null; diff --git a/lib/shared/services/pdf_processor.dart b/lib/shared/services/pdf_processor.dart index 8446853..27ca3c3 100644 --- a/lib/shared/services/pdf_processor.dart +++ b/lib/shared/services/pdf_processor.dart @@ -5,6 +5,7 @@ import 'package:path/path.dart' as path; import 'package:pdfx/pdfx.dart'; import 'package:uuid/uuid.dart'; +import '../errors/pdf_import_cancelled_exception.dart'; import 'file_picker_service.dart'; import 'file_storage_service.dart'; import 'pdf_processed_data.dart'; @@ -41,7 +42,7 @@ class PdfProcessor { // 1. PDF ํŒŒ์ผ ์„ ํƒ final sourcePdfPath = await FilePickerService.pickPdfFile(); if (sourcePdfPath == null) { - print('โ„น๏ธ PDF ํŒŒ์ผ ์„ ํƒ ์ทจ์†Œ'); + print('โš ๏ธ PDF ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.'); return null; } @@ -55,6 +56,9 @@ class PdfProcessor { sourcePdfPath: sourcePdfPath, noteId: noteId, ); + } on PdfImportCancelledException { + // ์‚ฌ์šฉ์ž๊ฐ€ ๋ช…์‹œ์ ์œผ๋กœ ์„ ํƒ์„ ์ทจ์†Œํ•œ ๊ฒฝ์šฐ ํ˜ธ์ถœ์ž๋กœ ์ „๋‹ฌ + rethrow; } catch (e) { print('โŒ PDF ์ฒ˜๋ฆฌ ์‹คํŒจ: $e'); return null; diff --git a/lib/shared/services/vault_notes_service.dart b/lib/shared/services/vault_notes_service.dart index 4ac3fe8..62b0f42 100644 --- a/lib/shared/services/vault_notes_service.dart +++ b/lib/shared/services/vault_notes_service.dart @@ -13,6 +13,7 @@ import '../../features/vaults/models/folder_model.dart'; import '../../features/vaults/models/note_placement.dart'; import '../../features/vaults/models/vault_item.dart'; import '../../features/vaults/models/vault_model.dart'; +import '../errors/pdf_import_cancelled_exception.dart'; import '../repositories/link_repository.dart'; import '../repositories/vault_tree_repository.dart'; import 'db_txn_runner.dart'; @@ -284,7 +285,7 @@ class VaultNotesService { } final note = await noteService.createPdfNote(title: normalizedName); if (note == null) { - throw Exception('PDF note creation was cancelled or failed'); + throw const PdfImportCancelledException(); } // 2) ์ตœ์ข… ์ œ๋ชฉ ํ™•์ •(์ž๋™ ์ ‘๋ฏธ์‚ฌ ํฌํ•จ)