// lib/screens/notebook_screen.dart // // OneNote-style notebook container screen: lists the members (pages/imported // documents) of one vault folder holding a `notebook.json` manifest, and // routes taps to the matching editor by `NotebookMemberKind`. The open-by-type // routing intentionally MIRRORS `home_screen.dart`'s `_DocumentTile._openDocument` // so a member opens in exactly the editor its extension would on the home // screen (pdf → PenEditorScreen; pptx/docx → OfficeDocumentScreen; legacy ppt → // image-converted PenSlideScreen; note → PenNoteScreen). import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../editor/canvas/office_document_screen.dart'; import '../editor/canvas/pen_editor_screen.dart'; import '../editor/canvas/pen_note_screen.dart'; import '../editor/canvas/pen_slide_screen.dart'; import '../l10n/app_localizations.dart'; import '../models/note.dart'; import '../providers/document_provider.dart' show vaultServiceProvider; import '../services/pptx_service.dart'; import '../services/vault_service.dart'; import '../storage/notebook_manifest.dart'; /// Shows one notebook container's table of contents and lets the user add a /// blank ink page or import a file into it. class NotebookScreen extends ConsumerStatefulWidget { const NotebookScreen({ super.key, required this.folderPath, required this.title, }); /// Absolute path to the notebook container folder (holds `notebook.json`). final String folderPath; /// Title shown before the manifest loads (and as a fallback if it has none). final String title; @override ConsumerState createState() => _NotebookScreenState(); } class _NotebookScreenState extends ConsumerState { NotebookManifest? _manifest; bool _loading = true; @override void initState() { super.initState(); _reload(); } Future _reload() async { final manifest = await NotebookManifest.read(widget.folderPath); if (!mounted) return; setState(() { _manifest = manifest; _loading = false; }); } @override Widget build(BuildContext context) { final l = AppLocalizations.of(context); final members = _manifest?.members ?? const []; final title = (_manifest?.title.trim().isNotEmpty ?? false) ? _manifest!.title.trim() : widget.title; return Scaffold( appBar: AppBar( title: Text(title), actions: [ IconButton( icon: const Icon(Icons.note_add_outlined), tooltip: l.addBlankPage, onPressed: _addBlankPage, ), IconButton( icon: const Icon(Icons.file_open), tooltip: l.importIntoNotebook, onPressed: _importFile, ), ], ), body: _loading ? const Center(child: CircularProgressIndicator()) : members.isEmpty ? Center( child: Text( l.notebookMembersEmpty, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ) : RefreshIndicator( onRefresh: _reload, child: ListView.builder( itemCount: members.length, itemBuilder: (context, index) { final member = members[index]; return _MemberTile( member: member, onTap: () => _openMember(member), ); }, ), ), ); } Future _addBlankPage() async { final vault = await ref.read(vaultServiceProvider.future); await vault.addBlankPageToContainer(widget.folderPath); await _reload(); } Future _importFile() async { final l = AppLocalizations.of(context); final result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: VaultService.importableExtensions.toList(), ); final pickedPath = result?.files.first.path; if (pickedPath == null || !mounted) return; final messenger = ScaffoldMessenger.of(context); messenger.showSnackBar(SnackBar(content: Text(l.processingImport))); try { final vault = await ref.read(vaultServiceProvider.future); await vault.importFileIntoContainer(widget.folderPath, pickedPath); await _reload(); } catch (e) { messenger.showSnackBar(SnackBar(content: Text(l.importFailed('$e')))); } } /// Route [member] to the correct editor by kind — same switch as /// `home_screen.dart`'s `_DocumentTile._openDocument`. Future _openMember(NotebookMember member) async { final vault = await ref.read(vaultServiceProvider.future); final absolutePath = vault.memberAbsolutePath(widget.folderPath, member); if (!mounted) return; switch (member.kind) { case NotebookMemberKind.note: final now = DateTime.now(); final note = Note( id: absolutePath, title: member.title, createdAt: now, updatedAt: now, ); await Navigator.of(context).push( MaterialPageRoute(builder: (_) => PenNoteScreen(note: note)), ); case NotebookMemberKind.pdf: await Navigator.of(context).push( MaterialPageRoute( builder: (_) => PenEditorScreen(pdfPath: absolutePath), ), ); case NotebookMemberKind.pptx: case NotebookMemberKind.docx: await Navigator.of(context).push( MaterialPageRoute( builder: (_) => OfficeDocumentScreen(filePath: absolutePath), ), ); case NotebookMemberKind.ppt: // Legacy binary PPT — same image-fallback path as the home screen. await _openLegacyPpt(absolutePath); } } Future _openLegacyPpt(String filePath) async { final l = AppLocalizations.of(context); final pptxService = PptxService(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l.processingPresentation)), ); final slideImages = await pptxService.convertToImages(filePath); final extractedText = await pptxService.extractText(filePath); if (!mounted) return; if (slideImages.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l.couldNotOpenPresentation)), ); return; } await Navigator.of(context).push( MaterialPageRoute( builder: (_) => PenSlideScreen( filePath: filePath, slideImagePaths: slideImages, extractedText: extractedText.isEmpty ? null : extractedText, ), ), ); } } class _MemberTile extends StatelessWidget { const _MemberTile({required this.member, required this.onTap}); final NotebookMember member; final VoidCallback onTap; @override Widget build(BuildContext context) { return ListTile( leading: Icon(_iconFor(member.kind), color: _colorFor(member.kind)), title: Text( member.title.isEmpty ? 'Untitled' : member.title, maxLines: 1, overflow: TextOverflow.ellipsis, ), subtitle: Text(member.kind.name.toUpperCase()), onTap: onTap, ); } IconData _iconFor(NotebookMemberKind kind) => switch (kind) { NotebookMemberKind.note => Icons.edit_note, NotebookMemberKind.pdf => Icons.picture_as_pdf, NotebookMemberKind.pptx || NotebookMemberKind.ppt => Icons.slideshow, NotebookMemberKind.docx => Icons.description, }; Color _colorFor(NotebookMemberKind kind) => switch (kind) { NotebookMemberKind.note => Colors.blue, NotebookMemberKind.pdf => Colors.red, NotebookMemberKind.pptx || NotebookMemberKind.ppt => Colors.orange, NotebookMemberKind.docx => Colors.indigo, }; }