Custom drawing implementation

This commit is contained in:
2026-02-05 17:47:03 +01:00
parent e1d72de718
commit d4d6e41a9d
14 changed files with 1291 additions and 147 deletions

View File

@@ -1,8 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_drawing_board/flutter_drawing_board.dart';
import 'package:flutter_drawing_board/paint_contents.dart';
import 'package:flutter_fullscreen/flutter_fullscreen.dart';
import 'package:logging/logging.dart';
import 'package:pdfrx/pdfrx.dart';
@@ -12,7 +10,7 @@ import 'package:sheetless/core/services/api_client.dart';
import 'package:sheetless/core/services/storage_service.dart';
import '../../shared/input/pedal_shortcuts.dart';
import 'widgets/paint_mode_layer.dart';
import 'drawing/drawing.dart';
import 'widgets/pdf_page_display.dart';
import 'widgets/touch_navigation_layer.dart';
@@ -43,20 +41,21 @@ class _SheetViewerPageState extends State<SheetViewerPage>
int _currentPage = 1;
int _totalPages = 1;
bool _isPaintMode = false;
late DrawingController _drawingController;
/// Drawing controller for the current left page
late DrawingController _leftDrawingController;
/// Drawing controller for the right page (two-page mode)
late DrawingController _rightDrawingController;
@override
void initState() {
super.initState();
// Initialize drawing controller with default configuration
_drawingController = DrawingController(
config: DrawConfig(
contentType: SimpleLine,
strokeWidth: 4.0,
color: Colors.black,
),
maxHistorySteps: 100, // Limit undo/redo history (default: 100)
);
// Initialize drawing controllers
_leftDrawingController = DrawingController(maxHistorySteps: 50);
_rightDrawingController = DrawingController(maxHistorySteps: 50);
FullScreen.addListener(this);
FullScreen.setFullScreen(widget.config.fullscreen);
_documentLoaded = _loadPdf();
@@ -64,12 +63,38 @@ class _SheetViewerPageState extends State<SheetViewerPage>
@override
void dispose() {
_drawingController.dispose();
// Save current annotations synchronously before disposing
// Note: This is fire-and-forget, but Hive operations are fast enough
_saveCurrentAnnotationsSync();
_leftDrawingController.dispose();
_rightDrawingController.dispose();
FullScreen.removeListener(this);
_document?.dispose();
super.dispose();
}
/// Synchronous version that doesn't await - used in dispose
void _saveCurrentAnnotationsSync() {
// Save left page (always, since paint mode is single-page only)
final leftJson = _leftDrawingController.toJsonString();
_storageService.writeAnnotations(
widget.sheet.uuid,
_currentPage,
leftJson.isEmpty || leftJson == '[]' ? null : leftJson,
);
// Save right page if in two-page mode
if (widget.config.twoPageMode && _currentPage < _totalPages) {
final rightJson = _rightDrawingController.toJsonString();
_storageService.writeAnnotations(
widget.sheet.uuid,
_currentPage + 1,
rightJson.isEmpty || rightJson == '[]' ? null : rightJson,
);
}
}
// ---------------------------------------------------------------------------
// PDF Loading
// ---------------------------------------------------------------------------
@@ -89,9 +114,64 @@ class _SheetViewerPageState extends State<SheetViewerPage>
_totalPages = _document!.pages.length;
});
// Load annotations for current page(s)
await _loadAnnotationsForCurrentPages();
return true;
}
// ---------------------------------------------------------------------------
// Annotation Persistence
// ---------------------------------------------------------------------------
/// Loads annotations for the current page(s) from storage.
Future<void> _loadAnnotationsForCurrentPages() async {
// Load left page annotations
final leftJson = await _storageService.readAnnotations(
widget.sheet.uuid,
_currentPage,
);
if (leftJson != null && leftJson.isNotEmpty) {
_leftDrawingController.fromJsonString(leftJson);
} else {
_leftDrawingController.clear();
}
// Load right page annotations (two-page mode)
if (widget.config.twoPageMode && _currentPage < _totalPages) {
final rightJson = await _storageService.readAnnotations(
widget.sheet.uuid,
_currentPage + 1,
);
if (rightJson != null && rightJson.isNotEmpty) {
_rightDrawingController.fromJsonString(rightJson);
} else {
_rightDrawingController.clear();
}
}
}
/// Saves the current page(s) annotations to storage.
Future<void> _saveCurrentAnnotations() async {
// Save left page
final leftJson = _leftDrawingController.toJsonString();
await _storageService.writeAnnotations(
widget.sheet.uuid,
_currentPage,
leftJson.isEmpty || leftJson == '[]' ? null : leftJson,
);
// Save right page (two-page mode)
if (widget.config.twoPageMode && _currentPage < _totalPages) {
final rightJson = _rightDrawingController.toJsonString();
await _storageService.writeAnnotations(
widget.sheet.uuid,
_currentPage + 1,
rightJson.isEmpty || rightJson == '[]' ? null : rightJson,
);
}
}
// ---------------------------------------------------------------------------
// Fullscreen
// ---------------------------------------------------------------------------
@@ -105,10 +185,6 @@ class _SheetViewerPageState extends State<SheetViewerPage>
}
void _toggleFullscreen() {
if (_isPaintMode) {
_showSnackBar('Cannot enter fullscreen while in paint mode');
return;
}
FullScreen.setFullScreen(!widget.config.fullscreen);
}
@@ -116,10 +192,19 @@ class _SheetViewerPageState extends State<SheetViewerPage>
// Navigation
// ---------------------------------------------------------------------------
void _turnPage(int delta) {
setState(() {
_currentPage = (_currentPage + delta).clamp(1, _totalPages);
});
Future<void> _turnPage(int delta) async {
// Save current annotations before turning
await _saveCurrentAnnotations();
// Calculate new page
final newPage = (_currentPage + delta).clamp(1, _totalPages);
// Load annotations for new page(s) BEFORE updating state
_currentPage = newPage;
await _loadAnnotationsForCurrentPages();
// Now update UI
setState(() {});
}
// ---------------------------------------------------------------------------
@@ -132,6 +217,11 @@ class _SheetViewerPageState extends State<SheetViewerPage>
return;
}
if (_isPaintMode) {
// Exiting paint mode - save annotations
_saveCurrentAnnotations();
}
setState(() => _isPaintMode = !_isPaintMode);
}
@@ -145,6 +235,9 @@ class _SheetViewerPageState extends State<SheetViewerPage>
widget.config.twoPageMode = !widget.config.twoPageMode;
_storageService.writeConfig(widget.config);
});
// Reload annotations for new mode
_loadAnnotationsForCurrentPages();
}
// ---------------------------------------------------------------------------
@@ -156,7 +249,12 @@ class _SheetViewerPageState extends State<SheetViewerPage>
return PedalShortcuts(
onPageForward: () => _turnPage(1),
onPageBackward: () => _turnPage(-1),
child: Scaffold(appBar: _buildAppBar(), body: _buildBody()),
child: Scaffold(
appBar: _buildAppBar(),
body: _buildBody(),
floatingActionButton: _isPaintMode ? _buildDrawingToolbar() : null,
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
),
);
}
@@ -173,23 +271,21 @@ class _SheetViewerPageState extends State<SheetViewerPage>
icon: Icon(
widget.config.fullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
),
tooltip: widget.config.fullscreen
? 'Exit Fullscreen'
: 'Enter Fullscreen',
tooltip:
widget.config.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
onPressed: _toggleFullscreen,
),
IconButton(
icon: Icon(_isPaintMode ? Icons.brush : Icons.brush_outlined),
tooltip: 'Toggle Paint Mode',
tooltip: _isPaintMode ? 'Exit Paint Mode' : 'Enter Paint Mode',
onPressed: _togglePaintMode,
),
IconButton(
icon: Icon(
widget.config.twoPageMode ? Icons.filter_1 : Icons.filter_2,
),
tooltip: widget.config.twoPageMode
? 'Single Page Mode'
: 'Two Page Mode',
tooltip:
widget.config.twoPageMode ? 'Single Page Mode' : 'Two Page Mode',
onPressed: _toggleTwoPageMode,
),
],
@@ -220,30 +316,37 @@ class _SheetViewerPageState extends State<SheetViewerPage>
numPages: _totalPages,
currentPageNumber: _currentPage,
config: widget.config,
leftDrawingController: _leftDrawingController,
rightDrawingController:
widget.config.twoPageMode ? _rightDrawingController : null,
drawingEnabled: _isPaintMode,
);
return Stack(
children: [
// Show touch navigation when not in paint mode
Visibility(
visible: !_isPaintMode,
child: TouchNavigationLayer(
pageDisplay: pageDisplay,
config: widget.config,
onToggleFullscreen: _toggleFullscreen,
onExit: () => Navigator.pop(context),
onPageTurn: _turnPage,
),
// When in paint mode, show the page display directly (DrawingBoard handles zoom/pan)
if (_isPaintMode) {
return pageDisplay;
}
// When not in paint mode, wrap with touch navigation
return TouchNavigationLayer(
pageDisplay: pageDisplay,
config: widget.config,
onToggleFullscreen: _toggleFullscreen,
onExit: () => Navigator.pop(context),
onPageTurn: _turnPage,
child: pageDisplay,
);
}
Widget _buildDrawingToolbar() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: DrawingToolbar(
controller: _leftDrawingController,
onClose: _togglePaintMode,
),
// Show paint mode layer when active
Visibility(
visible: _isPaintMode,
child: PaintModeLayer(
pageDisplay: pageDisplay,
drawingController: _drawingController,
),
),
],
),
);
}
@@ -253,9 +356,10 @@ class _SheetViewerPageState extends State<SheetViewerPage>
padding: const EdgeInsets.all(16.0),
child: Text(
message,
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(color: Colors.red),
style: Theme.of(context)
.textTheme
.titleMedium
?.copyWith(color: Colors.red),
textAlign: TextAlign.center,
),
),
@@ -264,7 +368,7 @@ class _SheetViewerPageState extends State<SheetViewerPage>
void _showSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), duration: Duration(seconds: 2)),
SnackBar(content: Text(message), duration: const Duration(seconds: 2)),
);
}
}