feat: OneNote-style notebooks, text fonts, and page navigation
All checks were successful
CI / Windows build (push) Successful in 8m42s
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>
This commit is contained in:
160
lib/storage/notebook_manifest.dart
Normal file
160
lib/storage/notebook_manifest.dart
Normal file
@@ -0,0 +1,160 @@
|
||||
// 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()),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user