All checks were successful
CI / Windows build (push) Successful in 8m42s
Add notebook.json containers with multi-member pages, fix PDF text editing (size/bold/drag/double-tap), index SidecarText in search, and share keyboard page shortcuts plus a PDF scrubber. Co-authored-by: Cursor <cursoragent@cursor.com>
161 lines
4.2 KiB
Dart
161 lines
4.2 KiB
Dart
// 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()),
|
|
);
|
|
}
|
|
}
|