Fix bugs across app + server, optimize UI/UX, add Gitea CI
Bug fixes (Flutter): - Wrap multi-statement DB writes (insert/update/delete note, deleteDocument, deletePageData, OCR FTS merge, migrations) in transactions to prevent data loss on interruption and a read-modify-write FTS race. - Fix PdfDocument leaks on exception (try/finally dispose) and preserve image aspect ratio when stamping images onto PDF pages. - Guard file-picker against empty selection (was .single -> crash). - Fix eraser ConcurrentModificationError and unmodifiable-list crash on PDF pages; capture page synchronously on save to stop wrong-page data loss. - Fix Riverpod DB-not-ready races, broken pull-to-refresh, settings load race, and search N+1; transform stored annotations on PDF page rotation. - Normalize pen pressure for devices without a pressure range. - PPT: single source of truth for slide strokes so ink displays and exports. UI/UX: - Material 3 typography, theme-aware colors (dark-mode fixes), hover cursors and right-click/visible actions on desktop, keyboard shortcuts (undo/redo/ save/find), toolbar overflow handling, friendlier empty states, semantic OCR status badges, relative timestamps, 1-based page indicators, large-deck PPT navigation, and a scratchpad-scope label in split view. Server (optional backend): - Persist JWT secret (was per-process random), block path traversal in storage, fix CORS '*'+credentials, add OCR job ownership checks, last-writer-wins sync guard, constant-time login, and split out heavy OCR deps so the API/tests run without them. CI: Gitea workflows for format+analyze+test (Linux, system sqlite) and a Windows release build; pristine `flutter analyze`, all Flutter and server tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
162
lib/widgets/pdf_annotation_layer.dart
Normal file
162
lib/widgets/pdf_annotation_layer.dart
Normal file
@@ -0,0 +1,162 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/ink_point.dart';
|
||||
import '../models/ink_stroke.dart';
|
||||
import '../models/pen_tool.dart';
|
||||
import '../widgets/ink_canvas.dart';
|
||||
|
||||
/// Transparent overlay widget positioned on top of the PDF viewer.
|
||||
///
|
||||
/// Reuses the existing [InkCanvas] widget for ink rendering.
|
||||
/// Coordinates are normalized to [0, 1] relative to the overlay size,
|
||||
/// enabling correct mapping to PDF page coordinates during export.
|
||||
class PdfAnnotationLayer extends StatefulWidget {
|
||||
final List<InkStroke> strokes;
|
||||
final void Function(InkStroke stroke)? onStrokeComplete;
|
||||
final void Function(String strokeId, List<InkStroke> replacements)? onErase;
|
||||
final PenTool tool;
|
||||
final Color color;
|
||||
final double strokeWidth;
|
||||
final bool filled;
|
||||
final InteractionMode interactionMode;
|
||||
final int rotation;
|
||||
|
||||
const PdfAnnotationLayer({
|
||||
super.key,
|
||||
required this.strokes,
|
||||
this.onStrokeComplete,
|
||||
this.onErase,
|
||||
this.tool = PenTool.pen,
|
||||
this.color = Colors.black,
|
||||
this.strokeWidth = 2.0,
|
||||
this.filled = false,
|
||||
this.interactionMode = InteractionMode.draw,
|
||||
this.rotation = 0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PdfAnnotationLayer> createState() => _PdfAnnotationLayerState();
|
||||
}
|
||||
|
||||
class _PdfAnnotationLayerState extends State<PdfAnnotationLayer> {
|
||||
Size _canvasSize = Size.zero;
|
||||
|
||||
/// Applies inverse rotation to normalized coordinates for rendering.
|
||||
/// Converts from stored (possibly rotated) coords back to display coords.
|
||||
Offset _inverseRotate(double nx, double ny, int rotation) {
|
||||
switch (rotation % 360) {
|
||||
case 90:
|
||||
return Offset(1.0 - ny, nx);
|
||||
case 180:
|
||||
return Offset(1.0 - nx, 1.0 - ny);
|
||||
case 270:
|
||||
return Offset(ny, 1.0 - nx);
|
||||
default:
|
||||
return Offset(nx, ny);
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies forward rotation to normalized coordinates before storage.
|
||||
/// Converts from display coords to the canonical rotated representation.
|
||||
Offset _forwardRotate(double nx, double ny, int rotation) {
|
||||
switch (rotation % 360) {
|
||||
case 90:
|
||||
return Offset(ny, 1.0 - nx);
|
||||
case 180:
|
||||
return Offset(1.0 - nx, 1.0 - ny);
|
||||
case 270:
|
||||
return Offset(1.0 - ny, nx);
|
||||
default:
|
||||
return Offset(nx, ny);
|
||||
}
|
||||
}
|
||||
|
||||
/// Scales a stroke's points from normalized [0, 1] coordinates to
|
||||
/// the current canvas pixel coordinates for rendering.
|
||||
/// Applies inverse rotation before scaling so strokes render correctly
|
||||
/// on a rotated page.
|
||||
List<InkStroke> get _scaledStrokes {
|
||||
if (_canvasSize == Size.zero) return widget.strokes;
|
||||
return widget.strokes.map((stroke) {
|
||||
return InkStroke(
|
||||
id: stroke.id,
|
||||
points: stroke.points.map((pt) {
|
||||
final rotated = _inverseRotate(pt.x, pt.y, widget.rotation);
|
||||
return InkPoint(
|
||||
x: rotated.dx * _canvasSize.width,
|
||||
y: rotated.dy * _canvasSize.height,
|
||||
pressure: pt.pressure,
|
||||
tilt: pt.tilt,
|
||||
timestamp: pt.timestamp,
|
||||
pointerDeviceKind: pt.pointerDeviceKind,
|
||||
);
|
||||
}).toList(),
|
||||
tool: stroke.tool,
|
||||
color: stroke.color,
|
||||
strokeWidth: stroke.strokeWidth,
|
||||
createdAt: stroke.createdAt,
|
||||
filled: stroke.filled,
|
||||
textContent: stroke.textContent,
|
||||
fontSize: stroke.fontSize,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// Normalizes a stroke's points from canvas pixel coordinates to
|
||||
/// [0, 1] relative to the overlay size.
|
||||
/// Applies forward rotation before storage so the canonical representation
|
||||
/// accounts for the current page rotation.
|
||||
InkStroke _normalizeStroke(InkStroke stroke) {
|
||||
if (_canvasSize == Size.zero) return stroke;
|
||||
return InkStroke(
|
||||
id: stroke.id,
|
||||
points: stroke.points.map((pt) {
|
||||
final nx = pt.x / _canvasSize.width;
|
||||
final ny = pt.y / _canvasSize.height;
|
||||
final rotated = _forwardRotate(nx, ny, widget.rotation);
|
||||
return InkPoint(
|
||||
x: rotated.dx,
|
||||
y: rotated.dy,
|
||||
pressure: pt.pressure,
|
||||
tilt: pt.tilt,
|
||||
timestamp: pt.timestamp,
|
||||
pointerDeviceKind: pt.pointerDeviceKind,
|
||||
);
|
||||
}).toList(),
|
||||
tool: stroke.tool,
|
||||
color: stroke.color,
|
||||
strokeWidth: stroke.strokeWidth,
|
||||
createdAt: stroke.createdAt,
|
||||
filled: stroke.filled,
|
||||
textContent: stroke.textContent,
|
||||
fontSize: stroke.fontSize,
|
||||
);
|
||||
}
|
||||
|
||||
void _onStrokeComplete(InkStroke stroke) {
|
||||
widget.onStrokeComplete?.call(_normalizeStroke(stroke));
|
||||
}
|
||||
|
||||
void _onErase(String strokeId, List<InkStroke> replacements) {
|
||||
widget.onErase?.call(strokeId, replacements.map(_normalizeStroke).toList());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
_canvasSize = Size(constraints.maxWidth, constraints.maxHeight);
|
||||
return InkCanvas(
|
||||
strokes: _scaledStrokes,
|
||||
onStrokeComplete: _onStrokeComplete,
|
||||
onErase: _onErase,
|
||||
tool: widget.tool,
|
||||
color: widget.color,
|
||||
strokeWidth: widget.strokeWidth,
|
||||
filled: widget.filled,
|
||||
interactionMode: widget.interactionMode,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user