mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-12 18:12:23 -06:00
feat: Add document scanner package
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedTouchBubblePart extends StatefulWidget {
|
||||
AnimatedTouchBubblePart({
|
||||
required this.dragging,
|
||||
required this.size,
|
||||
});
|
||||
|
||||
final bool dragging;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
_AnimatedTouchBubblePartState createState() =>
|
||||
_AnimatedTouchBubblePartState();
|
||||
}
|
||||
|
||||
class _AnimatedTouchBubblePartState extends State<AnimatedTouchBubblePart>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<Color?> _colorAnimation;
|
||||
late Animation<double> _sizeAnimation;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_sizeAnimation = Tween<double>(begin: 0.5, end: 1.0).animate(_controller);
|
||||
|
||||
_colorAnimation = ColorTween(
|
||||
begin: Theme.of(context).colorScheme.primary,
|
||||
end: Theme.of(context).colorScheme.primary,
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Interval(0.5, 1.0),
|
||||
),
|
||||
);
|
||||
|
||||
_controller.repeat();
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: widget.dragging ? 0 : widget.size / 2,
|
||||
height: widget.dragging ? 0 : widget.size / 2,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).accentColor.withOpacity(0.5),
|
||||
borderRadius: widget.dragging
|
||||
? BorderRadius.circular(widget.size)
|
||||
: BorderRadius.circular(widget.size / 4)))),
|
||||
AnimatedBuilder(
|
||||
builder: (context, child) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: widget.dragging
|
||||
? 0
|
||||
: widget.size * _sizeAnimation.value,
|
||||
height: widget.dragging
|
||||
? 0
|
||||
: widget.size * _sizeAnimation.value,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _colorAnimation.value ?? Colors.transparent,
|
||||
width: widget.size / 20),
|
||||
borderRadius: widget.dragging
|
||||
? BorderRadius.zero
|
||||
: BorderRadius.circular(
|
||||
widget.size * _sizeAnimation.value / 2))));
|
||||
},
|
||||
animation: _controller,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
import 'dart:math';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:paperless_document_scanner/paperless_document_scanner.dart';
|
||||
import 'package:paperless_document_scanner/types/edge_detection_result.dart';
|
||||
|
||||
import 'edge_painter.dart';
|
||||
import 'magnifier.dart' as m;
|
||||
import 'touch_bubble.dart';
|
||||
|
||||
class EdgeDetectionShape extends StatefulWidget {
|
||||
const EdgeDetectionShape({
|
||||
super.key,
|
||||
required this.renderedImageSize,
|
||||
required this.originalImageSize,
|
||||
required this.edgeDetectionResult,
|
||||
});
|
||||
|
||||
final Size renderedImageSize;
|
||||
final Size originalImageSize;
|
||||
final EdgeDetectionResult edgeDetectionResult;
|
||||
|
||||
@override
|
||||
State<EdgeDetectionShape> createState() => _EdgeDetectionShapeState();
|
||||
}
|
||||
|
||||
class _EdgeDetectionShapeState extends State<EdgeDetectionShape> {
|
||||
late double edgeDraggerSize;
|
||||
|
||||
List<Offset> points = [];
|
||||
|
||||
late Offset _topLeft;
|
||||
late Offset _topRight;
|
||||
late Offset _bottomLeft;
|
||||
late Offset _bottomRight;
|
||||
|
||||
late double renderedImageWidth;
|
||||
late double renderedImageHeight;
|
||||
late double top;
|
||||
late double left;
|
||||
|
||||
Offset? currentDragPosition;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
double shortestSide = min(
|
||||
MediaQuery.of(context).size.width, MediaQuery.of(context).size.height);
|
||||
edgeDraggerSize = shortestSide / 12;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
top = 0.0;
|
||||
left = 0.0;
|
||||
_topLeft = widget.edgeDetectionResult.topLeft;
|
||||
_topRight = widget.edgeDetectionResult.topRight;
|
||||
_bottomLeft = widget.edgeDetectionResult.bottomLeft;
|
||||
_bottomRight = widget.edgeDetectionResult.bottomRight;
|
||||
|
||||
double widthFactor =
|
||||
widget.renderedImageSize.width / widget.originalImageSize.width;
|
||||
double heightFactor =
|
||||
widget.renderedImageSize.height / widget.originalImageSize.height;
|
||||
double sizeFactor = min(widthFactor, heightFactor);
|
||||
|
||||
renderedImageHeight = widget.originalImageSize.height * sizeFactor;
|
||||
top = ((widget.renderedImageSize.height - renderedImageHeight) / 2);
|
||||
|
||||
renderedImageWidth = widget.originalImageSize.width * sizeFactor;
|
||||
left = ((widget.renderedImageSize.width - renderedImageWidth) / 2);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return m.Magnifier(
|
||||
visible: currentDragPosition != null,
|
||||
position: currentDragPosition ?? Offset.zero,
|
||||
child: Stack(
|
||||
children: [
|
||||
_buildTouchBubbles(),
|
||||
CustomPaint(
|
||||
painter: EdgePainter(
|
||||
points: points,
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Offset _getNewPositionAfterDrag(Offset position) {
|
||||
return Offset(
|
||||
position.dx / renderedImageWidth,
|
||||
position.dy / renderedImageHeight,
|
||||
);
|
||||
}
|
||||
|
||||
Offset _clampOffset(Offset givenOffset) {
|
||||
double absoluteX = givenOffset.dx * renderedImageWidth;
|
||||
double absoluteY = givenOffset.dy * renderedImageHeight;
|
||||
|
||||
return Offset(absoluteX.clamp(0.0, renderedImageWidth) / renderedImageWidth,
|
||||
absoluteY.clamp(0.0, renderedImageHeight) / renderedImageHeight);
|
||||
}
|
||||
|
||||
Widget _buildTouchBubbles() {
|
||||
points = [
|
||||
Offset(
|
||||
left + _topLeft.dx * renderedImageWidth,
|
||||
top + _topLeft.dy * renderedImageHeight,
|
||||
),
|
||||
Offset(
|
||||
left + _topRight.dx * renderedImageWidth,
|
||||
top + _topRight.dy * renderedImageHeight,
|
||||
),
|
||||
Offset(
|
||||
left + _bottomRight.dx * renderedImageWidth,
|
||||
top + _bottomRight.dy * renderedImageHeight,
|
||||
),
|
||||
Offset(
|
||||
left + _bottomLeft.dx * renderedImageWidth,
|
||||
top + _bottomLeft.dy * renderedImageHeight,
|
||||
),
|
||||
Offset(
|
||||
left + _topLeft.dx * renderedImageWidth,
|
||||
top + _topLeft.dy * renderedImageHeight,
|
||||
),
|
||||
];
|
||||
|
||||
return SizedBox(
|
||||
width: widget.renderedImageSize.width,
|
||||
height: widget.renderedImageSize.height,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: points[0].dx - (edgeDraggerSize / 2),
|
||||
top: points[0].dy - (edgeDraggerSize / 2),
|
||||
child: TouchBubble(
|
||||
size: edgeDraggerSize,
|
||||
onDragFinished: () => setState(() => currentDragPosition = null),
|
||||
onDrag: (position) {
|
||||
setState(
|
||||
() {
|
||||
currentDragPosition = Offset(points[0].dx, points[0].dy);
|
||||
_topLeft = _clampOffset(
|
||||
widget.edgeDetectionResult.topLeft +
|
||||
_getNewPositionAfterDrag(position),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: points[1].dx - (edgeDraggerSize / 2),
|
||||
top: points[1].dy - (edgeDraggerSize / 2),
|
||||
child: TouchBubble(
|
||||
size: edgeDraggerSize,
|
||||
onDrag: (position) {
|
||||
setState(() {
|
||||
currentDragPosition = Offset(points[1].dx, points[1].dy);
|
||||
_topRight = _clampOffset(
|
||||
widget.edgeDetectionResult.topRight +
|
||||
_getNewPositionAfterDrag(position),
|
||||
);
|
||||
});
|
||||
},
|
||||
onDragFinished: () => setState(() => currentDragPosition = null),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: points[2].dx - (edgeDraggerSize / 2),
|
||||
top: points[2].dy - (edgeDraggerSize / 2),
|
||||
child: TouchBubble(
|
||||
size: edgeDraggerSize,
|
||||
onDrag: (position) {
|
||||
setState(() {
|
||||
currentDragPosition = Offset(points[2].dx, points[2].dy);
|
||||
_bottomRight = _clampOffset(
|
||||
widget.edgeDetectionResult.bottomRight +
|
||||
_getNewPositionAfterDrag(position),
|
||||
);
|
||||
});
|
||||
},
|
||||
onDragFinished: () => setState(() => currentDragPosition = null),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: points[3].dx - (edgeDraggerSize / 2),
|
||||
top: points[3].dy - (edgeDraggerSize / 2),
|
||||
child: TouchBubble(
|
||||
size: edgeDraggerSize,
|
||||
onDrag: (position) {
|
||||
setState(() {
|
||||
_bottomLeft = _clampOffset(
|
||||
widget.edgeDetectionResult.bottomLeft +
|
||||
_getNewPositionAfterDrag(position),
|
||||
);
|
||||
currentDragPosition = Offset(points[3].dx, points[3].dy);
|
||||
});
|
||||
},
|
||||
onDragFinished: () => setState(() => currentDragPosition = null),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EdgePainter extends CustomPainter {
|
||||
EdgePainter({required this.points, required this.color});
|
||||
|
||||
final List<Offset> points;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
final paint = Paint()
|
||||
..color = color.withOpacity(0.5)
|
||||
..strokeWidth = 2
|
||||
..strokeCap = StrokeCap.round;
|
||||
|
||||
canvas.drawPoints(PointMode.polygon, points, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'magnifier_painter.dart';
|
||||
|
||||
class Magnifier extends StatefulWidget {
|
||||
const Magnifier({
|
||||
super.key,
|
||||
required this.child,
|
||||
required this.position,
|
||||
this.visible = true,
|
||||
this.scale = 1.5,
|
||||
this.size = const Size(160, 160),
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final Offset position;
|
||||
final bool visible;
|
||||
final double scale;
|
||||
final Size size;
|
||||
|
||||
@override
|
||||
_MagnifierState createState() => _MagnifierState();
|
||||
}
|
||||
|
||||
class _MagnifierState extends State<Magnifier> {
|
||||
late Size _magnifierSize;
|
||||
late double _scale;
|
||||
late Matrix4 _matrix;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_magnifierSize = widget.size;
|
||||
_scale = widget.scale;
|
||||
_calculateMatrix();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(Magnifier oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
_calculateMatrix();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
widget.child,
|
||||
if (widget.visible && widget.position != null) _getMagnifier(context)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _calculateMatrix() {
|
||||
if (widget.position == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
double newX = widget.position.dx - (_magnifierSize.width / 2 / _scale);
|
||||
double newY = widget.position.dy - (_magnifierSize.height / 2 / _scale);
|
||||
|
||||
final Matrix4 updatedMatrix = Matrix4.identity()
|
||||
..scale(_scale, _scale)
|
||||
..translate(-newX, -newY);
|
||||
|
||||
_matrix = updatedMatrix;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _getMagnifier(BuildContext context) {
|
||||
return Align(
|
||||
alignment: _getAlignment(),
|
||||
child: ClipOval(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.matrix(_matrix.storage),
|
||||
child: CustomPaint(
|
||||
painter: MagnifierPainter(color: Theme.of(context).accentColor),
|
||||
size: _magnifierSize,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Alignment _getAlignment() {
|
||||
if (_bubbleCrossesMagnifier()) {
|
||||
return Alignment.topRight;
|
||||
}
|
||||
|
||||
return Alignment.topLeft;
|
||||
}
|
||||
|
||||
bool _bubbleCrossesMagnifier() =>
|
||||
widget.position.dx < widget.size.width &&
|
||||
widget.position.dy < widget.size.height;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class MagnifierPainter extends CustomPainter {
|
||||
const MagnifierPainter({required this.color, this.strokeWidth = 5});
|
||||
|
||||
final double strokeWidth;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
_drawCircle(canvas, size);
|
||||
_drawCrosshair(canvas, size);
|
||||
}
|
||||
|
||||
void _drawCircle(Canvas canvas, Size size) {
|
||||
Paint paintObject = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..color = color;
|
||||
|
||||
canvas.drawCircle(
|
||||
size.center(Offset(0, 0)), size.longestSide / 2, paintObject);
|
||||
}
|
||||
|
||||
void _drawCrosshair(Canvas canvas, Size size) {
|
||||
Paint crossPaint = Paint()
|
||||
..strokeWidth = strokeWidth / 2
|
||||
..color = color;
|
||||
|
||||
double crossSize = size.longestSide * 0.04;
|
||||
|
||||
canvas.drawLine(size.center(Offset(-crossSize, -crossSize)),
|
||||
size.center(Offset(crossSize, crossSize)), crossPaint);
|
||||
|
||||
canvas.drawLine(size.center(Offset(crossSize, -crossSize)),
|
||||
size.center(Offset(-crossSize, crossSize)), crossPaint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'animated_touch_bubble_part.dart';
|
||||
|
||||
class TouchBubble extends StatefulWidget {
|
||||
const TouchBubble({
|
||||
super.key,
|
||||
required this.size,
|
||||
required this.onDrag,
|
||||
required this.onDragFinished,
|
||||
});
|
||||
|
||||
final double size;
|
||||
final Function onDrag;
|
||||
final Function onDragFinished;
|
||||
|
||||
@override
|
||||
State<TouchBubble> createState() => _TouchBubbleState();
|
||||
}
|
||||
|
||||
class _TouchBubbleState extends State<TouchBubble> {
|
||||
bool dragging = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onPanStart: _startDragging,
|
||||
onPanUpdate: _drag,
|
||||
onPanCancel: _cancelDragging,
|
||||
onPanEnd: (_) => _cancelDragging(),
|
||||
child: Container(
|
||||
width: widget.size,
|
||||
height: widget.size,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(widget.size / 2)),
|
||||
child: AnimatedTouchBubblePart(
|
||||
dragging: dragging,
|
||||
size: widget.size,
|
||||
)));
|
||||
}
|
||||
|
||||
void _startDragging(DragStartDetails data) {
|
||||
setState(() {
|
||||
dragging = true;
|
||||
});
|
||||
widget
|
||||
.onDrag(data.localPosition - Offset(widget.size / 2, widget.size / 2));
|
||||
}
|
||||
|
||||
void _cancelDragging() {
|
||||
setState(() {
|
||||
dragging = false;
|
||||
});
|
||||
widget.onDragFinished();
|
||||
}
|
||||
|
||||
void _drag(DragUpdateDetails data) {
|
||||
if (!dragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.onDrag(data.delta);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user