Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion lib/features/canvas/pages/note_editor_screen.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -51,7 +53,11 @@ class NoteEditorScreen extends ConsumerStatefulWidget {

class _NoteEditorScreenState extends ConsumerState<NoteEditorScreen>
with RouteAware {
late final ProviderSubscription<NoteEditorUiState> _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.
Expand Down Expand Up @@ -96,25 +102,51 @@ class _NoteEditorScreenState extends ConsumerState<NoteEditorScreen>
WidgetsBinding.instance.addPostFrameCallback((_) => attempt());
}

@override
void initState() {
super.initState();
_uiSubscription = ref.listenManual<NoteEditorUiState>(
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();
final route = ModalRoute.of(context);
if (route != null) {
appRouteObserver.subscribe(this, route);
debugPrint('🧭 [RouteAware] subscribe noteId=${widget.noteId}');
_isRouteActive = route.isCurrent;
if (_isRouteActive) {
_scheduleApplySystemUi();
}
}
}

@override
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',
);
Expand Down Expand Up @@ -144,6 +176,8 @@ class _NoteEditorScreenState extends ConsumerState<NoteEditorScreen>
);
return;
}
_isRouteActive = true;
_scheduleApplySystemUi();
// Ensure re-enter runs one frame AFTER didPop's exit to avoid final null.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
Expand Down Expand Up @@ -175,6 +209,8 @@ class _NoteEditorScreenState extends ConsumerState<NoteEditorScreen>

@override
void didPushNext() {
_isRouteActive = false;
_restoreSystemUiIfNeeded();
debugPrint(
'🧭 [RouteAware] didPushNext noteId=${widget.noteId} (save & no-op)',
);
Expand All @@ -186,6 +222,8 @@ class _NoteEditorScreenState extends ConsumerState<NoteEditorScreen>

@override
void didPop() {
_isRouteActive = false;
_restoreSystemUiIfNeeded();
debugPrint(
'🧭 [RouteAware] didPop noteId=${widget.noteId} → schedule exit session',
);
Expand Down Expand Up @@ -219,6 +257,10 @@ class _NoteEditorScreenState extends ConsumerState<NoteEditorScreen>
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(
Expand Down Expand Up @@ -254,7 +296,9 @@ class _NoteEditorScreenState extends ConsumerState<NoteEditorScreen>
if (_lastLoggedNoteId != note.noteId) {
_lastLoggedNoteId = note.noteId;
unawaited(
ref.read(firebaseAnalyticsLoggerProvider).logNoteOpen(
ref
.read(firebaseAnalyticsLoggerProvider)
.logNoteOpen(
noteId: note.noteId,
source: 'route',
),
Expand Down Expand Up @@ -373,4 +417,39 @@ class _NoteEditorScreenState extends ConsumerState<NoteEditorScreen>
),
);
}

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);
});
}
}
3 changes: 3 additions & 0 deletions lib/features/notes/providers/note_list_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -134,6 +135,8 @@ class NoteListController extends StateNotifier<NoteListState> {
parentFolderId: folderId,
);
return AppErrorSpec.success('PDF 노트 "${pdfNote.title}"가 생성되었습니다.');
} on PdfImportCancelledException {
return AppErrorSpec.info('PDF 가져오기를 취소했어요.');
} catch (error) {
return AppErrorMapper.toSpec(error);
} finally {
Expand Down
8 changes: 8 additions & 0 deletions lib/shared/errors/pdf_import_cancelled_exception.dart
Original file line number Diff line number Diff line change
@@ -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';
}
4 changes: 3 additions & 1 deletion lib/shared/services/file_picker_service.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';

import '../errors/pdf_import_cancelled_exception.dart';

/// 📁 파일 선택 서비스 (모바일 앱 전용)
///
/// PDF 파일 선택 기능을 제공합니다.
Expand Down Expand Up @@ -34,7 +36,7 @@ class FilePickerService {
}
} else {
debugPrint('ℹ️ PDF 파일 선택 취소됨.');
return null;
throw const PdfImportCancelledException();
}
} catch (e) {
debugPrint('❌ 파일 선택 중 오류 발생: $e');
Expand Down
6 changes: 5 additions & 1 deletion lib/shared/services/note_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -90,7 +91,7 @@ class NoteService {
// 1. PDF 처리 (PdfProcessor에 위임)
final pdfData = await PdfProcessor.processFromSelection();
if (pdfData == null) {
print('ℹ️ PDF 노트 생성 취소');
print(' PDF 데이터를 불러오지 못했습니다.');
return null;
}

Expand All @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion lib/shared/services/pdf_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -41,7 +42,7 @@ class PdfProcessor {
// 1. PDF 파일 선택
final sourcePdfPath = await FilePickerService.pickPdfFile();
if (sourcePdfPath == null) {
print('ℹ️ PDF 파일 선택 취소');
print('⚠️ PDF 파일 경로를 확인할 수 없습니다.');
return null;
}

Expand All @@ -55,6 +56,9 @@ class PdfProcessor {
sourcePdfPath: sourcePdfPath,
noteId: noteId,
);
} on PdfImportCancelledException {
// 사용자가 명시적으로 선택을 취소한 경우 호출자로 전달
rethrow;
} catch (e) {
print('❌ PDF 처리 실패: $e');
return null;
Expand Down
3 changes: 2 additions & 1 deletion lib/shared/services/vault_notes_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) 최종 제목 확정(자동 접미사 포함)
Expand Down