// lib/storage/notebook_manifest.dart // // OneNote-style notebook container: a vault folder with `notebook.json` that // lists members (blank notes + imported PDF/PPTX/DOCX). Each member still uses // its own sidecar; this file is only the table of contents. import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; /// Filename of the notebook container manifest inside a vault folder. const String kNotebookManifestName = 'notebook.json'; /// Default annotation font family (matches app UI theme). const String kAnnotationFontFamily = 'IBM Plex Sans'; /// Kind of a notebook member. enum NotebookMemberKind { note, pdf, pptx, ppt, docx, } NotebookMemberKind? notebookMemberKindFromExt(String ext) { switch (ext.toLowerCase()) { case 'pdf': return NotebookMemberKind.pdf; case 'pptx': return NotebookMemberKind.pptx; case 'ppt': return NotebookMemberKind.ppt; case 'docx': return NotebookMemberKind.docx; case 'note': case 'notebook': return NotebookMemberKind.note; default: return null; } } String notebookMemberKindToExt(NotebookMemberKind kind) => switch (kind) { NotebookMemberKind.note => 'note', NotebookMemberKind.pdf => 'pdf', NotebookMemberKind.pptx => 'pptx', NotebookMemberKind.ppt => 'ppt', NotebookMemberKind.docx => 'docx', }; /// One page/section inside a notebook container. class NotebookMember { const NotebookMember({ required this.id, required this.kind, required this.relativePath, required this.title, }); final String id; final NotebookMemberKind kind; /// Path relative to the notebook folder (POSIX separators preferred). final String relativePath; final String title; Map toJson() => { 'id': id, 'kind': kind.name, 'path': relativePath, 'title': title, }; factory NotebookMember.fromJson(Map json) { final kindName = (json['kind'] as String?) ?? 'note'; final kind = NotebookMemberKind.values.firstWhere( (k) => k.name == kindName, orElse: () => NotebookMemberKind.note, ); return NotebookMember( id: (json['id'] as String?) ?? '', kind: kind, relativePath: (json['path'] as String?) ?? '', title: (json['title'] as String?) ?? '', ); } NotebookMember copyWith({String? title, String? relativePath}) => NotebookMember( id: id, kind: kind, relativePath: relativePath ?? this.relativePath, title: title ?? this.title, ); } /// Table of contents for a multi-document notebook folder. class NotebookManifest { const NotebookManifest({ required this.title, required this.members, this.version = 1, }); final int version; final String title; final List members; Map toJson() => { 'version': version, 'title': title, 'members': members.map((m) => m.toJson()).toList(), }; factory NotebookManifest.fromJson(Map json) { final raw = (json['members'] as List?) ?? const []; return NotebookManifest( version: (json['version'] as num?)?.toInt() ?? 1, title: (json['title'] as String?) ?? '', members: [ for (final e in raw) NotebookMember.fromJson(e as Map), ], ); } NotebookManifest copyWith({ String? title, List? members, }) => NotebookManifest( version: version, title: title ?? this.title, members: members ?? this.members, ); static File fileIn(String folderPath) => File(p.join(folderPath, kNotebookManifestName)); static Future read(String folderPath) async { final file = fileIn(folderPath); if (!await file.exists()) return null; try { final map = jsonDecode(await file.readAsString()) as Map; return NotebookManifest.fromJson(map); } catch (_) { return null; } } static Future write(String folderPath, NotebookManifest manifest) async { final file = fileIn(folderPath); await file.writeAsString( const JsonEncoder.withIndent(' ').convert(manifest.toJson()), ); } }