Files
BadNote/lib/storage/notebook_manifest.dart

161 lines
4.2 KiB
Dart
Raw Normal View History

// 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<String, dynamic> toJson() => {
'id': id,
'kind': kind.name,
'path': relativePath,
'title': title,
};
factory NotebookMember.fromJson(Map<String, dynamic> 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<NotebookMember> members;
Map<String, dynamic> toJson() => {
'version': version,
'title': title,
'members': members.map((m) => m.toJson()).toList(),
};
factory NotebookManifest.fromJson(Map<String, dynamic> json) {
final raw = (json['members'] as List<dynamic>?) ?? 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<String, dynamic>),
],
);
}
NotebookManifest copyWith({
String? title,
List<NotebookMember>? members,
}) =>
NotebookManifest(
version: version,
title: title ?? this.title,
members: members ?? this.members,
);
static File fileIn(String folderPath) =>
File(p.join(folderPath, kNotebookManifestName));
static Future<NotebookManifest?> read(String folderPath) async {
final file = fileIn(folderPath);
if (!await file.exists()) return null;
try {
final map = jsonDecode(await file.readAsString()) as Map<String, dynamic>;
return NotebookManifest.fromJson(map);
} catch (_) {
return null;
}
}
static Future<void> write(String folderPath, NotebookManifest manifest) async {
final file = fileIn(folderPath);
await file.writeAsString(
const JsonEncoder.withIndent(' ').convert(manifest.toJson()),
);
}
}