feat(note): rebuild note editor on the pen-first canvas
Some checks failed
CI / Windows build (push) Has been cancelled
Some checks failed
CI / Windows build (push) Has been cancelled
Notes now use the single performant inking engine (PenCanvas) instead of the old ink_canvas, per "all note features on the pen-first canvas". - ink_stroke_adapter: pure InkStroke<->PenStroke bridge (normalize against a logical note page; drop non-freehand shapes/text). Round-trip tested. - pen_palette_widgets: shared M3 ToolButton/PaletteDivider/RoundIconButton so PDF + note editors use identical chrome (PenEditorScreen migrated to them; its private copies deleted). - PenNoteScreen: PenCanvas over a white logical page, undo/redo, title, save -> Note.strokes (+ local OCR for search). Pressure curve, eraser size/mode and palm rejection all inherited from the shared canvas. - Route home (new/open) + search note hits -> PenNoteScreen; remove the now-redundant "Pen Canvas (beta)" spike button; delete the dead old note_editor_screen. Tests: ink_stroke_adapter (5) + pen_note_screen widget (load + commit, 2). flutter analyze: 0 issues. Full suite: 265/265.
This commit is contained in:
@@ -7,10 +7,9 @@ import '../providers/document_provider.dart';
|
||||
import '../providers/note_provider.dart';
|
||||
import '../providers/ocr_provider.dart';
|
||||
import '../editor/canvas/pen_editor_screen.dart';
|
||||
import '../editor/pdf/spike_launcher.dart';
|
||||
import '../services/pdf_service.dart';
|
||||
import '../services/pptx_service.dart';
|
||||
import 'note_editor_screen.dart';
|
||||
import '../editor/canvas/pen_note_screen.dart';
|
||||
import 'ppt_annotator_screen.dart';
|
||||
import 'search_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
@@ -70,12 +69,6 @@ class HomeScreen extends ConsumerWidget {
|
||||
).push(MaterialPageRoute(builder: (_) => const SearchScreen()));
|
||||
},
|
||||
),
|
||||
// New pen-first canvas editor (beta).
|
||||
IconButton(
|
||||
icon: const Icon(Icons.draw_outlined),
|
||||
tooltip: l.penCanvasBeta,
|
||||
onPressed: () => openM1Spike(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
@@ -196,7 +189,7 @@ class HomeScreen extends ConsumerWidget {
|
||||
if (context.mounted) {
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => NoteEditorScreen(note: note)));
|
||||
).push(MaterialPageRoute(builder: (_) => PenNoteScreen(note: note)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +368,7 @@ class _NoteTileState extends ConsumerState<_NoteTile> {
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => NoteEditorScreen(note: note)),
|
||||
MaterialPageRoute(builder: (_) => PenNoteScreen(note: note)),
|
||||
);
|
||||
},
|
||||
onLongPress: () => _confirmDelete(context),
|
||||
@@ -425,7 +418,7 @@ class _NoteTileState extends ConsumerState<_NoteTile> {
|
||||
if (!mounted) return;
|
||||
if (result == 'open') {
|
||||
Navigator.of(this.context).push(
|
||||
MaterialPageRoute(builder: (_) => NoteEditorScreen(note: widget.note)),
|
||||
MaterialPageRoute(builder: (_) => PenNoteScreen(note: widget.note)),
|
||||
);
|
||||
} else if (result == 'delete') {
|
||||
_confirmDelete(this.context);
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' hide UndoManager;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../models/ink_stroke.dart';
|
||||
import '../models/note.dart';
|
||||
import '../models/pen_tool.dart';
|
||||
import '../models/pressure_curve.dart';
|
||||
import '../providers/note_provider.dart';
|
||||
import '../providers/ocr_provider.dart';
|
||||
import '../services/undo_manager.dart';
|
||||
import '../utils/stroke_stabilizer.dart';
|
||||
import '../widgets/annotation_toolbar.dart';
|
||||
import '../widgets/ink_canvas.dart';
|
||||
|
||||
class NoteEditorScreen extends ConsumerStatefulWidget {
|
||||
final Note? note;
|
||||
|
||||
const NoteEditorScreen({super.key, this.note});
|
||||
|
||||
@override
|
||||
ConsumerState<NoteEditorScreen> createState() => _NoteEditorScreenState();
|
||||
}
|
||||
|
||||
class _NoteEditorScreenState extends ConsumerState<NoteEditorScreen> {
|
||||
final UndoManager _undoManager = UndoManager();
|
||||
PenTool _currentTool = PenTool.pen;
|
||||
Color _currentColor = Colors.black;
|
||||
double _currentStrokeWidth = 2.0;
|
||||
bool _filled = false;
|
||||
String _title = 'Untitled';
|
||||
final TextEditingController _titleController = TextEditingController();
|
||||
PressureCurveType _pressureCurveType = PressureCurveType.linear;
|
||||
StabilizationLevel _stabilizationLevel = StabilizationLevel.none;
|
||||
final TransformationController _zoomController = TransformationController();
|
||||
double _zoomLevel = 1.0;
|
||||
|
||||
bool _isDirty = false;
|
||||
|
||||
Note? get _existingNote => widget.note;
|
||||
|
||||
PressureCurve get _pressureCurve {
|
||||
switch (_pressureCurveType) {
|
||||
case PressureCurveType.linear:
|
||||
return PressureCurve.linear;
|
||||
case PressureCurveType.soft:
|
||||
return PressureCurve.soft;
|
||||
case PressureCurveType.hard:
|
||||
return PressureCurve.hard;
|
||||
case PressureCurveType.custom:
|
||||
return const PressureCurve(type: PressureCurveType.custom);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (_existingNote != null) {
|
||||
_title = _existingNote!.title;
|
||||
for (final stroke in _existingNote!.strokes) {
|
||||
_undoManager.addStroke(stroke);
|
||||
}
|
||||
}
|
||||
_titleController.text = _title;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_titleController.dispose();
|
||||
_zoomController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onStrokeComplete(InkStroke stroke) {
|
||||
setState(() {
|
||||
_undoManager.addStroke(stroke);
|
||||
_isDirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _onErase(String strokeId, List<InkStroke> replacements) {
|
||||
setState(() {
|
||||
final original = _undoManager.currentStrokes
|
||||
.where((s) => s.id == strokeId)
|
||||
.firstOrNull;
|
||||
if (original != null) {
|
||||
_undoManager.removeStroke(original, replacements: replacements);
|
||||
_isDirty = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _undo() {
|
||||
setState(() {
|
||||
_undoManager.undo();
|
||||
_isDirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _redo() {
|
||||
setState(() {
|
||||
_undoManager.redo();
|
||||
_isDirty = true;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
final notifier = ref.read(noteListProvider.notifier);
|
||||
final now = DateTime.now();
|
||||
|
||||
Note savedNote;
|
||||
if (_existingNote != null) {
|
||||
final updated = _existingNote!.copyWith(
|
||||
title: _title,
|
||||
strokes: _undoManager.currentStrokes.toList(),
|
||||
updatedAt: now,
|
||||
);
|
||||
await notifier.updateNote(updated);
|
||||
savedNote = updated;
|
||||
} else {
|
||||
final note = await notifier.createNote(title: _title);
|
||||
final updated = note.copyWith(
|
||||
strokes: _undoManager.currentStrokes.toList(),
|
||||
);
|
||||
await notifier.updateNote(updated);
|
||||
savedNote = updated;
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isDirty = false;
|
||||
});
|
||||
|
||||
_runLocalOcr(savedNote);
|
||||
}
|
||||
|
||||
/// Run local OCR and index results for search.
|
||||
void _runLocalOcr(Note note) {
|
||||
final noteId = note.id;
|
||||
ref.read(ocrStatusProvider.notifier).state = {
|
||||
...ref.read(ocrStatusProvider),
|
||||
noteId: OcrStatus.processing,
|
||||
};
|
||||
|
||||
ref
|
||||
.read(ocrServiceProvider)
|
||||
.processNote(note)
|
||||
.then((_) {
|
||||
if (!mounted) return;
|
||||
ref.read(ocrStatusProvider.notifier).state = {
|
||||
...ref.read(ocrStatusProvider),
|
||||
noteId: OcrStatus.done,
|
||||
};
|
||||
})
|
||||
.catchError((_) {
|
||||
if (!mounted) return;
|
||||
ref.read(ocrStatusProvider.notifier).state = {
|
||||
...ref.read(ocrStatusProvider),
|
||||
noteId: OcrStatus.failed,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void _zoomIn() {
|
||||
final newLevel = (_zoomLevel + 0.25).clamp(0.5, 5.0);
|
||||
_applyZoom(newLevel);
|
||||
}
|
||||
|
||||
void _zoomOut() {
|
||||
final newLevel = (_zoomLevel - 0.25).clamp(0.5, 5.0);
|
||||
_applyZoom(newLevel);
|
||||
}
|
||||
|
||||
void _zoomReset() {
|
||||
_applyZoom(1.0);
|
||||
}
|
||||
|
||||
void _applyZoom(double level) {
|
||||
setState(() => _zoomLevel = level);
|
||||
_zoomController.value = Matrix4.diagonal3Values(level, level, 1.0);
|
||||
}
|
||||
|
||||
Future<void> _saveAndNotify() async {
|
||||
await _save();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('Saved')));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: true,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (didPop && _isDirty) _save();
|
||||
},
|
||||
child: CallbackShortcuts(
|
||||
bindings: {
|
||||
const SingleActivator(LogicalKeyboardKey.keyZ, control: true): _undo,
|
||||
const SingleActivator(LogicalKeyboardKey.keyY, control: true): _redo,
|
||||
const SingleActivator(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
control: true,
|
||||
shift: true,
|
||||
): _redo,
|
||||
SingleActivator(LogicalKeyboardKey.keyS, control: true):
|
||||
_saveAndNotify,
|
||||
},
|
||||
child: Focus(
|
||||
autofocus: true,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: SizedBox(
|
||||
height: 40,
|
||||
child: TextField(
|
||||
controller: _titleController,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: 'Note title...',
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
suffix: _isDirty
|
||||
? const Text(
|
||||
' •',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
onChanged: (value) {
|
||||
_title = value;
|
||||
setState(() => _isDirty = true);
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.check),
|
||||
tooltip: 'Save',
|
||||
onPressed: _saveAndNotify,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
AnnotationToolbar(
|
||||
currentTool: _currentTool,
|
||||
currentColor: _currentColor,
|
||||
currentStrokeWidth: _currentStrokeWidth,
|
||||
filled: _filled,
|
||||
pressureCurveType: _pressureCurveType,
|
||||
stabilizationLevel: _stabilizationLevel,
|
||||
canUndo: _undoManager.canUndo,
|
||||
canRedo: _undoManager.canRedo,
|
||||
onToolChanged: (tool) => setState(() => _currentTool = tool),
|
||||
onColorChanged: (color) =>
|
||||
setState(() => _currentColor = color),
|
||||
onStrokeWidthChanged: (w) =>
|
||||
setState(() => _currentStrokeWidth = w),
|
||||
onFilledChanged: (f) => setState(() => _filled = f),
|
||||
onPressureCurveChanged: (v) =>
|
||||
setState(() => _pressureCurveType = v),
|
||||
onStabilizationChanged: (v) =>
|
||||
setState(() => _stabilizationLevel = v),
|
||||
onUndo: _undo,
|
||||
onRedo: _redo,
|
||||
onZoomIn: _zoomIn,
|
||||
onZoomOut: _zoomOut,
|
||||
onZoomFitWidth: _zoomReset,
|
||||
zoomLabel: '${(_zoomLevel * 100).round()}%',
|
||||
),
|
||||
Expanded(
|
||||
child: InteractiveViewer(
|
||||
transformationController: _zoomController,
|
||||
minScale: 0.5,
|
||||
maxScale: 5.0,
|
||||
child: InkCanvas(
|
||||
strokes: _undoManager.currentStrokes,
|
||||
onStrokeComplete: _onStrokeComplete,
|
||||
onErase: _onErase,
|
||||
tool: _currentTool,
|
||||
color: _currentColor,
|
||||
strokeWidth: _currentStrokeWidth,
|
||||
pressureCurve: _pressureCurve,
|
||||
stabilizationLevel: _stabilizationLevel,
|
||||
filled: _filled,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import '../editor/canvas/pen_editor_screen.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/note.dart';
|
||||
import '../providers/search_provider.dart';
|
||||
import 'note_editor_screen.dart';
|
||||
import '../editor/canvas/pen_note_screen.dart';
|
||||
|
||||
class SearchScreen extends ConsumerStatefulWidget {
|
||||
const SearchScreen({super.key});
|
||||
@@ -179,7 +179,7 @@ class _NoteSearchResultTile extends StatelessWidget {
|
||||
onTap: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (_) => NoteEditorScreen(note: note)));
|
||||
).push(MaterialPageRoute(builder: (_) => PenNoteScreen(note: note)));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user