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:
@@ -121,6 +121,8 @@ class SidecarText {
|
||||
required this.text,
|
||||
this.fontSize = 0.03,
|
||||
this.color = 0xFF000000,
|
||||
this.fontWeight = 400,
|
||||
this.fontFamily,
|
||||
});
|
||||
|
||||
/// Stable id (uuid) so edits/deletes address a specific box.
|
||||
@@ -141,6 +143,12 @@ class SidecarText {
|
||||
/// ARGB text color.
|
||||
final int color;
|
||||
|
||||
/// CSS-like numeric weight (100–900). Default 400 (regular).
|
||||
final int fontWeight;
|
||||
|
||||
/// Optional family name. Null → editor default (IBM Plex Sans).
|
||||
final String? fontFamily;
|
||||
|
||||
SidecarText copyWith({
|
||||
String? id,
|
||||
double? nx,
|
||||
@@ -148,6 +156,9 @@ class SidecarText {
|
||||
String? text,
|
||||
double? fontSize,
|
||||
int? color,
|
||||
int? fontWeight,
|
||||
String? fontFamily,
|
||||
bool clearFontFamily = false,
|
||||
}) =>
|
||||
SidecarText(
|
||||
id: id ?? this.id,
|
||||
@@ -156,6 +167,9 @@ class SidecarText {
|
||||
text: text ?? this.text,
|
||||
fontSize: fontSize ?? this.fontSize,
|
||||
color: color ?? this.color,
|
||||
fontWeight: fontWeight ?? this.fontWeight,
|
||||
fontFamily:
|
||||
clearFontFamily ? null : (fontFamily ?? this.fontFamily),
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -165,6 +179,8 @@ class SidecarText {
|
||||
'text': text,
|
||||
'fontSize': fontSize,
|
||||
'color': color,
|
||||
'fontWeight': fontWeight,
|
||||
if (fontFamily != null) 'fontFamily': fontFamily,
|
||||
};
|
||||
|
||||
factory SidecarText.fromJson(Map<String, dynamic> json) => SidecarText(
|
||||
@@ -174,6 +190,8 @@ class SidecarText {
|
||||
text: (json['text'] as String?) ?? '',
|
||||
fontSize: (json['fontSize'] as num?)?.toDouble() ?? 0.03,
|
||||
color: (json['color'] as num?)?.toInt() ?? 0xFF000000,
|
||||
fontWeight: (json['fontWeight'] as num?)?.toInt() ?? 400,
|
||||
fontFamily: json['fontFamily'] as String?,
|
||||
);
|
||||
|
||||
@override
|
||||
@@ -186,15 +204,26 @@ class SidecarText {
|
||||
ny == other.ny &&
|
||||
text == other.text &&
|
||||
fontSize == other.fontSize &&
|
||||
color == other.color;
|
||||
color == other.color &&
|
||||
fontWeight == other.fontWeight &&
|
||||
fontFamily == other.fontFamily;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, nx, ny, text, fontSize, color);
|
||||
int get hashCode => Object.hash(
|
||||
id,
|
||||
nx,
|
||||
ny,
|
||||
text,
|
||||
fontSize,
|
||||
color,
|
||||
fontWeight,
|
||||
fontFamily,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'SidecarText(id: $id, nx: $nx, ny: $ny, text: $text, '
|
||||
'fontSize: $fontSize, color: $color)';
|
||||
'fontSize: $fontSize, weight: $fontWeight, family: $fontFamily)';
|
||||
}
|
||||
|
||||
/// An anchor's private infinite scratchpad: a list of [InkStroke]s in ABSOLUTE
|
||||
|
||||
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