feat: OneNote-style notebooks, text fonts, and page navigation
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:
2026-08-05 20:27:35 +08:00
parent 4a6fe7d05e
commit 2b1c6ba7e0
18 changed files with 1344 additions and 112 deletions

View File

@@ -9,6 +9,7 @@ import '../models/document.dart';
import '../models/note.dart';
import '../providers/document_provider.dart';
import '../providers/note_provider.dart';
import '../providers/notebook_container_provider.dart';
import '../providers/ocr_provider.dart';
import '../providers/search_provider.dart';
import '../editor/canvas/pen_editor_screen.dart';
@@ -17,6 +18,7 @@ import '../services/pptx_service.dart';
import '../services/vault_service.dart';
import '../editor/canvas/pen_note_screen.dart';
import '../editor/canvas/pen_slide_screen.dart';
import 'notebook_screen.dart';
import 'search_screen.dart';
import 'settings_screen.dart';
@@ -88,8 +90,10 @@ class HomeScreen extends ConsumerWidget {
data: (notes) {
final documentsAsync = ref.watch(documentListProvider);
final documents = documentsAsync.valueOrNull ?? [];
final containersAsync = ref.watch(notebookContainerListProvider);
final containers = containersAsync.valueOrNull ?? [];
if (notes.isEmpty && documents.isEmpty) {
if (notes.isEmpty && documents.isEmpty && containers.isEmpty) {
return _buildEmptyState(context, ref);
}
return RefreshIndicator(
@@ -97,10 +101,32 @@ class HomeScreen extends ConsumerWidget {
await Future.wait([
ref.read(noteListProvider.notifier).loadNotes(),
ref.read(documentListProvider.notifier).loadDocuments(),
ref
.read(notebookContainerListProvider.notifier)
.loadContainers(),
]);
},
child: CustomScrollView(
slivers: [
if (containers.isNotEmpty) ...[
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Text(
l.notebooksSection,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.bold),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) =>
_ContainerTile(container: containers[index]),
childCount: containers.length,
),
),
],
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
@@ -189,9 +215,10 @@ class HomeScreen extends ConsumerWidget {
);
}
/// "Create notebook": prompt a title (defaulting to Untitled), create the
/// standalone notebook FOLDER + `notebook.badnote.json` via
/// `VaultService.createEmptyNotebook`, then open the editor on the new note.
/// "Create notebook" (OneNote style): prompt a title (defaulting to
/// Untitled), create the notebook container FOLDER + `notebook.json` (with
/// one blank ink page) via `VaultService.createNotebookContainer`, then open
/// [NotebookScreen] on it.
Future<void> _createAndOpenNote(BuildContext context, WidgetRef ref) async {
final title = await _promptNotebookTitle(context);
if (title == null) return; // cancelled
@@ -199,12 +226,18 @@ class HomeScreen extends ConsumerWidget {
final resolved = title.trim().isEmpty
? (l?.untitledNote ?? 'Untitled')
: title.trim();
final note =
await ref.read(noteListProvider.notifier).createNote(title: resolved);
final container = await ref
.read(notebookContainerListProvider.notifier)
.createContainer(resolved);
if (context.mounted) {
Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => PenNoteScreen(note: note)));
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => NotebookScreen(
folderPath: container.folderPath,
title: container.title,
),
),
);
}
}
@@ -397,6 +430,45 @@ class HomeScreen extends ConsumerWidget {
}
}
/// Simple tile for a OneNote-style notebook container on the home screen.
/// Tapping opens [NotebookScreen] on the container's folder.
class _ContainerTile extends StatelessWidget {
const _ContainerTile({required this.container});
final VaultContainer container;
@override
Widget build(BuildContext context) {
final l = AppLocalizations.of(context);
final dateStr = _formatDate(context, container.modified);
return ListTile(
leading: Icon(
Icons.menu_book,
color: Theme.of(context).colorScheme.primary,
),
title: Text(
container.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${l.memberCount(container.memberCount)} · $dateStr',
style: Theme.of(context).textTheme.bodySmall,
),
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => NotebookScreen(
folderPath: container.folderPath,
title: container.title,
),
),
);
},
);
}
}
class _NoteTile extends ConsumerStatefulWidget {
final Note note;
const _NoteTile({required this.note});