diff --git a/.vscode/settings.json b/.vscode/settings.json index a72b26549..82baafd4c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,5 @@ "files.exclude": { "submodules/": true }, - "editor.formatOnSave": false + "editor.formatOnSave": true } \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index f6da3a948..552be56c7 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -72,5 +72,7 @@ linter: use_string_buffers: true + curly_braces_in_flow_control_structures: false + # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/lib/components/canvas/_asset_cache.dart b/lib/components/canvas/_asset_cache.dart index 21e593fcd..435d71700 100644 --- a/lib/components/canvas/_asset_cache.dart +++ b/lib/components/canvas/_asset_cache.dart @@ -5,10 +5,10 @@ import 'package:flutter/painting.dart'; import 'package:saber/components/canvas/image/editor_image.dart'; /// A cache for assets that are loaded from disk. -/// +/// /// This is the analogue to Flutter's image cache, /// but for non-image assets. -/// +/// /// There should be one instance of this class per /// [EditorCoreInfo] instance. class AssetCache { @@ -16,13 +16,14 @@ class AssetCache { /// Maps a file to its value. final Map _cache = {}; + /// Maps a file to the visible images that use it. final Map> _images = {}; /// Marks [image] as currently visible. /// /// It's safe to call this method multiple times. - /// + /// /// [file] is allowed to be null for convenience, /// in which case this function does nothing. void addImage(EditorImage image, File? file, T value) { @@ -98,7 +99,8 @@ class OrderedAssetCache { } else if (item is FileImage) { return item.file.readAsBytes(); } else { - throw Exception('OrderedAssetCache.getBytes: unknown type ${item.runtimeType}'); + throw Exception( + 'OrderedAssetCache.getBytes: unknown type ${item.runtimeType}'); } } } diff --git a/lib/components/canvas/_canvas_background_painter.dart b/lib/components/canvas/_canvas_background_painter.dart index 2bef3e985..7b5689152 100644 --- a/lib/components/canvas/_canvas_background_painter.dart +++ b/lib/components/canvas/_canvas_background_painter.dart @@ -1,4 +1,3 @@ - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:saber/data/extensions/color_extensions.dart'; @@ -17,11 +16,14 @@ class CanvasBackgroundPainter extends CustomPainter { final bool invert; final Color backgroundColor; + /// The pattern to use for the background. See [CanvasBackgroundPatterns]. final CanvasBackgroundPattern backgroundPattern; + /// The height between each line in the background pattern final int lineHeight; final Color primaryColor, secondaryColor; + /// Whether to draw the background pattern in a preview mode (more opaque). final bool preview; @@ -54,13 +56,14 @@ class CanvasBackgroundPainter extends CustomPainter { } @override - bool shouldRepaint(CanvasBackgroundPainter oldDelegate) => kDebugMode - || oldDelegate.invert != invert - || oldDelegate.backgroundColor != backgroundColor - || oldDelegate.backgroundPattern != backgroundPattern - || oldDelegate.lineHeight != lineHeight - || oldDelegate.primaryColor != primaryColor - || oldDelegate.secondaryColor != secondaryColor; + bool shouldRepaint(CanvasBackgroundPainter oldDelegate) => + kDebugMode || + oldDelegate.invert != invert || + oldDelegate.backgroundColor != backgroundColor || + oldDelegate.backgroundPattern != backgroundPattern || + oldDelegate.lineHeight != lineHeight || + oldDelegate.primaryColor != primaryColor || + oldDelegate.secondaryColor != secondaryColor; static Iterable getPatternElements({ required CanvasBackgroundPattern pattern, @@ -130,8 +133,8 @@ class CanvasBackgroundPainter extends CustomPainter { final staffSpacing = lineHeight * 3; for (double topOfStaff = staffSpacing.toDouble() - lineHeight; - topOfStaff + staffHeight < size.height; - topOfStaff += staffHeight + staffSpacing) { + topOfStaff + staffHeight < size.height; + topOfStaff += staffHeight + staffSpacing) { // horizontal lines for (int line = 0; line < staffSpaces + 1; line++) { yield PatternElement( @@ -185,17 +188,23 @@ class CanvasBackgroundPainter extends CustomPainter { } } } - } class PatternElement { final Offset start, end; + /// Whether this is a line or a dot final bool isLine; + /// Whether this should use a secondary color final bool secondaryColor; - PatternElement(this.start, this.end, {this.isLine = true, this.secondaryColor = false}); + PatternElement( + this.start, + this.end, { + this.isLine = true, + this.secondaryColor = false, + }); } enum CanvasBackgroundPattern { @@ -205,6 +214,7 @@ enum CanvasBackgroundPattern { /// College ruled paper (ltr): horizontal lines with one /// vertical line along the left margin collegeLtr('college'), + /// College ruled paper (rtl): horizontal lines with one /// vertical line along the right margin collegeRtl('college-rtl'), @@ -222,8 +232,9 @@ enum CanvasBackgroundPattern { /// Music staffs staffs('staffs'), + /// Music tablature - /// + /// /// Like staffs but with 6 lines instead of 5 (and 5 spaces instead of 4). tablature('tablature'), diff --git a/lib/components/canvas/_canvas_painter.dart b/lib/components/canvas/_canvas_painter.dart index c2ac0dbf7..7b5c47dc4 100644 --- a/lib/components/canvas/_canvas_painter.dart +++ b/lib/components/canvas/_canvas_painter.dart @@ -17,14 +17,12 @@ import 'package:saber/data/tools/shape_pen.dart'; class CanvasPainter extends CustomPainter { const CanvasPainter({ super.repaint, - this.invert = false, required this.strokes, required this.laserStrokes, required this.currentStroke, required this.currentSelection, required this.primaryColor, - required this.page, required this.showPageIndicator, required this.pageIndex, @@ -57,9 +55,9 @@ class CanvasPainter extends CustomPainter { @override bool shouldRepaint(CanvasPainter oldDelegate) { - return currentStroke != null - || oldDelegate.currentStroke != null - || strokes.length != oldDelegate.strokes.length; + return currentStroke != null || + oldDelegate.currentStroke != null || + strokes.length != oldDelegate.strokes.length; } void _drawHighlighterStrokes(Canvas canvas, Rect canvasRect) { @@ -72,9 +70,11 @@ class CanvasPainter extends CustomPainter { for (Stroke stroke in strokes) { if (stroke.penType != (Highlighter).toString()) continue; - final color = stroke.strokeProperties.color.withOpacity(1).withInversion(invert); + final color = + stroke.strokeProperties.color.withOpacity(1).withInversion(invert); - if (color != lastColor) { // new layer for each color + if (color != lastColor) { + // new layer for each color if (needToRestoreCanvasLayer) canvas.restore(); canvas.saveLayer(canvasRect, layerPaint); @@ -102,9 +102,9 @@ class CanvasPainter extends CustomPainter { if (stroke.penType == (Pencil).toString()) { paint.color = Colors.white; paint.shader = page.pencilShader - ..setFloat(0, color.red / 255) - ..setFloat(1, color.green / 255) - ..setFloat(2, color.blue / 255); + ..setFloat(0, color.red / 255) + ..setFloat(1, color.green / 255) + ..setFloat(2, color.blue / 255); } else { paint.color = color; paint.shader = null; @@ -130,7 +130,8 @@ class CanvasPainter extends CustomPainter { ), shapePaint, ); - } else if (stroke.length <= 2) { // a dot + } else if (stroke.length <= 2) { + // a dot final bounds = stroke.path.getBounds(); final radius = max(bounds.size.width, stroke.strokeProperties.size) / 2; canvas.drawCircle(bounds.center, radius, paint); @@ -149,17 +150,21 @@ class CanvasPainter extends CustomPainter { if (currentStroke!.penType == (Pencil).toString()) { paint.color = Colors.white; paint.shader = page.pencilShader - ..setFloat(0, color.red / 255) - ..setFloat(1, color.green / 255) - ..setFloat(2, color.blue / 255); + ..setFloat(0, color.red / 255) + ..setFloat(1, color.green / 255) + ..setFloat(2, color.blue / 255); } else { paint.color = color; paint.shader = null; } - if (currentStroke!.length <= 2) { // a dot + if (currentStroke!.length <= 2) { + // a dot final bounds = currentStroke!.path.getBounds(); - final radius = max(bounds.size.width, currentStroke!.strokeProperties.size * 0.5) / 2; + final radius = max( + bounds.size.width * 0.5, + currentStroke!.strokeProperties.size * 0.25, + ); canvas.drawCircle(bounds.center, radius, paint); } else { canvas.drawPath(currentStroke!.path, paint); @@ -170,12 +175,12 @@ class CanvasPainter extends CustomPainter { final shape = ShapePen.detectedShape; if (shape == null) return; - final color = currentStroke?.strokeProperties.color.withInversion(invert) - ?? Colors.black; + final color = currentStroke?.strokeProperties.color.withInversion(invert) ?? + Colors.black; final shapePaint = Paint() - ..color = Color.lerp(color, primaryColor, 0.5)!.withOpacity(0.7) - ..style = PaintingStyle.stroke - ..strokeWidth = currentStroke?.strokeProperties.size ?? 3; + ..color = Color.lerp(color, primaryColor, 0.5)!.withOpacity(0.7) + ..style = PaintingStyle.stroke + ..strokeWidth = currentStroke?.strokeProperties.size ?? 3; switch (shape.name) { case null: @@ -215,9 +220,9 @@ class CanvasPainter extends CustomPainter { dashArray: CircularIntervalList([10, 10]), ), Paint() - ..color = primaryColor - ..strokeWidth = 3 - ..style = PaintingStyle.stroke, + ..color = primaryColor + ..strokeWidth = 3 + ..style = PaintingStyle.stroke, ); } diff --git a/lib/components/canvas/_circle_stroke.dart b/lib/components/canvas/_circle_stroke.dart index a3ddf3b18..87f5f51d7 100644 --- a/lib/components/canvas/_circle_stroke.dart +++ b/lib/components/canvas/_circle_stroke.dart @@ -52,6 +52,7 @@ class CircleStroke extends Stroke { bool _polygonNeedsUpdating = true; late List _polygon = const []; late Path _path = Path(); + /// A list of 25 points that form a circle /// with [center] and [radius]. @override @@ -59,11 +60,13 @@ class CircleStroke extends Stroke { if (_polygonNeedsUpdating) _updatePolygon(); return _polygon; } + @override Path get path { if (_polygonNeedsUpdating) _updatePolygon(); return _path; } + void _updatePolygon() { _polygon = List.generate(25, (i) => i / 25 * 2 * pi) .map((radians) => Offset(cos(radians), sin(radians))) @@ -119,10 +122,10 @@ class CircleStroke extends Stroke { @override CircleStroke copy() => CircleStroke( - strokeProperties: strokeProperties.copy(), - pageIndex: pageIndex, - penType: penType, - center: center, - radius: radius, - ); + strokeProperties: strokeProperties.copy(), + pageIndex: pageIndex, + penType: penType, + center: center, + radius: radius, + ); } diff --git a/lib/components/canvas/_rectangle_stroke.dart b/lib/components/canvas/_rectangle_stroke.dart index 413968d92..266907b45 100644 --- a/lib/components/canvas/_rectangle_stroke.dart +++ b/lib/components/canvas/_rectangle_stroke.dart @@ -50,6 +50,7 @@ class RectangleStroke extends Stroke { bool _polygonNeedsUpdating = true; late List _polygon = const []; late Path _path = Path(); + /// A list of points that form the /// rectangle's perimeter. /// Each side has 25 points. @@ -58,28 +59,35 @@ class RectangleStroke extends Stroke { if (_polygonNeedsUpdating) _updatePolygon(); return _polygon; } + @override Path get path { if (_polygonNeedsUpdating) _updatePolygon(); return _path; } + void _updatePolygon() { _polygon = _getPolygon(); _path = Path()..addPolygon(_polygon, true); _polygonNeedsUpdating = false; } + List _getPolygon() { final polygon = []; - for (int i = 0; i < 25; ++i) { // left side + for (int i = 0; i < 25; ++i) { + // left side polygon.add(Offset(rect.left, rect.top + rect.height * i / 25)); } - for (int i = 0; i < 25; ++i) { // bottom side + for (int i = 0; i < 25; ++i) { + // bottom side polygon.add(Offset(rect.left + rect.width * i / 25, rect.bottom)); } - for (int i = 0; i < 25; ++i) { // right side + for (int i = 0; i < 25; ++i) { + // right side polygon.add(Offset(rect.right, rect.bottom - rect.height * i / 25)); } - for (int i = 0; i < 25; ++i) { // top side + for (int i = 0; i < 25; ++i) { + // top side polygon.add(Offset(rect.right - rect.width * i / 25, rect.top)); } return polygon; @@ -135,9 +143,9 @@ class RectangleStroke extends Stroke { @override RectangleStroke copy() => RectangleStroke( - strokeProperties: strokeProperties.copy(), - pageIndex: pageIndex, - penType: penType, - rect: rect, - ); + strokeProperties: strokeProperties.copy(), + pageIndex: pageIndex, + penType: penType, + rect: rect, + ); } diff --git a/lib/components/canvas/_stroke.dart b/lib/components/canvas/_stroke.dart index 331891444..420e02f33 100644 --- a/lib/components/canvas/_stroke.dart +++ b/lib/components/canvas/_stroke.dart @@ -41,10 +41,12 @@ class Stroke { if (_polygonNeedsUpdating) _updatePolygon(); return _polygon; } + Path get path { if (_polygonNeedsUpdating) _updatePolygon(); return _path; } + void _updatePolygon() { _polygon = _getPolygon(); _path = Path()..addPolygon(_polygon, true); @@ -86,15 +88,15 @@ class Stroke { final Iterable points; if (fileVersion >= 13) { points = pointsJson.map((point) => PointExtensions.fromBsonBinary( - json: point, - offset: offset, - )); + json: point, + offset: offset, + )); } else { // ignore: deprecated_member_use_from_same_package points = pointsJson.map((point) => PointExtensions.fromJson( - json: Map.from(point), - offset: offset, - )); + json: Map.from(point), + offset: offset, + )); } return Stroke( @@ -116,7 +118,7 @@ class Stroke { }..addAll(strokeProperties.toJson()); } - void addPoint(Offset point, [ double? pressure ]) { + void addPoint(Offset point, [double? pressure]) { if (!strokeProperties.pressureEnabled) pressure = null; if (pressure != null) strokeProperties.simulatePressure = false; @@ -140,11 +142,12 @@ class Stroke { /// threshold multiplied by the stroke's size /// will be counted as duplicates. static const double _optimisePointsThreshold = 0.1; + /// Removes points that are too close together. See [_optimisePointsThreshold]. - /// + /// /// This function is idempotent, so running it multiple times /// will not change the result. - /// + /// /// This function does not change [_polygonNeedsUpdating]. void optimisePoints({double thresholdMultiplier = _optimisePointsThreshold}) { if (points.length <= 3) return; @@ -165,13 +168,13 @@ class Stroke { } List _getPolygon() { - final simulatePressure = strokeProperties.simulatePressure && strokeProperties.pressureEnabled; + final simulatePressure = + strokeProperties.simulatePressure && strokeProperties.pressureEnabled; final rememberSimulatedPressure = simulatePressure && isComplete; final polygon = getStroke( points, isComplete: isComplete, - size: strokeProperties.size, thinning: strokeProperties.thinning, smoothing: strokeProperties.smoothing, @@ -182,9 +185,7 @@ class Stroke { capEnd: strokeProperties.capEnd, simulatePressure: simulatePressure, rememberSimulatedPressure: rememberSimulatedPressure, - ) - .map((Point point) => Offset(point.x, point.y)) - .toList(growable: false); + ).map((point) => Offset(point.x, point.y)).toList(growable: false); if (rememberSimulatedPressure) { strokeProperties.simulatePressure = false; @@ -213,7 +214,7 @@ class Stroke { } double get maxY { - return points.isEmpty ? 0 : points.map((Point point) => point.y).reduce(max); + return points.isEmpty ? 0 : points.map((point) => point.y).reduce(max); } static num sqrDistBetweenPoints(Point p1, Point p2) { @@ -228,8 +229,8 @@ class Stroke { } Stroke copy() => Stroke( - strokeProperties: strokeProperties.copy(), - pageIndex: pageIndex, - penType: penType, - )..points.addAll(points); + strokeProperties: strokeProperties.copy(), + pageIndex: pageIndex, + penType: penType, + )..points.addAll(points); } diff --git a/lib/components/canvas/canvas.dart b/lib/components/canvas/canvas.dart index e40278a88..1288db495 100644 --- a/lib/components/canvas/canvas.dart +++ b/lib/components/canvas/canvas.dart @@ -46,37 +46,42 @@ class Canvas extends StatelessWidget { child: FittedBox( child: DecoratedBox( decoration: BoxDecoration( - boxShadow: [BoxShadow( - color: Colors.black.withOpacity(0.1), // dark regardless of theme - blurRadius: 10, - spreadRadius: 2, - )], + boxShadow: [ + BoxShadow( + color: + Colors.black.withOpacity(0.1), // dark regardless of theme + blurRadius: 10, + spreadRadius: 2, + ) + ], ), - child: !placeholder ? ClipRect( - child: SizedBox( - width: page.size.width, - height: page.size.height, - child: OnyxSdkPenArea( - child: InnerCanvas( - key: page.innerCanvasKey, - pageIndex: pageIndex, - redrawPageListenable: page, + child: !placeholder + ? ClipRect( + child: SizedBox( + width: page.size.width, + height: page.size.height, + child: OnyxSdkPenArea( + child: InnerCanvas( + key: page.innerCanvasKey, + pageIndex: pageIndex, + redrawPageListenable: page, + width: page.size.width, + height: page.size.height, + textEditing: textEditing, + coreInfo: coreInfo, + currentStroke: currentStroke, + currentStrokeDetectedShape: currentStrokeDetectedShape, + currentSelection: currentSelection, + setAsBackground: setAsBackground, + currentToolIsSelect: currentToolIsSelect, + ), + ), + ), + ) + : SizedBox( width: page.size.width, height: page.size.height, - textEditing: textEditing, - coreInfo: coreInfo, - currentStroke: currentStroke, - currentStrokeDetectedShape: currentStrokeDetectedShape, - currentSelection: currentSelection, - setAsBackground: setAsBackground, - currentToolIsSelect: currentToolIsSelect, ), - ), - ), - ) : SizedBox( - width: page.size.width, - height: page.size.height, - ), ), ), ); diff --git a/lib/components/canvas/canvas_background_preview.dart b/lib/components/canvas/canvas_background_preview.dart index 0849fa650..37160f7ee 100644 --- a/lib/components/canvas/canvas_background_preview.dart +++ b/lib/components/canvas/canvas_background_preview.dart @@ -60,14 +60,17 @@ class CanvasBackgroundPreview extends StatelessWidget { painter: CanvasBackgroundPainter( invert: invert, backgroundColor: () { - if (backgroundImage != null && Prefs.editorOpaqueBackgrounds.value) { + if (backgroundImage != null && + Prefs.editorOpaqueBackgrounds.value) { return Colors.white; } else { - return backgroundColor ?? InnerCanvas.defaultBackgroundColor; + return backgroundColor ?? + InnerCanvas.defaultBackgroundColor; } }(), backgroundPattern: () { - if (backgroundImage != null && Prefs.editorOpaqueBackgrounds.value) { + if (backgroundImage != null && + Prefs.editorOpaqueBackgrounds.value) { return CanvasBackgroundPattern.none; } else { return backgroundPattern; @@ -84,15 +87,16 @@ class CanvasBackgroundPreview extends StatelessWidget { ), ), ), - if (backgroundImage != null) CanvasImage( - filePath: '', - image: backgroundImage!, - overrideBoxFit: overrideBoxFit, - pageSize: previewSize, - setAsBackground: null, - isBackground: true, - readOnly: true, - ), + if (backgroundImage != null) + CanvasImage( + filePath: '', + image: backgroundImage!, + overrideBoxFit: overrideBoxFit, + pageSize: previewSize, + setAsBackground: null, + isBackground: true, + readOnly: true, + ), ], ), ), diff --git a/lib/components/canvas/canvas_gesture_detector.dart b/lib/components/canvas/canvas_gesture_detector.dart index be3021c04..d7b79d0e6 100644 --- a/lib/components/canvas/canvas_gesture_detector.dart +++ b/lib/components/canvas/canvas_gesture_detector.dart @@ -19,9 +19,7 @@ import 'package:vector_math/vector_math_64.dart'; class CanvasGestureDetector extends StatefulWidget { CanvasGestureDetector({ super.key, - required this.filePath, - required this.isDrawGesture, this.onInteractionEnd, required this.onDrawStart, @@ -29,17 +27,15 @@ class CanvasGestureDetector extends StatefulWidget { required this.onDrawEnd, required this.onPressureChanged, required this.onStylusButtonChanged, - required this.undo, required this.redo, - required this.pages, required this.initialPageIndex, required this.pageBuilder, required this.placeholderPageBuilder, - TransformationController? transformationController, - }) : _transformationController = transformationController ?? TransformationController(); + }) : _transformationController = + transformationController ?? TransformationController(); final String filePath; @@ -48,6 +44,7 @@ class CanvasGestureDetector extends StatefulWidget { final ValueChanged onDrawStart; final ValueChanged onDrawUpdate; final ValueChanged onDrawEnd; + /// Called when the pressure of the stylus changes, /// pressure is negative if stylus button is pressed final ValueChanged onPressureChanged; @@ -59,7 +56,8 @@ class CanvasGestureDetector extends StatefulWidget { final List pages; final int? initialPageIndex; final Widget Function(BuildContext context, int pageIndex) pageBuilder; - final Widget Function(BuildContext context, int pageIndex) placeholderPageBuilder; + final Widget Function(BuildContext context, int pageIndex) + placeholderPageBuilder; late final TransformationController _transformationController; @@ -91,6 +89,7 @@ class CanvasGestureDetector extends StatefulWidget { return top; } + static void scrollToPage({ required int pageIndex, required List pages, @@ -109,6 +108,7 @@ class CanvasGestureDetector extends StatefulWidget { 0, ); } + static int getPageIndex({ required double scrollY, required List pages, @@ -143,23 +143,27 @@ class CanvasGestureDetectorState extends State { late double? zoomLockedValue = Prefs.lastZoomLock.value ? widget._transformationController.value.getMaxScaleOnAxis() : null; + /// Whether single-finger panning is locked. /// Two-finger panning is always enabled. late bool singleFingerPanLock = Prefs.lastSingleFingerPanLock.value; + /// Whether panning is locked to being horizontal or vertical. /// Otherwise, panning can be done in any (i.e. diagonal) direction. late bool axisAlignedPanLock = Prefs.lastAxisAlignedPanLock.value; void zoomIn() => widget._transformationController.value = setZoom( - scaleDelta: 0.1, - transformation: widget._transformationController.value, - containerBounds: containerBounds, - ) ?? widget._transformationController.value; + scaleDelta: 0.1, + transformation: widget._transformationController.value, + containerBounds: containerBounds, + ) ?? + widget._transformationController.value; void zoomOut() => widget._transformationController.value = setZoom( - scaleDelta: -0.1, - transformation: widget._transformationController.value, - containerBounds: containerBounds, - ) ?? widget._transformationController.value; + scaleDelta: -0.1, + transformation: widget._transformationController.value, + containerBounds: containerBounds, + ) ?? + widget._transformationController.value; @visibleForTesting static Matrix4? setZoom({ required double scaleDelta, @@ -177,11 +181,11 @@ class CanvasGestureDetectorState extends State { containerBounds.maxHeight / 2, 0, ); - final translation = (transformation.getTranslation() - center) - * (newScale / oldScale) + center; + final translation = + (transformation.getTranslation() - center) * (newScale / oldScale) + + center; - return Matrix4.translation(translation) - ..scale(newScale); + return Matrix4.translation(translation)..scale(newScale); } final Map _arrowKeyPanTimers = {}; @@ -192,19 +196,18 @@ class CanvasGestureDetectorState extends State { _arrowKeyPanNow(direction); // Wait for 200ms, then pan every 100ms - _arrowKeyPanTimers[direction] = Timer( - const Duration(milliseconds: 200), - () { - _arrowKeyPanTimers[direction] = Timer.periodic( - const Duration(milliseconds: 100), - (_) => _arrowKeyPanNow(direction), - ); - } - ); + _arrowKeyPanTimers[direction] = + Timer(const Duration(milliseconds: 200), () { + _arrowKeyPanTimers[direction] = Timer.periodic( + const Duration(milliseconds: 100), + (_) => _arrowKeyPanNow(direction), + ); + }); } else { _arrowKeyPanTimers.remove(direction); } } + void _arrowKeyPanNow(AxisDirection direction) { final transformation = widget._transformationController.value; const panAmount = 50.0; @@ -226,30 +229,43 @@ class CanvasGestureDetectorState extends State { widget._transformationController.notifyListenersPlease(); } - bool _setupKeybindings = false; late Keybinding _ctrlPlus, _ctrlEquals, _ctrlMinus; late Keybinding _leftKey, _rightKey, _upKey, _downKey; void _assignKeybindings() { - _ctrlPlus = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.add)], inclusive: true); - _ctrlEquals = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.equal)], inclusive: true); - _ctrlMinus = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.minus)], inclusive: true); + _ctrlPlus = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.add)], + inclusive: true); + _ctrlEquals = Keybinding( + [KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.equal)], + inclusive: true); + _ctrlMinus = Keybinding( + [KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.minus)], + inclusive: true); Keybinder.bind(_ctrlPlus, zoomIn); Keybinder.bind(_ctrlEquals, zoomIn); Keybinder.bind(_ctrlMinus, zoomOut); - _leftKey = Keybinding([KeyCode.from(LogicalKeyboardKey.arrowLeft)], inclusive: true); - _rightKey = Keybinding([KeyCode.from(LogicalKeyboardKey.arrowRight)], inclusive: true); - _upKey = Keybinding([KeyCode.from(LogicalKeyboardKey.arrowUp)], inclusive: true); - _downKey = Keybinding([KeyCode.from(LogicalKeyboardKey.arrowDown)], inclusive: true); + _leftKey = Keybinding([KeyCode.from(LogicalKeyboardKey.arrowLeft)], + inclusive: true); + _rightKey = Keybinding([KeyCode.from(LogicalKeyboardKey.arrowRight)], + inclusive: true); + _upKey = + Keybinding([KeyCode.from(LogicalKeyboardKey.arrowUp)], inclusive: true); + _downKey = Keybinding([KeyCode.from(LogicalKeyboardKey.arrowDown)], + inclusive: true); // TODO: disable scroll keybindings when in quill mode - Keybinder.bind(_leftKey, (bool pressed) => arrowKeyPan(AxisDirection.left, pressed)); - Keybinder.bind(_rightKey, (bool pressed) => arrowKeyPan(AxisDirection.right, pressed)); - Keybinder.bind(_upKey, (bool pressed) => arrowKeyPan(AxisDirection.up, pressed)); - Keybinder.bind(_downKey, (bool pressed) => arrowKeyPan(AxisDirection.down, pressed)); + Keybinder.bind( + _leftKey, (bool pressed) => arrowKeyPan(AxisDirection.left, pressed)); + Keybinder.bind( + _rightKey, (bool pressed) => arrowKeyPan(AxisDirection.right, pressed)); + Keybinder.bind( + _upKey, (bool pressed) => arrowKeyPan(AxisDirection.up, pressed)); + Keybinder.bind( + _downKey, (bool pressed) => arrowKeyPan(AxisDirection.down, pressed)); _setupKeybindings = true; } + void _removeKeybindings() { if (!_setupKeybindings) return; _setupKeybindings = false; @@ -277,8 +293,8 @@ class CanvasGestureDetectorState extends State { /// Wait for note to be loaded before setting the initial transform. @override void didUpdateWidget(CanvasGestureDetector oldWidget) { - if (oldWidget.initialPageIndex != widget.initialPageIndex - || oldWidget.filePath != widget.filePath) { + if (oldWidget.initialPageIndex != widget.initialPageIndex || + oldWidget.filePath != widget.filePath) { setInitialTransform(); } super.didUpdateWidget(oldWidget); @@ -357,10 +373,11 @@ class CanvasGestureDetectorState extends State { if (scale == 1) return; widget._transformationController.value = setZoom( - scaleDelta: 1 - scale, - transformation: transformation, - containerBounds: containerBounds, - ) ?? transformation; + scaleDelta: 1 - scale, + transformation: transformation, + containerBounds: containerBounds, + ) ?? + transformation; } void _listenerPointerEvent(PointerEvent event) { @@ -410,7 +427,8 @@ class CanvasGestureDetectorState extends State { panEnabled: !singleFingerPanLock, panAxis: axisAlignedPanLock ? PanAxis.aligned : PanAxis.free, - interactionEndFrictionCoefficient: InteractiveCanvasViewer.kDrag * 100, + interactionEndFrictionCoefficient: + InteractiveCanvasViewer.kDrag * 100, // we need a non-zero boundary margin so we can zoom out // past the size of the page (for minScale < 1) @@ -470,7 +488,8 @@ class CanvasGestureDetectorState extends State { @override void dispose() { - CanvasTransformCache.add(widget.filePath, widget._transformationController.value); + CanvasTransformCache.add( + widget.filePath, widget._transformationController.value); widget._transformationController.removeListener(onTransformChanged); widget._transformationController.dispose(); _removeKeybindings(); @@ -488,8 +507,10 @@ class CanvasGestureDetectorState extends State { quad.point3, ]; - final List xValues = points.map((Vector3 point) => point.x).toList(); - final List yValues = points.map((Vector3 point) => point.y).toList(); + final List xValues = + points.map((Vector3 point) => point.x).toList(); + final List yValues = + points.map((Vector3 point) => point.y).toList(); final double left = xValues.reduce(min); final double right = xValues.reduce(max); @@ -504,7 +525,6 @@ class _PagesBuilder extends StatelessWidget { const _PagesBuilder({ // ignore: unused_element super.key, - required this.pages, required this.pageBuilder, required this.placeholderPageBuilder, @@ -514,7 +534,8 @@ class _PagesBuilder extends StatelessWidget { final List pages; final Widget Function(BuildContext context, int pageIndex) pageBuilder; - final Widget Function(BuildContext context, int pageIndex) placeholderPageBuilder; + final Widget Function(BuildContext context, int pageIndex) + placeholderPageBuilder; final Rect boundingBox; final double containerWidth; @@ -528,7 +549,8 @@ class _PagesBuilder extends StatelessWidget { double topOfPage = Editor.gapBetweenPages * 2; for (int pageIndex = 0; pageIndex < pages.length; pageIndex++) { final Size pageSize = pages[pageIndex].size; - final double pageWidth = min(pageSize.width, containerWidth); // because of FittedBox + final double pageWidth = + min(pageSize.width, containerWidth); // because of FittedBox final double pageHeight = pageWidth / pageSize.width * pageSize.height; final double bottomOfPage = topOfPage + pageHeight; @@ -585,7 +607,8 @@ class CanvasTransformCache { } @visibleForTesting -base class CanvasTransformCacheItem extends LinkedListEntry { +base class CanvasTransformCacheItem + extends LinkedListEntry { final String filePath; final Matrix4 transform; diff --git a/lib/components/canvas/canvas_image.dart b/lib/components/canvas/canvas_image.dart index 20990463c..5d96aa808 100644 --- a/lib/components/canvas/canvas_image.dart +++ b/lib/components/canvas/canvas_image.dart @@ -39,6 +39,7 @@ class CanvasImage extends StatefulWidget { /// The minimum size of the interactive area for the image. static double minInteractiveSize = 50; + /// The minimum size of the image itself, inside of the interactive area. static double minImageSize = 10; @@ -48,13 +49,15 @@ class CanvasImage extends StatefulWidget { class _CanvasImageState extends State { bool _active = false; + /// Whether this image can be dragged bool get active => _active; set active(bool value) { if (active == value) return; if (value) { - CanvasImage.activeListener.notifyListenersPlease(); // de-activate all other images + CanvasImage.activeListener + .notifyListenersPlease(); // de-activate all other images } _active = value; @@ -79,7 +82,8 @@ class _CanvasImageState extends State { void initState() { widget.image.loadIn(); - if (widget.image.newImage) { // if the image is new, make it [active] + if (widget.image.newImage) { + // if the image is new, make it [active] active = true; widget.image.newImage = false; } @@ -135,51 +139,67 @@ class _CanvasImageState extends State { }, onLongPress: active ? showModal : null, onSecondaryTap: active ? showModal : null, - onPanStart: active ? (details) { - panStartRect = widget.image.dstRect; - } : null, - onPanUpdate: active ? (details) { - setState(() { - double fivePercent = min(widget.pageSize.width * 0.05, widget.pageSize.height * 0.05); - widget.image.dstRect = Rect.fromLTWH( - (widget.image.dstRect.left + details.delta.dx).clamp( - fivePercent - widget.image.dstRect.width, - widget.pageSize.width - fivePercent, - ).toDouble(), - (widget.image.dstRect.top + details.delta.dy).clamp( - fivePercent - widget.image.dstRect.height, - widget.pageSize.height - fivePercent, - ).toDouble(), - widget.image.dstRect.width, - widget.image.dstRect.height, - ); - }); - } : null, - onPanEnd: active ? (details) { - if (panStartRect == widget.image.dstRect) return; - widget.image.onMoveImage?.call(widget.image, Rect.fromLTRB( - widget.image.dstRect.left - panStartRect.left, - widget.image.dstRect.top - panStartRect.top, - widget.image.dstRect.right - panStartRect.right, - widget.image.dstRect.bottom - panStartRect.bottom, - )); - panStartRect = Rect.zero; - } : null, + onPanStart: active + ? (details) { + panStartRect = widget.image.dstRect; + } + : null, + onPanUpdate: active + ? (details) { + setState(() { + double fivePercent = min(widget.pageSize.width * 0.05, + widget.pageSize.height * 0.05); + widget.image.dstRect = Rect.fromLTWH( + (widget.image.dstRect.left + details.delta.dx) + .clamp( + fivePercent - widget.image.dstRect.width, + widget.pageSize.width - fivePercent, + ) + .toDouble(), + (widget.image.dstRect.top + details.delta.dy) + .clamp( + fivePercent - widget.image.dstRect.height, + widget.pageSize.height - fivePercent, + ) + .toDouble(), + widget.image.dstRect.width, + widget.image.dstRect.height, + ); + }); + } + : null, + onPanEnd: active + ? (details) { + if (panStartRect == widget.image.dstRect) return; + widget.image.onMoveImage?.call( + widget.image, + Rect.fromLTRB( + widget.image.dstRect.left - panStartRect.left, + widget.image.dstRect.top - panStartRect.top, + widget.image.dstRect.right - panStartRect.right, + widget.image.dstRect.bottom - panStartRect.bottom, + )); + panStartRect = Rect.zero; + } + : null, child: DecoratedBox( decoration: BoxDecoration( border: Border.all( - color: active ? colorScheme.onBackground : Colors.transparent, + color: + active ? colorScheme.onBackground : Colors.transparent, width: 2, ), ), child: Center( child: SizedBox( width: widget.isBackground - ? widget.pageSize.width - : max(widget.image.dstRect.width, CanvasImage.minImageSize), + ? widget.pageSize.width + : max(widget.image.dstRect.width, + CanvasImage.minImageSize), height: widget.isBackground - ? widget.pageSize.height - : max(widget.image.dstRect.height, CanvasImage.minImageSize), + ? widget.pageSize.height + : max(widget.image.dstRect.height, + CanvasImage.minImageSize), child: SizedOverflowBox( size: widget.image.srcRect.size, child: Transform.translate( @@ -230,12 +250,10 @@ class _CanvasImageState extends State { return AnimatedPositioned( duration: const Duration(milliseconds: 300), curve: Curves.fastLinearToSlowEaseIn, - left: 0, top: 0, right: 0, bottom: 0, - child: unpositioned, ); } @@ -273,7 +291,6 @@ class _CanvasImageState extends State { filePath: widget.filePath, image: widget.image, redrawImage: () => setState(() {}), - isBackground: false, toggleAsBackground: () { widget.setAsBackground?.call(widget.image); @@ -310,87 +327,106 @@ class _CanvasImageResizeHandle extends StatelessWidget { child: DeferPointer( paintOnTop: true, child: MouseRegion( - cursor: (){ + cursor: () { if (!active) return MouseCursor.defer; - if (position.dx == 0 && position.dy < 0) return SystemMouseCursors.resizeUp; - if (position.dx == 0 && position.dy > 0) return SystemMouseCursors.resizeDown; - if (position.dx < 0 && position.dy == 0) return SystemMouseCursors.resizeLeft; - if (position.dx > 0 && position.dy == 0) return SystemMouseCursors.resizeRight; - - if (position.dx < 0 && position.dy < 0) return SystemMouseCursors.resizeUpLeft; - if (position.dx < 0 && position.dy > 0) return SystemMouseCursors.resizeDownLeft; - if (position.dx > 0 && position.dy < 0) return SystemMouseCursors.resizeUpRight; - if (position.dx > 0 && position.dy > 0) return SystemMouseCursors.resizeDownRight; + if (position.dx == 0 && position.dy < 0) + return SystemMouseCursors.resizeUp; + if (position.dx == 0 && position.dy > 0) + return SystemMouseCursors.resizeDown; + if (position.dx < 0 && position.dy == 0) + return SystemMouseCursors.resizeLeft; + if (position.dx > 0 && position.dy == 0) + return SystemMouseCursors.resizeRight; + + if (position.dx < 0 && position.dy < 0) + return SystemMouseCursors.resizeUpLeft; + if (position.dx < 0 && position.dy > 0) + return SystemMouseCursors.resizeDownLeft; + if (position.dx > 0 && position.dy < 0) + return SystemMouseCursors.resizeUpRight; + if (position.dx > 0 && position.dy > 0) + return SystemMouseCursors.resizeDownRight; return MouseCursor.defer; }(), child: GestureDetector( behavior: HitTestBehavior.opaque, - onPanStart: active ? (details) { - parent.panStartRect = parent.widget.image.dstRect; - parent.panStartPosition = details.localPosition; - } : null, - onPanUpdate: active ? (details) { - final Offset delta = details.localPosition - parent.panStartPosition; - - double newWidth; - if (position.dx < 0) { - newWidth = parent.panStartRect.width - delta.dx; - } else if (position.dx > 0) { - newWidth = parent.panStartRect.width + delta.dx; - } else { - newWidth = parent.panStartRect.width; - } - - double newHeight; - if (position.dy < 0) { - newHeight = parent.panStartRect.height - delta.dy; - } else if (position.dy > 0) { - newHeight = parent.panStartRect.height + delta.dy; - } else { - newHeight = parent.panStartRect.height; - } - - if (newWidth <= 0 || newHeight <= 0) return; - - // preserve aspect ratio if diagonal - if (position.dx != 0 && position.dy != 0) { // if diagonal - final double aspectRatio = image.dstRect.width / image.dstRect.height; - if (newWidth / newHeight > aspectRatio) { - newHeight = newWidth / aspectRatio; - } else { - newWidth = newHeight * aspectRatio; - } - } - - // resize from the correct corner - double left = image.dstRect.left, top = image.dstRect.top; - if (position.dx < 0) { - left = image.dstRect.right - newWidth; - } - if (position.dy < 0) { - top = image.dstRect.bottom - newHeight; - } - - image.dstRect = Rect.fromLTWH( - left, - top, - newWidth, - newHeight, - ); - afterDrag(); - } : null, - onPanEnd: active ? (details) { - if (parent.panStartRect == image.dstRect) return; - image.onMoveImage?.call(image, Rect.fromLTRB( - image.dstRect.left - parent.panStartRect.left, - image.dstRect.top - parent.panStartRect.top, - image.dstRect.right - parent.panStartRect.right, - image.dstRect.bottom - parent.panStartRect.bottom, - )); - parent.panStartRect = Rect.zero; - } : null, + onPanStart: active + ? (details) { + parent.panStartRect = parent.widget.image.dstRect; + parent.panStartPosition = details.localPosition; + } + : null, + onPanUpdate: active + ? (details) { + final Offset delta = + details.localPosition - parent.panStartPosition; + + double newWidth; + if (position.dx < 0) { + newWidth = parent.panStartRect.width - delta.dx; + } else if (position.dx > 0) { + newWidth = parent.panStartRect.width + delta.dx; + } else { + newWidth = parent.panStartRect.width; + } + + double newHeight; + if (position.dy < 0) { + newHeight = parent.panStartRect.height - delta.dy; + } else if (position.dy > 0) { + newHeight = parent.panStartRect.height + delta.dy; + } else { + newHeight = parent.panStartRect.height; + } + + if (newWidth <= 0 || newHeight <= 0) return; + + // preserve aspect ratio if diagonal + if (position.dx != 0 && position.dy != 0) { + // if diagonal + final double aspectRatio = + image.dstRect.width / image.dstRect.height; + if (newWidth / newHeight > aspectRatio) { + newHeight = newWidth / aspectRatio; + } else { + newWidth = newHeight * aspectRatio; + } + } + + // resize from the correct corner + double left = image.dstRect.left, top = image.dstRect.top; + if (position.dx < 0) { + left = image.dstRect.right - newWidth; + } + if (position.dy < 0) { + top = image.dstRect.bottom - newHeight; + } + + image.dstRect = Rect.fromLTWH( + left, + top, + newWidth, + newHeight, + ); + afterDrag(); + } + : null, + onPanEnd: active + ? (details) { + if (parent.panStartRect == image.dstRect) return; + image.onMoveImage?.call( + image, + Rect.fromLTRB( + image.dstRect.left - parent.panStartRect.left, + image.dstRect.top - parent.panStartRect.top, + image.dstRect.right - parent.panStartRect.right, + image.dstRect.bottom - parent.panStartRect.bottom, + )); + parent.panStartRect = Rect.zero; + } + : null, child: AnimatedOpacity( opacity: active ? 1 : 0, duration: const Duration(milliseconds: 100), diff --git a/lib/components/canvas/canvas_image_dialog.dart b/lib/components/canvas/canvas_image_dialog.dart index b50bfb73e..ed717f79c 100644 --- a/lib/components/canvas/canvas_image_dialog.dart +++ b/lib/components/canvas/canvas_image_dialog.dart @@ -14,11 +14,9 @@ class CanvasImageDialog extends StatefulWidget { required this.filePath, required this.image, required this.redrawImage, - required this.isBackground, required this.toggleAsBackground, - - this.singleRow = false + this.singleRow = false, }); final String filePath; @@ -33,20 +31,21 @@ class CanvasImageDialog extends StatefulWidget { @override State createState() => _CanvasImageDialogState(); } + class _CanvasImageDialogState extends State { void setInvertible([bool? value]) => setState(() { - widget.image.invertible = value ?? !widget.image.invertible; - widget.image.onMiscChange?.call(); - widget.redrawImage(); - }); + widget.image.invertible = value ?? !widget.image.invertible; + widget.image.onMiscChange?.call(); + widget.redrawImage(); + }); @override Widget build(BuildContext context) { final platform = Theme.of(context).platform; - final cupertino = platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; + final cupertino = + platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; final children = [ - MergeSemantics( child: _CanvasImageDialogItem( onTap: Prefs.editorAutoInvert.value ? setInvertible : null, @@ -54,18 +53,18 @@ class _CanvasImageDialogState extends State { child: Switch.adaptive( value: widget.image.invertible, onChanged: Prefs.editorAutoInvert.value ? setInvertible : null, - thumbIcon: MaterialStateProperty.all( - widget.image.invertible - ? const Icon(Icons.invert_colors) - : const Icon(Icons.invert_colors_off) - ), + thumbIcon: MaterialStateProperty.all(widget.image.invertible + ? const Icon(Icons.invert_colors) + : const Icon(Icons.invert_colors_off)), ), ), ), _CanvasImageDialogItem( onTap: () async { - final String filePathSanitized = widget.filePath.replaceAll(RegExp(r'[^a-zA-Z\d]'), '_'); - final String imageFileName = 'image$filePathSanitized${widget.image.id}${widget.image.extension}'; + final String filePathSanitized = + widget.filePath.replaceAll(RegExp(r'[^a-zA-Z\d]'), '_'); + final String imageFileName = + 'image$filePathSanitized${widget.image.id}${widget.image.extension}'; final List bytes; switch (widget.image) { case PdfEditorImage image: @@ -78,9 +77,11 @@ class _CanvasImageDialogState extends State { if (image.imageProvider is MemoryImage) { bytes = (image.imageProvider as MemoryImage).bytes; } else if (image.imageProvider is FileImage) { - bytes = await (image.imageProvider as FileImage).file.readAsBytes(); + bytes = + await (image.imageProvider as FileImage).file.readAsBytes(); } else { - throw Exception('Unknown image provider type: ${image.imageProvider.runtimeType}'); + throw Exception( + 'Unknown image provider type: ${image.imageProvider.runtimeType}'); } } FileManager.exportFile(imageFileName, bytes, isImage: true); @@ -140,7 +141,6 @@ class _CanvasImageDialogState extends State { ); } } - } class _CanvasImageDialogItem extends StatelessWidget { diff --git a/lib/components/canvas/canvas_preview.dart b/lib/components/canvas/canvas_preview.dart index 3de1697da..a5f491559 100644 --- a/lib/components/canvas/canvas_preview.dart +++ b/lib/components/canvas/canvas_preview.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:saber/components/canvas/inner_canvas.dart'; import 'package:saber/data/editor/editor_core_info.dart'; diff --git a/lib/components/canvas/hud/canvas_gesture_lock_btn.dart b/lib/components/canvas/hud/canvas_gesture_lock_btn.dart index acab2a5e7..6aa913000 100644 --- a/lib/components/canvas/hud/canvas_gesture_lock_btn.dart +++ b/lib/components/canvas/hud/canvas_gesture_lock_btn.dart @@ -11,7 +11,7 @@ class CanvasGestureLockBtn extends StatelessWidget { required this.tooltip, this.icon, this.child, - }) : assert(icon != null || child != null); + }) : assert(icon != null || child != null); final bool lock; final ValueChanged setLock; @@ -32,13 +32,14 @@ class CanvasGestureLockBtn extends StatelessWidget { padding: const EdgeInsets.all(5), child: Tooltip( message: tooltip, - child: child ?? AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: Icon( - icon, - color: colorScheme.onBackground, - ), - ), + child: child ?? + AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Icon( + icon, + color: colorScheme.onBackground, + ), + ), ), ), ); diff --git a/lib/components/canvas/hud/canvas_hud.dart b/lib/components/canvas/hud/canvas_hud.dart index fdea824cf..a2a29a082 100644 --- a/lib/components/canvas/hud/canvas_hud.dart +++ b/lib/components/canvas/hud/canvas_hud.dart @@ -69,7 +69,9 @@ class _CanvasHudState extends State { lock: widget.zoomLock, setLock: widget.setZoomLock, icon: widget.zoomLock ? Icons.lock : Icons.lock_open, - tooltip: widget.zoomLock ? t.editor.hud.unlockZoom : t.editor.hud.lockZoom, + tooltip: widget.zoomLock + ? t.editor.hud.unlockZoom + : t.editor.hud.lockZoom, ), ), Positioned( @@ -90,10 +92,12 @@ class _CanvasHudState extends State { child: CanvasGestureLockBtn( lock: widget.axisAlignedPanLock, setLock: widget.setAxisAlignedPanLock, - tooltip: widget.axisAlignedPanLock ? t.editor.hud.unlockAxisAlignedPan : t.editor.hud.lockAxisAlignedPan, + tooltip: widget.axisAlignedPanLock + ? t.editor.hud.unlockAxisAlignedPan + : t.editor.hud.lockAxisAlignedPan, child: AnimatedRotation( duration: const Duration(milliseconds: 200), - turns: widget.axisAlignedPanLock ? 0 : 1/8, + turns: widget.axisAlignedPanLock ? 0 : 1 / 8, // TODO: use [Icons.drag_pan] once it's available child: const FaIcon(FontAwesomeIcons.arrowsUpDownLeftRight), ), @@ -105,7 +109,8 @@ class _CanvasHudState extends State { child: AnimatedBuilder( animation: widget.transformationController, builder: (context, _) => CanvasZoomIndicator( - scale: widget.transformationController.value.getMaxScaleOnAxis(), + scale: + widget.transformationController.value.getMaxScaleOnAxis(), resetZoom: widget.resetZoom, ), ), diff --git a/lib/components/canvas/image/editor_image.dart b/lib/components/canvas/image/editor_image.dart index 8e1b1f2a9..9f66e7eb8 100644 --- a/lib/components/canvas/image/editor_image.dart +++ b/lib/components/canvas/image/editor_image.dart @@ -59,6 +59,7 @@ sealed class EditorImage extends ChangeNotifier { /// Defines the aspect ratio of the image. Size naturalSize; + /// The size of the page this image is on, /// used to make sure the image isn't too big. Size? pageSize; @@ -90,11 +91,12 @@ sealed class EditorImage extends ChangeNotifier { Rect dstRect = Rect.zero, this.srcRect = Rect.zero, bool isThumbnail = false, - }): assert(extension.startsWith('.')), - _dstRect = dstRect, - _isThumbnail = isThumbnail; + }) : assert(extension.startsWith('.')), + _dstRect = dstRect, + _isThumbnail = isThumbnail; - factory EditorImage.fromJson(Map json, { + factory EditorImage.fromJson( + Map json, { required List? inlineAssets, bool isThumbnail = false, required String sbnPath, @@ -131,29 +133,27 @@ sealed class EditorImage extends ChangeNotifier { @mustBeOverridden @mustCallSuper Map toJson(OrderedAssetCache assets) => { - 'id': id, - 'e': extension, - 'i': pageIndex, - 'v': invertible, - 'f': backgroundFit.index, - 'x': dstRect.left, - 'y': dstRect.top, - 'w': dstRect.width, - 'h': dstRect.height, - - if (srcRect.left != 0) 'sx': srcRect.left, - if (srcRect.top != 0) 'sy': srcRect.top, - if (srcRect.width != 0) 'sw': srcRect.width, - if (srcRect.height != 0) 'sh': srcRect.height, - - if (naturalSize.width != 0) 'nw': naturalSize.width, - if (naturalSize.height != 0) 'nh': naturalSize.height, - }; + 'id': id, + 'e': extension, + 'i': pageIndex, + 'v': invertible, + 'f': backgroundFit.index, + 'x': dstRect.left, + 'y': dstRect.top, + 'w': dstRect.width, + 'h': dstRect.height, + if (srcRect.left != 0) 'sx': srcRect.left, + if (srcRect.top != 0) 'sy': srcRect.top, + if (srcRect.width != 0) 'sw': srcRect.width, + if (srcRect.height != 0) 'sh': srcRect.height, + if (naturalSize.width != 0) 'nw': naturalSize.width, + if (naturalSize.height != 0) 'nh': naturalSize.height, + }; /// Images are loaded out after 5 seconds of not being visible. - /// + /// /// Set this to true to load out immediately. - /// + /// /// This is useful for tests that can't have pending timers. @visibleForTesting static bool shouldLoadOutImmediately = false; @@ -186,8 +186,9 @@ sealed class EditorImage extends ChangeNotifier { _shouldLoadOut?.complete(false); _loadedIn = true; } + /// Free up resources when the image is no longer visible. - /// + /// /// See also: /// * [loadIn], which will be called again when the image is visible again. /// * [loadedIn], which is true after [loadIn] and false after [loadOut] diff --git a/lib/components/canvas/image/pdf_editor_image.dart b/lib/components/canvas/image/pdf_editor_image.dart index 5928d88a0..f0c9b3dd1 100644 --- a/lib/components/canvas/image/pdf_editor_image.dart +++ b/lib/components/canvas/image/pdf_editor_image.dart @@ -28,14 +28,17 @@ class PdfEditorImage extends EditorImage { super.dstRect, required super.naturalSize, super.isThumbnail, - }): assert(!naturalSize.isEmpty, 'naturalSize must be set for PdfEditorImage'), - assert(pdfBytes != null || pdfFile != null, 'pdfFile must be set if pdfBytes is null'), - super( - extension: '.pdf', - srcRect: Rect.zero, - ); + }) : assert( + !naturalSize.isEmpty, 'naturalSize must be set for PdfEditorImage'), + assert(pdfBytes != null || pdfFile != null, + 'pdfFile must be set if pdfBytes is null'), + super( + extension: '.pdf', + srcRect: Rect.zero, + ); - factory PdfEditorImage.fromJson(Map json, { + factory PdfEditorImage.fromJson( + Map json, { required List? inlineAssets, bool isThumbnail = false, required String sbnPath, @@ -49,7 +52,8 @@ class PdfEditorImage extends EditorImage { File? pdfFile; if (assetIndex != null) { if (inlineAssets == null) { - pdfFile = FileManager.getFile('$sbnPath${Editor.extension}.$assetIndex'); + pdfFile = + FileManager.getFile('$sbnPath${Editor.extension}.$assetIndex'); pdfBytes = assetCache.get(pdfFile); } else { pdfBytes = inlineAssets[assetIndex]; @@ -62,7 +66,8 @@ class PdfEditorImage extends EditorImage { } return PdfEditorImage( - id: json['id'] ?? -1, // -1 will be replaced by EditorCoreInfo._handleEmptyImageIds() + id: json['id'] ?? + -1, // -1 will be replaced by EditorCoreInfo._handleEmptyImageIds() assetCache: assetCache, pdfBytes: pdfBytes, pdfFile: pdfFile, @@ -70,7 +75,8 @@ class PdfEditorImage extends EditorImage { pageIndex: json['i'] ?? 0, pageSize: Size.infinite, invertible: json['v'] ?? true, - backgroundFit: json['f'] != null ? BoxFit.values[json['f']] : BoxFit.contain, + backgroundFit: + json['f'] != null ? BoxFit.values[json['f']] : BoxFit.contain, onMoveImage: null, onDeleteImage: null, onMiscChange: null, @@ -139,6 +145,7 @@ class PdfEditorImage extends EditorImage { pdfBytes ??= await pdfFile!.readAsBytes(); assetCache.addImage(this, pdfFile, pdfBytes!); } + @override Future loadOut() async { final shouldLoadOut = await super.loadOut(); @@ -161,6 +168,7 @@ class PdfEditorImage extends EditorImage { return await _precacheImage(memoryImage); } + Future _precacheImage(ImageProvider imageProvider) async { final context = _lastPrecacheContext; if (context == null) return; @@ -239,24 +247,24 @@ class PdfEditorImage extends EditorImage { @override PdfEditorImage copy() => PdfEditorImage( - id: id, - assetCache: assetCache, - pdfBytes: pdfBytes, - pdfPage: pdfPage, - pdfFile: pdfFile, - pageIndex: pageIndex, - pageSize: Size.infinite, - invertible: invertible, - backgroundFit: backgroundFit, - onMoveImage: onMoveImage, - onDeleteImage: onDeleteImage, - onMiscChange: onMiscChange, - onLoad: onLoad, - newImage: true, - dstRect: dstRect, - naturalSize: naturalSize, - isThumbnail: isThumbnail, - ); + id: id, + assetCache: assetCache, + pdfBytes: pdfBytes, + pdfPage: pdfPage, + pdfFile: pdfFile, + pageIndex: pageIndex, + pageSize: Size.infinite, + invertible: invertible, + backgroundFit: backgroundFit, + onMoveImage: onMoveImage, + onDeleteImage: onDeleteImage, + onMiscChange: onMiscChange, + onLoad: onLoad, + newImage: true, + dstRect: dstRect, + naturalSize: naturalSize, + isThumbnail: isThumbnail, + ); static Timer? _checkIfHighDpiNeededDebounce; static Future _checkIfHighDpiNeededDebounceCallback({ @@ -278,7 +286,8 @@ class PdfEditorImage extends EditorImage { for (int pageIndex = 0; pageIndex < pages.length; pageIndex++) { // high dpi on current page, the page before, and the page after - final highDpiPage = highDpiNeeded && (pageIndex - currentPageIndex).abs() <= 1; + final highDpiPage = + highDpiNeeded && (pageIndex - currentPageIndex).abs() <= 1; final page = pages[pageIndex]; for (final image in [...page.images, page.backgroundImage]) { @@ -301,6 +310,7 @@ class PdfEditorImage extends EditorImage { } } } + /// When a user has not moved for 500ms, /// check if they are zoomed in to a PDF page, /// and increase the raster dpi if needed. diff --git a/lib/components/canvas/image/png_editor_image.dart b/lib/components/canvas/image/png_editor_image.dart index abe69287c..8890dc90a 100644 --- a/lib/components/canvas/image/png_editor_image.dart +++ b/lib/components/canvas/image/png_editor_image.dart @@ -16,7 +16,8 @@ class PngEditorImage extends EditorImage { if (isThumbnail && thumbnailBytes != null) { imageProvider = MemoryImage(thumbnailBytes!); final scale = thumbnailSize.width / naturalSize.width; - srcRect = Rect.fromLTWH(srcRect.left * scale, srcRect.top * scale, srcRect.width * scale, srcRect.height * scale); + srcRect = Rect.fromLTWH(srcRect.left * scale, srcRect.top * scale, + srcRect.width * scale, srcRect.height * scale); } } @@ -42,7 +43,8 @@ class PngEditorImage extends EditorImage { super.isThumbnail, }); - factory PngEditorImage.fromJson(Map json, { + factory PngEditorImage.fromJson( + Map json, { required List? inlineAssets, bool isThumbnail = false, required String sbnPath, @@ -53,7 +55,8 @@ class PngEditorImage extends EditorImage { File? imageFile; if (assetIndex != null) { if (inlineAssets == null) { - imageFile = FileManager.getFile('$sbnPath${Editor.extension}.$assetIndex'); + imageFile = + FileManager.getFile('$sbnPath${Editor.extension}.$assetIndex'); bytes = assetCache.get(imageFile); } else { bytes = inlineAssets[assetIndex]; @@ -66,7 +69,8 @@ class PngEditorImage extends EditorImage { } bytes = Uint8List(0); } - assert(bytes != null || imageFile != null, 'Either bytes or imageFile must be non-null'); + assert(bytes != null || imageFile != null, + 'Either bytes or imageFile must be non-null'); return PngEditorImage( // -1 will be replaced by [EditorCoreInfo._handleEmptyImageIds()] @@ -74,12 +78,13 @@ class PngEditorImage extends EditorImage { assetCache: assetCache, extension: json['e'] ?? '.jpg', imageProvider: bytes != null - ? MemoryImage(bytes) as ImageProvider - : FileImage(imageFile!), + ? MemoryImage(bytes) as ImageProvider + : FileImage(imageFile!), pageIndex: json['i'] ?? 0, pageSize: Size.infinite, invertible: json['v'] ?? true, - backgroundFit: json['f'] != null ? BoxFit.values[json['f']] : BoxFit.contain, + backgroundFit: + json['f'] != null ? BoxFit.values[json['f']] : BoxFit.contain, onMoveImage: null, onDeleteImage: null, onMiscChange: null, @@ -101,7 +106,9 @@ class PngEditorImage extends EditorImage { json['nw'] ?? 0, json['nh'] ?? 0, ), - thumbnailBytes: json['t'] != null ? Uint8List.fromList((json['t'] as List).cast()) : null, + thumbnailBytes: json['t'] != null + ? Uint8List.fromList((json['t'] as List).cast()) + : null, isThumbnail: isThumbnail, ); } @@ -109,8 +116,7 @@ class PngEditorImage extends EditorImage { @override Map toJson(OrderedAssetCache assets) => super.toJson(assets) ..addAll({ - if (imageProvider != null) - 'a': assets.add(imageProvider!), + if (imageProvider != null) 'a': assets.add(imageProvider!), }); @override @@ -124,12 +130,14 @@ class PngEditorImage extends EditorImage { } else if (imageProvider is FileImage) { bytes = await (imageProvider as FileImage).file.readAsBytes(); } else { - throw Exception('EditorImage.getImage: imageProvider is ${imageProvider.runtimeType}'); + throw Exception( + 'EditorImage.getImage: imageProvider is ${imageProvider.runtimeType}'); } naturalSize = await ui.ImmutableBuffer.fromUint8List(bytes) .then((buffer) => ui.ImageDescriptor.encoded(buffer)) - .then((descriptor) => Size(descriptor.width.toDouble(), descriptor.height.toDouble())); + .then((descriptor) => + Size(descriptor.width.toDouble(), descriptor.height.toDouble())); if (maxSize == null) { await Prefs.maxImageSize.waitUntilLoaded(); @@ -156,8 +164,8 @@ class PngEditorImage extends EditorImage { } if (dstRect.shortestSide == 0) { final Size dstSize = pageSize != null - ? EditorImage.resize(naturalSize, pageSize!) - : naturalSize; + ? EditorImage.resize(naturalSize, pageSize!) + : naturalSize; dstRect = dstRect.topLeft & dstSize; } } @@ -204,23 +212,23 @@ class PngEditorImage extends EditorImage { @override PngEditorImage copy() => PngEditorImage( - id: id, - assetCache: assetCache, - extension: extension, - imageProvider: imageProvider, - pageIndex: pageIndex, - pageSize: Size.infinite, - invertible: invertible, - backgroundFit: backgroundFit, - onMoveImage: onMoveImage, - onDeleteImage: onDeleteImage, - onMiscChange: onMiscChange, - onLoad: onLoad, - newImage: true, - dstRect: dstRect, - srcRect: srcRect, - naturalSize: naturalSize, - thumbnailBytes: thumbnailBytes, - isThumbnail: isThumbnail, - ); + id: id, + assetCache: assetCache, + extension: extension, + imageProvider: imageProvider, + pageIndex: pageIndex, + pageSize: Size.infinite, + invertible: invertible, + backgroundFit: backgroundFit, + onMoveImage: onMoveImage, + onDeleteImage: onDeleteImage, + onMiscChange: onMiscChange, + onLoad: onLoad, + newImage: true, + dstRect: dstRect, + srcRect: srcRect, + naturalSize: naturalSize, + thumbnailBytes: thumbnailBytes, + isThumbnail: isThumbnail, + ); } diff --git a/lib/components/canvas/image/svg_editor_image.dart b/lib/components/canvas/image/svg_editor_image.dart index 198e41d1f..d424b390c 100644 --- a/lib/components/canvas/image/svg_editor_image.dart +++ b/lib/components/canvas/image/svg_editor_image.dart @@ -27,12 +27,14 @@ class SvgEditorImage extends EditorImage { super.srcRect, super.naturalSize, super.isThumbnail, - }): assert(svgString != null || svgFile != null, 'svgFile must be set if svgString is null'), - super( - extension: '.svg', - ); - - factory SvgEditorImage.fromJson(Map json, { + }) : assert(svgString != null || svgFile != null, + 'svgFile must be set if svgString is null'), + super( + extension: '.svg', + ); + + factory SvgEditorImage.fromJson( + Map json, { required List? inlineAssets, bool isThumbnail = false, required String sbnPath, @@ -46,7 +48,8 @@ class SvgEditorImage extends EditorImage { File? svgFile; if (assetIndex != null) { if (inlineAssets == null) { - svgFile = FileManager.getFile('$sbnPath${Editor.extension}.$assetIndex'); + svgFile = + FileManager.getFile('$sbnPath${Editor.extension}.$assetIndex'); svgString = assetCache.get(svgFile); } else { svgString = utf8.decode(inlineAssets[assetIndex]); @@ -59,14 +62,16 @@ class SvgEditorImage extends EditorImage { } return SvgEditorImage( - id: json['id'] ?? -1, // -1 will be replaced by EditorCoreInfo._handleEmptyImageIds() + id: json['id'] ?? + -1, // -1 will be replaced by EditorCoreInfo._handleEmptyImageIds() assetCache: assetCache, svgString: svgString, svgFile: svgFile, pageIndex: json['i'] ?? 0, pageSize: Size.infinite, invertible: json['v'] ?? true, - backgroundFit: json['f'] != null ? BoxFit.values[json['f']] : BoxFit.contain, + backgroundFit: + json['f'] != null ? BoxFit.values[json['f']] : BoxFit.contain, onMoveImage: null, onDeleteImage: null, onMiscChange: null, @@ -119,7 +124,8 @@ class SvgEditorImage extends EditorImage { assetCache.addImage(this, svgFile, svgString!); if (srcRect.shortestSide == 0 || dstRect.shortestSide == 0) { - final pictureInfo = await vg.loadPicture(SvgStringLoader(svgString!), null); + final pictureInfo = + await vg.loadPicture(SvgStringLoader(svgString!), null); naturalSize = pictureInfo.size; if (srcRect.shortestSide == 0) { @@ -146,6 +152,7 @@ class SvgEditorImage extends EditorImage { svgString ??= await svgFile!.readAsString(); assetCache.addImage(this, svgFile, svgString!); } + @override Future loadOut() async { final shouldLoadOut = await super.loadOut(); @@ -196,22 +203,22 @@ class SvgEditorImage extends EditorImage { @override SvgEditorImage copy() => SvgEditorImage( - id: id, - assetCache: assetCache, - svgString: svgString, - svgFile: svgFile, - pageIndex: pageIndex, - pageSize: Size.infinite, - invertible: invertible, - backgroundFit: backgroundFit, - onMoveImage: onMoveImage, - onDeleteImage: onDeleteImage, - onMiscChange: onMiscChange, - onLoad: onLoad, - newImage: newImage, - dstRect: dstRect, - srcRect: srcRect, - naturalSize: naturalSize, - isThumbnail: isThumbnail, - ); + id: id, + assetCache: assetCache, + svgString: svgString, + svgFile: svgFile, + pageIndex: pageIndex, + pageSize: Size.infinite, + invertible: invertible, + backgroundFit: backgroundFit, + onMoveImage: onMoveImage, + onDeleteImage: onDeleteImage, + onMiscChange: onMiscChange, + onLoad: onLoad, + newImage: newImage, + dstRect: dstRect, + srcRect: srcRect, + naturalSize: naturalSize, + isThumbnail: isThumbnail, + ); } diff --git a/lib/components/canvas/inner_canvas.dart b/lib/components/canvas/inner_canvas.dart index 52a2de011..9741af099 100644 --- a/lib/components/canvas/inner_canvas.dart +++ b/lib/components/canvas/inner_canvas.dart @@ -59,13 +59,14 @@ class InnerCanvas extends StatefulWidget { } class _InnerCanvasState extends State { - @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final Brightness brightness = Theme.of(context).brightness; - final bool invert = Prefs.editorAutoInvert.value && brightness == Brightness.dark; - final Color backgroundColor = widget.coreInfo.backgroundColor ?? InnerCanvas.defaultBackgroundColor; + final bool invert = + Prefs.editorAutoInvert.value && brightness == Brightness.dark; + final Color backgroundColor = + widget.coreInfo.backgroundColor ?? InnerCanvas.defaultBackgroundColor; if (widget.coreInfo.pages.isEmpty) { return SizedBox( @@ -82,41 +83,47 @@ class _InnerCanvasState extends State { ? null : TranslationProvider.of(context).flutterLocale; - Widget? quillEditor = widget.coreInfo.pages.isNotEmpty ? QuillProvider( - configurations: QuillConfigurations( - controller: widget.coreInfo.pages[widget.pageIndex].quill.controller, - sharedConfigurations: QuillSharedConfigurations( - locale: locale, - ), - ), - child: QuillEditor( - configurations: QuillEditorConfigurations( - customStyles: _getQuillStyles(invert: invert), - scrollable: false, - autoFocus: false, - readOnly: false, - expands: true, - placeholder: widget.textEditing ? t.editor.quill.typeSomething : null, - showCursor: true, - keyboardAppearance: invert ? Brightness.dark : Brightness.light, - padding: EdgeInsets.only( - top: widget.coreInfo.lineHeight * 1.2, - left: widget.coreInfo.lineHeight * 0.5, - right: widget.coreInfo.lineHeight * 0.5, - bottom: widget.coreInfo.lineHeight * 0.5, - ), - ), - scrollController: ScrollController(), - focusNode: widget.coreInfo.pages[widget.pageIndex].quill.focusNode, - ), - ) : null; + Widget? quillEditor = widget.coreInfo.pages.isNotEmpty + ? QuillProvider( + configurations: QuillConfigurations( + controller: + widget.coreInfo.pages[widget.pageIndex].quill.controller, + sharedConfigurations: QuillSharedConfigurations( + locale: locale, + ), + ), + child: QuillEditor( + configurations: QuillEditorConfigurations( + customStyles: _getQuillStyles(invert: invert), + scrollable: false, + autoFocus: false, + readOnly: false, + expands: true, + placeholder: + widget.textEditing ? t.editor.quill.typeSomething : null, + showCursor: true, + keyboardAppearance: invert ? Brightness.dark : Brightness.light, + padding: EdgeInsets.only( + top: widget.coreInfo.lineHeight * 1.2, + left: widget.coreInfo.lineHeight * 0.5, + right: widget.coreInfo.lineHeight * 0.5, + bottom: widget.coreInfo.lineHeight * 0.5, + ), + ), + scrollController: ScrollController(), + focusNode: + widget.coreInfo.pages[widget.pageIndex].quill.focusNode, + ), + ) + : null; return RepaintBoundary( child: CustomPaint( painter: CanvasBackgroundPainter( invert: invert, backgroundColor: () { - if (page.backgroundImage != null && Prefs.editorOpaqueBackgrounds.value) { + if (page.backgroundImage != null && + Prefs.editorOpaqueBackgrounds.value) { return Colors.white; } else if (widget.hideBackground) { return InnerCanvas.defaultBackgroundColor; @@ -125,7 +132,8 @@ class _InnerCanvasState extends State { } }(), backgroundPattern: () { - if (page.backgroundImage != null && Prefs.editorOpaqueBackgrounds.value) { + if (page.backgroundImage != null && + Prefs.editorOpaqueBackgrounds.value) { return CanvasBackgroundPattern.none; } else if (widget.hideBackground) { return CanvasBackgroundPattern.none; @@ -139,16 +147,15 @@ class _InnerCanvasState extends State { ), foregroundPainter: CanvasPainter( repaint: widget.redrawPageListenable, - invert: invert, strokes: page.strokes, laserStrokes: page.laserStrokes, currentStroke: widget.currentStroke, currentSelection: widget.currentSelection, primaryColor: colorScheme.primary, - page: page, - showPageIndicator: !widget.isPreview && (!widget.isPrint || Prefs.printPageIndicators.value), + showPageIndicator: !widget.isPreview && + (!widget.isPrint || Prefs.printPageIndicators.value), pageIndex: widget.pageIndex, totalPages: widget.coreInfo.pages.length, ), @@ -185,11 +192,11 @@ class _InnerCanvasState extends State { image: page.images[i], pageSize: Size(widget.width, widget.height), setAsBackground: widget.setAsBackground, - readOnly: widget.coreInfo.readOnly - || !widget.currentToolIsSelect, - selected: widget.currentSelection - ?.images.contains(page.images[i]) - ?? false, + readOnly: + widget.coreInfo.readOnly || !widget.currentToolIsSelect, + selected: widget.currentSelection?.images + .contains(page.images[i]) ?? + false, ), ], ), @@ -236,7 +243,6 @@ class _InnerCanvasState extends State { displayLarge: defaultStyle.copyWith( fontSize: lineHeight * 1.15, height: 1 / 1.15, - decoration: TextDecoration.underline, decorationColor: defaultStyle.color?.withOpacity(0.6), decorationThickness: 3, @@ -244,7 +250,6 @@ class _InnerCanvasState extends State { displayMedium: defaultStyle.copyWith( fontSize: lineHeight * 1, height: 1 / 1, - decoration: TextDecoration.underline, decorationColor: defaultStyle.color?.withOpacity(0.5), decorationThickness: 3, @@ -252,7 +257,6 @@ class _InnerCanvasState extends State { displaySmall: defaultStyle.copyWith( fontSize: lineHeight * 0.9, height: 1 / 0.9, - decoration: TextDecoration.underline, decorationColor: defaultStyle.color?.withOpacity(0.4), decorationThickness: 3, @@ -263,21 +267,13 @@ class _InnerCanvasState extends State { return DefaultStyles( h1: DefaultTextBlockStyle( - textTheme.displayLarge!, - zeroSpacing, zeroSpacing, null - ), + textTheme.displayLarge!, zeroSpacing, zeroSpacing, null), h2: DefaultTextBlockStyle( - textTheme.displayMedium!, - zeroSpacing, zeroSpacing, null - ), + textTheme.displayMedium!, zeroSpacing, zeroSpacing, null), h3: DefaultTextBlockStyle( - textTheme.displaySmall!, - zeroSpacing, zeroSpacing, null - ), + textTheme.displaySmall!, zeroSpacing, zeroSpacing, null), paragraph: DefaultTextBlockStyle( - textTheme.bodyLarge!, - zeroSpacing, zeroSpacing, null - ), + textTheme.bodyLarge!, zeroSpacing, zeroSpacing, null), small: TextStyle( fontSize: lineHeight * 0.4, height: 1 / 0.4, @@ -305,18 +301,18 @@ class _InnerCanvasState extends State { decoration: TextDecoration.underline, ), placeHolder: DefaultTextBlockStyle( - textTheme.bodyLarge!.copyWith( - color: Colors.grey.withOpacity(0.6), - ), - zeroSpacing, zeroSpacing, null - ), + textTheme.bodyLarge!.copyWith( + color: Colors.grey.withOpacity(0.6), + ), + zeroSpacing, + zeroSpacing, + null), lists: DefaultListBlockStyle( - textTheme.bodyLarge!, - zeroSpacing, zeroSpacing, null, null - ), + textTheme.bodyLarge!, zeroSpacing, zeroSpacing, null, null), quote: DefaultTextBlockStyle( TextStyle(color: textTheme.bodyLarge!.color!.withOpacity(0.6)), - zeroSpacing, zeroSpacing, + zeroSpacing, + zeroSpacing, BoxDecoration( border: Border( left: BorderSide( @@ -336,17 +332,11 @@ class _InnerCanvasState extends State { ), ), indent: DefaultTextBlockStyle( - textTheme.bodyLarge!, - zeroSpacing, zeroSpacing, null - ), + textTheme.bodyLarge!, zeroSpacing, zeroSpacing, null), align: DefaultTextBlockStyle( - textTheme.bodyLarge!, - zeroSpacing, zeroSpacing, null - ), + textTheme.bodyLarge!, zeroSpacing, zeroSpacing, null), leading: DefaultTextBlockStyle( - textTheme.bodyLarge!, - zeroSpacing, zeroSpacing, null - ), + textTheme.bodyLarge!, zeroSpacing, zeroSpacing, null), sizeSmall: TextStyle( fontSize: textTheme.bodyLarge!.fontSize!, ), diff --git a/lib/components/canvas/interactive_canvas.dart b/lib/components/canvas/interactive_canvas.dart index 38904d406..846d927ce 100644 --- a/lib/components/canvas/interactive_canvas.dart +++ b/lib/components/canvas/interactive_canvas.dart @@ -15,7 +15,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/physics.dart'; import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3; -typedef InteractiveCanvasViewerWidgetBuilder = Widget Function(BuildContext context, Quad viewport); +typedef InteractiveCanvasViewerWidgetBuilder = Widget Function( + BuildContext context, Quad viewport); @immutable class InteractiveCanvasViewer extends StatefulWidget { @@ -25,7 +26,7 @@ class InteractiveCanvasViewer extends StatefulWidget { this.clipBehavior = Clip.hardEdge, @Deprecated( 'Use panAxis instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', + 'This feature was deprecated after v3.3.0-0.5.pre.', ) this.alignPanAxis = false, this.panAxis = PanAxis.free, @@ -48,19 +49,21 @@ class InteractiveCanvasViewer extends StatefulWidget { this.alignment, this.trackpadScrollCausesScale = false, required Widget this.child, - }) : assert(minScale > 0), + }) : assert(minScale > 0), assert(interactionEndFrictionCoefficient > 0), assert(minScale.isFinite), assert(maxScale > 0), assert(!maxScale.isNaN), assert(maxScale >= minScale), - // boundaryMargin must be either fully infinite or fully finite, but not - // a mix of both. + // boundaryMargin must be either fully infinite or fully finite, but not + // a mix of both. assert( - (boundaryMargin.horizontal.isInfinite - && boundaryMargin.vertical.isInfinite) || (boundaryMargin.top.isFinite - && boundaryMargin.right.isFinite && boundaryMargin.bottom.isFinite - && boundaryMargin.left.isFinite), + (boundaryMargin.horizontal.isInfinite && + boundaryMargin.vertical.isInfinite) || + (boundaryMargin.top.isFinite && + boundaryMargin.right.isFinite && + boundaryMargin.bottom.isFinite && + boundaryMargin.left.isFinite), ), builder = null; @@ -76,7 +79,7 @@ class InteractiveCanvasViewer extends StatefulWidget { this.clipBehavior = Clip.hardEdge, @Deprecated( 'Use panAxis instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', + 'This feature was deprecated after v3.3.0-0.5.pre.', ) this.alignPanAxis = false, this.panAxis = PanAxis.free, @@ -98,20 +101,21 @@ class InteractiveCanvasViewer extends StatefulWidget { this.alignment, this.trackpadScrollCausesScale = false, required InteractiveCanvasViewerWidgetBuilder this.builder, - }) : assert(minScale > 0), + }) : assert(minScale > 0), assert(interactionEndFrictionCoefficient > 0), assert(minScale.isFinite), assert(maxScale > 0), assert(!maxScale.isNaN), assert(maxScale >= minScale), - // boundaryMargin must be either fully infinite or fully finite, but not - // a mix of both. + // boundaryMargin must be either fully infinite or fully finite, but not + // a mix of both. assert( - (boundaryMargin.horizontal.isInfinite && boundaryMargin.vertical.isInfinite) || - (boundaryMargin.top.isFinite && - boundaryMargin.right.isFinite && - boundaryMargin.bottom.isFinite && - boundaryMargin.left.isFinite), + (boundaryMargin.horizontal.isInfinite && + boundaryMargin.vertical.isInfinite) || + (boundaryMargin.top.isFinite && + boundaryMargin.right.isFinite && + boundaryMargin.bottom.isFinite && + boundaryMargin.left.isFinite), ), constrained = false, child = null; @@ -141,7 +145,7 @@ class InteractiveCanvasViewer extends StatefulWidget { /// alignPanAxis. @Deprecated( 'Use panAxis instead. ' - 'This feature was deprecated after v3.3.0-0.5.pre.', + 'This feature was deprecated after v3.3.0-0.5.pre.', ) final bool alignPanAxis; @@ -356,6 +360,7 @@ class InteractiveCanvasViewer extends StatefulWidget { /// A function to distinguish a draw gesture and a pan/zoom gesture. final bool Function(ScaleStartDetails scaleDetails)? isDrawGesture; + /// Called when any gesture ends. final GestureScaleEndCallback? onInteractionEnd; @@ -386,8 +391,8 @@ class InteractiveCanvasViewer extends StatefulWidget { /// Returns the closest point to the given point on the given line segment. @visibleForTesting static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) { - final double lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() - + math.pow(l2.y - l1.y, 2.0).toDouble(); + final double lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() + + math.pow(l2.y - l1.y, 2.0).toDouble(); // In this case, l1 == l2. if (lengthSquared == 0) { @@ -482,16 +487,21 @@ class InteractiveCanvasViewer extends StatefulWidget { // Otherwise, return the nearest point on the quad. final List closestPoints = [ - InteractiveCanvasViewer.getNearestPointOnLine(point, quad.point0, quad.point1), - InteractiveCanvasViewer.getNearestPointOnLine(point, quad.point1, quad.point2), - InteractiveCanvasViewer.getNearestPointOnLine(point, quad.point2, quad.point3), - InteractiveCanvasViewer.getNearestPointOnLine(point, quad.point3, quad.point0), + InteractiveCanvasViewer.getNearestPointOnLine( + point, quad.point0, quad.point1), + InteractiveCanvasViewer.getNearestPointOnLine( + point, quad.point1, quad.point2), + InteractiveCanvasViewer.getNearestPointOnLine( + point, quad.point2, quad.point3), + InteractiveCanvasViewer.getNearestPointOnLine( + point, quad.point3, quad.point0), ]; double minDistance = double.infinity; late Vector3 closestOverall; for (final Vector3 closePoint in closestPoints) { final double distance = math.sqrt( - math.pow(point.x - closePoint.x, 2) + math.pow(point.y - closePoint.y, 2), + math.pow(point.x - closePoint.x, 2) + + math.pow(point.y - closePoint.y, 2), ); if (distance < minDistance) { minDistance = distance; @@ -502,10 +512,12 @@ class InteractiveCanvasViewer extends StatefulWidget { } @override - State createState() => _InteractiveCanvasViewerState(); + State createState() => + _InteractiveCanvasViewerState(); } -class _InteractiveCanvasViewerState extends State with TickerProviderStateMixin { +class _InteractiveCanvasViewerState extends State + with TickerProviderStateMixin { TransformationController? _transformationController; final GlobalKey _childKey = GlobalKey(); @@ -536,22 +548,24 @@ class _InteractiveCanvasViewerState extends State with assert(!widget.boundaryMargin.top.isNaN); assert(!widget.boundaryMargin.bottom.isNaN); - final RenderBox childRenderBox = _childKey.currentContext!.findRenderObject()! as RenderBox; + final RenderBox childRenderBox = + _childKey.currentContext!.findRenderObject()! as RenderBox; final Size childSize = childRenderBox.size; - final Rect boundaryRect = widget.boundaryMargin.inflateRect(Offset.zero & childSize); + final Rect boundaryRect = + widget.boundaryMargin.inflateRect(Offset.zero & childSize); assert( - !boundaryRect.isEmpty, - "InteractiveCanvasViewer's child must have nonzero dimensions.", + !boundaryRect.isEmpty, + "InteractiveCanvasViewer's child must have nonzero dimensions.", ); // Boundaries that are partially infinite are not allowed because Matrix4's // rotation and translation methods don't handle infinites well. assert( - boundaryRect.isFinite || - (boundaryRect.left.isInfinite - && boundaryRect.top.isInfinite - && boundaryRect.right.isInfinite - && boundaryRect.bottom.isInfinite), - 'boundaryRect must either be infinite in all directions or finite in all directions.', + boundaryRect.isFinite || + (boundaryRect.left.isInfinite && + boundaryRect.top.isInfinite && + boundaryRect.right.isInfinite && + boundaryRect.bottom.isInfinite), + 'boundaryRect must either be infinite in all directions or finite in all directions.', ); return boundaryRect; } @@ -559,7 +573,8 @@ class _InteractiveCanvasViewerState extends State with // The Rect representing the child's parent. Rect get _viewport { assert(_parentKey.currentContext != null); - final RenderBox parentRenderBox = _parentKey.currentContext!.findRenderObject()! as RenderBox; + final RenderBox parentRenderBox = + _parentKey.currentContext!.findRenderObject()! as RenderBox; return Offset.zero & parentRenderBox.size; } @@ -573,7 +588,7 @@ class _InteractiveCanvasViewerState extends State with late final Offset alignedTranslation; if (_currentAxis != null) { - switch(widget.panAxis){ + switch (widget.panAxis) { case PanAxis.horizontal: alignedTranslation = _alignAxis(translation, Axis.horizontal); case PanAxis.vertical: @@ -587,10 +602,11 @@ class _InteractiveCanvasViewerState extends State with alignedTranslation = translation; } - final Matrix4 nextMatrix = matrix.clone()..translate( - alignedTranslation.dx, - alignedTranslation.dy, - ); + final Matrix4 nextMatrix = matrix.clone() + ..translate( + alignedTranslation.dx, + alignedTranslation.dy, + ); // Transform the viewport to determine where its four corners will be after // the child has been transformed. @@ -612,7 +628,8 @@ class _InteractiveCanvasViewerState extends State with ); // If the given translation fits completely within the boundaries, allow it. - final Offset offendingDistance = _exceedsBy(boundariesAabbQuad, nextViewport); + final Offset offendingDistance = + _exceedsBy(boundariesAabbQuad, nextViewport); if (offendingDistance == Offset.zero) { return nextMatrix; } @@ -630,15 +647,18 @@ class _InteractiveCanvasViewerState extends State with // calculating the translation to put the viewport inside that Quad is more // complicated than this when rotated. // https://github.com/flutter/flutter/issues/57698 - final Matrix4 correctedMatrix = matrix.clone()..setTranslation(Vector3( - correctedTotalTranslation.dx, - correctedTotalTranslation.dy, - 0, - )); + final Matrix4 correctedMatrix = matrix.clone() + ..setTranslation(Vector3( + correctedTotalTranslation.dx, + correctedTotalTranslation.dy, + 0, + )); // Double check that the corrected translation fits. - final Quad correctedViewport = _transformViewport(correctedMatrix, _viewport); - final Offset offendingCorrectedDistance = _exceedsBy(boundariesAabbQuad, correctedViewport); + final Quad correctedViewport = + _transformViewport(correctedMatrix, _viewport); + final Offset offendingCorrectedDistance = + _exceedsBy(boundariesAabbQuad, correctedViewport); if (offendingCorrectedDistance == Offset.zero) { return correctedMatrix; } @@ -646,7 +666,8 @@ class _InteractiveCanvasViewerState extends State with // If the corrected translation doesn't fit in either direction, don't allow // any translation at all. This happens when the viewport is larger than the // entire boundary. - if (offendingCorrectedDistance.dx != 0.0 && offendingCorrectedDistance.dy != 0.0) { + if (offendingCorrectedDistance.dx != 0.0 && + offendingCorrectedDistance.dy != 0.0) { return matrix.clone(); } @@ -656,11 +677,12 @@ class _InteractiveCanvasViewerState extends State with offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0, offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0, ); - return matrix.clone()..setTranslation(Vector3( - unidirectionalCorrectedTotalTranslation.dx, - unidirectionalCorrectedTotalTranslation.dy, - 0, - )); + return matrix.clone() + ..setTranslation(Vector3( + unidirectionalCorrectedTotalTranslation.dx, + unidirectionalCorrectedTotalTranslation.dy, + 0, + )); } // Return a new matrix representing the given matrix after applying the given @@ -673,7 +695,8 @@ class _InteractiveCanvasViewerState extends State with // Don't allow a scale that results in an overall scale beyond min/max // scale. - final double currentScale = _transformationController!.value.getMaxScaleOnAxis(); + final double currentScale = + _transformationController!.value.getMaxScaleOnAxis(); final double totalScale = math.max( currentScale * scale, // Ensure that the scale cannot make the child so big that it can't fit @@ -683,7 +706,8 @@ class _InteractiveCanvasViewerState extends State with _viewport.height / _boundaryRect.height, ), ); - final double clampedTotalScale = clampDouble(totalScale, + final double clampedTotalScale = clampDouble( + totalScale, widget.minScale, widget.maxScale, ); @@ -700,8 +724,7 @@ class _InteractiveCanvasViewerState extends State with final Offset focalPointScene = _transformationController!.toScene( focalPoint, ); - return matrix - .clone() + return matrix.clone() ..translate(focalPointScene.dx, focalPointScene.dy) ..rotateZ(-rotation) ..translate(-focalPointScene.dx, -focalPointScene.dy); @@ -856,7 +879,8 @@ class _InteractiveCanvasViewerState extends State with _currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene); // Translate so that the same point in the scene is underneath the // focal point before and after the movement. - final Offset translationChange = focalPointScene - _referenceFocalPoint!; + final Offset translationChange = + focalPointScene - _referenceFocalPoint!; _transformationController!.value = _matrixTranslate( _transformationController!.value, translationChange, @@ -897,8 +921,10 @@ class _InteractiveCanvasViewerState extends State with _currentAxis = null; return; } - final Vector3 translationVector = _transformationController!.value.getTranslation(); - final Offset translation = Offset(translationVector.x, translationVector.y); + final Vector3 translationVector = + _transformationController!.value.getTranslation(); + final Offset translation = + Offset(translationVector.x, translationVector.y); final FrictionSimulation frictionSimulationX = FrictionSimulation( widget.interactionEndFrictionCoefficient, translation.dx, @@ -932,17 +958,16 @@ class _InteractiveCanvasViewerState extends State with final FrictionSimulation frictionSimulation = FrictionSimulation( widget.interactionEndFrictionCoefficient * widget.scaleFactor, scale, - details.scaleVelocity / 10 - ); - final double tFinal = _getFinalTime(details.scaleVelocity.abs(), widget.interactionEndFrictionCoefficient, effectivelyMotionless: 0.1); - _scaleAnimation = Tween( - begin: scale, - end: frictionSimulation.x(tFinal) - ).animate(CurvedAnimation( - parent: _scaleController, - curve: Curves.decelerate - )); - _scaleController.duration = Duration(milliseconds: (tFinal * 1000).round()); + details.scaleVelocity / 10); + final double tFinal = _getFinalTime( + details.scaleVelocity.abs(), widget.interactionEndFrictionCoefficient, + effectivelyMotionless: 0.1); + _scaleAnimation = + Tween(begin: scale, end: frictionSimulation.x(tFinal)) + .animate(CurvedAnimation( + parent: _scaleController, curve: Curves.decelerate)); + _scaleController.duration = + Duration(milliseconds: (tFinal * 1000).round()); _scaleAnimation!.addListener(_onScaleAnimate); _scaleController.forward(); } @@ -952,7 +977,8 @@ class _InteractiveCanvasViewerState extends State with void _receivedPointerSignal(PointerSignalEvent event) { final double scaleChange; if (event is PointerScrollEvent) { - if (event.kind == PointerDeviceKind.trackpad && !widget.trackpadScrollCausesScale) { + if (event.kind == PointerDeviceKind.trackpad && + !widget.trackpadScrollCausesScale) { // Trackpad scroll, so treat it as a pan. if (!_gestureIsSupported(_GestureType.pan)) return; @@ -972,8 +998,7 @@ class _InteractiveCanvasViewerState extends State with _transformationController!.value = _matrixTranslate( _transformationController!.value, - newFocalPointScene - focalPointScene - ); + newFocalPointScene - focalPointScene); return; } @@ -982,11 +1007,9 @@ class _InteractiveCanvasViewerState extends State with return; } scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor); - } - else if (event is PointerScaleEvent) { + } else if (event is PointerScaleEvent) { scaleChange = event.scale; - } - else { + } else { return; } @@ -1022,7 +1045,8 @@ class _InteractiveCanvasViewerState extends State with return; } // Translate such that the resulting translation is _animation.value. - final Vector3 translationVector = _transformationController!.value.getTranslation(); + final Vector3 translationVector = + _transformationController!.value.getTranslation(); final Offset translation = Offset(translationVector.x, translationVector.y); final Offset translationScene = _transformationController!.toScene( translation, @@ -1047,7 +1071,8 @@ class _InteractiveCanvasViewerState extends State with return; } final double desiredScale = _scaleAnimation!.value; - final double scaleChange = desiredScale / _transformationController!.value.getMaxScaleOnAxis(); + final double scaleChange = + desiredScale / _transformationController!.value.getMaxScaleOnAxis(); final Offset referenceFocalPoint = _transformationController!.toScene( _scaleAnimationFocalPoint, ); @@ -1079,15 +1104,13 @@ class _InteractiveCanvasViewerState extends State with void initState() { super.initState(); - _transformationController = widget.transformationController - ?? TransformationController(); + _transformationController = + widget.transformationController ?? TransformationController(); _transformationController!.addListener(_onTransformationControllerChange); _controller = AnimationController( vsync: this, ); - _scaleController = AnimationController( - vsync: this - ); + _scaleController = AnimationController(vsync: this); } @override @@ -1097,20 +1120,27 @@ class _InteractiveCanvasViewerState extends State with // transformationControllers. if (oldWidget.transformationController == null) { if (widget.transformationController != null) { - _transformationController!.removeListener(_onTransformationControllerChange); + _transformationController! + .removeListener(_onTransformationControllerChange); _transformationController!.dispose(); _transformationController = widget.transformationController; - _transformationController!.addListener(_onTransformationControllerChange); + _transformationController! + .addListener(_onTransformationControllerChange); } } else { if (widget.transformationController == null) { - _transformationController!.removeListener(_onTransformationControllerChange); + _transformationController! + .removeListener(_onTransformationControllerChange); _transformationController = TransformationController(); - _transformationController!.addListener(_onTransformationControllerChange); - } else if (widget.transformationController != oldWidget.transformationController) { - _transformationController!.removeListener(_onTransformationControllerChange); + _transformationController! + .addListener(_onTransformationControllerChange); + } else if (widget.transformationController != + oldWidget.transformationController) { + _transformationController! + .removeListener(_onTransformationControllerChange); _transformationController = widget.transformationController; - _transformationController!.addListener(_onTransformationControllerChange); + _transformationController! + .addListener(_onTransformationControllerChange); } } } @@ -1119,7 +1149,8 @@ class _InteractiveCanvasViewerState extends State with void dispose() { _controller.dispose(); _scaleController.dispose(); - _transformationController!.removeListener(_onTransformationControllerChange); + _transformationController! + .removeListener(_onTransformationControllerChange); if (widget.transformationController == null) { _transformationController!.dispose(); } @@ -1170,7 +1201,7 @@ class _InteractiveCanvasViewerState extends State with onScaleStart: _onScaleStart, onScaleUpdate: _onScaleUpdate, trackpadScrollCausesScale: widget.trackpadScrollCausesScale, - trackpadScrollToScaleFactor: Offset(0, -1/widget.scaleFactor), + trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor), child: child, ), ); @@ -1235,7 +1266,8 @@ enum _GestureType { // Given a velocity and drag, calculate the time at which motion will come to // a stop, within the margin of effectivelyMotionless. -double _getFinalTime(double velocity, double drag, {double effectivelyMotionless = 10}) { +double _getFinalTime(double velocity, double drag, + {double effectivelyMotionless = 10}) { return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); } @@ -1296,11 +1328,15 @@ Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) { // Offset.zero. Offset _exceedsBy(Quad boundary, Quad viewport) { final List viewportPoints = [ - viewport.point0, viewport.point1, viewport.point2, viewport.point3, + viewport.point0, + viewport.point1, + viewport.point2, + viewport.point3, ]; Offset largestExcess = Offset.zero; for (final Vector3 point in viewportPoints) { - final Vector3 pointInside = InteractiveCanvasViewer.getNearestPointInside(point, boundary); + final Vector3 pointInside = + InteractiveCanvasViewer.getNearestPointInside(point, boundary); final Offset excess = Offset( pointInside.x - point.x, pointInside.y - point.y, diff --git a/lib/components/canvas/shader_sampler.dart b/lib/components/canvas/shader_sampler.dart index 0fa825bc7..5e01542ea 100644 --- a/lib/components/canvas/shader_sampler.dart +++ b/lib/components/canvas/shader_sampler.dart @@ -82,15 +82,16 @@ class _ShaderSnapshotPainter extends SnapshotPainter { }); @override - void paint(PaintingContext context, Offset offset, Size size, PaintingContextCallback painter) { + void paint(PaintingContext context, Offset offset, Size size, + PaintingContextCallback painter) { painter(context, offset); } @override - void paintSnapshot(PaintingContext context, Offset offset, Size size, ui.Image image, Size sourceSize, double pixelRatio) { + void paintSnapshot(PaintingContext context, Offset offset, Size size, + ui.Image image, Size sourceSize, double pixelRatio) { final shader = shaderBuilder(image, size); - final Paint paint = Paint() - ..shader = shader; + final Paint paint = Paint()..shader = shader; context.pushTransform( true, Offset.zero, diff --git a/lib/components/files/file_tree.dart b/lib/components/files/file_tree.dart index 9167e43e2..9feb3e107 100644 --- a/lib/components/files/file_tree.dart +++ b/lib/components/files/file_tree.dart @@ -6,7 +6,6 @@ import 'package:go_router/go_router.dart'; import 'package:saber/data/file_manager/file_manager.dart'; import 'package:saber/data/routes.dart'; - class FileTree extends StatelessWidget { const FileTree({super.key}); @@ -15,15 +14,13 @@ class FileTree extends StatelessWidget { return const Padding( padding: EdgeInsets.all(12), child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: FileTreeBranch( - path: null, - isDirectory: true, - ) - ), + scrollDirection: Axis.vertical, + child: FileTreeBranch( + path: null, + isDirectory: true, + )), ); } - } class FileTreeBranch extends StatefulWidget { @@ -38,7 +35,6 @@ class FileTreeBranch extends StatefulWidget { @override State createState() => _FileTreeBranchState(); - } class _FileTreeBranchState extends State { @@ -55,9 +51,10 @@ class _FileTreeBranchState extends State { } void _getInfo([FileOperation? _]) async { - if (widget.isDirectory) children = await FileManager.getChildrenOfDirectory(widget.path ?? '/'); + if (widget.isDirectory) + children = await FileManager.getChildrenOfDirectory(widget.path ?? '/'); areChildrenVisible = children != null && children!.onlyOneChild(); - if (mounted) setState(() { }); + if (mounted) setState(() {}); } @override @@ -71,61 +68,62 @@ class _FileTreeBranchState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (widget.path != null) Material( - color: backgroundColor, - child: InkWell( - onTap: () { - setState(() { - if (widget.isDirectory) { - areChildrenVisible = !areChildrenVisible; - } else { - context.push(RoutePaths.editFilePath(widget.path ?? '/')); - } - }); - }, - child: Row( - children: [ - if (widget.isDirectory) ...[ - Icon(areChildrenVisible ? Icons.folder_open: Icons.folder, color: colorScheme.primary, size: 25), - ] else ...[ - const Icon(Icons.insert_drive_file, size: 25), - ], - - const SizedBox(width: 5), - Expanded( - child: Text( - widget.path!.substring(widget.path!.lastIndexOf('/') + 1), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontSize: 14, + if (widget.path != null) + Material( + color: backgroundColor, + child: InkWell( + onTap: () { + setState(() { + if (widget.isDirectory) { + areChildrenVisible = !areChildrenVisible; + } else { + context.push(RoutePaths.editFilePath(widget.path ?? '/')); + } + }); + }, + child: Row( + children: [ + if (widget.isDirectory) ...[ + Icon(areChildrenVisible ? Icons.folder_open : Icons.folder, + color: colorScheme.primary, size: 25), + ] else ...[ + const Icon(Icons.insert_drive_file, size: 25), + ], + const SizedBox(width: 5), + Expanded( + child: Text( + widget.path!.substring(widget.path!.lastIndexOf('/') + 1), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + fontSize: 14, + ), + overflow: TextOverflow.ellipsis, ), - overflow: TextOverflow.ellipsis, ), - ), - ], + ], + ), ), ), - ), - - - if ((widget.path == null || areChildrenVisible) && children != null) Padding( - padding: (widget.path != null) ? const EdgeInsets.only(left: 25) : EdgeInsets.zero, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (var i = 0; i < children!.directories.length; i++) - FileTreeBranch( - path: "${widget.path ?? ""}/${children!.directories[i]}", - isDirectory: true, - ), - for (var i = 0; i < children!.files.length; i++) - FileTreeBranch( - path: "${widget.path ?? ""}/${children!.files[i]}", - isDirectory: false, - ), - ], + if ((widget.path == null || areChildrenVisible) && children != null) + Padding( + padding: (widget.path != null) + ? const EdgeInsets.only(left: 25) + : EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var i = 0; i < children!.directories.length; i++) + FileTreeBranch( + path: "${widget.path ?? ""}/${children!.directories[i]}", + isDirectory: true, + ), + for (var i = 0; i < children!.files.length; i++) + FileTreeBranch( + path: "${widget.path ?? ""}/${children!.files[i]}", + isDirectory: false, + ), + ], + ), ), - ), - ], ); } @@ -135,5 +133,4 @@ class _FileTreeBranchState extends State { fileWriteSubscription?.cancel(); super.dispose(); } - } diff --git a/lib/components/home/banner_ad_widget.dart b/lib/components/home/banner_ad_widget.dart index b6151558d..abbede81c 100644 --- a/lib/components/home/banner_ad_widget.dart +++ b/lib/components/home/banner_ad_widget.dart @@ -21,7 +21,8 @@ abstract class AdState { static final log = Logger('AdState'); static void init() { - if (kDebugMode) { // test ads + if (kDebugMode) { + // test ads if (Platform.isAndroid) { _bannerAdUnitId = 'ca-app-pub-3940256099942544/6300978111'; } else if (Platform.isIOS) { @@ -29,7 +30,8 @@ abstract class AdState { } else { _bannerAdUnitId = ''; } - } else { // actual ads + } else { + // actual ads if (Platform.isAndroid) { _bannerAdUnitId = 'ca-app-pub-1312561055261176/7616317590'; } else if (Platform.isIOS) { @@ -85,17 +87,16 @@ abstract class AdState { (formError) {}, ); } + static void showConsentForm() { ConsentForm.loadConsentForm( (ConsentForm consentForm) async { - consentForm.show( - (formError) async { - if (formError != null) { - // Handle dismissal by reloading form - showConsentForm(); - } + consentForm.show((formError) async { + if (formError != null) { + // Handle dismissal by reloading form + showConsentForm(); } - ); + }); }, (formError) {}, ); @@ -143,7 +144,8 @@ class BannerAdWidget extends StatefulWidget { State createState() => _BannerAdWidgetState(); } -class _BannerAdWidgetState extends State with AutomaticKeepAliveClientMixin { +class _BannerAdWidgetState extends State + with AutomaticKeepAliveClientMixin { BannerAd? _bannerAd; @override @@ -191,9 +193,7 @@ class _BannerAdWidgetState extends State with AutomaticKeepAlive child: SizedBox( width: widget.adSize.width.toDouble(), height: widget.adSize.height.toDouble(), - child: _bannerAd == null - ? null - : AdWidget(ad: _bannerAd!), + child: _bannerAd == null ? null : AdWidget(ad: _bannerAd!), ), ), ], @@ -208,7 +208,7 @@ class _BannerAdWidgetState extends State with AutomaticKeepAlive _bannerAd = null; super.dispose(); } - + @override bool get wantKeepAlive => _bannerAd != null; } diff --git a/lib/components/home/delete_folder_button.dart b/lib/components/home/delete_folder_button.dart index 0b17ca496..2073c03d4 100644 --- a/lib/components/home/delete_folder_button.dart +++ b/lib/components/home/delete_folder_button.dart @@ -73,29 +73,35 @@ class _DeleteFolderDialogState extends State<_DeleteFolderDialog> { bool deleteAllowed = isFolderEmpty || alsoDeleteContents; return AdaptiveAlertDialog( title: Text(t.home.deleteFolder.deleteName(f: widget.folderName)), - content: isFolderEmpty ? const SizedBox.shrink() : Row( - children: [ - Checkbox( - value: alsoDeleteContents, - onChanged: isFolderEmpty ? null : (value) { - setState(() => alsoDeleteContents = value!); - }, - ), - Expanded( - child: Text(t.home.deleteFolder.alsoDeleteContents), - ), - ], - ), + content: isFolderEmpty + ? const SizedBox.shrink() + : Row( + children: [ + Checkbox( + value: alsoDeleteContents, + onChanged: isFolderEmpty + ? null + : (value) { + setState(() => alsoDeleteContents = value!); + }, + ), + Expanded( + child: Text(t.home.deleteFolder.alsoDeleteContents), + ), + ], + ), actions: [ CupertinoDialogAction( onPressed: () => Navigator.of(context).pop(), child: Text(t.editor.newerFileFormat.cancel), ), CupertinoDialogAction( - onPressed: deleteAllowed ? () async { - await widget.deleteFolder(widget.folderName); - if (mounted) Navigator.of(context).pop(); - } : null, + onPressed: deleteAllowed + ? () async { + await widget.deleteFolder(widget.folderName); + if (mounted) Navigator.of(context).pop(); + } + : null, isDestructiveAction: true, child: Text(t.home.deleteFolder.delete), ), diff --git a/lib/components/home/grid_folders.dart b/lib/components/home/grid_folders.dart index 5b909e391..dcfcda6c2 100644 --- a/lib/components/home/grid_folders.dart +++ b/lib/components/home/grid_folders.dart @@ -81,7 +81,8 @@ class _GridFolder extends StatefulWidget { required this.isFolderEmpty, required this.deleteFolder, required this.onTap, - }) : assert((folderName == null) ^ (cardType == _FolderCardType.realFolder), 'Real folders must specify a folder name'); + }) : assert((folderName == null) ^ (cardType == _FolderCardType.realFolder), + 'Real folders must specify a folder name'); final _FolderCardType cardType; final String? folderName; @@ -147,30 +148,35 @@ class _GridFolderState extends State<_GridFolder> { child: Tooltip( message: switch (widget.cardType) { _FolderCardType.backFolder => t.home.backFolder, - _FolderCardType.newFolder => t.home.newFolder.newFolder, + _FolderCardType.newFolder => + t.home.newFolder.newFolder, _FolderCardType.realFolder => '', }, child: AdaptiveIcon( icon: switch (widget.cardType) { _FolderCardType.backFolder => Icons.folder_open, - _FolderCardType.newFolder => Icons.create_new_folder, + _FolderCardType.newFolder => + Icons.create_new_folder, _FolderCardType.realFolder => Icons.folder, }, cupertinoIcon: switch (widget.cardType) { - _FolderCardType.backFolder => CupertinoIcons.folder_open, - _FolderCardType.newFolder => CupertinoIcons.folder_fill_badge_plus, - _FolderCardType.realFolder => CupertinoIcons.folder_fill, + _FolderCardType.backFolder => + CupertinoIcons.folder_open, + _FolderCardType.newFolder => + CupertinoIcons.folder_fill_badge_plus, + _FolderCardType.realFolder => + CupertinoIcons.folder_fill, }, size: 50, ), ), ), - if (widget.cardType == _FolderCardType.realFolder) Positioned.fill( child: ValueListenableBuilder( valueListenable: expanded, - builder: (context, expanded, child) => AnimatedOpacity( + builder: (context, expanded, child) => + AnimatedOpacity( opacity: expanded ? 1 : 0, duration: const Duration(milliseconds: 200), child: IgnorePointer( @@ -200,7 +206,8 @@ class _GridFolderState extends State<_GridFolder> { folderName: widget.folderName!, doesFolderExist: widget.doesFolderExist, renameFolder: (String folderName) async { - await widget.renameFolder(widget.folderName!, folderName); + await widget.renameFolder( + widget.folderName!, folderName); expanded.value = false; }, ), @@ -221,9 +228,7 @@ class _GridFolderState extends State<_GridFolder> { ], ), ), - const SizedBox(height: 8), - switch (widget.cardType) { _FolderCardType.backFolder => const Icon(Icons.arrow_back), _FolderCardType.newFolder => Text(t.home.newFolder.newFolder), diff --git a/lib/components/home/masonry_files.dart b/lib/components/home/masonry_files.dart index 6c2abc01e..c2a7fd62d 100644 --- a/lib/components/home/masonry_files.dart +++ b/lib/components/home/masonry_files.dart @@ -66,7 +66,8 @@ class _MasonryFilesState extends State { } final file = files[index]; - if (file == null) { // ad + if (file == null) { + // ad return const BannerAdWidget( adSize: AdSize( width: 300, @@ -75,16 +76,15 @@ class _MasonryFilesState extends State { ); } else { return ValueListenableBuilder( - valueListenable: isAnythingSelected, - builder: (context, isAnythingSelected, _) { - return PreviewCard( - filePath: file, - toggleSelection: toggleSelection, - selected: widget.selectedFiles.value.contains(file), - isAnythingSelected: isAnythingSelected, - ); - } - ); + valueListenable: isAnythingSelected, + builder: (context, isAnythingSelected, _) { + return PreviewCard( + filePath: file, + toggleSelection: toggleSelection, + selected: widget.selectedFiles.value.contains(file), + isAnythingSelected: isAnythingSelected, + ); + }); } }, ), diff --git a/lib/components/home/move_note_button.dart b/lib/components/home/move_note_button.dart index 9d136b137..af34ea881 100644 --- a/lib/components/home/move_note_button.dart +++ b/lib/components/home/move_note_button.dart @@ -46,6 +46,7 @@ class _MoveNoteDialog extends StatefulWidget { @override State<_MoveNoteDialog> createState() => _MoveNoteDialogState(); } + class _MoveNoteDialogState extends State<_MoveNoteDialog> { /// The original file names of the notes. late final List originalFileNames = widget.filesToMove @@ -60,9 +61,8 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { /// Whether each file uses [Editor.extensionOldJson]. /// This is populated in [findOldExtensions]. - late List oldExtensions = widget.filesToMove - .map((name) => false) - .toList(); + late List oldExtensions = + widget.filesToMove.map((name) => false).toList(); Future findOldExtensions() async { final futures = >[]; for (int i = 0; i < widget.filesToMove.length; ++i) { @@ -74,6 +74,7 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { } late String _currentFolder; + /// The current folder browsed to in the dialog. String get currentFolder => _currentFolder; set currentFolder(String folder) { @@ -84,17 +85,20 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { /// The children of [currentFolder]. DirectoryChildren? currentFolderChildren; + /// The file names that the notes will be moved to. - /// + /// /// These will be the same as in [fileNames], unless /// a file needs to be renamed to avoid a name conflict. /// Such a file will also be in [changedFileNames]. late List newFileNames = []; + /// The new names of the files that needed to be renamed. late List changedFileNames = []; Future findChildrenOfCurrentFolder() async { - currentFolderChildren = await FileManager.getChildrenOfDirectory(currentFolder); + currentFolderChildren = + await FileManager.getChildrenOfDirectory(currentFolder); newFileNames = []; changedFileNames = []; @@ -102,10 +106,10 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { final oldExtension = oldExtensions[i]; final newFileName = await FileManager.suffixFilePathToMakeItUnique( '$currentFolder${originalFileNames[i]}', - intendedExtension: oldExtension - ? Editor.extensionOldJson - : Editor.extension, - currentPath: '${widget.filesToMove[i]}${oldExtension ? Editor.extensionOldJson : Editor.extension}', + intendedExtension: + oldExtension ? Editor.extensionOldJson : Editor.extension, + currentPath: + '${widget.filesToMove[i]}${oldExtension ? Editor.extensionOldJson : Editor.extension}', ).then((newPath) => newPath.substring(newPath.lastIndexOf('/') + 1)); newFileNames.add(newFileName); @@ -133,14 +137,14 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { } super.initState(); - findOldExtensions() - .then((_) => findChildrenOfCurrentFolder()); + findOldExtensions().then((_) => findChildrenOfCurrentFolder()); } String _findMostCommonParentFolder() { final parentFolderCounts = {}; for (final parentFolder in parentFolders) { - parentFolderCounts[parentFolder] = (parentFolderCounts[parentFolder] ?? 0) + 1; + parentFolderCounts[parentFolder] = + (parentFolderCounts[parentFolder] ?? 0) + 1; } return parentFolderCounts.entries .reduce((a, b) => a.value >= b.value ? a : b) @@ -151,12 +155,12 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { Widget build(BuildContext context) { return AdaptiveAlertDialog( title: originalFileNames.length < 5 - ? Text(t.home.moveNote.moveName( - f: originalFileNames.join(', '), - )) - : Text(t.home.moveNote.moveNotes( - n: originalFileNames.length, - )), + ? Text(t.home.moveNote.moveName( + f: originalFileNames.join(', '), + )) + : Text(t.home.moveNote.moveNotes( + n: originalFileNames.length, + )), content: SizedBox( width: 300, height: 300, @@ -175,7 +179,9 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { if (folder == '..') { currentFolder = currentFolder.substring( 0, - currentFolder.lastIndexOf('/', currentFolder.length - 2) + 1, + currentFolder.lastIndexOf( + '/', currentFolder.length - 2) + + 1, ); } else { currentFolder = '$currentFolder$folder/'; @@ -184,7 +190,9 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { }, createFolder: createFolder, doesFolderExist: (String folderName) { - return currentFolderChildren?.directories.contains(folderName) ?? false; + return currentFolderChildren?.directories + .contains(folderName) ?? + false; }, renameFolder: (String oldName, String newName) async { final oldPath = '$currentFolder$oldName'; @@ -193,7 +201,8 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { }, isFolderEmpty: (String folderName) async { final folderPath = '$currentFolder$folderName'; - final children = await FileManager.getChildrenOfDirectory(folderPath); + final children = + await FileManager.getChildrenOfDirectory(folderPath); return children?.isEmpty ?? true; }, deleteFolder: (String folderName) async { @@ -202,14 +211,14 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { findChildrenOfCurrentFolder(); }, folders: [ - for (final directoryPath in currentFolderChildren?.directories ?? const []) + for (final directoryPath + in currentFolderChildren?.directories ?? const []) directoryPath, ], ), ], ), ), - if (changedFileNames.isEmpty) const SizedBox.shrink() else if (changedFileNames.length == 1) @@ -232,7 +241,8 @@ class _MoveNoteDialogState extends State<_MoveNoteDialog> { CupertinoDialogAction( onPressed: () async { for (int i = 0; i < widget.filesToMove.length; ++i) { - final extension = oldExtensions[i] ? Editor.extensionOldJson : Editor.extension; + final extension = + oldExtensions[i] ? Editor.extensionOldJson : Editor.extension; await FileManager.moveFile( '${widget.filesToMove[i]}$extension', '$currentFolder${newFileNames[i]}$extension', diff --git a/lib/components/home/new_folder_dialog.dart b/lib/components/home/new_folder_dialog.dart index d28b402d4..8129dbf72 100644 --- a/lib/components/home/new_folder_dialog.dart +++ b/lib/components/home/new_folder_dialog.dart @@ -19,6 +19,7 @@ class NewFolderDialog extends StatefulWidget { @override State createState() => _NewFolderDialogState(); } + class _NewFolderDialogState extends State { final _formKey = GlobalKey(); final TextEditingController _controller = TextEditingController(); diff --git a/lib/components/home/new_note_button.dart b/lib/components/home/new_note_button.dart index 7fced029d..ed98bc232 100644 --- a/lib/components/home/new_note_button.dart +++ b/lib/components/home/new_note_button.dart @@ -21,7 +21,7 @@ class NewNoteButton extends StatefulWidget { State createState() => _NewNoteButtonState(); } -class _NewNoteButtonState extends State{ +class _NewNoteButtonState extends State { final ValueNotifier isDialOpen = ValueNotifier(false); @override @@ -34,11 +34,10 @@ class _NewNoteButtonState extends State{ spaceBetweenChildren: 4, dialRoot: (ctx, open, toggleChildren) { return FloatingActionButton( - shape: widget.cupertino ? const CircleBorder() : null, - onPressed: toggleChildren, - tooltip: t.home.tooltips.newNote, - child: const Icon(Icons.add) - ); + shape: widget.cupertino ? const CircleBorder() : null, + onPressed: toggleChildren, + tooltip: t.home.tooltips.newNote, + child: const Icon(Icons.add)); }, children: [ SpeedDialChild( @@ -47,8 +46,9 @@ class _NewNoteButtonState extends State{ onTap: () async { if (widget.path == null) { context.push(RoutePaths.edit); - } else{ - final newFilePath = await FileManager.newFilePath('${widget.path}/'); + } else { + final newFilePath = + await FileManager.newFilePath('${widget.path}/'); if (!mounted) return; context.push(RoutePaths.editFilePath(newFilePath)); } @@ -69,7 +69,9 @@ class _NewNoteButtonState extends State{ final fileName = result.files.single.name; if (filePath == null) return; - if (filePath.endsWith('.sbn') || filePath.endsWith('.sbn2') || filePath.endsWith('.sba')) { + if (filePath.endsWith('.sbn') || + filePath.endsWith('.sbn2') || + filePath.endsWith('.sba')) { final path = await FileManager.importFile( filePath, '${widget.path ?? ''}/', @@ -81,9 +83,11 @@ class _NewNoteButtonState extends State{ } else if (filePath.endsWith('.pdf')) { if (!Editor.canRasterPdf) return; if (!mounted) return; - - final fileNameWithoutExtension = fileName.substring(0, fileName.length - '.pdf'.length); - final sbnFilePath = await FileManager.suffixFilePathToMakeItUnique( + + final fileNameWithoutExtension = + fileName.substring(0, fileName.length - '.pdf'.length); + final sbnFilePath = + await FileManager.suffixFilePathToMakeItUnique( '${widget.path ?? ''}/$fileNameWithoutExtension', ); if (!mounted) return; diff --git a/lib/components/home/no_files.dart b/lib/components/home/no_files.dart index fda65cebb..27bd98c90 100644 --- a/lib/components/home/no_files.dart +++ b/lib/components/home/no_files.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:saber/i18n/strings.g.dart'; diff --git a/lib/components/home/preview_card.dart b/lib/components/home/preview_card.dart index 1919cc9ec..b416ebe42 100644 --- a/lib/components/home/preview_card.dart +++ b/lib/components/home/preview_card.dart @@ -34,33 +34,39 @@ class PreviewCard extends StatefulWidget { @override State createState() => _PreviewCardState(); - static EditorCoreInfo getCachedCoreInfo(String filePath) - => _PreviewCardState.getCachedCoreInfo(filePath); - static void moveFileInCache(String oldPath, String newPath) - => _PreviewCardState.moveFileInCache(oldPath, newPath); + static EditorCoreInfo getCachedCoreInfo(String filePath) => + _PreviewCardState.getCachedCoreInfo(filePath); + static void moveFileInCache(String oldPath, String newPath) => + _PreviewCardState.moveFileInCache(oldPath, newPath); } class _PreviewCardState extends State { /// cache strokes so there's no delay the second time we see this preview card static final Map _mapFilePathToEditorInfo = {}; static EditorCoreInfo getCachedCoreInfo(String filePath) { - return _mapFilePathToEditorInfo[filePath] ?? EditorCoreInfo(filePath: filePath); + return _mapFilePathToEditorInfo[filePath] ?? + EditorCoreInfo(filePath: filePath); } + static void moveFileInCache(String oldPath, String newPath) { if (oldPath.endsWith(Editor.extension)) { oldPath = oldPath.substring(0, oldPath.length - Editor.extension.length); } else if (oldPath.endsWith(Editor.extensionOldJson)) { - oldPath = oldPath.substring(0, oldPath.length - Editor.extensionOldJson.length); + oldPath = + oldPath.substring(0, oldPath.length - Editor.extensionOldJson.length); } else { - assert(false, 'oldPath must end with ${Editor.extension} or ${Editor.extensionOldJson}'); + assert(false, + 'oldPath must end with ${Editor.extension} or ${Editor.extensionOldJson}'); } if (newPath.endsWith(Editor.extension)) { newPath = newPath.substring(0, newPath.length - Editor.extension.length); } else if (newPath.endsWith(Editor.extensionOldJson)) { - newPath = newPath.substring(0, newPath.length - Editor.extensionOldJson.length); + newPath = + newPath.substring(0, newPath.length - Editor.extensionOldJson.length); } else { - assert(false, 'newPath must end with ${Editor.extension} or ${Editor.extensionOldJson}'); + assert(false, + 'newPath must end with ${Editor.extension} or ${Editor.extensionOldJson}'); } if (!_mapFilePathToEditorInfo.containsKey(oldPath)) return; @@ -115,20 +121,20 @@ class _PreviewCardState extends State { } if (!firstPage.quill.controller.document.isEmpty()) { // this does not account for text that wraps to the next line - int linesOfText = firstPage.quill.controller.document.toPlainText().split('\n').length; - maxY = max(maxY, linesOfText * coreInfo.lineHeight * 1.5); // ×1.5 fudge factor + int linesOfText = + firstPage.quill.controller.document.toPlainText().split('\n').length; + maxY = max( + maxY, linesOfText * coreInfo.lineHeight * 1.5); // ×1.5 fudge factor } /// The height of the first page (uncropped). /// e.g. the default height is 1400 [EditorPage.defaultHeight] /// and the default width is 1000 [EditorPage.defaultWidth]. double fullHeight = firstPage.size.height; + /// The height of the canvas (cropped), /// adjusted to be between 10% and 100% of the full height. - final canvasHeight = min( - fullHeight, - max(maxY, 0) + (0.1 * fullHeight) - ); + final canvasHeight = min(fullHeight, max(maxY, 0) + (0.1 * fullHeight)); return canvasHeight / firstPage.size.width; } @@ -138,7 +144,8 @@ class _PreviewCardState extends State { if (_coreInfo.isEmpty) { findStrokes(); } - fileWriteSubscription = FileManager.fileWriteStream.stream.listen(fileWriteListener); + fileWriteSubscription = + FileManager.fileWriteStream.stream.listen(fileWriteListener); expanded.value = widget.selected; super.initState(); @@ -194,11 +201,12 @@ class _PreviewCardState extends State { final theme = Theme.of(context); final colorScheme = theme.colorScheme; final disableAnimations = MediaQuery.of(context).disableAnimations; - final transitionDuration = Duration(milliseconds: disableAnimations ? 0 : 300); - final background = coreInfo.backgroundColor - ?? InnerCanvas.defaultBackgroundColor; - final invert = theme.brightness == Brightness.dark - && Prefs.editorAutoInvert.value; + final transitionDuration = + Duration(milliseconds: disableAnimations ? 0 : 300); + final background = + coreInfo.backgroundColor ?? InnerCanvas.defaultBackgroundColor; + final invert = + theme.brightness == Brightness.dark && Prefs.editorAutoInvert.value; final firstPageWidth = coreInfo.pages.isEmpty ? EditorPage.defaultWidth : coreInfo.pages.first.size.width; @@ -206,9 +214,7 @@ class _PreviewCardState extends State { Widget card = MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( - onTap: widget.isAnythingSelected - ? _toggleCardSelection - : null, + onTap: widget.isAnythingSelected ? _toggleCardSelection : null, onSecondaryTap: _toggleCardSelection, onLongPress: _toggleCardSelection, child: ColoredBox( @@ -238,7 +244,6 @@ class _PreviewCardState extends State { ), ), ), - Positioned.fill( left: -1, top: -1, @@ -246,7 +251,8 @@ class _PreviewCardState extends State { bottom: -1, child: ValueListenableBuilder( valueListenable: expanded, - builder: (context, expanded, child) => AnimatedOpacity( + builder: (context, expanded, child) => + AnimatedOpacity( opacity: expanded ? 1 : 0, duration: const Duration(milliseconds: 200), child: IgnorePointer( @@ -277,14 +283,13 @@ class _PreviewCardState extends State { ), ], ), - Padding( padding: const EdgeInsets.all(8), - child: Text(widget.filePath.substring(widget.filePath.lastIndexOf('/') + 1)), + child: Text(widget.filePath + .substring(widget.filePath.lastIndexOf('/') + 1)), ), ], ), - UploadingIndicator( filePath: widget.filePath, ), @@ -295,33 +300,33 @@ class _PreviewCardState extends State { ); return ValueListenableBuilder( - valueListenable: expanded, - builder: (context, expanded, _) { - return OpenContainer( - closedColor: colorScheme.surface, - closedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - closedElevation: expanded ? 4 : 1, - closedBuilder: (context, action) => card, - - openColor: colorScheme.background, - openBuilder: (context, action) => Editor(path: widget.filePath), - - transitionDuration: transitionDuration, - routeSettings: RouteSettings( - name: RoutePaths.editFilePath(widget.filePath), - ), - - onClosed: (_) async { - findStrokes(); - - await Future.delayed(transitionDuration); - if (!mounted) return; - if (!GoRouterState.of(context).uri.toString().startsWith(RoutePaths.prefixOfHome)) return; - ResponsiveNavbar.setAndroidNavBarColor(theme); - }, - ); - } - ); + valueListenable: expanded, + builder: (context, expanded, _) { + return OpenContainer( + closedColor: colorScheme.surface, + closedShape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + closedElevation: expanded ? 4 : 1, + closedBuilder: (context, action) => card, + openColor: colorScheme.background, + openBuilder: (context, action) => Editor(path: widget.filePath), + transitionDuration: transitionDuration, + routeSettings: RouteSettings( + name: RoutePaths.editFilePath(widget.filePath), + ), + onClosed: (_) async { + findStrokes(); + + await Future.delayed(transitionDuration); + if (!mounted) return; + if (!GoRouterState.of(context) + .uri + .toString() + .startsWith(RoutePaths.prefixOfHome)) return; + ResponsiveNavbar.setAndroidNavBarColor(theme); + }, + ); + }); } @override diff --git a/lib/components/home/rename_folder_button.dart b/lib/components/home/rename_folder_button.dart index b8672f6c5..1dc290531 100644 --- a/lib/components/home/rename_folder_button.dart +++ b/lib/components/home/rename_folder_button.dart @@ -54,6 +54,7 @@ class _RenameFolderDialog extends StatefulWidget { @override State<_RenameFolderDialog> createState() => _RenameFolderDialogState(); } + class _RenameFolderDialogState extends State<_RenameFolderDialog> { final _formKey = GlobalKey(); final TextEditingController _controller = TextEditingController(); diff --git a/lib/components/home/rename_note_button.dart b/lib/components/home/rename_note_button.dart index 81bfecb25..241341060 100644 --- a/lib/components/home/rename_note_button.dart +++ b/lib/components/home/rename_note_button.dart @@ -48,6 +48,7 @@ class _RenameNoteDialog extends StatefulWidget { @override State<_RenameNoteDialog> createState() => _RenameNoteDialogState(); } + class _RenameNoteDialogState extends State<_RenameNoteDialog> { final _formKey = GlobalKey(); final TextEditingController _controller = TextEditingController(); diff --git a/lib/components/home/syncing_button.dart b/lib/components/home/syncing_button.dart index e0fc88dba..d92b70e47 100644 --- a/lib/components/home/syncing_button.dart +++ b/lib/components/home/syncing_button.dart @@ -1,4 +1,3 @@ - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:saber/components/theming/adaptive_icon.dart'; @@ -13,7 +12,6 @@ class SyncingButton extends StatefulWidget { } class _SyncingButtonState extends State { - @override void initState() { FileSyncer.filesDone.addListener(listener); @@ -45,15 +43,17 @@ class _SyncingButtonState extends State { bool loggedIn = Prefs.username.loaded && Prefs.username.value.isNotEmpty; return IconButton( - onPressed: loggedIn ? () { - FileSyncer.filesDone.value = null; // reset progress indicator - FileSyncer.startSync(); - } : null, + onPressed: loggedIn + ? () { + FileSyncer.filesDone.value = null; // reset progress indicator + FileSyncer.startSync(); + } + : null, icon: Stack( alignment: Alignment.center, children: [ AnimatedOpacity( - opacity: (loggedIn && (percentage ?? 0) < 1) ? 1 : 0, + opacity: (loggedIn && (percentage ?? 0) < 1) ? 1 : 0, duration: const Duration(milliseconds: 200), child: _AnimatedCircularProgressIndicator( duration: const Duration(milliseconds: 200), @@ -86,18 +86,19 @@ class _AnimatedCircularProgressIndicator extends ImplicitlyAnimatedWidget { final double? percentage; @override - ImplicitlyAnimatedWidgetState createState() => _AnimatedCircularProgressIndicatorState(); + ImplicitlyAnimatedWidgetState createState() => + _AnimatedCircularProgressIndicatorState(); } -class _AnimatedCircularProgressIndicatorState extends AnimatedWidgetBaseState<_AnimatedCircularProgressIndicator> { + +class _AnimatedCircularProgressIndicatorState + extends AnimatedWidgetBaseState<_AnimatedCircularProgressIndicator> { Tween? _valueTween; @override void forEachTween(TweenVisitor visitor) { - _valueTween = visitor( - _valueTween, - widget.percentage ?? 0.0, - (dynamic value) => Tween(begin: (value ?? 0.0) as double) - ) as Tween?; + _valueTween = visitor(_valueTween, widget.percentage ?? 0.0, + (dynamic value) => Tween(begin: (value ?? 0.0) as double)) + as Tween?; } @override diff --git a/lib/components/home/uploading_indicator.dart b/lib/components/home/uploading_indicator.dart index 9b10bdc25..260ff6c14 100644 --- a/lib/components/home/uploading_indicator.dart +++ b/lib/components/home/uploading_indicator.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:saber/data/nextcloud/file_syncer.dart'; import 'package:saber/data/prefs.dart'; @@ -32,8 +31,10 @@ class _UploadingIndicatorState extends State { }); } - bool _isInUploadQueue() => Prefs.fileSyncUploadQueue.value.contains(widget.filePath) - || Prefs.fileSyncUploadQueue.value.contains(widget.filePath + Editor.extension); + bool _isInUploadQueue() => + Prefs.fileSyncUploadQueue.value.contains(widget.filePath) || + Prefs.fileSyncUploadQueue.value + .contains(widget.filePath + Editor.extension); late bool isInUploadQueue = _isInUploadQueue(); @override diff --git a/lib/components/home/welcome.dart b/lib/components/home/welcome.dart index 818ea1bb0..4b1e690ca 100644 --- a/lib/components/home/welcome.dart +++ b/lib/components/home/welcome.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:saber/i18n/strings.g.dart'; diff --git a/lib/components/misc/faq.dart b/lib/components/misc/faq.dart index c9fe3c6ca..8371da980 100644 --- a/lib/components/misc/faq.dart +++ b/lib/components/misc/faq.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; class SliverFaq extends StatefulWidget { diff --git a/lib/components/navbar/horizontal_navbar.dart b/lib/components/navbar/horizontal_navbar.dart index 586238845..280fb89f7 100644 --- a/lib/components/navbar/horizontal_navbar.dart +++ b/lib/components/navbar/horizontal_navbar.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; class HorizontalNavbar extends StatelessWidget { diff --git a/lib/components/navbar/responsive_navbar.dart b/lib/components/navbar/responsive_navbar.dart index 8c40e00cd..9b88e65a2 100644 --- a/lib/components/navbar/responsive_navbar.dart +++ b/lib/components/navbar/responsive_navbar.dart @@ -28,9 +28,8 @@ class ResponsiveNavbar extends StatefulWidget { await null; final brightness = theme.brightness; - final otherBrightness = brightness == Brightness.dark - ? Brightness.light - : Brightness.dark; + final otherBrightness = + brightness == Brightness.dark ? Brightness.light : Brightness.dark; final overlayStyle = brightness == Brightness.dark ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light; @@ -41,6 +40,7 @@ class ResponsiveNavbar extends StatefulWidget { )); } } + class _ResponsiveNavbarState extends State { @override void initState() { @@ -57,7 +57,8 @@ class _ResponsiveNavbarState extends State { if (index == widget.selectedIndex) return; // if on whiteboard, check if saved - final whiteboardPath = pathToFunction(RoutePaths.home)({'subpage': HomePage.whiteboardSubpage}); + final whiteboardPath = pathToFunction(RoutePaths.home)( + {'subpage': HomePage.whiteboardSubpage}); if (HomeRoutes.getRoute(widget.selectedIndex) == whiteboardPath) { final savingState = Whiteboard.savingState; switch (savingState) { diff --git a/lib/components/navbar/vertical_navbar.dart b/lib/components/navbar/vertical_navbar.dart index f8fba09b9..ff5cc7cf7 100644 --- a/lib/components/navbar/vertical_navbar.dart +++ b/lib/components/navbar/vertical_navbar.dart @@ -1,4 +1,3 @@ - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:saber/components/files/file_tree.dart'; @@ -19,6 +18,7 @@ class VerticalNavbar extends StatefulWidget { @override State createState() => _VerticalNavbarState(); } + class _VerticalNavbarState extends State { bool expanded = false; @@ -38,34 +38,32 @@ class _VerticalNavbarState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: kToolbarHeight), - Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), child: TextButton( - onPressed: () {setState(() { - expanded = !expanded; - });}, + onPressed: () { + setState(() { + expanded = !expanded; + }); + }, child: AdaptiveIcon( icon: expanded ? Icons.chevron_left : Icons.chevron_right, - cupertinoIcon: expanded ? CupertinoIcons.chevron_left : CupertinoIcons.chevron_right, + cupertinoIcon: expanded + ? CupertinoIcons.chevron_left + : CupertinoIcons.chevron_right, ), ), ), - IntrinsicHeight( child: NavigationRail( destinations: widget.destinations, selectedIndex: widget.selectedIndex, - backgroundColor: backgroundColor, - extended: expanded, minExtendedWidth: 300, - onDestinationSelected: widget.onDestinationSelected, ), ), - if (expanded) const Expanded(child: FileTree()), ], ), diff --git a/lib/components/nextcloud/login_group.dart b/lib/components/nextcloud/login_group.dart index 0134c3a04..d7c636bf2 100644 --- a/lib/components/nextcloud/login_group.dart +++ b/lib/components/nextcloud/login_group.dart @@ -96,7 +96,8 @@ class _LoginInputGroupState extends State { if (!valid) return; if (_usingCustomServer) { - _customServerController.text = LoginInputGroup.prefixUrlWithHttps(_customServerController.text); + _customServerController.text = + LoginInputGroup.prefixUrlWithHttps(_customServerController.text); } try { @@ -136,8 +137,10 @@ class _LoginInputGroupState extends State { final username = Prefs.username.value; if (url.isNotEmpty) { - if (_customServerController.text.isEmpty) _customServerController.text = url; - if (url != NextcloudClientExtension.defaultNextcloudUri.toString()) _toggleCustomServer(true); + if (_customServerController.text.isEmpty) + _customServerController.text = url; + if (url != NextcloudClientExtension.defaultNextcloudUri.toString()) + _toggleCustomServer(true); } if (_usernameController.text.isEmpty) { _usernameController.text = username; @@ -181,41 +184,41 @@ class _LoginInputGroupState extends State { ), ), const SizedBox(height: 8), - Collapsible( - collapsed: !_usingCustomServer, - axis: CollapsibleAxis.vertical, - alignment: Alignment.topCenter, - fade: true, - maintainState: true, - child: Column(children: [ - const SizedBox(height: 4), - AdaptiveTextField( - controller: _customServerController, - placeholder: t.login.form.customServerUrl, - keyboardType: TextInputType.url, - textInputAction: TextInputAction.next, - focusOrder: const NumericFocusOrder(1), - autofillHints: const [AutofillHints.url], - prefixIcon: const AdaptiveIcon( - icon: Icons.link, - cupertinoIcon: CupertinoIcons.link, + collapsed: !_usingCustomServer, + axis: CollapsibleAxis.vertical, + alignment: Alignment.topCenter, + fade: true, + maintainState: true, + child: Column(children: [ + const SizedBox(height: 4), + AdaptiveTextField( + controller: _customServerController, + placeholder: t.login.form.customServerUrl, + keyboardType: TextInputType.url, + textInputAction: TextInputAction.next, + focusOrder: const NumericFocusOrder(1), + autofillHints: const [AutofillHints.url], + prefixIcon: const AdaptiveIcon( + icon: Icons.link, + cupertinoIcon: CupertinoIcons.link, + ), + validator: (String? value) { + if (!_usingCustomServer) return null; + return LoginInputGroup.validateCustomServer(value); + }, ), - validator: (String? value) { - if (!_usingCustomServer) return null; - return LoginInputGroup.validateCustomServer(value); - }, - ), - const SizedBox(height: 8), - ]) - ), - + const SizedBox(height: 8), + ])), AdaptiveTextField( controller: _usernameController, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, focusOrder: const NumericFocusOrder(2), - autofillHints: const [AutofillHints.username, AutofillHints.email], + autofillHints: const [ + AutofillHints.username, + AutofillHints.email + ], placeholder: t.login.form.username, prefixIcon: const AdaptiveIcon( icon: Icons.person, @@ -246,7 +249,6 @@ class _LoginInputGroupState extends State { validator: LoginInputGroup.validateEncPassword, ), const SizedBox(height: 16), - if (_errorMessage != null) ...[ Text( _errorMessage!, @@ -254,15 +256,15 @@ class _LoginInputGroupState extends State { ), const SizedBox(height: 8), ], - Text.rich( t.login.signup( linkToSignup: (text) => TextSpan( text: text, style: TextStyle(color: colorScheme.primary), - recognizer: TapGestureRecognizer()..onTap = () { - launchUrl(NcLoginPage.signupUrl); - }, + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl(NcLoginPage.signupUrl); + }, ), ), ), @@ -272,19 +274,21 @@ class _LoginInputGroupState extends State { linkToPrivacyPolicy: (text) => TextSpan( text: text, style: TextStyle(color: colorScheme.primary), - recognizer: TapGestureRecognizer()..onTap = () { - launchUrl(AppInfo.privacyPolicyUrl); - }, + recognizer: TapGestureRecognizer() + ..onTap = () { + launchUrl(AppInfo.privacyPolicyUrl); + }, ), ), ), - const SizedBox(height: 16), FocusTraversalOrder( order: const NumericFocusOrder(5), child: AdaptiveButton( onPressed: _isLoading ? null : _login, - child: _isLoading ? const SpinningLoadingIcon() : Text(t.login.form.login), + child: _isLoading + ? const SpinningLoadingIcon() + : Text(t.login.form.login), ), ), ], @@ -306,21 +310,24 @@ class LoginDetailsStruct { required this.loginName, required this.ncPassword, required this.encPassword, - }): uri = url != null - ? Uri.parse(url) - : NextcloudClientExtension.defaultNextcloudUri; + }) : uri = url != null + ? Uri.parse(url) + : NextcloudClientExtension.defaultNextcloudUri; } abstract class LoginFailure implements Exception { final String message = 'Login failed'; } + class NcLoginFailure implements LoginFailure { @override final String message = t.login.feedbacks.ncLoginFailed; } + class NcUnsupportedFailure implements LoginFailure { /// The Nextcloud version of the server final int? currentVersion; + /// The Nextcloud version supported with the [nextcloud] package final int supportedVersion; @@ -335,6 +342,7 @@ class NcUnsupportedFailure implements LoginFailure { s: supportedVersion, ); } + class EncLoginFailure implements LoginFailure { @override final String message = t.login.feedbacks.encLoginFailed; diff --git a/lib/components/nextcloud/spinning_loading_icon.dart b/lib/components/nextcloud/spinning_loading_icon.dart index 1f126690f..a18e0e92d 100644 --- a/lib/components/nextcloud/spinning_loading_icon.dart +++ b/lib/components/nextcloud/spinning_loading_icon.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; class SpinningLoadingIcon extends StatefulWidget { @@ -8,7 +7,8 @@ class SpinningLoadingIcon extends StatefulWidget { State createState() => _SpinningLoadingIconState(); } -class _SpinningLoadingIconState extends State with TickerProviderStateMixin { +class _SpinningLoadingIconState extends State + with TickerProviderStateMixin { late AnimationController _controller; @override @@ -17,8 +17,8 @@ class _SpinningLoadingIconState extends State with TickerPr vsync: this, duration: const Duration(milliseconds: 1000), )..addListener(() { - setState(() {}); - }); + setState(() {}); + }); _controller.repeat(); super.initState(); } @@ -31,6 +31,7 @@ class _SpinningLoadingIconState extends State with TickerPr @override Widget build(BuildContext context) { - return RotationTransition(turns: _controller, child: const Icon(Icons.refresh)); + return RotationTransition( + turns: _controller, child: const Icon(Icons.refresh)); } } diff --git a/lib/components/settings/app_info.dart b/lib/components/settings/app_info.dart index 81da3068e..f5238ae15 100644 --- a/lib/components/settings/app_info.dart +++ b/lib/components/settings/app_info.dart @@ -6,25 +6,25 @@ import 'package:saber/data/version.dart'; import 'package:saber/i18n/strings.g.dart'; import 'package:url_launcher/url_launcher.dart'; - class AppInfo extends StatelessWidget { const AppInfo({super.key}); - static final Uri sponsorUrl = Uri.parse('https://github.com/sponsors/adil192'); - static final Uri privacyPolicyUrl = Uri.parse('https://github.com/saber-notes/saber/blob/main/privacy_policy.md'); - static final Uri licenseUrl = Uri.parse('https://github.com/saber-notes/saber/blob/main/LICENSE.md'); - static final Uri releasesUrl = Uri.parse('https://github.com/saber-notes/saber/releases'); + static final Uri sponsorUrl = + Uri.parse('https://github.com/sponsors/adil192'); + static final Uri privacyPolicyUrl = Uri.parse( + 'https://github.com/saber-notes/saber/blob/main/privacy_policy.md'); + static final Uri licenseUrl = + Uri.parse('https://github.com/saber-notes/saber/blob/main/LICENSE.md'); + static final Uri releasesUrl = + Uri.parse('https://github.com/saber-notes/saber/releases'); static String get info => [ - 'v$buildName', - if (FlavorConfig.flavor.isNotEmpty) - FlavorConfig.flavor, - if (FlavorConfig.dirty) - t.appInfo.dirty, - if (kDebugMode) - t.appInfo.debug, - '($buildNumber)', - ].join(' '); + 'v$buildName', + if (FlavorConfig.flavor.isNotEmpty) FlavorConfig.flavor, + if (FlavorConfig.dirty) t.appInfo.dirty, + if (kDebugMode) t.appInfo.debug, + '($buildNumber)', + ].join(' '); @override Widget build(BuildContext context) { @@ -38,37 +38,34 @@ class AppInfo extends StatelessWidget { } void _showAboutDialog(BuildContext context) => showAboutDialog( - context: context, - applicationVersion: info, - applicationIcon: Image.asset( - 'assets/icon/resized/icon-128x128.png', - width: 50, - height: 50, - ), - applicationLegalese: t.appInfo.licenseNotice(buildYear: buildYear), - children: [ - const SizedBox(height: 10), - TextButton( - onPressed: () => launchUrl(sponsorUrl), - child: SizedBox( - width: double.infinity, - child: Text(t.appInfo.sponsorButton) - ), - ), - TextButton( - onPressed: () => launchUrl(licenseUrl), - child: SizedBox( - width: double.infinity, - child: Text(t.appInfo.licenseButton), + context: context, + applicationVersion: info, + applicationIcon: Image.asset( + 'assets/icon/resized/icon-128x128.png', + width: 50, + height: 50, ), - ), - TextButton( - onPressed: () => launchUrl(privacyPolicyUrl), - child: SizedBox( - width: double.infinity, - child: Text(t.appInfo.privacyPolicyButton) - ), - ), - ], - ); + applicationLegalese: t.appInfo.licenseNotice(buildYear: buildYear), + children: [ + const SizedBox(height: 10), + TextButton( + onPressed: () => launchUrl(sponsorUrl), + child: SizedBox( + width: double.infinity, child: Text(t.appInfo.sponsorButton)), + ), + TextButton( + onPressed: () => launchUrl(licenseUrl), + child: SizedBox( + width: double.infinity, + child: Text(t.appInfo.licenseButton), + ), + ), + TextButton( + onPressed: () => launchUrl(privacyPolicyUrl), + child: SizedBox( + width: double.infinity, + child: Text(t.appInfo.privacyPolicyButton)), + ), + ], + ); } diff --git a/lib/components/settings/nextcloud_profile.dart b/lib/components/settings/nextcloud_profile.dart index 9d27eea7b..586b1f46c 100644 --- a/lib/components/settings/nextcloud_profile.dart +++ b/lib/components/settings/nextcloud_profile.dart @@ -72,53 +72,58 @@ class _NextcloudProfileState extends State { ), title: Text(heading), subtitle: Text(subheading), - trailing: loggedIn ? Row( - mainAxisSize: MainAxisSize.min, - children: [ - FutureBuilder( - future: NextcloudProfile.getStorageQuota(), - initialData: Prefs.lastStorageQuota.value, - builder: (BuildContext context, AsyncSnapshot snapshot) { - final Quota? quota = snapshot.data; - final double? relativePercent; - if (quota != null) { - relativePercent = quota.relative / 100; - } else { - relativePercent = null; - } - - return Stack( - alignment: Alignment.center, - children: [ - CircularProgressIndicator( - value: relativePercent, - color: colorScheme.primary.withOpacity(0.5), - backgroundColor: colorScheme.primary.withOpacity(0.1), - strokeWidth: 8, - semanticsLabel: 'Storage usage', - semanticsValue: snapshot.data != null ? '${snapshot.data}%' : null, + trailing: loggedIn + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + FutureBuilder( + future: NextcloudProfile.getStorageQuota(), + initialData: Prefs.lastStorageQuota.value, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final Quota? quota = snapshot.data; + final double? relativePercent; + if (quota != null) { + relativePercent = quota.relative / 100; + } else { + relativePercent = null; + } + + return Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: relativePercent, + color: colorScheme.primary.withOpacity(0.5), + backgroundColor: colorScheme.primary.withOpacity(0.1), + strokeWidth: 8, + semanticsLabel: 'Storage usage', + semanticsValue: snapshot.data != null + ? '${snapshot.data}%' + : null, + ), + Text(readableQuota(quota)), + ], + ); + }, + ), + IconButton( + icon: const AdaptiveIcon( + icon: Icons.cloud_upload, + cupertinoIcon: CupertinoIcons.cloud_upload, ), - Text(readableQuota(quota)), - ], - ); - }, - ), - IconButton( - icon: const AdaptiveIcon( - icon: Icons.cloud_upload, - cupertinoIcon: CupertinoIcons.cloud_upload, - ), - tooltip: t.settings.resyncEverything, - onPressed: () async { - final allFiles = await FileManager.getAllFiles(); - Prefs.fileSyncResyncEverythingDate.value = DateTime.now(); - for (final file in allFiles) { - FileSyncer.addToUploadQueue(file); - } - }, - ), - ], - ) : null, + tooltip: t.settings.resyncEverything, + onPressed: () async { + final allFiles = await FileManager.getAllFiles(); + Prefs.fileSyncResyncEverythingDate.value = DateTime.now(); + for (final file in allFiles) { + FileSyncer.addToUploadQueue(file); + } + }, + ), + ], + ) + : null, ); } @@ -127,24 +132,29 @@ class _NextcloudProfileState extends State { final total = readableBytes(quota?.total); return '$used / $total'; } + static String readableBytes(num? bytes) { if (bytes == null) { return '... B'; } else if (bytes < 1024) { return '$bytes B'; - } else if (bytes < 1024 * 2) { // e.g. 1.5 KB + } else if (bytes < 1024 * 2) { + // e.g. 1.5 KB return '${(bytes / 1024).toStringAsFixed(1)} KB'; } else if (bytes < 1024 * 1024) { return '${(bytes / 1024).round()} KB'; - } else if (bytes < 1024 * 1024 * 2) { // e.g. 1.5 MB + } else if (bytes < 1024 * 1024 * 2) { + // e.g. 1.5 MB return '${(bytes / 1024 / 1024).toStringAsFixed(1)} MB'; } else if (bytes < 1024 * 1024 * 1024) { return '${(bytes / 1024 / 1024).round()} MB'; - } else if (bytes < 1024 * 1024 * 1024 * 2) { // e.g. 1.5 GB + } else if (bytes < 1024 * 1024 * 1024 * 2) { + // e.g. 1.5 GB return '${(bytes / 1024 / 1024 / 1024).toStringAsFixed(1)} GB'; } else if (bytes < 1024 * 1024 * 1024 * 1024) { return '${(bytes / 1024 / 1024 / 1024).round()} GB'; - } else if (bytes < 1024 * 1024 * 1024 * 1024 * 2) { // e.g. 1.5 TB + } else if (bytes < 1024 * 1024 * 1024 * 1024 * 2) { + // e.g. 1.5 TB return '${(bytes / 1024 / 1024 / 1024 / 1024).toStringAsFixed(1)} TB'; } else { return '${(bytes / 1024 / 1024 / 1024 / 1024).round()} TB'; diff --git a/lib/components/settings/settings_color.dart b/lib/components/settings/settings_color.dart index 52b880e2b..8f510abd0 100644 --- a/lib/components/settings/settings_color.dart +++ b/lib/components/settings/settings_color.dart @@ -1,4 +1,3 @@ - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart'; @@ -14,10 +13,10 @@ class SettingsColor extends StatefulWidget { this.subtitle, this.icon, this.iconBuilder, - required this.pref, this.afterChange, - }): assert(icon == null || iconBuilder == null, 'Cannot set both icon and iconBuilder'); + }) : assert(icon == null || iconBuilder == null, + 'Cannot set both icon and iconBuilder'); final String title; final String? subtitle; @@ -47,12 +46,13 @@ class _SettingsSwitchState extends State { defaultColor = color; } widget.afterChange?.call(color); - setState(() { }); + setState(() {}); } AdaptiveAlertDialog get colorPickerDialog { final platform = Theme.of(context).platform; - final cupertino = platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; + final cupertino = + platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; return AdaptiveAlertDialog( title: Text(t.settings.accentColorPicker.pickAColor), content: SingleChildScrollView( @@ -97,7 +97,8 @@ class _SettingsSwitchState extends State { : null, ), ), - subtitle: Text(widget.subtitle ?? '', style: const TextStyle(fontSize: 13)), + subtitle: + Text(widget.subtitle ?? '', style: const TextStyle(fontSize: 13)), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/components/settings/settings_dropdown.dart b/lib/components/settings/settings_dropdown.dart index 5469b7bbd..307b61f37 100644 --- a/lib/components/settings/settings_dropdown.dart +++ b/lib/components/settings/settings_dropdown.dart @@ -1,4 +1,3 @@ - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:saber/components/theming/adaptive_toggle_buttons.dart'; @@ -12,11 +11,11 @@ class SettingsDropdown extends StatefulWidget { this.subtitle, this.icon, this.iconBuilder, - required this.pref, required this.options, this.afterChange, - }): assert(icon == null || iconBuilder == null, 'Cannot set both icon and iconBuilder'); + }) : assert(icon == null || iconBuilder == null, + 'Cannot set both icon and iconBuilder'); final String title; final String? subtitle; @@ -39,7 +38,8 @@ class SettingsDropdown extends StatefulWidget { } class _SettingsDropdownState extends State> { - late FocusNode dropdownFocusNode = FocusNode(debugLabel: 'dropdownFocusNode(${widget.pref.key})'); + late FocusNode dropdownFocusNode = + FocusNode(debugLabel: 'dropdownFocusNode(${widget.pref.key})'); @override void initState() { @@ -49,13 +49,15 @@ class _SettingsDropdownState extends State> { void onChanged() { widget.afterChange?.call(widget.pref.value); - setState(() { }); + setState(() {}); } @override Widget build(BuildContext context) { if (widget.indexOf(widget.pref.value) == null) { - if (kDebugMode) throw Exception('SettingsDropdown (${widget.pref.key}): Value ${widget.pref.value} is not in the list of values, set it to ${widget.options.first.value}?'); + if (kDebugMode) + throw Exception( + 'SettingsDropdown (${widget.pref.key}): Value ${widget.pref.value} is not in the list of values, set it to ${widget.options.first.value}?'); widget.pref.value = widget.options.first.value; } @@ -109,7 +111,8 @@ class _SettingsDropdownState extends State> { : null, ), ), - subtitle: Text(widget.subtitle ?? '', style: const TextStyle(fontSize: 13)), + subtitle: + Text(widget.subtitle ?? '', style: const TextStyle(fontSize: 13)), trailing: dropdown, ), ); diff --git a/lib/components/settings/settings_selection.dart b/lib/components/settings/settings_selection.dart index f064362c2..96f529fcd 100644 --- a/lib/components/settings/settings_selection.dart +++ b/lib/components/settings/settings_selection.dart @@ -12,13 +12,13 @@ class SettingsSelection extends StatefulWidget { this.subtitle, this.icon, this.iconBuilder, - required this.pref, required this.options, this.afterChange, this.optionsWidth = 72, this.optionsHeight = 40, - }): assert(icon == null || iconBuilder == null, 'Cannot set both icon and iconBuilder'); + }) : assert(icon == null || iconBuilder == null, + 'Cannot set both icon and iconBuilder'); final String title; final String? subtitle; @@ -35,8 +35,10 @@ class SettingsSelection extends StatefulWidget { State createState() => _SettingsSelectionState(); } -class _SettingsSelectionState extends State> { - late FocusNode dropdownFocusNode = FocusNode(debugLabel: 'dropdownFocusNode(${widget.pref.key})'); +class _SettingsSelectionState + extends State> { + late FocusNode dropdownFocusNode = + FocusNode(debugLabel: 'dropdownFocusNode(${widget.pref.key})'); @override void initState() { @@ -46,13 +48,16 @@ class _SettingsSelectionState extends State> void onChanged() { widget.afterChange?.call(widget.pref.value); - setState(() { }); + setState(() {}); } @override Widget build(BuildContext context) { - if (!widget.options.any((ToggleButtonsOption option) => widget.pref.value == option.value)) { - if (kDebugMode) throw Exception('SettingsSelection (${widget.pref.key}): Value ${widget.pref.value} is not in the list of values, set it to ${widget.options.first.value}?'); + if (!widget.options.any( + (ToggleButtonsOption option) => widget.pref.value == option.value)) { + if (kDebugMode) + throw Exception( + 'SettingsSelection (${widget.pref.key}): Value ${widget.pref.value} is not in the list of values, set it to ${widget.options.first.value}?'); widget.pref.value = widget.options.first.value; } @@ -67,9 +72,13 @@ class _SettingsSelectionState extends State> onTap: () { if (useDropdownInstead) { dropdownFocusNode.requestFocus(); - } else { // cycle through options - final int i = widget.options.indexWhere((ToggleButtonsOption option) => option.value == widget.pref.value); - widget.pref.value = widget.options[(i + 1) % widget.options.length].value; + } else { + // cycle through options + final int i = widget.options.indexWhere( + (ToggleButtonsOption option) => + option.value == widget.pref.value); + widget.pref.value = + widget.options[(i + 1) % widget.options.length].value; } }, onLongPress: () { @@ -93,37 +102,40 @@ class _SettingsSelectionState extends State> : null, ), ), - subtitle: Text(widget.subtitle ?? '', style: const TextStyle(fontSize: 13)), - trailing: !useDropdownInstead ? AdaptiveToggleButtons( - value: widget.pref.value, - options: widget.options, - onChange: (T? value) { - // setState is automatically called when the pref changes - if (value != null) { - widget.pref.value = value; - } - }, - optionsWidth: widget.optionsWidth, - optionsHeight: widget.optionsHeight, - ) : DropdownButton( - value: widget.pref.value, - onChanged: (T? value) { - if (value == null) return; - widget.pref.value = value; - }, - items: widget.options.map((ToggleButtonsOption option) { - return DropdownMenuItem( - value: option.value, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: option.widget, + subtitle: + Text(widget.subtitle ?? '', style: const TextStyle(fontSize: 13)), + trailing: !useDropdownInstead + ? AdaptiveToggleButtons( + value: widget.pref.value, + options: widget.options, + onChange: (T? value) { + // setState is automatically called when the pref changes + if (value != null) { + widget.pref.value = value; + } + }, + optionsWidth: widget.optionsWidth, + optionsHeight: widget.optionsHeight, + ) + : DropdownButton( + value: widget.pref.value, + onChanged: (T? value) { + if (value == null) return; + widget.pref.value = value; + }, + items: widget.options.map((ToggleButtonsOption option) { + return DropdownMenuItem( + value: option.value, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: option.widget, + ), + ); + }).toList(), + focusNode: dropdownFocusNode, + borderRadius: BorderRadius.circular(32), + underline: const SizedBox.shrink(), ), - ); - }).toList(), - focusNode: dropdownFocusNode, - borderRadius: BorderRadius.circular(32), - underline: const SizedBox.shrink(), - ), ); } diff --git a/lib/components/settings/settings_switch.dart b/lib/components/settings/settings_switch.dart index 80104261c..b53bcc895 100644 --- a/lib/components/settings/settings_switch.dart +++ b/lib/components/settings/settings_switch.dart @@ -9,10 +9,10 @@ class SettingsSwitch extends StatefulWidget { this.subtitle, this.icon, this.iconBuilder, - required this.pref, this.afterChange, - }): assert(icon == null || iconBuilder == null, 'Cannot set both icon and iconBuilder'); + }) : assert(icon == null || iconBuilder == null, + 'Cannot set both icon and iconBuilder'); final String title; final String? subtitle; @@ -35,7 +35,7 @@ class _SettingsSwitchState extends State { void onChanged() { widget.afterChange?.call(widget.pref.value); - setState(() { }); + setState(() {}); } @override @@ -67,7 +67,8 @@ class _SettingsSwitchState extends State { : null, ), ), - subtitle: Text(widget.subtitle ?? '', style: const TextStyle(fontSize: 13)), + subtitle: + Text(widget.subtitle ?? '', style: const TextStyle(fontSize: 13)), value: widget.pref.value, onChanged: (bool value) { widget.pref.value = value; diff --git a/lib/components/settings/update_manager.dart b/lib/components/settings/update_manager.dart index c28c64a7b..72913e49b 100644 --- a/lib/components/settings/update_manager.dart +++ b/lib/components/settings/update_manager.dart @@ -22,21 +22,28 @@ import 'package:url_launcher/url_launcher.dart'; abstract class UpdateManager { static final log = Logger('UpdateManager'); - static final Uri versionUrl = Uri.parse('https://raw.githubusercontent.com/saber-notes/saber/main/lib/data/version.dart'); - static final Uri apiUrl = Uri.parse('https://api.github.com/repos/saber-notes/saber/releases/latest'); + static final Uri versionUrl = Uri.parse( + 'https://raw.githubusercontent.com/saber-notes/saber/main/lib/data/version.dart'); + static final Uri apiUrl = Uri.parse( + 'https://api.github.com/repos/saber-notes/saber/releases/latest'); + /// The availability of an update. - static final ValueNotifier status = ValueNotifier(UpdateStatus.upToDate); + static final ValueNotifier status = + ValueNotifier(UpdateStatus.upToDate); static int? newestVersion; static bool _hasShownUpdateDialog = false; - static Future showUpdateDialog(BuildContext context, {bool userTriggered = false}) async { + static Future showUpdateDialog(BuildContext context, + {bool userTriggered = false}) async { if (!userTriggered) { - if (status.value == UpdateStatus.upToDate) { // check for updates if not already done + if (status.value == UpdateStatus.upToDate) { + // check for updates if not already done await Prefs.shouldCheckForUpdates.waitUntilLoaded(); if (!Prefs.shouldCheckForUpdates.value) return; status.value = await _checkForUpdate(); } - if (status.value != UpdateStatus.updateRecommended) return; // no update available + if (status.value != UpdateStatus.updateRecommended) + return; // no update available if (_hasShownUpdateDialog) return; // already shown } @@ -73,12 +80,10 @@ abstract class UpdateManager { mainAxisSize: MainAxisSize.min, children: [ Text(t.update.updateAvailableDescription), - if (showTranslatedChangelog) Text(translatedChangelog!) else if (englishChangelog != null) Text(englishChangelog), - if (translatedChangelog != null && englishChangelog != null) TextButton( onPressed: () => setState(() { @@ -90,25 +95,27 @@ abstract class UpdateManager { : localeNames['en']!, ), ), - ], ), actions: [ CupertinoDialogAction( onPressed: () => Navigator.pop(context), - child: Text(MaterialLocalizations.of(context).modalBarrierDismissLabel), + child: Text( + MaterialLocalizations.of(context).modalBarrierDismissLabel), ), CupertinoDialogAction( - onPressed: directDownloadStarted ? null : () { - if (directDownloadLink != null) { - _directlyDownloadUpdate(directDownloadLink) - .then((_) => Navigator.pop(context)); - setState(() => directDownloadStarted = true); - } else { - launchUrl(AppInfo.releasesUrl); - } - }, - child: (){ + onPressed: directDownloadStarted + ? null + : () { + if (directDownloadLink != null) { + _directlyDownloadUpdate(directDownloadLink) + .then((_) => Navigator.pop(context)); + setState(() => directDownloadStarted = true); + } else { + launchUrl(AppInfo.releasesUrl); + } + }, + child: () { if (directDownloadStarted) { return const SpinningLoadingIcon(); } else { @@ -144,7 +151,8 @@ abstract class UpdateManager { // extract the number from the latest version.dart final RegExp numberRegex = RegExp(r'(\d+)'); - final RegExpMatch? newestVersionMatch = numberRegex.firstMatch(latestVersionFile); + final RegExpMatch? newestVersionMatch = + numberRegex.firstMatch(latestVersionFile); if (newestVersionMatch == null) return null; final int newestVersion = int.tryParse(newestVersionMatch[0] ?? '0') ?? 0; @@ -161,18 +169,22 @@ abstract class UpdateManager { } catch (e) { throw SocketException('Failed to download version.dart, ${e.toString()}'); } - if (response.statusCode >= 400) throw SocketException('Failed to download version.dart, HTTP status code ${response.statusCode}'); + if (response.statusCode >= 400) + throw SocketException( + 'Failed to download version.dart, HTTP status code ${response.statusCode}'); return response.body; } @visibleForTesting - static UpdateStatus getUpdateStatus(int currentVersionNumber, int newestVersionNumber) { + static UpdateStatus getUpdateStatus( + int currentVersionNumber, int newestVersionNumber) { final currentVersion = parseVersionNumber(currentVersionNumber); final newestVersion = parseVersionNumber(newestVersionNumber); // Check if we're up to date - if ((newestVersionNumber - newestVersion.silent) <= (currentVersionNumber - currentVersion.silent)) { + if ((newestVersionNumber - newestVersion.silent) <= + (currentVersionNumber - currentVersion.silent)) { return UpdateStatus.upToDate; } // Now we know that there is a new update available @@ -180,7 +192,8 @@ abstract class UpdateManager { // Check if the update is low priority if (!Prefs.shouldAlwaysAlertForUpdates.value) { // Only prompt user every second patch - if (currentVersion.major == newestVersion.major && currentVersion.minor == newestVersion.minor) { + if (currentVersion.major == newestVersion.major && + currentVersion.minor == newestVersion.minor) { if ((newestVersion.patch - currentVersion.patch) < 2) { return UpdateStatus.updateOptional; } @@ -197,7 +210,8 @@ abstract class UpdateManager { } @visibleForTesting - static ({int major, int minor, int patch, int silent}) parseVersionNumber(int versionNumber) { + static ({int major, int minor, int patch, int silent}) parseVersionNumber( + int versionNumber) { // rightmost digit is silent update final silent = versionNumber % 10; // next 2 digits are patch version @@ -235,15 +249,16 @@ abstract class UpdateManager { } catch (e) { throw const SocketException('Failed to fetch latest release'); } - if (response.statusCode >= 400) throw SocketException('Failed to fetch latest release, HTTP status code ${response.statusCode}'); + if (response.statusCode >= 400) + throw SocketException( + 'Failed to fetch latest release, HTTP status code ${response.statusCode}'); apiResponse = response.body; } final Map json = jsonDecode(apiResponse); final RegExp platformFileRegex = _platformFileRegex[platform]!; - return (json['assets'] as List) - .firstWhereOrNull((asset) => platformFileRegex.hasMatch(asset['name'])) - ?['browser_download_url']; + return (json['assets'] as List).firstWhereOrNull((asset) => + platformFileRegex.hasMatch(asset['name']))?['browser_download_url']; } static final Map _platformFileRegex = { @@ -295,8 +310,10 @@ abstract class UpdateManager { enum UpdateStatus { /// The app is up to date, or we failed to check for an update. upToDate, + /// An update is available, but the user doesn't need to be notified updateOptional, + /// An update is available and the user should be notified updateRecommended, } diff --git a/lib/components/theming/adaptive_alert_dialog.dart b/lib/components/theming/adaptive_alert_dialog.dart index 9d9be57f2..3aa4ec7f5 100644 --- a/lib/components/theming/adaptive_alert_dialog.dart +++ b/lib/components/theming/adaptive_alert_dialog.dart @@ -1,4 +1,3 @@ - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -16,15 +15,16 @@ class AdaptiveAlertDialog extends StatelessWidget { List get _materialActions => actions .map((CupertinoDialogAction action) => TextButton( - onPressed: action.onPressed, - child: action.child, - )) + onPressed: action.onPressed, + child: action.child, + )) .toList(); @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); - bool cupertino = theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.macOS; + bool cupertino = theme.platform == TargetPlatform.iOS || + theme.platform == TargetPlatform.macOS; if (cupertino) { return CupertinoAlertDialog( diff --git a/lib/components/theming/adaptive_button.dart b/lib/components/theming/adaptive_button.dart index 571bed1ad..75c7fd5e7 100644 --- a/lib/components/theming/adaptive_button.dart +++ b/lib/components/theming/adaptive_button.dart @@ -1,4 +1,3 @@ - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -15,7 +14,8 @@ class AdaptiveButton extends StatelessWidget { @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); - bool cupertino = theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.macOS; + bool cupertino = theme.platform == TargetPlatform.iOS || + theme.platform == TargetPlatform.macOS; if (cupertino) { return CupertinoButton( onPressed: onPressed, diff --git a/lib/components/theming/adaptive_icon.dart b/lib/components/theming/adaptive_icon.dart index d9ae244fc..1eb60ae1a 100644 --- a/lib/components/theming/adaptive_icon.dart +++ b/lib/components/theming/adaptive_icon.dart @@ -15,7 +15,8 @@ class AdaptiveIcon extends StatelessWidget { @override Widget build(BuildContext context) { final platform = Theme.of(context).platform; - final cupertino = platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; + final cupertino = + platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; if (cupertino) { return Icon(cupertinoIcon ?? icon, size: size); diff --git a/lib/components/theming/adaptive_text_field.dart b/lib/components/theming/adaptive_text_field.dart index debe3ac98..8823cd94f 100644 --- a/lib/components/theming/adaptive_text_field.dart +++ b/lib/components/theming/adaptive_text_field.dart @@ -1,4 +1,3 @@ - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -37,7 +36,11 @@ class _AdaptiveTextFieldState extends State { return IconButton( icon: Icon(obscureText ? Icons.visibility_off : Icons.visibility), iconSize: 18, - onPressed: () { setState(() { obscureText = !obscureText; }); }, + onPressed: () { + setState(() { + obscureText = !obscureText; + }); + }, ); } @@ -51,7 +54,8 @@ class _AdaptiveTextFieldState extends State { Widget build(BuildContext context) { ThemeData theme = Theme.of(context); final colorScheme = theme.colorScheme; - bool cupertino = theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.macOS; + bool cupertino = theme.platform == TargetPlatform.iOS || + theme.platform == TargetPlatform.macOS; TextInputType? keyboardType = widget.keyboardType; if (widget.isPassword) { @@ -75,29 +79,35 @@ class _AdaptiveTextFieldState extends State { textInputAction: widget.textInputAction, obscureText: obscureText, decoration: BoxDecoration( - border: Border.all(color: colorScheme.onSurface.withOpacity(0.12)), + border: Border.all( + color: colorScheme.onSurface.withOpacity(0.12)), borderRadius: BorderRadius.circular(8), ), style: TextStyle(color: colorScheme.onSurface), placeholder: widget.placeholder, - prefix: widget.prefixIcon != null ? Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: widget.prefixIcon, - ) : null, + prefix: widget.prefixIcon != null + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: widget.prefixIcon, + ) + : null, validator: widget.validator, ), ), ), - if (suffixIcon != null) Align( - alignment: Alignment.topCenter, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: FocusTraversalOrder( - order: NumericFocusOrder(widget.focusOrder.order + 100), - child: suffixIcon!, + if (suffixIcon != null) + Align( + alignment: Alignment.topCenter, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: FocusTraversalOrder( + order: NumericFocusOrder(widget.focusOrder.order + 100), + child: suffixIcon!, + ), ), - ), - ) else const SizedBox(height: 40), + ) + else + const SizedBox(height: 40), ], ); } else { @@ -112,15 +122,20 @@ class _AdaptiveTextFieldState extends State { validator: widget.validator, decoration: InputDecoration( labelText: widget.placeholder, - labelStyle: TextStyle(color: colorScheme.onSurface.withOpacity(0.5)), + labelStyle: + TextStyle(color: colorScheme.onSurface.withOpacity(0.5)), prefixIcon: widget.prefixIcon, - suffixIcon: suffixIcon != null ? FocusTraversalOrder( - order: NumericFocusOrder(widget.focusOrder.order + 100), - child: suffixIcon!, - ) : null, - contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + suffixIcon: suffixIcon != null + ? FocusTraversalOrder( + order: NumericFocusOrder(widget.focusOrder.order + 100), + child: suffixIcon!, + ) + : null, + contentPadding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 12), border: OutlineInputBorder( - borderSide: BorderSide(color: colorScheme.onSurface.withOpacity(0.12)), + borderSide: + BorderSide(color: colorScheme.onSurface.withOpacity(0.12)), borderRadius: BorderRadius.circular(8), ), ), diff --git a/lib/components/theming/adaptive_toggle_buttons.dart b/lib/components/theming/adaptive_toggle_buttons.dart index 6aa126a8c..8eb6c0cb7 100644 --- a/lib/components/theming/adaptive_toggle_buttons.dart +++ b/lib/components/theming/adaptive_toggle_buttons.dart @@ -1,4 +1,3 @@ - import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -10,7 +9,8 @@ class AdaptiveToggleButtons extends StatelessWidget { required this.onChange, this.optionsWidth = 72, this.optionsHeight = 40, - }): assert(optionsWidth > 0), assert(optionsHeight > 0); + }) : assert(optionsWidth > 0), + assert(optionsHeight > 0); final T value; final List> options; @@ -21,7 +21,8 @@ class AdaptiveToggleButtons extends StatelessWidget { @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); - bool cupertino = theme.platform == TargetPlatform.iOS || theme.platform == TargetPlatform.macOS; + bool cupertino = theme.platform == TargetPlatform.iOS || + theme.platform == TargetPlatform.macOS; if (cupertino) { return _buildCupertino(context); @@ -48,9 +49,11 @@ class AdaptiveToggleButtons extends StatelessWidget { ], ); } + Widget _buildCupertino(BuildContext context) { return CupertinoSlidingSegmentedControl( - children: options.asMap().map((_, ToggleButtonsOption option) => MapEntry(option.value, option.widget)), + children: options.asMap().map((_, ToggleButtonsOption option) => + MapEntry(option.value, option.widget)), groupValue: value, onValueChanged: onChange, padding: const EdgeInsets.all(8), diff --git a/lib/components/theming/dynamic_material_app.dart b/lib/components/theming/dynamic_material_app.dart index 45891e9f1..f7931a720 100644 --- a/lib/components/theming/dynamic_material_app.dart +++ b/lib/components/theming/dynamic_material_app.dart @@ -35,14 +35,14 @@ class DynamicMaterialApp extends StatefulWidget { windowManager.setFullScreen(value); } else { SystemChrome.setEnabledSystemUIMode( - value ? SystemUiMode.immersive : SystemUiMode.edgeToEdge - ); + value ? SystemUiMode.immersive : SystemUiMode.edgeToEdge); } } static void addFullscreenListener(void Function() listener) { _isFullscreen.addListener(listener); } + static void removeFullscreenListener(void Function() listener) { _isFullscreen.removeListener(listener); } @@ -51,7 +51,8 @@ class DynamicMaterialApp extends StatefulWidget { State createState() => _DynamicMaterialAppState(); } -class _DynamicMaterialAppState extends State with WindowListener { +class _DynamicMaterialAppState extends State + with WindowListener { bool requiresCustomFont = false; @override @@ -69,19 +70,22 @@ class _DynamicMaterialAppState extends State with WindowList } void onChanged() { - setState(() { }); + setState(() {}); } @override void onWindowEnterFullScreen() { DynamicMaterialApp.setFullscreen(true, updateSystem: false); } + @override void onWindowLeaveFullScreen() { DynamicMaterialApp.setFullscreen(false, updateSystem: false); } + Future _onFullscreenChange(bool systemOverlaysAreVisible) async { - DynamicMaterialApp.setFullscreen(!systemOverlaysAreVisible, updateSystem: false); + DynamicMaterialApp.setFullscreen(!systemOverlaysAreVisible, + updateSystem: false); } /// We need to use a custom font if macOS < 10.13, @@ -90,7 +94,8 @@ class _DynamicMaterialAppState extends State with WindowList if (!Platform.isMacOS) return; final RegExp numberRegex = RegExp(r'\d+\.\d+'); // e.g. 10.13 or 12.5 - final RegExpMatch? osVersionMatch = numberRegex.firstMatch(Platform.operatingSystemVersion); + final RegExpMatch? osVersionMatch = + numberRegex.firstMatch(Platform.operatingSystemVersion); if (osVersionMatch == null) return; final double osVersion = double.tryParse(osVersionMatch[0] ?? '0') ?? 0; @@ -147,13 +152,15 @@ class _DynamicMaterialAppState extends State with WindowList ); } - final ColorScheme highContrastLightColorScheme = SeedColorScheme.fromSeeds( + final ColorScheme highContrastLightColorScheme = + SeedColorScheme.fromSeeds( brightness: Brightness.light, primaryKey: seedColor, background: Colors.white, tones: FlexTones.ultraContrast(Brightness.light), ); - final ColorScheme highContrastDarkColorScheme = SeedColorScheme.fromSeeds( + final ColorScheme highContrastDarkColorScheme = + SeedColorScheme.fromSeeds( brightness: Brightness.dark, primaryKey: seedColor, background: Colors.black, @@ -168,54 +175,58 @@ class _DynamicMaterialAppState extends State with WindowList }; return YaruBuilder( - enabled: platform == TargetPlatform.linux, - primary: lightColorScheme.primary, - builder: (context, yaruTheme, yaruHighContrastTheme) { - return MaterialApp.router( - routeInformationProvider: widget.router.routeInformationProvider, - routeInformationParser: widget.router.routeInformationParser, - routerDelegate: widget.router.routerDelegate, - - locale: TranslationProvider.of(context).flutterLocale, - supportedLocales: AppLocaleUtils.supportedLocales, - localizationsDelegates: GlobalMaterialLocalizations.delegates, - - title: widget.title, - - themeMode: Prefs.appTheme.loaded ? Prefs.appTheme.value : ThemeMode.system, - theme: yaruTheme?.theme ?? ThemeData( - useMaterial3: true, - colorScheme: lightColorScheme, - textTheme: getTextTheme(Brightness.light), - scaffoldBackgroundColor: lightColorScheme.background, - platform: platform, - ), - darkTheme: yaruTheme?.darkTheme ?? ThemeData( - useMaterial3: true, - colorScheme: darkColorScheme, - textTheme: getTextTheme(Brightness.dark), - scaffoldBackgroundColor: darkColorScheme.background, - platform: platform, - ), - highContrastTheme: yaruHighContrastTheme?.theme ?? ThemeData( - useMaterial3: true, - colorScheme: highContrastLightColorScheme, - textTheme: getTextTheme(Brightness.light), - scaffoldBackgroundColor: highContrastLightColorScheme.background, - platform: platform, - ), - highContrastDarkTheme: yaruHighContrastTheme?.darkTheme ?? ThemeData( - useMaterial3: true, - colorScheme: highContrastDarkColorScheme, - textTheme: getTextTheme(Brightness.dark), - scaffoldBackgroundColor: highContrastDarkColorScheme.background, - platform: platform, - ), - - debugShowCheckedModeBanner: false, - ); - } - ); + enabled: platform == TargetPlatform.linux, + primary: lightColorScheme.primary, + builder: (context, yaruTheme, yaruHighContrastTheme) { + return MaterialApp.router( + routeInformationProvider: + widget.router.routeInformationProvider, + routeInformationParser: widget.router.routeInformationParser, + routerDelegate: widget.router.routerDelegate, + locale: TranslationProvider.of(context).flutterLocale, + supportedLocales: AppLocaleUtils.supportedLocales, + localizationsDelegates: GlobalMaterialLocalizations.delegates, + title: widget.title, + themeMode: Prefs.appTheme.loaded + ? Prefs.appTheme.value + : ThemeMode.system, + theme: yaruTheme?.theme ?? + ThemeData( + useMaterial3: true, + colorScheme: lightColorScheme, + textTheme: getTextTheme(Brightness.light), + scaffoldBackgroundColor: lightColorScheme.background, + platform: platform, + ), + darkTheme: yaruTheme?.darkTheme ?? + ThemeData( + useMaterial3: true, + colorScheme: darkColorScheme, + textTheme: getTextTheme(Brightness.dark), + scaffoldBackgroundColor: darkColorScheme.background, + platform: platform, + ), + highContrastTheme: yaruHighContrastTheme?.theme ?? + ThemeData( + useMaterial3: true, + colorScheme: highContrastLightColorScheme, + textTheme: getTextTheme(Brightness.light), + scaffoldBackgroundColor: + highContrastLightColorScheme.background, + platform: platform, + ), + highContrastDarkTheme: yaruHighContrastTheme?.darkTheme ?? + ThemeData( + useMaterial3: true, + colorScheme: highContrastDarkColorScheme, + textTheme: getTextTheme(Brightness.dark), + scaffoldBackgroundColor: + highContrastDarkColorScheme.background, + platform: platform, + ), + debugShowCheckedModeBanner: false, + ); + }); }, ); } diff --git a/lib/components/theming/sliver_width_box.dart b/lib/components/theming/sliver_width_box.dart index 6e4a7b32a..cc964ebf6 100644 --- a/lib/components/theming/sliver_width_box.dart +++ b/lib/components/theming/sliver_width_box.dart @@ -15,9 +15,8 @@ class SliverWidthBox extends StatelessWidget { final windowSize = MediaQuery.of(context).size; return SliverPadding( padding: EdgeInsets.symmetric( - horizontal: windowSize.width > width - ? (windowSize.width - width) / 2 - : 0, + horizontal: + windowSize.width > width ? (windowSize.width - width) / 2 : 0, ), sliver: sliver, ); diff --git a/lib/components/theming/yaru_builder.dart b/lib/components/theming/yaru_builder.dart index d45483264..cc6e1917a 100644 --- a/lib/components/theming/yaru_builder.dart +++ b/lib/components/theming/yaru_builder.dart @@ -42,11 +42,12 @@ class _YaruBuilderState extends State { static YaruVariant findClosestYaruVariant(Color primary) { final primaryHue = HSLColor.fromColor(primary).hue; return YaruVariant.values - .map((variant) { - final variantHue = HSLColor.fromColor(variant.color).hue; - return MapEntry(variant, (variantHue - primaryHue).abs()); - }) - .reduce((a, b) => a.value < b.value ? a : b).key; + .map((variant) { + final variantHue = HSLColor.fromColor(variant.color).hue; + return MapEntry(variant, (variantHue - primaryHue).abs()); + }) + .reduce((a, b) => a.value < b.value ? a : b) + .key; } @override diff --git a/lib/components/toolbar/color_bar.dart b/lib/components/toolbar/color_bar.dart index c4dbdae93..cccc1bb1a 100644 --- a/lib/components/toolbar/color_bar.dart +++ b/lib/components/toolbar/color_bar.dart @@ -24,9 +24,8 @@ class ColorBar extends StatefulWidget { final Color? currentColor; final bool invert; - static List get colorPresets => Prefs.preferGreyscale.value - ? greyScaleColorOptions - : normalColorOptions; + static List get colorPresets => + Prefs.preferGreyscale.value ? greyScaleColorOptions : normalColorOptions; static final List normalColorOptions = [ (name: t.editor.colors.black, color: Colors.black), (name: t.editor.colors.red, color: Colors.red), @@ -41,20 +40,47 @@ class ColorBar extends StatefulWidget { ..._pastelColorOptions, ]; static final List _pastelColorOptions = [ - (name: t.editor.colors.pastelRed, color: const Color.fromRGBO(255, 173, 173, 1)), - (name: t.editor.colors.pastelOrange, color: const Color.fromRGBO(255, 214, 165, 1)), - (name: t.editor.colors.pastelYellow, color: const Color.fromRGBO(253, 255, 182, 1)), - (name: t.editor.colors.pastelGreen, color: const Color.fromRGBO(202, 255, 191, 1)), - (name: t.editor.colors.pastelCyan, color: const Color.fromRGBO(155, 246, 255, 1)), - (name: t.editor.colors.pastelBlue, color: const Color.fromRGBO(160, 196, 255, 1)), - (name: t.editor.colors.pastelPurple, color: const Color.fromRGBO(189, 178, 255, 1)), - (name: t.editor.colors.pastelPink, color: const Color.fromRGBO(255, 198, 255, 1)), + ( + name: t.editor.colors.pastelRed, + color: const Color.fromRGBO(255, 173, 173, 1) + ), + ( + name: t.editor.colors.pastelOrange, + color: const Color.fromRGBO(255, 214, 165, 1) + ), + ( + name: t.editor.colors.pastelYellow, + color: const Color.fromRGBO(253, 255, 182, 1) + ), + ( + name: t.editor.colors.pastelGreen, + color: const Color.fromRGBO(202, 255, 191, 1) + ), + ( + name: t.editor.colors.pastelCyan, + color: const Color.fromRGBO(155, 246, 255, 1) + ), + ( + name: t.editor.colors.pastelBlue, + color: const Color.fromRGBO(160, 196, 255, 1) + ), + ( + name: t.editor.colors.pastelPurple, + color: const Color.fromRGBO(189, 178, 255, 1) + ), + ( + name: t.editor.colors.pastelPink, + color: const Color.fromRGBO(255, 198, 255, 1) + ), ]; static final List greyScaleColorOptions = [ (name: t.editor.colors.black, color: Colors.black), (name: t.editor.colors.darkGrey, color: Colors.grey[800] ?? Colors.black54), (name: t.editor.colors.grey, color: Colors.grey), - (name: t.editor.colors.lightGrey, color: Colors.grey[200] ?? Colors.black12), + ( + name: t.editor.colors.lightGrey, + color: Colors.grey[200] ?? Colors.black12 + ), (name: t.editor.colors.white, color: Colors.white), ]; static final List _allColors = [ @@ -114,11 +140,13 @@ class ColorBar extends StatefulWidget { Prefs.pinnedColors.value.remove(colorString); Prefs.recentColorsChronological.value.remove(colorString); Prefs.recentColorsPositioned.value.remove(colorString); - if (Prefs.recentColorsChronological.value.length >= Prefs.recentColorsLength.value) { + if (Prefs.recentColorsChronological.value.length >= + Prefs.recentColorsLength.value) { // if full, replace oldest final oldestColor = Prefs.recentColorsChronological.value.removeAt(0); Prefs.recentColorsChronological.value.add(colorString); - final int oldestColorPosition = Prefs.recentColorsPositioned.value.indexOf(oldestColor); + final int oldestColorPosition = + Prefs.recentColorsPositioned.value.indexOf(oldestColor); Prefs.recentColorsPositioned.value[oldestColorPosition] = colorString; } else { // not full, add to end @@ -152,12 +180,42 @@ class _ColorBarState extends State { const ColorOptionSeparatorIcon( icon: Icons.pin_drop, ), + for (String colorString in Prefs.pinnedColors.value) + ColorOption( + isSelected: widget.currentColor?.withAlpha(255).value == + int.parse(colorString), + enabled: widget.currentColor != null, + onTap: () => widget.setColor(Color(int.parse(colorString))), + onLongPress: () => + setState(() => ColorBar.toggleColorPinned(colorString)), + tooltip: ColorBar.findColorName(Color(int.parse(colorString))), + child: DecoratedBox( + decoration: BoxDecoration( + color: + Color(int.parse(colorString)).withInversion(widget.invert), + shape: BoxShape.circle, + border: Border.all( + color: colorScheme.onSurface.withOpacity(0.2), + width: 1, + ), + ), + ), + ), + ], - for (String colorString in Prefs.pinnedColors.value) ColorOption( - isSelected: widget.currentColor?.withAlpha(255).value == int.parse(colorString), + const ColorOptionSeparatorIcon( + icon: Icons.history, + ), + + // recent colors + for (String colorString in Prefs.recentColorsPositioned.value.reversed) + ColorOption( + isSelected: widget.currentColor?.withAlpha(255).value == + int.parse(colorString), enabled: widget.currentColor != null, onTap: () => widget.setColor(Color(int.parse(colorString))), - onLongPress: () => setState(() => ColorBar.toggleColorPinned(colorString)), + onLongPress: () => + setState(() => ColorBar.toggleColorPinned(colorString)), tooltip: ColorBar.findColorName(Color(int.parse(colorString))), child: DecoratedBox( decoration: BoxDecoration( @@ -170,47 +228,28 @@ class _ColorBarState extends State { ), ), ), - ], - - const ColorOptionSeparatorIcon( - icon: Icons.history, - ), - - // recent colors - for (String colorString in Prefs.recentColorsPositioned.value.reversed) ColorOption( - isSelected: widget.currentColor?.withAlpha(255).value == int.parse(colorString), - enabled: widget.currentColor != null, - onTap: () => widget.setColor(Color(int.parse(colorString))), - onLongPress: () => setState(() => ColorBar.toggleColorPinned(colorString)), - tooltip: ColorBar.findColorName(Color(int.parse(colorString))), - child: DecoratedBox( - decoration: BoxDecoration( - color: Color(int.parse(colorString)).withInversion(widget.invert), - shape: BoxShape.circle, - border: Border.all( - color: colorScheme.onSurface.withOpacity(0.2), - width: 1, - ), - ), - ), - ), // placeholders for `recentColorsLength` recent colors - for (int i = 0; i < Prefs.recentColorsLength.value - Prefs.recentColorsPositioned.value.length; ++i) ColorOption( - isSelected: false, - enabled: widget.currentColor != null, - onTap: null, - tooltip: null, - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.transparent, - shape: BoxShape.circle, - border: Border.all( - color: colorScheme.onSurface.withOpacity(0.2), - width: 1, + for (int i = 0; + i < + Prefs.recentColorsLength.value - + Prefs.recentColorsPositioned.value.length; + ++i) + ColorOption( + isSelected: false, + enabled: widget.currentColor != null, + onTap: null, + tooltip: null, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.transparent, + shape: BoxShape.circle, + border: Border.all( + color: colorScheme.onSurface.withOpacity(0.2), + width: 1, + ), ), ), ), - ), const ColorOptionSeparatorIcon( icon: Icons.palette, @@ -218,7 +257,8 @@ class _ColorBarState extends State { // custom color ColorOption( - isSelected: widget.currentColor?.withAlpha(255).value == pickedColor.value, + isSelected: + widget.currentColor?.withAlpha(255).value == pickedColor.value, enabled: true, onTap: () => openColorPicker(context), tooltip: t.editor.colors.colorPicker, @@ -232,22 +272,24 @@ class _ColorBarState extends State { ), // color presets - for (NamedColor namedColor in ColorBar.colorPresets) ColorOption( - isSelected: widget.currentColor?.withAlpha(255).value == namedColor.color.value, - enabled: widget.currentColor != null, - onTap: () => widget.setColor(namedColor.color), - tooltip: namedColor.name, - child: DecoratedBox( - decoration: BoxDecoration( - color: namedColor.color.withInversion(widget.invert), - shape: BoxShape.circle, - border: Border.all( - color: colorScheme.onSurface.withOpacity(0.2), - width: 1, + for (NamedColor namedColor in ColorBar.colorPresets) + ColorOption( + isSelected: widget.currentColor?.withAlpha(255).value == + namedColor.color.value, + enabled: widget.currentColor != null, + onTap: () => widget.setColor(namedColor.color), + tooltip: namedColor.name, + child: DecoratedBox( + decoration: BoxDecoration( + color: namedColor.color.withInversion(widget.invert), + shape: BoxShape.circle, + border: Border.all( + color: colorScheme.onSurface.withOpacity(0.2), + width: 1, + ), ), ), ), - ), ]; return Center( @@ -274,22 +316,22 @@ class _ColorBarState extends State { } Widget _colorPickerDialog(BuildContext context) => AdaptiveAlertDialog( - title: Text(t.settings.accentColorPicker.pickAColor), - content: SingleChildScrollView( - child: ColorPicker( - pickerColor: pickedColor, - onColorChanged: (Color color) { - pickedColor = color; - }, - ), - ), - actions: [ - CupertinoDialogAction( - child: Text(MaterialLocalizations.of(context).saveButtonLabel), - onPressed: () { - Navigator.of(context).pop(true); - }, - ), - ], - ); + title: Text(t.settings.accentColorPicker.pickAColor), + content: SingleChildScrollView( + child: ColorPicker( + pickerColor: pickedColor, + onColorChanged: (Color color) { + pickedColor = color; + }, + ), + ), + actions: [ + CupertinoDialogAction( + child: Text(MaterialLocalizations.of(context).saveButtonLabel), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + ); } diff --git a/lib/components/toolbar/color_option.dart b/lib/components/toolbar/color_option.dart index ca54fbb7d..a78e63311 100644 --- a/lib/components/toolbar/color_option.dart +++ b/lib/components/toolbar/color_option.dart @@ -80,7 +80,8 @@ class ColorOptionSeparatorIcon extends StatelessWidget { colorScheme.onSurface, colorScheme.primary, 0.2, - )!.withOpacity(0.7), + )! + .withOpacity(0.7), ), ); } diff --git a/lib/components/toolbar/editor_bottom_sheet.dart b/lib/components/toolbar/editor_bottom_sheet.dart index f907ee1e1..6a578b47c 100644 --- a/lib/components/toolbar/editor_bottom_sheet.dart +++ b/lib/components/toolbar/editor_bottom_sheet.dart @@ -86,10 +86,12 @@ class _EditorBottomSheetState extends State { spacing: 8, children: [ ElevatedButton( - onPressed: widget.coreInfo.isNotEmpty ? () { - widget.clearPage(); - Navigator.pop(context); - } : null, + onPressed: widget.coreInfo.isNotEmpty + ? () { + widget.clearPage(); + Navigator.pop(context); + } + : null, child: Wrap( children: [ const Icon(Icons.cleaning_services), @@ -104,10 +106,12 @@ class _EditorBottomSheetState extends State { ), ), ElevatedButton( - onPressed: widget.coreInfo.isNotEmpty ? () { - widget.clearAllPages(); - Navigator.pop(context); - } : null, + onPressed: widget.coreInfo.isNotEmpty + ? () { + widget.clearAllPages(); + Navigator.pop(context); + } + : null, child: Wrap( children: [ const Icon(Icons.cleaning_services), @@ -143,8 +147,10 @@ class _EditorBottomSheetState extends State { CanvasBackgroundPreview( selected: backgroundImage?.backgroundFit == boxFit, invert: widget.invert, - backgroundColor: widget.coreInfo.backgroundColor ?? InnerCanvas.defaultBackgroundColor, - backgroundPattern: widget.coreInfo.backgroundPattern, + backgroundColor: widget.coreInfo.backgroundColor ?? + InnerCanvas.defaultBackgroundColor, + backgroundPattern: + widget.coreInfo.backgroundPattern, backgroundImage: backgroundImage, overrideBoxFit: boxFit, pageSize: pageSize, @@ -190,7 +196,8 @@ class _EditorBottomSheetState extends State { itemCount: CanvasBackgroundPattern.values.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (context, index) { - final backgroundPattern = CanvasBackgroundPattern.values[index]; + final backgroundPattern = + CanvasBackgroundPattern.values[index]; return InkWell( borderRadius: BorderRadius.circular(8), onTap: () => setState(() { @@ -199,9 +206,11 @@ class _EditorBottomSheetState extends State { child: Stack( children: [ CanvasBackgroundPreview( - selected: widget.coreInfo.backgroundPattern == backgroundPattern, + selected: widget.coreInfo.backgroundPattern == + backgroundPattern, invert: widget.invert, - backgroundColor: widget.coreInfo.backgroundColor ?? InnerCanvas.defaultBackgroundColor, + backgroundColor: widget.coreInfo.backgroundColor ?? + InnerCanvas.defaultBackgroundColor, backgroundPattern: backgroundPattern, backgroundImage: null, // focus on background pattern pageSize: pageSize, @@ -213,7 +222,8 @@ class _EditorBottomSheetState extends State { right: 0, child: Center( child: _PermanentTooltip( - text: CanvasBackgroundPattern.localizedName(backgroundPattern), + text: CanvasBackgroundPattern.localizedName( + backgroundPattern), ), ), ), @@ -266,16 +276,17 @@ class _EditorBottomSheetState extends State { }, child: Text(t.editor.toolbar.photo), ), - if (widget.canRasterPdf) ElevatedButton( - onPressed: () async { - bool pdfImported = await widget.importPdf(); - if (pdfImported) { - if (!mounted) return; - Navigator.pop(context); - } - }, - child: const Text('PDF'), - ), + if (widget.canRasterPdf) + ElevatedButton( + onPressed: () async { + bool pdfImported = await widget.importPdf(); + if (pdfImported) { + if (!mounted) return; + Navigator.pop(context); + } + }, + child: const Text('PDF'), + ), ], ), const SizedBox(height: 16), diff --git a/lib/components/toolbar/editor_page_manager.dart b/lib/components/toolbar/editor_page_manager.dart index a9f66e85b..523fa336f 100644 --- a/lib/components/toolbar/editor_page_manager.dart +++ b/lib/components/toolbar/editor_page_manager.dart @@ -14,12 +14,10 @@ class EditorPageManager extends StatefulWidget { required this.coreInfo, required this.currentPageIndex, required this.redrawAndSave, - required this.insertPageAfter, required this.duplicatePage, required this.clearPage, required this.deletePage, - required this.transformationController, }); @@ -40,16 +38,17 @@ class EditorPageManager extends StatefulWidget { class _EditorPageManagerState extends State { void scrollToPage(int pageIndex) => CanvasGestureDetector.scrollToPage( - pageIndex: pageIndex, - pages: widget.coreInfo.pages, - screenWidth: MediaQuery.of(context).size.width, - transformationController: widget.transformationController, - ); + pageIndex: pageIndex, + pages: widget.coreInfo.pages, + screenWidth: MediaQuery.of(context).size.width, + transformationController: widget.transformationController, + ); @override Widget build(BuildContext context) { final platform = Theme.of(context).platform; - final cupertino = platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; + final cupertino = + platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; return SizedBox( width: cupertino ? null : 300, height: cupertino ? 600 : null, @@ -57,7 +56,9 @@ class _EditorPageManagerState extends State { buildDefaultDragHandles: false, itemCount: widget.coreInfo.pages.length, itemBuilder: (context, pageIndex) { - final isEmptyLastPage = pageIndex == widget.coreInfo.pages.length - 1 && widget.coreInfo.pages[pageIndex].isEmpty; + final isEmptyLastPage = + pageIndex == widget.coreInfo.pages.length - 1 && + widget.coreInfo.pages[pageIndex].isEmpty; return InkWell( key: ValueKey(pageIndex), onTap: () => scrollToPage(pageIndex), @@ -129,10 +130,12 @@ class _EditorPageManagerState extends State { totalPages: widget.coreInfo.pages.length, ), icon: const Icon(Icons.cleaning_services), - onPressed: isEmptyLastPage ? null : () => setState(() { - widget.clearPage(pageIndex); - scrollToPage(pageIndex); - }), + onPressed: isEmptyLastPage + ? null + : () => setState(() { + widget.clearPage(pageIndex); + scrollToPage(pageIndex); + }), ), IconButton( tooltip: t.editor.menu.deletePage, @@ -140,10 +143,12 @@ class _EditorPageManagerState extends State { icon: Icons.delete, cupertinoIcon: CupertinoIcons.delete, ), - onPressed: isEmptyLastPage ? null : () => setState(() { - widget.deletePage(pageIndex); - scrollToPage(pageIndex); - }), + onPressed: isEmptyLastPage + ? null + : () => setState(() { + widget.deletePage(pageIndex); + scrollToPage(pageIndex); + }), ), ], ), @@ -157,7 +162,8 @@ class _EditorPageManagerState extends State { if (oldIndex < newIndex) { newIndex -= 1; } - widget.coreInfo.pages.insert(newIndex, widget.coreInfo.pages.removeAt(oldIndex)); + widget.coreInfo.pages + .insert(newIndex, widget.coreInfo.pages.removeAt(oldIndex)); // reassign pageIndex of pages' strokes and images for (int i = 0; i < widget.coreInfo.pages.length; i++) { diff --git a/lib/components/toolbar/export_bar.dart b/lib/components/toolbar/export_bar.dart index 3d85a25fe..56aa5f8b5 100644 --- a/lib/components/toolbar/export_bar.dart +++ b/lib/components/toolbar/export_bar.dart @@ -41,10 +41,12 @@ class _ExportBarState extends State { }); }; } + Widget _buttonChild(Future Function()? exportFunction, String text) { if (exportFunction == null || _currentlyExporting != exportFunction) { return Text(text); - } else { // if this is currently exporting, show a loading icon + } else { + // if this is currently exporting, show a loading icon return const SpinningLoadingIcon(); } } @@ -54,7 +56,6 @@ class _ExportBarState extends State { final children = [ Text(t.editor.toolbar.exportAs), const SizedBox.square(dimension: 8), - TextButton( onPressed: _onPressed(widget.exportAsSba), child: _buttonChild(widget.exportAsSba, 'SBA'), @@ -63,10 +64,11 @@ class _ExportBarState extends State { onPressed: _onPressed(widget.exportAsPdf), child: _buttonChild(widget.exportAsPdf, 'PDF'), ), - if (kDebugMode) TextButton( - onPressed: _onPressed(widget.exportAsPng), - child: _buttonChild(widget.exportAsPng, 'PNG'), - ), + if (kDebugMode) + TextButton( + onPressed: _onPressed(widget.exportAsPng), + child: _buttonChild(widget.exportAsPng, 'PNG'), + ), ]; return Center( @@ -75,8 +77,8 @@ class _ExportBarState extends State { child: SingleChildScrollView( scrollDirection: widget.axis, child: widget.axis == Axis.horizontal - ? Row(children: children) - : Column(children: children), + ? Row(children: children) + : Column(children: children), ), ), ); diff --git a/lib/components/toolbar/pen_modal.dart b/lib/components/toolbar/pen_modal.dart index cbd57ac0e..b80be71a3 100644 --- a/lib/components/toolbar/pen_modal.dart +++ b/lib/components/toolbar/pen_modal.dart @@ -40,7 +40,6 @@ class _PenModalState extends State { SizePicker( pen: currentPen, ), - if (currentPen is! Highlighter && currentPen is! Pencil) ...[ const SizedBox(width: 8), IconButton( diff --git a/lib/components/toolbar/size_picker.dart b/lib/components/toolbar/size_picker.dart index 58fee4d65..d18a03802 100644 --- a/lib/components/toolbar/size_picker.dart +++ b/lib/components/toolbar/size_picker.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'package:flutter/material.dart'; @@ -19,7 +18,6 @@ class SizePicker extends StatefulWidget { } class _SizePickerState extends State { - final TextEditingController _controller = TextEditingController(); Offset? startingOffset; @@ -44,7 +42,9 @@ class _SizePickerState extends State { void updateValue({double? newValue, bool manuallyTypedIn = false}) { if (newValue != null) { setState(() { - widget.pen.strokeProperties.size = newValue.clamp(widget.pen.sizeMin, widget.pen.sizeMax).roundToDouble(); + widget.pen.strokeProperties.size = newValue + .clamp(widget.pen.sizeMin, widget.pen.sizeMax) + .roundToDouble(); }); } @@ -62,8 +62,12 @@ class _SizePickerState extends State { void onDrag(Offset currentOffset) { if (startingOffset == null) return; - final double delta = (currentOffset.dx - startingOffset!.dx) / widget.pen.sizeMax * 4 * widget.pen.sizeStep; - final double newValue = startingValue + delta ~/ widget.pen.sizeStep * widget.pen.sizeStep; + final double delta = (currentOffset.dx - startingOffset!.dx) / + widget.pen.sizeMax * + 4 * + widget.pen.sizeStep; + final double newValue = + startingValue + delta ~/ widget.pen.sizeStep * widget.pen.sizeStep; setState(() { updateValue(newValue: newValue); }); @@ -118,8 +122,12 @@ class _SizePickerState extends State { child: Center( child: AnimatedContainer( duration: const Duration(milliseconds: 100), - width: widget.pen.strokeProperties.size / widget.pen.sizeMax * 25, - height: widget.pen.strokeProperties.size / widget.pen.sizeMax * 25, + width: widget.pen.strokeProperties.size / + widget.pen.sizeMax * + 25, + height: widget.pen.strokeProperties.size / + widget.pen.sizeMax * + 25, decoration: BoxDecoration( color: colorScheme.onBackground, shape: BoxShape.circle, @@ -167,7 +175,8 @@ class _SizePickerState extends State { backgroundColor: colorScheme.surface, action: SnackBarAction( label: MaterialLocalizations.of(context).modalBarrierDismissLabel, - onPressed: () => ScaffoldMessenger.of(context).hideCurrentSnackBar(), + onPressed: () => + ScaffoldMessenger.of(context).hideCurrentSnackBar(), ), content: Text( t.editor.penOptions.sizeDragHint, diff --git a/lib/components/toolbar/toolbar.dart b/lib/components/toolbar/toolbar.dart index a059ba19b..0d43a7f30 100644 --- a/lib/components/toolbar/toolbar.dart +++ b/lib/components/toolbar/toolbar.dart @@ -28,31 +28,22 @@ import 'package:saber/i18n/strings.g.dart'; class Toolbar extends StatefulWidget { const Toolbar({ super.key, - required this.readOnly, - required this.setTool, required this.currentTool, required this.setColor, - required this.quillFocus, required this.textEditing, required this.toggleTextEditing, - required this.undo, required this.isUndoPossible, required this.redo, required this.isRedoPossible, - required this.toggleFingerDrawing, - required this.pickPhoto, - required this.paste, - required this.duplicateSelection, required this.deleteSelection, - required this.exportAsSba, required this.exportAsPdf, required this.exportAsPng, @@ -89,8 +80,10 @@ class Toolbar extends StatefulWidget { @override State createState() => _ToolbarState(); - static const EdgeInsets _buttonPaddingHorizontal = EdgeInsets.symmetric(horizontal: 6); - static const EdgeInsets _buttonPaddingVertical = EdgeInsets.symmetric(vertical: 6); + static const EdgeInsets _buttonPaddingHorizontal = + EdgeInsets.symmetric(horizontal: 6); + static const EdgeInsets _buttonPaddingVertical = + EdgeInsets.symmetric(vertical: 6); } class _ToolbarState extends State { @@ -107,7 +100,7 @@ class _ToolbarState extends State { super.initState(); } - void _setState() => setState(() { }); + void _setState() => setState(() {}); Keybinding? _ctrlF; Keybinding? _ctrlE; @@ -116,12 +109,18 @@ class _ToolbarState extends State { Keybinding? _f11; Keybinding? _ctrlV; void _assignKeybindings() { - _ctrlF = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyF)], inclusive: true); - _ctrlE = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyE)], inclusive: true); - _ctrlC = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyC)], inclusive: true); - _ctrlShiftS = Keybinding([KeyCode.ctrl, KeyCode.shift, KeyCode.from(LogicalKeyboardKey.keyS)], inclusive: true); + _ctrlF = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyF)], + inclusive: true); + _ctrlE = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyE)], + inclusive: true); + _ctrlC = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyC)], + inclusive: true); + _ctrlShiftS = Keybinding( + [KeyCode.ctrl, KeyCode.shift, KeyCode.from(LogicalKeyboardKey.keyS)], + inclusive: true); _f11 = Keybinding([KeyCode.from(LogicalKeyboardKey.f11)], inclusive: true); - _ctrlV = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyV)], inclusive: true); + _ctrlV = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyV)], + inclusive: true); Keybinder.bind(_ctrlF!, widget.toggleFingerDrawing); Keybinder.bind(_ctrlE!, toggleEraser); @@ -130,6 +129,7 @@ class _ToolbarState extends State { Keybinder.bind(_f11!, toggleFullscreen); Keybinder.bind(_ctrlV!, widget.paste); } + void _removeKeybindings() { if (_ctrlF != null) Keybinder.remove(_ctrlF!); if (_ctrlE != null) Keybinder.remove(_ctrlE!); @@ -143,15 +143,18 @@ class _ToolbarState extends State { toolOptionsType.value = ToolOptions.hide; widget.setTool(Eraser()); // this toggles eraser } + void toggleColorOptions() { showColorOptions.value = !showColorOptions.value; } + void toggleExportBar() { showExportOptions.value = !showExportOptions.value; } void toggleFullscreen() async { - DynamicMaterialApp.setFullscreen(!DynamicMaterialApp.isFullscreen, updateSystem: true); + DynamicMaterialApp.setFullscreen(!DynamicMaterialApp.isFullscreen, + updateSystem: true); } @override @@ -161,8 +164,9 @@ class _ToolbarState extends State { Brightness brightness = Theme.of(context).brightness; bool invert = Prefs.editorAutoInvert.value && brightness == Brightness.dark; - final isToolbarVertical = Prefs.editorToolbarAlignment.value == AxisDirection.left - || Prefs.editorToolbarAlignment.value == AxisDirection.right; + final isToolbarVertical = + Prefs.editorToolbarAlignment.value == AxisDirection.left || + Prefs.editorToolbarAlignment.value == AxisDirection.right; final buttonPadding = isToolbarVertical ? Toolbar._buttonPaddingVertical @@ -186,7 +190,9 @@ class _ToolbarState extends State { valueListenable: showExportOptions, builder: (context, showExportOptions, child) { return Collapsible( - axis: isToolbarVertical ? CollapsibleAxis.horizontal : CollapsibleAxis.vertical, + axis: isToolbarVertical + ? CollapsibleAxis.horizontal + : CollapsibleAxis.vertical, maintainState: true, collapsed: !showExportOptions, child: child!, @@ -204,27 +210,29 @@ class _ToolbarState extends State { valueListenable: toolOptionsType, builder: (context, toolOptionsType, _) { return Collapsible( - axis: isToolbarVertical ? CollapsibleAxis.horizontal : CollapsibleAxis.vertical, + axis: isToolbarVertical + ? CollapsibleAxis.horizontal + : CollapsibleAxis.vertical, maintainState: false, collapsed: toolOptionsType == ToolOptions.hide, child: switch (toolOptionsType) { ToolOptions.hide => const SizedBox(), ToolOptions.pen => PenModal( - getTool: () => Pen.currentPen, - setTool: widget.setTool, - ), + getTool: () => Pen.currentPen, + setTool: widget.setTool, + ), ToolOptions.highlighter => PenModal( - getTool: () => Highlighter.currentHighlighter, - setTool: widget.setTool, - ), + getTool: () => Highlighter.currentHighlighter, + setTool: widget.setTool, + ), ToolOptions.pencil => PenModal( - getTool: () => Pencil.currentPencil, - setTool: widget.setTool, - ), + getTool: () => Pencil.currentPencil, + setTool: widget.setTool, + ), ToolOptions.select => SelectionBar( - duplicateSelection: widget.duplicateSelection, - deleteSelection: widget.deleteSelection, - ), + duplicateSelection: widget.duplicateSelection, + deleteSelection: widget.deleteSelection, + ), }, ); }, @@ -233,7 +241,9 @@ class _ToolbarState extends State { valueListenable: showColorOptions, builder: (context, showColorOptions, child) { return Collapsible( - axis: isToolbarVertical ? CollapsibleAxis.horizontal : CollapsibleAxis.vertical, + axis: isToolbarVertical + ? CollapsibleAxis.horizontal + : CollapsibleAxis.vertical, maintainState: true, collapsed: !showColorOptions, child: child!, @@ -247,49 +257,54 @@ class _ToolbarState extends State { ), ), ValueListenableBuilder( - valueListenable: widget.quillFocus, - builder: (context, quill, _) { - final iconTheme = QuillIconTheme( - iconSelectedColor: colorScheme.onPrimary, - iconUnselectedColor: colorScheme.primary, - iconSelectedFillColor: colorScheme.primary, - iconUnselectedFillColor: Colors.transparent, - disabledIconColor: colorScheme.onSurface.withOpacity(0.4), - disabledIconFillColor: Colors.transparent, - borderRadius: 22, - ); - return Collapsible( - axis: isToolbarVertical ? CollapsibleAxis.horizontal : CollapsibleAxis.vertical, - maintainState: false, - collapsed: !widget.textEditing || quill == null, - child: quill != null ? QuillProvider( - configurations: QuillConfigurations( - controller: quill.controller, - sharedConfigurations: QuillSharedConfigurations( - locale: TranslationProvider.of(context).flutterLocale, - ), - ), - child: QuillToolbar( - configurations: QuillToolbarConfigurations( - axis: isToolbarVertical ? Axis.vertical : Axis.horizontal, - buttonOptions: QuillToolbarButtonOptions( - base: QuillToolbarBaseButtonOptions( - globalIconSize: 22, - globalIconButtonFactor: 1.6, - iconTheme: iconTheme, - ), - ), - showUndo: false, - showRedo: false, - showFontSize: false, - showFontFamily: false, - showClearFormat: false, - ), - ), - ) : const SizedBox.shrink(), - ); - } - ), + valueListenable: widget.quillFocus, + builder: (context, quill, _) { + final iconTheme = QuillIconTheme( + iconSelectedColor: colorScheme.onPrimary, + iconUnselectedColor: colorScheme.primary, + iconSelectedFillColor: colorScheme.primary, + iconUnselectedFillColor: Colors.transparent, + disabledIconColor: colorScheme.onSurface.withOpacity(0.4), + disabledIconFillColor: Colors.transparent, + borderRadius: 22, + ); + return Collapsible( + axis: isToolbarVertical + ? CollapsibleAxis.horizontal + : CollapsibleAxis.vertical, + maintainState: false, + collapsed: !widget.textEditing || quill == null, + child: quill != null + ? QuillProvider( + configurations: QuillConfigurations( + controller: quill.controller, + sharedConfigurations: QuillSharedConfigurations( + locale: TranslationProvider.of(context).flutterLocale, + ), + ), + child: QuillToolbar( + configurations: QuillToolbarConfigurations( + axis: isToolbarVertical + ? Axis.vertical + : Axis.horizontal, + buttonOptions: QuillToolbarButtonOptions( + base: QuillToolbarBaseButtonOptions( + globalIconSize: 22, + globalIconButtonFactor: 1.6, + iconTheme: iconTheme, + ), + ), + showUndo: false, + showRedo: false, + showFontSize: false, + showFontFamily: false, + showClearFormat: false, + ), + ), + ) + : const SizedBox.shrink(), + ); + }), Center( child: Padding( padding: const EdgeInsets.all(8), @@ -317,25 +332,26 @@ class _ToolbarState extends State { padding: buttonPadding, child: FaIcon(Pen.currentPen.icon, size: 16), ), - if (kDebugMode) ToolbarIconButton( - tooltip: t.editor.pens.pencil, - selected: widget.currentTool == Pencil.currentPencil, - enabled: !widget.readOnly, - onPressed: () { - if (widget.currentTool == Pencil.currentPencil) { - if (toolOptionsType.value == ToolOptions.pencil) { - toolOptionsType.value = ToolOptions.hide; + if (kDebugMode) + ToolbarIconButton( + tooltip: t.editor.pens.pencil, + selected: widget.currentTool == Pencil.currentPencil, + enabled: !widget.readOnly, + onPressed: () { + if (widget.currentTool == Pencil.currentPencil) { + if (toolOptionsType.value == ToolOptions.pencil) { + toolOptionsType.value = ToolOptions.hide; + } else { + toolOptionsType.value = ToolOptions.pencil; + } } else { - toolOptionsType.value = ToolOptions.pencil; + toolOptionsType.value = ToolOptions.hide; + widget.setTool(Pencil.currentPencil); } - } else { - toolOptionsType.value = ToolOptions.hide; - widget.setTool(Pencil.currentPencil); - } - }, - padding: buttonPadding, - child: const FaIcon(Pencil.pencilIcon, size: 16), - ), + }, + padding: buttonPadding, + child: const FaIcon(Pencil.pencilIcon, size: 16), + ), ToolbarIconButton( tooltip: t.editor.pens.highlighter, selected: widget.currentTool == Highlighter.currentHighlighter, @@ -373,7 +389,8 @@ class _ToolbarState extends State { width: 18, height: 18, decoration: BoxDecoration( - color: currentColor.withInversion(invert).withOpacity(1), + color: + currentColor.withInversion(invert).withOpacity(1), shape: BoxShape.circle, border: Border.all( color: colorScheme.primary, @@ -391,18 +408,22 @@ class _ToolbarState extends State { widget.setTool(Select.currentSelect); }, padding: buttonPadding, - child: Icon(CupertinoIcons.lasso, shadows: !widget.readOnly ? [ - BoxShadow( - color: colorScheme.primary, - blurRadius: 0.1, - spreadRadius: 10, - blurStyle: BlurStyle.solid, - ), - ] : null), + child: Icon(CupertinoIcons.lasso, + shadows: !widget.readOnly + ? [ + BoxShadow( + color: colorScheme.primary, + blurRadius: 0.1, + spreadRadius: 10, + blurStyle: BlurStyle.solid, + ), + ] + : null), ), ToolbarIconButton( tooltip: t.editor.pens.laserPointer, - selected: widget.currentTool == LaserPointer.currentLaserPointer, + selected: + widget.currentTool == LaserPointer.currentLaserPointer, enabled: true, // even in read-only mode onPressed: () { toolOptionsType.value = ToolOptions.hide; @@ -441,14 +462,15 @@ class _ToolbarState extends State { cupertinoIcon: CupertinoIcons.text_cursor, ), ), - if (!Prefs.hideFingerDrawingToggle.value) ToolbarIconButton( - tooltip: t.editor.toolbar.toggleFingerDrawing, - selected: Prefs.editorFingerDrawing.value, - enabled: !widget.readOnly, - onPressed: widget.toggleFingerDrawing, - padding: buttonPadding, - child: const Icon(CupertinoIcons.hand_draw), - ), + if (!Prefs.hideFingerDrawingToggle.value) + ToolbarIconButton( + tooltip: t.editor.toolbar.toggleFingerDrawing, + selected: Prefs.editorFingerDrawing.value, + enabled: !widget.readOnly, + onPressed: widget.toggleFingerDrawing, + padding: buttonPadding, + child: const Icon(CupertinoIcons.hand_draw), + ), ToolbarIconButton( tooltip: t.editor.toolbar.fullscreen, selected: DynamicMaterialApp.isFullscreen, @@ -456,8 +478,12 @@ class _ToolbarState extends State { onPressed: toggleFullscreen, padding: buttonPadding, child: AdaptiveIcon( - icon: DynamicMaterialApp.isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen, - cupertinoIcon: DynamicMaterialApp.isFullscreen ? CupertinoIcons.fullscreen_exit : CupertinoIcons.fullscreen, + icon: DynamicMaterialApp.isFullscreen + ? Icons.fullscreen_exit + : Icons.fullscreen, + cupertinoIcon: DynamicMaterialApp.isFullscreen + ? CupertinoIcons.fullscreen_exit + : CupertinoIcons.fullscreen, ), ), Wrap( @@ -511,18 +537,20 @@ class _ToolbarState extends State { return Material( color: colorScheme.background, child: isToolbarVertical - ? Row( - textDirection: Prefs.editorToolbarAlignment.value == AxisDirection.left - ? TextDirection.rtl - : TextDirection.ltr, - children: children, - ) - : Column( - verticalDirection: Prefs.editorToolbarAlignment.value == AxisDirection.down - ? VerticalDirection.down - : VerticalDirection.up, - children: children, - ), + ? Row( + textDirection: + Prefs.editorToolbarAlignment.value == AxisDirection.left + ? TextDirection.rtl + : TextDirection.ltr, + children: children, + ) + : Column( + verticalDirection: + Prefs.editorToolbarAlignment.value == AxisDirection.down + ? VerticalDirection.down + : VerticalDirection.up, + children: children, + ), ); } diff --git a/lib/components/toolbar/toolbar_button.dart b/lib/components/toolbar/toolbar_button.dart index e37be7651..0e7a8dafd 100644 --- a/lib/components/toolbar/toolbar_button.dart +++ b/lib/components/toolbar/toolbar_button.dart @@ -30,7 +30,8 @@ class ToolbarIconButton extends StatelessWidget { ), padding: padding, child: IconButton( - color: (selected && enabled) ? colorScheme.onPrimary : colorScheme.primary, + color: + (selected && enabled) ? colorScheme.onPrimary : colorScheme.primary, disabledColor: colorScheme.onSurface.withOpacity(0.4), onPressed: (enabled) ? onPressed : null, tooltip: tooltip, diff --git a/lib/data/editor/editor_core_info.dart b/lib/data/editor/editor_core_info.dart index cb67e97aa..d4e7d2017 100644 --- a/lib/data/editor/editor_core_info.dart +++ b/lib/data/editor/editor_core_info.dart @@ -22,7 +22,7 @@ class EditorCoreInfo { /// The version of the file format. /// Increment this if earlier versions of the app can't satisfiably read the file. - /// + /// /// Version history: /// - 19: Assets are now stored in separate files, and added the `sba` file format. /// - 18: [Pencil] tool introduced @@ -48,6 +48,7 @@ class EditorCoreInfo { bool readOnlyBecauseOfVersion = false; String filePath; + /// The file name without its parent directories. String get fileName => filePath.substring(filePath.lastIndexOf('/') + 1); @@ -72,8 +73,7 @@ class EditorCoreInfo { pages: [], initialPageIndex: null, assetCache: null, - ) - .._migrateOldStrokesAndImages( + ).._migrateOldStrokesAndImages( fileVersion: sbnVersion, strokesJson: null, imagesJson: null, @@ -86,9 +86,9 @@ class EditorCoreInfo { EditorCoreInfo({ required this.filePath, - this.readOnly = true, // default to read-only, until it's loaded with [loadFromFilePath] - }): - nextImageId = 0, + this.readOnly = + true, // default to read-only, until it's loaded with [loadFromFilePath] + }) : nextImageId = 0, backgroundPattern = Prefs.lastBackgroundPattern.value, lineHeight = Prefs.lastLineHeight.value, pages = [], @@ -105,11 +105,12 @@ class EditorCoreInfo { required this.pages, required this.initialPageIndex, required AssetCache? assetCache, - }): assetCache = assetCache ?? AssetCache() { + }) : assetCache = assetCache ?? AssetCache() { _handleEmptyImageIds(); } - factory EditorCoreInfo.fromJson(Map json, { + factory EditorCoreInfo.fromJson( + Map json, { required String filePath, required bool readOnly, required bool onlyFirstPage, @@ -121,17 +122,18 @@ class EditorCoreInfo { /// Note that inline assets aren't used anymore /// since sbnVersion 19. final List? inlineAssets = (json['a'] as List?) - ?.map((asset) => switch (asset) { - (String base64) => base64Decode(base64), - (Uint8List bytes) => bytes, - (List bytes) => Uint8List.fromList(bytes.cast()), - (BsonBinary bsonBinary) => bsonBinary.byteList, - _ => (){ - log.severe('Invalid asset type in $filePath: ${asset.runtimeType}'); - return Uint8List(0); - }(), - }) - .toList(); + ?.map((asset) => switch (asset) { + (String base64) => base64Decode(base64), + (Uint8List bytes) => bytes, + (List bytes) => Uint8List.fromList(bytes.cast()), + (BsonBinary bsonBinary) => bsonBinary.byteList, + _ => () { + log.severe( + 'Invalid asset type in $filePath: ${asset.runtimeType}'); + return Uint8List(0); + }(), + }) + .toList(); final Color? backgroundColor; switch (json['b']) { @@ -142,7 +144,8 @@ class EditorCoreInfo { case null: backgroundColor = null; default: - throw Exception('Invalid color value: (${json['b'].runtimeType}) ${json['b']}'); + throw Exception( + 'Invalid color value: (${json['b'].runtimeType}) ${json['b']}'); } final assetCache = AssetCache(); @@ -153,7 +156,7 @@ class EditorCoreInfo { readOnlyBecauseOfVersion: readOnlyBecauseOfVersion, nextImageId: json['ni'] as int? ?? 0, backgroundColor: backgroundColor, - backgroundPattern: (){ + backgroundPattern: () { final String? pattern = json['p'] as String?; for (CanvasBackgroundPattern p in CanvasBackgroundPattern.values) { if (p.name == pattern) return p; @@ -184,16 +187,18 @@ class EditorCoreInfo { ) .._sortStrokes(); } + /// Old json format is just a list of strokes - EditorCoreInfo.fromOldJson(List json, { + EditorCoreInfo.fromOldJson( + List json, { required this.filePath, this.readOnly = false, required bool onlyFirstPage, - }): nextImageId = 0, - backgroundPattern = CanvasBackgroundPattern.none, - lineHeight = Prefs.lastLineHeight.value, - pages = [], - assetCache = AssetCache() { + }) : nextImageId = 0, + backgroundPattern = CanvasBackgroundPattern.none, + lineHeight = Prefs.lastLineHeight.value, + pages = [], + assetCache = AssetCache() { _migrateOldStrokesAndImages( fileVersion: 0, strokesJson: json, @@ -204,7 +209,8 @@ class EditorCoreInfo { _sortStrokes(); } - static List _parsePagesJson(List? pages, { + static List _parsePagesJson( + List? pages, { required List? inlineAssets, required bool readOnly, required bool onlyFirstPage, @@ -213,26 +219,27 @@ class EditorCoreInfo { required AssetCache assetCache, }) { if (pages == null || pages.isEmpty) return []; - if (pages[0] is List) { // old format (list of [width, height]) + if (pages[0] is List) { + // old format (list of [width, height]) return pages - .take(onlyFirstPage ? 1 : pages.length) - .map((dynamic page) => EditorPage( - width: page[0] as double?, - height: page[1] as double?, - )) - .toList(); + .take(onlyFirstPage ? 1 : pages.length) + .map((dynamic page) => EditorPage( + width: page[0] as double?, + height: page[1] as double?, + )) + .toList(); } else { return pages - .take(onlyFirstPage ? 1 : pages.length) - .map((dynamic page) => EditorPage.fromJson( - page as Map, - inlineAssets: inlineAssets, - readOnly: readOnly, - fileVersion: fileVersion, - sbnPath: sbnPath, - assetCache: assetCache, - )) - .toList(); + .take(onlyFirstPage ? 1 : pages.length) + .map((dynamic page) => EditorPage.fromJson( + page as Map, + inlineAssets: inlineAssets, + readOnly: readOnly, + fileVersion: fileVersion, + sbnPath: sbnPath, + assetCache: assetCache, + )) + .toList(); } } @@ -245,12 +252,12 @@ class EditorCoreInfo { } /// Performs the following migrations: - /// + /// /// Migrates from fileVersion 7 to 8. /// In version 8, strokes and images are stored in their respective pages. /// /// Creates a page if there are no pages. - /// + /// /// Migrates from fileVersion 11 to 12. /// In version 12, points are deleted if they are too close to each other. void _migrateOldStrokesAndImages({ @@ -271,7 +278,8 @@ class EditorCoreInfo { for (Stroke stroke in strokes) { if (onlyFirstPage) assert(stroke.pageIndex == 0); while (stroke.pageIndex >= pages.length) { - pages.add(EditorPage(width: fallbackPageWidth, height: fallbackPageHeight)); + pages.add( + EditorPage(width: fallbackPageWidth, height: fallbackPageHeight)); } pages[stroke.pageIndex].insertStroke(stroke); } @@ -289,7 +297,8 @@ class EditorCoreInfo { for (EditorImage image in images) { if (onlyFirstPage) assert(image.pageIndex == 0); while (image.pageIndex >= pages.length) { - pages.add(EditorPage(width: fallbackPageWidth, height: fallbackPageHeight)); + pages.add( + EditorPage(width: fallbackPageWidth, height: fallbackPageHeight)); } pages[image.pageIndex].images.add(image); } @@ -298,7 +307,8 @@ class EditorCoreInfo { // add a page if there are no pages, // or if the last page is not empty if (pages.isEmpty || pages.last.isNotEmpty && !onlyFirstPage) { - pages.add(EditorPage(width: fallbackPageWidth, height: fallbackPageHeight)); + pages.add( + EditorPage(width: fallbackPageWidth, height: fallbackPageHeight)); } // delete points that are too close to each other @@ -317,7 +327,8 @@ class EditorCoreInfo { } } - static Future loadFromFilePath(String path, { + static Future loadFromFilePath( + String path, { bool readOnly = false, bool onlyFirstPage = false, }) async { @@ -327,10 +338,11 @@ class EditorCoreInfo { if (bsonBytes != null) { jsonString = null; } else { - final jsonBytes = await FileManager.readFile(path + Editor.extensionOldJson); + final jsonBytes = + await FileManager.readFile(path + Editor.extensionOldJson); jsonString = jsonBytes != null ? utf8.decode(jsonBytes) : null; } - + if (bsonBytes == null && jsonString == null) { return EditorCoreInfo(filePath: path, readOnly: readOnly); } @@ -356,18 +368,20 @@ class EditorCoreInfo { EditorCoreInfo coreInfo; try { EditorCoreInfo isolate() => _loadFromFileIsolate( - jsonString, - bsonBytes, - path, - readOnly, - onlyFirstPage, - ); + jsonString, + bsonBytes, + path, + readOnly, + onlyFirstPage, + ); final length = jsonString?.length ?? bsonBytes!.length; - if (alwaysUseIsolate || length > 2 * 1024 * 1024) { // 2 MB + if (alwaysUseIsolate || length > 2 * 1024 * 1024) { + // 2 MB coreInfo = await workerManager.execute( isolate, - priority: WorkPriority.veryHigh, // less important than [WorkPriority.immediately] + priority: WorkPriority + .veryHigh, // less important than [WorkPriority.immediately] ); } else { // if the file is small, just run it on the main thread @@ -386,11 +400,11 @@ class EditorCoreInfo { } static EditorCoreInfo _loadFromFileIsolate( - String? jsonString, - Uint8List? bsonBytes, - String path, - bool readOnly, - bool onlyFirstPage, + String? jsonString, + Uint8List? bsonBytes, + String path, + bool readOnly, + bool onlyFirstPage, ) { final dynamic json; try { @@ -409,7 +423,8 @@ class EditorCoreInfo { if (json == null) { throw Exception('Failed to parse json from $path'); - } else if (json is List) { // old format + } else if (json is List) { + // old format return EditorCoreInfo.fromOldJson( json, filePath: path, @@ -448,10 +463,10 @@ class EditorCoreInfo { /// Converts the current note as an SBA (Saber Archive) file, /// which contains the main bson file and all the assets /// compressed into a zip file. - /// + /// /// In the archive, the main bson file is named `main.sbn2`, /// and the assets are named `main.sbn2.0`, `main.sbn2.1`, etc. - /// + /// /// If [currentPageIndex] isn't null, /// [initialPageIndex] will be updated to it before saving. Future> saveToSba({ @@ -468,15 +483,14 @@ class EditorCoreInfo { bson.length, bson, )); - + await Future.wait([ for (int i = 0; i < assets.length; ++i) - assets.getBytes(i) - .then((bytes) => archive.addFile(ArchiveFile( - '$filePath.$i', - bytes.length, - bytes, - ))), + assets.getBytes(i).then((bytes) => archive.addFile(ArchiveFile( + '$filePath.$i', + bytes.length, + bytes, + ))), ]); return ZipEncoder().encode(archive)!; @@ -513,7 +527,8 @@ class EditorCoreInfo { return EditorCoreInfo._( filePath: filePath ?? this.filePath, readOnly: readOnly ?? this.readOnly, - readOnlyBecauseOfVersion: readOnlyBecauseOfVersion ?? this.readOnlyBecauseOfVersion, + readOnlyBecauseOfVersion: + readOnlyBecauseOfVersion ?? this.readOnlyBecauseOfVersion, nextImageId: nextImageId ?? this.nextImageId, backgroundColor: backgroundColor ?? this.backgroundColor, backgroundPattern: backgroundPattern ?? this.backgroundPattern, diff --git a/lib/data/editor/editor_exporter.dart b/lib/data/editor/editor_exporter.dart index ea6675680..4c23061ce 100644 --- a/lib/data/editor/editor_exporter.dart +++ b/lib/data/editor/editor_exporter.dart @@ -27,38 +27,36 @@ abstract class EditorExporter { /// - Highlighter strokes, because PDFs don't support transparency /// - Pencil strokes, which need a special shader to look correct static bool _shouldRasterizeStroke(Stroke stroke) { - return stroke.penType == (Highlighter).toString() - || stroke.penType == (Pencil).toString(); + return stroke.penType == (Highlighter).toString() || + stroke.penType == (Pencil).toString(); } - static Future generatePdf(EditorCoreInfo coreInfo, BuildContext context) async { + static Future generatePdf( + EditorCoreInfo coreInfo, BuildContext context) async { coreInfo = coreInfo.copyWith( pages: coreInfo.pages - // don't export the empty last page - .whereIndexed((index, page) => - index != coreInfo.pages.length - 1 || page.isNotEmpty) - .toList(), + // don't export the empty last page + .whereIndexed((index, page) => + index != coreInfo.pages.length - 1 || page.isNotEmpty) + .toList(), ); final pw.Document pdf = pw.Document(); ScreenshotController screenshotController = ScreenshotController(); - List pageScreenshots = await Future.wait( - coreInfo.pages + List pageScreenshots = await Future.wait(coreInfo.pages // screenshot each page .mapIndexed((index, page) => screenshotPage( - coreInfo: coreInfo, - pageIndex: index, - screenshotController: screenshotController, - context: context, - )) - ); + coreInfo: coreInfo, + pageIndex: index, + screenshotController: screenshotController, + context: context, + ))); assert(pageScreenshots.length <= coreInfo.pages.length); for (int pageIndex = 0; pageIndex < pageScreenshots.length; ++pageIndex) { Size pageSize = coreInfo.pages[pageIndex].size; - pdf.addPage( - pw.Page( + pdf.addPage(pw.Page( pageFormat: PdfPageFormat(pageSize.width, pageSize.height), build: (pw.Context context) { return pw.SizedBox( @@ -68,54 +66,64 @@ abstract class EditorExporter { foregroundPainter: (PdfGraphics pdfGraphics, PdfPoint size) { final EditorPage page = coreInfo.pages[pageIndex]; final PdfColor backgroundColor = PdfColor.fromInt( - coreInfo.backgroundColor?.value - ?? InnerCanvas.defaultBackgroundColor.value - ).flatten(); + coreInfo.backgroundColor?.value ?? + InnerCanvas.defaultBackgroundColor.value) + .flatten(); final Iterable strokes = page.strokes - .where((stroke) => !_shouldRasterizeStroke(stroke)); + .where((stroke) => !_shouldRasterizeStroke(stroke)); for (Stroke stroke in strokes) { final bool shapePaint; if (stroke is CircleStroke) { shapePaint = true; pdfGraphics.drawEllipse( - stroke.center.dx, pageSize.height - stroke.center.dy, - stroke.radius, stroke.radius, + stroke.center.dx, + pageSize.height - stroke.center.dy, + stroke.radius, + stroke.radius, clockwise: false, ); } else if (stroke is RectangleStroke) { shapePaint = true; final strokeSize = stroke.strokeProperties.size; pdfGraphics.drawRRect( - stroke.rect.left, pageSize.height - stroke.rect.bottom, - stroke.rect.width, stroke.rect.height, - strokeSize / 4, strokeSize / 4, + stroke.rect.left, + pageSize.height - stroke.rect.bottom, + stroke.rect.width, + stroke.rect.height, + strokeSize / 4, + strokeSize / 4, ); - } else if (stroke.length <= 2) { // a dot + } else if (stroke.length <= 2) { + // a dot shapePaint = false; final bounds = stroke.path.getBounds(); - final radius = max(bounds.size.width, stroke.strokeProperties.size * 0.5) / 2; + final radius = max(bounds.size.width, + stroke.strokeProperties.size * 0.5) / + 2; pdfGraphics.drawEllipse( - bounds.center.dx, pageSize.height - bounds.center.dy, - radius, radius, + bounds.center.dx, + pageSize.height - bounds.center.dy, + radius, + radius, ); } else { shapePaint = false; pdfGraphics.drawShape(stroke.toSvgPath(pageSize)); } - if (shapePaint) { // stroke + if (shapePaint) { + // stroke pdfGraphics.setStrokeColor( PdfColor.fromInt(stroke.strokeProperties.color.value) - .flatten(background: backgroundColor) - ); + .flatten(background: backgroundColor)); pdfGraphics.setLineWidth(stroke.strokeProperties.size); pdfGraphics.strokePath(); - } else { // fill + } else { + // fill pdfGraphics.setFillColor( PdfColor.fromInt(stroke.strokeProperties.color.value) - .flatten(background: backgroundColor) - ); + .flatten(background: backgroundColor)); pdfGraphics.fillPath(); } } @@ -127,9 +135,7 @@ abstract class EditorExporter { ), ), ); - } - ) - ); + })); } return pdf; @@ -148,7 +154,8 @@ abstract class EditorExporter { }) async { final pageSize = coreInfo.pages[pageIndex].size; return await screenshotController.captureFromWidget( - Localizations( // needed to avoid errors with Quill, but not actually used + Localizations( + // needed to avoid errors with Quill, but not actually used locale: const Locale('en', 'US'), delegates: GlobalMaterialLocalizations.delegates, child: Theme( @@ -166,11 +173,13 @@ abstract class EditorExporter { isPrint: true, textEditing: false, coreInfo: coreInfo.copyWith( - pages: coreInfo.pages.map((page) => page.copyWith( - strokes: page.strokes - .where((stroke) => _shouldRasterizeStroke(stroke)) - .toList(), - )).toList(), + pages: coreInfo.pages + .map((page) => page.copyWith( + strokes: page.strokes + .where((stroke) => _shouldRasterizeStroke(stroke)) + .toList(), + )) + .toList(), ), currentStroke: null, currentStrokeDetectedShape: null, diff --git a/lib/data/editor/editor_history.dart b/lib/data/editor/editor_history.dart index 93a3b0f5a..f67cb5a92 100644 --- a/lib/data/editor/editor_history.dart +++ b/lib/data/editor/editor_history.dart @@ -71,7 +71,8 @@ class EditorHistory { /// Adds an item to the [_past] stack. void recordChange(EditorHistoryItem item) { - assert(item.type != EditorHistoryItemType.quillUndoneChange, 'EditorHistoryItemType.quillUndoneChange is just a hack to make undoing quill changes easier. It should just be recorded as a quill change.'); + assert(item.type != EditorHistoryItemType.quillUndoneChange, + 'EditorHistoryItemType.quillUndoneChange is just a hack to make undoing quill changes easier. It should just be recorded as a quill change.'); _past.add(item); if (_past.length > maxHistoryLength) _past.removeAt(0); @@ -83,9 +84,12 @@ class EditorHistory { EditorHistoryItem? removeAccidentalStroke() { _isRedoPossible = true; if (_past.isEmpty) return null; - assert(_past.last.type == EditorHistoryItemType.draw, 'Accidental stroke is not a draw'); - assert(_past.last.strokes.length == 1, 'Accidental strokes should be single-stroke'); - assert(_past.last.images.isEmpty, 'Accidental strokes should not contain images'); + assert(_past.last.type == EditorHistoryItemType.draw, + 'Accidental stroke is not a draw'); + assert(_past.last.strokes.length == 1, + 'Accidental strokes should be single-stroke'); + assert(_past.last.images.isEmpty, + 'Accidental strokes should not contain images'); return _past.removeLast(); } @@ -98,6 +102,7 @@ class EditorHistory { bool get canRedo { return _isRedoPossible && _future.isNotEmpty; } + set canRedo(bool isRedoPossible) { _isRedoPossible = isRedoPossible; } @@ -117,12 +122,22 @@ class EditorHistoryItem { this.page, this.quillChange, this.colorChange, - }) : assert(type != EditorHistoryItemType.move || offset != null, 'Offset must be provided for move'), - assert(type != EditorHistoryItemType.deletePage || page != null, 'Page must be provided for deletePage'), - assert(type != EditorHistoryItemType.insertPage || page != null, 'Page must be provided for insertPage'), - assert(type != EditorHistoryItemType.quillChange || quillChange != null, 'Quill change must be provided for quillChange'), - assert(type != EditorHistoryItemType.quillUndoneChange || quillChange != null, 'Quill change must be provided for quillUndoneChange'), - assert(type != EditorHistoryItemType.changeColor || colorChange?.length == strokes.length, 'colorChange must be provided and contain each of strokes'); + }) : assert(type != EditorHistoryItemType.move || offset != null, + 'Offset must be provided for move'), + assert(type != EditorHistoryItemType.deletePage || page != null, + 'Page must be provided for deletePage'), + assert(type != EditorHistoryItemType.insertPage || page != null, + 'Page must be provided for insertPage'), + assert(type != EditorHistoryItemType.quillChange || quillChange != null, + 'Quill change must be provided for quillChange'), + assert( + type != EditorHistoryItemType.quillUndoneChange || + quillChange != null, + 'Quill change must be provided for quillUndoneChange'), + assert( + type != EditorHistoryItemType.changeColor || + colorChange?.length == strokes.length, + 'colorChange must be provided and contain each of strokes'); final EditorHistoryItemType type; final int pageIndex; diff --git a/lib/data/editor/page.dart b/lib/data/editor/page.dart index 1d9e8566f..fdcc307f0 100644 --- a/lib/data/editor/page.dart +++ b/lib/data/editor/page.dart @@ -22,8 +22,8 @@ class EditorPage extends Listenable { late final CanvasKey innerCanvasKey = CanvasKey(); RenderBox? _renderBox; RenderBox? get renderBox { - return _renderBox - ??= innerCanvasKey.currentState?.context.findRenderObject() as RenderBox?; + return _renderBox ??= + innerCanvasKey.currentState?.context.findRenderObject() as RenderBox?; } bool _isRendered = false; @@ -45,33 +45,35 @@ class EditorPage extends Listenable { EditorImage? backgroundImage; - bool get isEmpty => strokes.isEmpty - && images.isEmpty - && quill.controller.document.isEmpty() - && backgroundImage == null; + bool get isEmpty => + strokes.isEmpty && + images.isEmpty && + quill.controller.document.isEmpty() && + backgroundImage == null; bool get isNotEmpty => !isEmpty; EditorPage({ Size? size, double? width, double? height, - List? strokes, List? images, QuillStruct? quill, this.backgroundImage, - }): assert((size == null) || (width == null && height == null), - "size and width/height shouldn't both be specified"), - size = size ?? Size(width ?? defaultWidth, height ?? defaultHeight), - strokes = strokes ?? [], - laserStrokes = [], - images = images ?? [], - quill = quill ?? QuillStruct( - controller: QuillController.basic(), - focusNode: FocusNode(debugLabel: 'Quill Focus Node'), - ); + }) : assert((size == null) || (width == null && height == null), + "size and width/height shouldn't both be specified"), + size = size ?? Size(width ?? defaultWidth, height ?? defaultHeight), + strokes = strokes ?? [], + laserStrokes = [], + images = images ?? [], + quill = quill ?? + QuillStruct( + controller: QuillController.basic(), + focusNode: FocusNode(debugLabel: 'Quill Focus Node'), + ); - factory EditorPage.fromJson(Map json, { + factory EditorPage.fromJson( + Map json, { required List? inlineAssets, required bool readOnly, required int fileVersion, @@ -94,10 +96,12 @@ class EditorPage extends Listenable { assetCache: assetCache, ), quill: QuillStruct( - controller: json['q'] != null ? QuillController( - document: Document.fromJson(json['q'] as List), - selection: const TextSelection.collapsed(offset: 0), - ) : QuillController.basic(), + controller: json['q'] != null + ? QuillController( + document: Document.fromJson(json['q'] as List), + selection: const TextSelection.collapsed(offset: 0), + ) + : QuillController.basic(), focusNode: FocusNode(debugLabel: 'Quill Focus Node'), ), backgroundImage: json['b'] != null @@ -113,17 +117,15 @@ class EditorPage extends Listenable { } Map toJson(OrderedAssetCache assets) => { - 'w': size.width, - 'h': size.height, - if (strokes.isNotEmpty) - 's': strokes, - if (images.isNotEmpty) - 'i': images.map((image) => image.toJson(assets)).toList(), - if (!quill.controller.document.isEmpty()) - 'q': quill.controller.document.toDelta().toJson(), - if (backgroundImage != null) - 'b': backgroundImage?.toJson(assets) - }; + 'w': size.width, + 'h': size.height, + if (strokes.isNotEmpty) 's': strokes, + if (images.isNotEmpty) + 'i': images.map((image) => image.toJson(assets)).toList(), + if (!quill.controller.document.isEmpty()) + 'q': quill.controller.document.toDelta().toJson(), + if (backgroundImage != null) 'b': backgroundImage?.toJson(assets) + }; /// Inserts a stroke, while keeping the strokes sorted by /// pen type and color. @@ -136,8 +138,9 @@ class EditorPage extends Listenable { int color = stroke.strokeProperties.color.value; if (penTypeComparison > 0) { break; // this stroke's pen type comes after the new stroke's pen type - } else if (stroke.penType == (Highlighter).toString() - && penTypeComparison == 0 && color > newStrokeColor) { + } else if (stroke.penType == (Highlighter).toString() && + penTypeComparison == 0 && + color > newStrokeColor) { break; // this highlighter color comes after the new highlighter color } index++; @@ -145,63 +148,73 @@ class EditorPage extends Listenable { strokes.insert(index, newStroke); } + /// Sorts the strokes by pen type and color. void sortStrokes() { strokes.sort((Stroke a, Stroke b) { int penTypeComparison = a.penType.compareTo(b.penType); if (penTypeComparison != 0) return penTypeComparison; if (a.penType != (Highlighter).toString()) return 0; - return a.strokeProperties.color.value.compareTo(b.strokeProperties.color.value); + return a.strokeProperties.color.value + .compareTo(b.strokeProperties.color.value); }); } - static List parseStrokesJson(List? strokes, { + static List parseStrokesJson( + List? strokes, { required bool onlyFirstPage, required int fileVersion, - }) => strokes - ?.map((dynamic stroke) { - final map = stroke as Map; - if (onlyFirstPage && map['i'] > 0) return null; - return Stroke.fromJson(map, fileVersion); - }) - .where((element) => element != null) - .cast() - .toList() ?? []; + }) => + strokes + ?.map((dynamic stroke) { + final map = stroke as Map; + if (onlyFirstPage && map['i'] > 0) return null; + return Stroke.fromJson(map, fileVersion); + }) + .where((element) => element != null) + .cast() + .toList() ?? + []; - static List parseImagesJson(List? images, { + static List parseImagesJson( + List? images, { required List? inlineAssets, required bool isThumbnail, required bool onlyFirstPage, required String sbnPath, required AssetCache assetCache, - }) => images - ?.cast>() - .map((Map image) { - if (onlyFirstPage && image['i'] > 0) return null; - return parseImageJson( - image, - inlineAssets: inlineAssets, - isThumbnail: isThumbnail, - sbnPath: sbnPath, - assetCache: assetCache, - ); - }) - .where((element) => element != null) - .cast() - .toList() ?? []; + }) => + images + ?.cast>() + .map((Map image) { + if (onlyFirstPage && image['i'] > 0) return null; + return parseImageJson( + image, + inlineAssets: inlineAssets, + isThumbnail: isThumbnail, + sbnPath: sbnPath, + assetCache: assetCache, + ); + }) + .where((element) => element != null) + .cast() + .toList() ?? + []; - static EditorImage parseImageJson(Map json, { + static EditorImage parseImageJson( + Map json, { required List? inlineAssets, required bool isThumbnail, required String sbnPath, required AssetCache assetCache, - }) => EditorImage.fromJson( - json, - inlineAssets: inlineAssets, - isThumbnail: isThumbnail, - sbnPath: sbnPath, - assetCache: assetCache, - ); + }) => + EditorImage.fromJson( + json, + inlineAssets: inlineAssets, + isThumbnail: isThumbnail, + sbnPath: sbnPath, + assetCache: assetCache, + ); final List _listeners = []; bool _disposed = false; @@ -217,7 +230,8 @@ class EditorPage extends Listenable { @override void addListener(VoidCallback listener) { - if (_disposed) throw Exception('Cannot add listener to disposed EditorPage'); + if (_disposed) + throw Exception('Cannot add listener to disposed EditorPage'); _listeners.add(listener); } @@ -237,13 +251,14 @@ class EditorPage extends Listenable { List? images, QuillStruct? quill, EditorImage? backgroundImage, - }) => EditorPage( - size: size ?? this.size, - strokes: strokes ?? this.strokes, - images: images ?? this.images, - quill: quill ?? this.quill, - backgroundImage: backgroundImage ?? this.backgroundImage, - ); + }) => + EditorPage( + size: size ?? this.size, + strokes: strokes ?? this.strokes, + images: images ?? this.images, + quill: quill ?? this.quill, + backgroundImage: backgroundImage ?? this.backgroundImage, + ); } class QuillStruct { diff --git a/lib/data/extensions/change_notifier_extensions.dart b/lib/data/extensions/change_notifier_extensions.dart index a3ff41626..2703d63b3 100644 --- a/lib/data/extensions/change_notifier_extensions.dart +++ b/lib/data/extensions/change_notifier_extensions.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; extension ChangeNotifierExtensions on ChangeNotifier { /// This is a hack to allow us to call [notifyListeners] /// which is usually protected. - /// + /// /// In my (@adil192) opinion, [notifyListeners] should be public. See /// - https://github.com/flutter/flutter/issues/135478 /// - https://github.com/flutter/flutter/issues/27448 diff --git a/lib/data/extensions/color_extensions.dart b/lib/data/extensions/color_extensions.dart index 1dd8556f5..60400a0e8 100644 --- a/lib/data/extensions/color_extensions.dart +++ b/lib/data/extensions/color_extensions.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; extension ColorExtensions on Color { diff --git a/lib/data/extensions/point_extensions.dart b/lib/data/extensions/point_extensions.dart index 9ccbdf8cb..ce798d4b0 100644 --- a/lib/data/extensions/point_extensions.dart +++ b/lib/data/extensions/point_extensions.dart @@ -5,15 +5,17 @@ import 'package:bson/bson.dart'; import 'package:perfect_freehand/perfect_freehand.dart'; extension PointExtensions on Point { - @Deprecated('Use fromBsonBinary instead; fromJson is only for backward compatibility') + @Deprecated( + 'Use fromBsonBinary instead; fromJson is only for backward compatibility') static Point fromJson({ required Map json, Offset offset = Offset.zero, - }) => Point( - json['x'] + offset.dx, - json['y'] + offset.dy, - json['p'] ?? 0.5, - ); + }) => + Point( + json['x'] + offset.dx, + json['y'] + offset.dy, + json['p'] ?? 0.5, + ); static Point fromBsonBinary({ required BsonBinary json, @@ -37,8 +39,8 @@ extension PointExtensions on Point { } Point operator +(Offset offset) => Point( - x + offset.dx, - y + offset.dy, - p, - ); + x + offset.dx, + y + offset.dy, + p, + ); } diff --git a/lib/data/extensions/string_extensions.dart b/lib/data/extensions/string_extensions.dart index d42b11f4e..fb0dfed50 100644 --- a/lib/data/extensions/string_extensions.dart +++ b/lib/data/extensions/string_extensions.dart @@ -12,8 +12,7 @@ extension StringExtensions on String { /// Pre-calculate all replacements asynchronously final replacements = await Future.wait([ - for (Match match in matches) - replace(match), + for (Match match in matches) replace(match), ]); int stringIndex = 0; diff --git a/lib/data/file_manager/file_manager.dart b/lib/data/file_manager/file_manager.dart index 6456fb417..ef583d04d 100644 --- a/lib/data/file_manager/file_manager.dart +++ b/lib/data/file_manager/file_manager.dart @@ -27,7 +27,8 @@ class FileManager { @visibleForTesting static late final String documentsDirectory; - static final StreamController fileWriteStream = StreamController.broadcast( + static final StreamController fileWriteStream = + StreamController.broadcast( onListen: () => _fileWriteStreamIsListening = true, onCancel: () => _fileWriteStreamIsListening = false, ); @@ -42,7 +43,8 @@ class FileManager { static final assetFileRegex = RegExp(r'\.sbn2?\.\d+$'); static Future init() async { - documentsDirectory = '${(await getApplicationDocumentsDirectory()).path}/$appRootDirectoryPrefix'; + documentsDirectory = + '${(await getApplicationDocumentsDirectory()).path}/$appRootDirectoryPrefix'; unawaited(watchRootDirectory()); } @@ -51,9 +53,9 @@ class FileManager { Directory rootDir = Directory(documentsDirectory); await rootDir.create(recursive: true); rootDir.watch(recursive: true).listen((FileSystemEvent event) { - final type = event.type == FileSystemEvent.create - || event.type == FileSystemEvent.modify - || event.type == FileSystemEvent.move + final type = event.type == FileSystemEvent.create || + event.type == FileSystemEvent.modify || + event.type == FileSystemEvent.move ? FileOperationType.write : FileOperationType.delete; String path = event.path @@ -111,13 +113,15 @@ class FileManager { if (shouldUseRawFilePath) { return File(filePath); } else { - assert(filePath.startsWith('/'), 'Expected filePath to start with a slash, got $filePath'); + assert(filePath.startsWith('/'), + 'Expected filePath to start with a slash, got $filePath'); return File(documentsDirectory + filePath); } } /// Writes [toWrite] to [filePath]. - static Future writeFile(String filePath, List toWrite, { bool awaitWrite = false, bool alsoUpload = true }) async { + static Future writeFile(String filePath, List toWrite, + {bool awaitWrite = false, bool alsoUpload = true}) async { filePath = _sanitisePath(filePath); log.fine('Writing to $filePath'); @@ -131,12 +135,12 @@ class FileManager { // if we're using a new format, also delete the old file if (filePath.endsWith(Editor.extension)) getFile( - '${filePath.substring(0, filePath.length - Editor.extension.length)}' - '${Editor.extensionOldJson}' - ) + '${filePath.substring(0, filePath.length - Editor.extension.length)}' + '${Editor.extensionOldJson}') .delete() // ignore if the file doesn't exist - .catchError((_) => File(''), test: (e) => e is PathNotFoundException), + .catchError((_) => File(''), + test: (e) => e is PathNotFoundException), ]); void afterWrite() { @@ -144,9 +148,8 @@ class FileManager { if (alsoUpload) FileSyncer.addToUploadQueue(filePath); if (filePath.endsWith(Editor.extension)) { _removeReferences( - '${filePath.substring(0, filePath.length - Editor.extension.length)}' - '${Editor.extensionOldJson}' - ); + '${filePath.substring(0, filePath.length - Editor.extension.length)}' + '${Editor.extensionOldJson}'); } } @@ -161,7 +164,8 @@ class FileManager { await dir.create(recursive: true); } - static Future exportFile(String fileName, List bytes, {bool isImage = false}) async { + static Future exportFile(String fileName, List bytes, + {bool isImage = false}) async { File? tempFile; Future getTempFile() async { final String tempFolder = (await getTemporaryDirectory()).path; @@ -171,17 +175,20 @@ class FileManager { } if (Platform.isAndroid || Platform.isIOS) { - if (isImage) { // save image to gallery + if (isImage) { + // save image to gallery await ImageSave.saveImage( Uint8List.fromList(bytes), fileName, albumName: 'Saber', ); - } else { // share file + } else { + // share file tempFile = await getTempFile(); await Share.shareXFiles([XFile(tempFile.path)]); } - } else { // desktop, open save-as dialog + } else { + // desktop, open save-as dialog String? outputFile = await FilePicker.platform.saveFile( fileName: fileName, initialDirectory: (await getDownloadsDirectory())?.path, @@ -206,16 +213,19 @@ class FileManager { /// If [replaceExistingFile] is true but the file is a reserved file name, /// the filename will be suffixed with a number instead /// (like if [replaceExistingFile] was false). - static Future moveFile(String fromPath, String toPath, {bool replaceExistingFile = false, bool alsoMoveAssets = true}) async { + static Future moveFile(String fromPath, String toPath, + {bool replaceExistingFile = false, bool alsoMoveAssets = true}) async { fromPath = _sanitisePath(fromPath); toPath = _sanitisePath(toPath); - if (!toPath.contains('/')) { // if toPath is a relative path + if (!toPath.contains('/')) { + // if toPath is a relative path toPath = fromPath.substring(0, fromPath.lastIndexOf('/') + 1) + toPath; } if (!replaceExistingFile || Editor.isReservedPath(toPath)) { - toPath = await suffixFilePathToMakeItUnique(toPath, currentPath: fromPath); + toPath = + await suffixFilePathToMakeItUnique(toPath, currentPath: fromPath); } if (fromPath == toPath) return toPath; @@ -260,7 +270,8 @@ class FileManager { return toPath; } - static Future deleteFile(String filePath, {bool alsoUpload = true, bool alsoDeleteAssets = true}) async { + static Future deleteFile(String filePath, + {bool alsoUpload = true, bool alsoDeleteAssets = true}) async { filePath = _sanitisePath(filePath); final File file = getFile(filePath); @@ -284,13 +295,13 @@ class FileManager { } await Future.wait([ - for (final assetNumber in assets) - deleteFile('$filePath.$assetNumber'), + for (final assetNumber in assets) deleteFile('$filePath.$assetNumber'), ]); } } - static Future removeUnusedAssets(String filePath, {required int numAssets}) async { + static Future removeUnusedAssets(String filePath, + {required int numAssets}) async { final futures = []; for (int assetNumber = numAssets; true; assetNumber++) { @@ -319,7 +330,9 @@ class FileManager { } } - final String newPath = directoryPath.substring(0, directoryPath.lastIndexOf('/') + 1) + newName; + final String newPath = + directoryPath.substring(0, directoryPath.lastIndexOf('/') + 1) + + newName; await directory.rename(documentsDirectory + newPath); for (final child in children) { @@ -329,7 +342,8 @@ class FileManager { } } - static Future deleteDirectory(String directoryPath, [bool recursive = true]) async { + static Future deleteDirectory(String directoryPath, + [bool recursive = true]) async { directoryPath = _sanitisePath(directoryPath); final Directory directory = Directory(documentsDirectory + directoryPath); @@ -347,7 +361,8 @@ class FileManager { await directory.delete(recursive: recursive); } - static Future getChildrenOfDirectory(String directory) async { + static Future getChildrenOfDirectory( + String directory) async { directory = _sanitisePath(directory); if (!directory.endsWith('/')) directory += '/'; @@ -357,28 +372,36 @@ class FileManager { final Directory dir = Directory(documentsDirectory + directory); if (!dir.existsSync()) return null; - int directoryPrefixLength = directory.endsWith('/') ? directory.length : directory.length + 1; // +1 for the trailing slash - allChildren = await dir.list() - .map((FileSystemEntity entity) { - String filePath = entity.path.substring(documentsDirectory.length); + int directoryPrefixLength = directory.endsWith('/') + ? directory.length + : directory.length + 1; // +1 for the trailing slash + allChildren = await dir + .list() + .map((FileSystemEntity entity) { + String filePath = entity.path.substring(documentsDirectory.length); - // remove extension - if (filePath.endsWith(Editor.extension)) { - filePath = filePath.substring(0, filePath.length - Editor.extension.length); - } else if (filePath.endsWith(Editor.extensionOldJson)) { - filePath = filePath.substring(0, filePath.length - Editor.extensionOldJson.length); - } + // remove extension + if (filePath.endsWith(Editor.extension)) { + filePath = filePath.substring( + 0, filePath.length - Editor.extension.length); + } else if (filePath.endsWith(Editor.extensionOldJson)) { + filePath = filePath.substring( + 0, filePath.length - Editor.extensionOldJson.length); + } - if (Editor.isReservedPath(filePath)) return null; // filter out reserved files + if (Editor.isReservedPath(filePath)) + return null; // filter out reserved files - return filePath.substring(directoryPrefixLength); // remove directory prefix - }) - .where((String? file) => file != null) - .cast() - .toList(); + return filePath + .substring(directoryPrefixLength); // remove directory prefix + }) + .where((String? file) => file != null) + .cast() + .toList(); await Future.wait(allChildren.map((child) async { - if (await FileManager.isDirectory(directory + child) && !directories.contains(child)) { + if (await FileManager.isDirectory(directory + child) && + !directories.contains(child)) { directories.add(child); } else if (assetFileRegex.hasMatch(child)) { // if the file is an asset, don't add it to the list of files @@ -398,7 +421,7 @@ class FileManager { final directory = directories.removeLast(); final children = await getChildrenOfDirectory(directory); if (children == null) continue; - + for (final file in children.files) { allFiles.add('$directory$file'); } @@ -415,14 +438,17 @@ class FileManager { return Prefs.recentFiles.value .map((String filePath) { if (filePath.endsWith(Editor.extension)) { - return filePath.substring(0, filePath.length - Editor.extension.length); + return filePath.substring( + 0, filePath.length - Editor.extension.length); } else if (filePath.endsWith(Editor.extensionOldJson)) { - return filePath.substring(0, filePath.length - Editor.extensionOldJson.length); + return filePath.substring( + 0, filePath.length - Editor.extensionOldJson.length); } else { return filePath; } }) - .where((String file) => !Editor.isReservedPath(file)) // filter out reserved file names + .where((String file) => + !Editor.isReservedPath(file)) // filter out reserved file names .toList(); } @@ -461,7 +487,7 @@ class FileManager { /// /// Providing a [currentPath] means that e.g. "/Untitled (2)" being renamed /// to "/Untitled" will be returned as "/Untitled (2)" not "/Untitled (3)". - /// + /// /// If [currentPath] is provided, it must /// end with [Editor.extension] or [Editor.extensionOldJson]. static Future suffixFilePathToMakeItUnique( @@ -473,12 +499,14 @@ class FileManager { bool hasExtension = false; if (filePath.endsWith(Editor.extension)) { - filePath = filePath.substring(0, filePath.length - Editor.extension.length); + filePath = + filePath.substring(0, filePath.length - Editor.extension.length); newFilePath = filePath; hasExtension = true; intendedExtension ??= Editor.extension; } else if (filePath.endsWith(Editor.extensionOldJson)) { - filePath = filePath.substring(0, filePath.length - Editor.extensionOldJson.length); + filePath = filePath.substring( + 0, filePath.length - Editor.extensionOldJson.length); newFilePath = filePath; hasExtension = true; intendedExtension ??= Editor.extensionOldJson; @@ -488,10 +516,8 @@ class FileManager { int i = 1; while (true) { - if ( - !await doesFileExist(newFilePath + Editor.extension) && - !await doesFileExist(newFilePath + Editor.extensionOldJson) - ) break; + if (!await doesFileExist(newFilePath + Editor.extension) && + !await doesFileExist(newFilePath + Editor.extensionOldJson)) break; if (newFilePath + Editor.extension == currentPath) break; if (newFilePath + Editor.extensionOldJson == currentPath) break; i++; @@ -502,15 +528,17 @@ class FileManager { } /// Imports a file from a sharing intent. - /// + /// /// [parentDir], if provided, must start and end with a slash. - /// + /// /// [extension], if provided, must start with a dot. /// If not provided, it will be inferred from the [path]. - /// + /// /// Returns the file path of the imported file. - static Future importFile(String path, String? parentDir, {String? extension, bool awaitWrite = true}) async { - assert(parentDir == null || parentDir.startsWith('/') && parentDir.endsWith('/')); + static Future importFile(String path, String? parentDir, + {String? extension, bool awaitWrite = true}) async { + assert(parentDir == null || + parentDir.startsWith('/') && parentDir.endsWith('/')); if (extension == null) { extension = '.${path.split('.').last}'; @@ -578,7 +606,8 @@ class FileManager { ), ); } - } else { // import sbn or sbn2 + } else { + // import sbn or sbn2 final file = File(path); final fileContents = await file.readAsBytes(); importedPath = await suffixFilePathToMakeItUnique( @@ -593,18 +622,19 @@ class FileManager { ), ); } - + await Future.wait(writeFutures); return importedPath; } - /// Creates the parent directories of filePath if they don't exist. static Future _createFileDirectory(String filePath) async { assert(filePath.contains('/'), 'filePath must be a path, not a file name'); - final String parentDirectory = filePath.substring(0, filePath.lastIndexOf('/')); - await Directory(documentsDirectory + parentDirectory).create(recursive: true); + final String parentDirectory = + filePath.substring(0, filePath.lastIndexOf('/')); + await Directory(documentsDirectory + parentDirectory) + .create(recursive: true); } static Future _renameReferences(String fromPath, String toPath) async { @@ -624,6 +654,7 @@ class FileManager { } Prefs.recentFiles.notifyListeners(); } + static Future _removeReferences(String filePath) async { // remove file from recently accessed for (int i = 0; i < Prefs.recentFiles.value.length; i++) { @@ -639,7 +670,8 @@ class FileManager { Prefs.recentFiles.value.remove(filePath); Prefs.recentFiles.value.insert(0, filePath); - if (Prefs.recentFiles.value.length > maxRecentlyAccessedFiles) Prefs.recentFiles.value.removeLast(); + if (Prefs.recentFiles.value.length > maxRecentlyAccessedFiles) + Prefs.recentFiles.value.removeLast(); Prefs.recentFiles.notifyListeners(); } @@ -663,6 +695,7 @@ enum FileOperationType { write, delete, } + class FileOperation { final FileOperationType type; final String filePath; diff --git a/lib/data/flavor_config.dart b/lib/data/flavor_config.dart index 3a9077cb2..7f4c1b28b 100644 --- a/lib/data/flavor_config.dart +++ b/lib/data/flavor_config.dart @@ -8,9 +8,11 @@ class FlavorConfig { static String get appStore => _appStore; static late final bool _shouldCheckForUpdatesByDefault; - static bool get shouldCheckForUpdatesByDefault => _shouldCheckForUpdatesByDefault; + static bool get shouldCheckForUpdatesByDefault => + _shouldCheckForUpdatesByDefault; static late final bool _dirty; + /// If a build is dirty, it has commits that are ahead of the latest release. static bool get dirty => _dirty; diff --git a/lib/data/locales.dart b/lib/data/locales.dart index 610508f12..8b7d53ca8 100644 --- a/lib/data/locales.dart +++ b/lib/data/locales.dart @@ -7,18 +7,18 @@ const Map localeNames = { 'en': 'English', 'ar': 'العربية', - 'cs' : 'čeština', - 'de' : 'Deutsch', - 'es' : 'español', - 'fa' : 'فارسی', - 'fr' : 'français', + 'cs': 'čeština', + 'de': 'Deutsch', + 'es': 'español', + 'fa': 'فارسی', + 'fr': 'français', 'he': 'עברית', 'hu': 'magyar', - 'it' : 'italiano', + 'it': 'italiano', 'ja': '日本語', - 'pt-BR' : 'português (Brasil)', - 'ru' : 'русский', - 'tr' : 'Türkçe', - 'zh-Hans-CN' : '中文 (简体中文, 中国)', - 'zh-Hant-TW' : '中文 (繁體, 台灣)', + 'pt-BR': 'português (Brasil)', + 'ru': 'русский', + 'tr': 'Türkçe', + 'zh-Hans-CN': '中文 (简体中文, 中国)', + 'zh-Hant-TW': '中文 (繁體, 台灣)', }; diff --git a/lib/data/nextcloud/file_syncer.dart b/lib/data/nextcloud/file_syncer.dart index a390e65ac..e8805156d 100644 --- a/lib/data/nextcloud/file_syncer.dart +++ b/lib/data/nextcloud/file_syncer.dart @@ -29,7 +29,8 @@ abstract class FileSyncer { static final ChangeNotifier uploadNotifier = ChangeNotifier(); static final ValueNotifier filesDone = ValueNotifier(null); - static int get filesToSync => _uploadQueue.value.length + _downloadQueue.length; + static int get filesToSync => + _uploadQueue.value.length + _downloadQueue.length; static const int filesDoneLimit = 100000000; /// We write [deletedFileDummyContent] to a deleted file on the cloud @@ -63,21 +64,25 @@ abstract class FileSyncer { // Get list of remote files from server List remoteFiles; try { - remoteFiles = await _client!.webdav.propfind( - PathUri.parse(FileManager.appRootDirectoryPrefix), - prop: WebDavPropWithoutValues.fromBools( - davgetcontentlength: true, - davgetlastmodified: true, - ), - ).then((multistatus) => multistatus.toWebDavFiles()); + remoteFiles = await _client!.webdav + .propfind( + PathUri.parse(FileManager.appRootDirectoryPrefix), + prop: WebDavPropWithoutValues.fromBools( + davgetcontentlength: true, + davgetlastmodified: true, + ), + ) + .then((multistatus) => multistatus.toWebDavFiles()); } on DynamiteApiException catch (e) { if (e.statusCode == HttpStatus.notFound) { log.info('startSync: App directory doesn\'t exist; creating it', e); - await _client!.webdav.mkcol(PathUri.parse(FileManager.appRootDirectoryPrefix)); + await _client!.webdav + .mkcol(PathUri.parse(FileManager.appRootDirectoryPrefix)); log.fine('startSync: Generating config'); - await _client!.getConfig() - .then((config) => _client!.generateConfig(config: config)) - .then((config) => _client!.setConfig(config)); + await _client! + .getConfig() + .then((config) => _client!.generateConfig(config: config)) + .then((config) => _client!.setConfig(config)); remoteFiles = []; } else { log.severe('Failed to get list of remote files: $e', e); @@ -86,7 +91,8 @@ abstract class FileSyncer { if (kDebugMode) rethrow; return; } - } on SocketException catch (e) { // network error + } on SocketException catch (e) { + // network error log.warning('startSync: Network error: $e', e); filesDone.value = filesDoneLimit; downloadCancellable.cancelled = true; @@ -98,8 +104,8 @@ abstract class FileSyncer { log.finer('startSync: Adding files to download queue'); await Future.wait( remoteFiles.map((WebDavFile file) { - return _addToDownloadQueue(file) - .catchError((e) => log.severe('Failed to add ${file.name} to download queue: $e', e)); + return _addToDownloadQueue(file).catchError((e) => + log.severe('Failed to add ${file.name} to download queue: $e', e)); }), ); log.finer('startSync: Sorting download queue'); @@ -113,7 +119,8 @@ abstract class FileSyncer { // Start downloading files one by one while (_downloadQueue.isNotEmpty) { final SyncFile file = _downloadQueue.removeFirst(); - log.finer('startSync: Downloading ${file.localPath} (${file.remotePath})'); + log.finer( + 'startSync: Downloading ${file.localPath} (${file.remotePath})'); final bool success = await downloadFile(file); if (downloadCancellable.cancelled) return; if (success) { @@ -173,7 +180,8 @@ abstract class FileSyncer { priority: WorkPriority.veryHigh, ); - final syncFile = SyncFile(encryptedName: encryptedName, localPath: filePathUnencrypted); + final syncFile = SyncFile( + encryptedName: encryptedName, localPath: filePathUnencrypted); if (!await _shouldLocalFileBeKept(syncFile, inUploadQueue: true)) { // remote file is newer; download it instead _downloadQueue.add(syncFile); @@ -184,7 +192,8 @@ abstract class FileSyncer { final Uint8List localDataEncrypted; if (await FileManager.doesFileExist(filePathUnencrypted)) { - Uint8List? localDataUnencrypted = await FileManager.readFile(filePathUnencrypted); + Uint8List? localDataUnencrypted = + await FileManager.readFile(filePathUnencrypted); if (localDataUnencrypted == null) { log.severe('Failed to read file $filePathUnencrypted to upload'); return; @@ -202,14 +211,16 @@ abstract class FileSyncer { } else { localDataEncrypted = await workerManager.execute( () async { - final encrypted = encrypter.encryptBytes(localDataUnencrypted, iv: iv); + final encrypted = + encrypter.encryptBytes(localDataUnencrypted, iv: iv); return encrypted.bytes; }, priority: WorkPriority.highRegular, ); } - log.info('Uploading $filePathUnencrypted (${syncFile.remotePath}): ${localDataEncrypted.length} bytes'); + log.info( + 'Uploading $filePathUnencrypted (${syncFile.remotePath}): ${localDataEncrypted.length} bytes'); } else { localDataEncrypted = utf8.encode(deletedFileDummyContent); } @@ -227,7 +238,8 @@ abstract class FileSyncer { PathUri.parse(syncFile.remotePath), lastModified: lastModified, ); - } on SocketException catch (e) { // network error + } on SocketException catch (e) { + // network error log.warning('Failed to upload $filePathUnencrypted: network error', e); _uploadQueue.value.add(filePathUnencrypted); await Future.delayed(const Duration(seconds: 2)); @@ -250,7 +262,8 @@ abstract class FileSyncer { // remove extension final String encryptedName; if (file.name.endsWith(encExtension)) { - encryptedName = file.name.substring(0, file.name.length - encExtension.length); + encryptedName = + file.name.substring(0, file.name.length - encExtension.length); } else { log.info('remote file not in recognised encrypted format: ${file.path}'); return; @@ -291,23 +304,28 @@ abstract class FileSyncer { final files = _downloadQueue.toSet(); for (final file in files) { - late final knownCorrupted = Prefs.fileSyncCorruptFiles.value.contains(file.localPath); + late final knownCorrupted = + Prefs.fileSyncCorruptFiles.value.contains(file.localPath); late final deleted = (file.webDavFile!.size ?? 0) == 0; if (knownCorrupted) { corruptedFiles.add(file); } else if (deleted) { - final alreadyDeleted = Prefs.fileSyncAlreadyDeleted.value.contains(file.localPath); + final alreadyDeleted = + Prefs.fileSyncAlreadyDeleted.value.contains(file.localPath); if (!alreadyDeleted) deletedFiles.add(file); } else { newFiles.add(file); } } - log.info(() => 'New files: ${newFiles.length}, deleted files: ${deletedFiles.length}, corrupted files: ${corruptedFiles.length}'); + log.info(() => + 'New files: ${newFiles.length}, deleted files: ${deletedFiles.length}, corrupted files: ${corruptedFiles.length}'); log.fine(() => 'New files: ${newFiles.map((file) => file.localPath)}'); - log.fine(() => 'Deleted files: ${deletedFiles.map((file) => file.localPath)}'); - log.fine(() => 'Corrupted files: ${corruptedFiles.map((file) => file.localPath)}'); + log.fine( + () => 'Deleted files: ${deletedFiles.map((file) => file.localPath)}'); + log.fine(() => + 'Corrupted files: ${corruptedFiles.map((file) => file.localPath)}'); _downloadQueue.clear(); _downloadQueue.addAll(newFiles); @@ -315,13 +333,16 @@ abstract class FileSyncer { _downloadQueue.addAll(corruptedFiles); // forget un-deleted files that were previously deleted locally - Prefs.fileSyncAlreadyDeleted.value.removeWhere((filePath) => !deletedFiles.any((file) => file.localPath == filePath)); + Prefs.fileSyncAlreadyDeleted.value.removeWhere( + (filePath) => !deletedFiles.any((file) => file.localPath == filePath)); Prefs.fileSyncAlreadyDeleted.notifyListeners(); } @visibleForTesting - static Future downloadFile(SyncFile file, { bool awaitWrite = false }) async { - if (file.webDavFile!.size == 0) { // deleted file + static Future downloadFile(SyncFile file, + {bool awaitWrite = false}) async { + if (file.webDavFile!.size == 0) { + // deleted file FileManager.deleteFile(file.localPath); Prefs.fileSyncAlreadyDeleted.value.add(file.localPath); Prefs.fileSyncAlreadyDeleted.notifyListeners(); @@ -330,7 +351,8 @@ abstract class FileSyncer { final Uint8List encryptedDataBytes; try { - encryptedDataBytes = await _client!.webdav.get(PathUri.parse(file.remotePath)); + encryptedDataBytes = + await _client!.webdav.get(PathUri.parse(file.remotePath)); } on DynamiteApiException { return false; } @@ -343,7 +365,8 @@ abstract class FileSyncer { if (file.localPath.endsWith(Editor.extensionOldJson)) { decryptedData = await workerManager.execute( () async { - final String encrypted = utf8.decode(encryptedDataBytes.cast()); + final String encrypted = + utf8.decode(encryptedDataBytes.cast()); final String decrypted = encrypter.decrypt64(encrypted, iv: iv); return utf8.encode(decrypted); }, @@ -353,24 +376,30 @@ abstract class FileSyncer { decryptedData = await workerManager.execute( () async { final Encrypted encrypted = Encrypted(encryptedDataBytes); - final List decrypted = encrypter.decryptBytes(encrypted, iv: iv); + final List decrypted = + encrypter.decryptBytes(encrypted, iv: iv); return Uint8List.fromList(decrypted); }, priority: WorkPriority.regular, ); } - assert(decryptedData.isNotEmpty, 'Decrypted data is empty but file.webDavFile!.size is ${file.webDavFile!.size}'); - FileManager.writeFile(file.localPath, decryptedData, awaitWrite: awaitWrite, alsoUpload: false); + assert(decryptedData.isNotEmpty, + 'Decrypted data is empty but file.webDavFile!.size is ${file.webDavFile!.size}'); + FileManager.writeFile(file.localPath, decryptedData, + awaitWrite: awaitWrite, alsoUpload: false); return true; } catch (e) { - log.severe('Failed to download file ${file.localPath} (${file.remotePath}): $e', e); + log.severe( + 'Failed to download file ${file.localPath} (${file.remotePath}): $e', + e); return false; } } /// Decides if the local or remote version of a file should be kept /// by comparing the last modified date of each file. - static Future _shouldLocalFileBeKept(SyncFile file, {bool inUploadQueue = false}) async { + static Future _shouldLocalFileBeKept(SyncFile file, + {bool inUploadQueue = false}) async { // if local file doesn't exist, keep remote (unless we're "uploading" a file that we want to delete) if (!await FileManager.doesFileExist(file.localPath)) { return inUploadQueue; @@ -378,14 +407,16 @@ abstract class FileSyncer { // get remote file try { - file.webDavFile ??= await _client!.webdav.propfind( - PathUri.parse(file.remotePath), - depth: WebDavDepth.zero, - prop: WebDavPropWithoutValues.fromBools( - davgetlastmodified: true, - davgetcontentlength: true, - ), - ).then((multistatus) => multistatus.toWebDavFiles().single); + file.webDavFile ??= await _client!.webdav + .propfind( + PathUri.parse(file.remotePath), + depth: WebDavDepth.zero, + prop: WebDavPropWithoutValues.fromBools( + davgetlastmodified: true, + davgetcontentlength: true, + ), + ) + .then((multistatus) => multistatus.toWebDavFiles().single); } catch (e) { // remote file doesn't exist; keep local return true; @@ -393,18 +424,20 @@ abstract class FileSyncer { // If we've prompted a full resync at [resyncEverythingDate] // keep the local file if it was modified before [resyncEverythingDate] - final resyncEverythingDate = Prefs.fileSyncResyncEverythingDate.value; final DateTime? lastModifiedRemote = file.webDavFile!.lastModified; - if (inUploadQueue && resyncEverythingDate != null && file.webDavFile!.lastModified != null) { + if (inUploadQueue && file.webDavFile!.lastModified != null) { final lastModifiedRemote = file.webDavFile!.lastModified!; - if (lastModifiedRemote.isBefore(resyncEverythingDate)) { + if (lastModifiedRemote + .isBefore(Prefs.fileSyncResyncEverythingDate.value)) { return true; } } // file exists locally, check if it's newer - final DateTime lastModifiedLocal = await FileManager.lastModified(file.localPath); - if (lastModifiedRemote != null && lastModifiedRemote.isAfter(lastModifiedLocal)) { + final DateTime lastModifiedLocal = + await FileManager.lastModified(file.localPath); + if (lastModifiedRemote != null && + lastModifiedRemote.isAfter(lastModifiedLocal)) { // remote is newer; keep remote return false; } else { @@ -428,11 +461,11 @@ class SyncFile { required this.encryptedName, required this.localPath, this.webDavFile, - }): assert(encryptedName.isNotEmpty), - assert(!encryptedName.contains('/')), - assert(!encryptedName.contains('\\')), - assert(!encryptedName.contains('.')), - assert(localPath.isNotEmpty); + }) : assert(encryptedName.isNotEmpty), + assert(!encryptedName.contains('/')), + assert(!encryptedName.contains('\\')), + assert(!encryptedName.contains('.')), + assert(localPath.isNotEmpty); } class CancellableStruct { diff --git a/lib/data/nextcloud/nc_http_overrides.dart b/lib/data/nextcloud/nc_http_overrides.dart index 077a7f261..b773bc8e0 100644 --- a/lib/data/nextcloud/nc_http_overrides.dart +++ b/lib/data/nextcloud/nc_http_overrides.dart @@ -12,7 +12,7 @@ class NcHttpOverrides extends HttpOverrides { static final log = Logger('NcHttpOverrides'); @override - HttpClient createHttpClient(SecurityContext? context){ + HttpClient createHttpClient(SecurityContext? context) { return super.createHttpClient(context) ..badCertificateCallback = _badCertificateCallback; } @@ -22,11 +22,13 @@ class NcHttpOverrides extends HttpOverrides { // false if it should be rejected. bool _badCertificateCallback(X509Certificate cert, String host, int port) { if (!Prefs.allowInsecureConnections.loaded || !Prefs.url.loaded) { - log.severe('The Prefs [allowInsecureConnections] or [url] are not loaded yet. Make sure to await pref.waitUntilLoaded() for both.'); + log.severe( + 'The Prefs [allowInsecureConnections] or [url] are not loaded yet. Make sure to await pref.waitUntilLoaded() for both.'); return false; } - if (host == Uri.tryParse(Prefs.url.value)?.host || host == temporarilyExemptHost) { + if (host == Uri.tryParse(Prefs.url.value)?.host || + host == temporarilyExemptHost) { // Allow self-signed certificates for self-hosted Nextcloud servers return Prefs.allowInsecureConnections.value; } else { diff --git a/lib/data/nextcloud/nextcloud_client_extension.dart b/lib/data/nextcloud/nextcloud_client_extension.dart index 734256179..c1f25aeaf 100644 --- a/lib/data/nextcloud/nextcloud_client_extension.dart +++ b/lib/data/nextcloud/nextcloud_client_extension.dart @@ -11,10 +11,13 @@ import 'package:saber/data/file_manager/file_manager.dart'; import 'package:saber/data/prefs.dart'; extension NextcloudClientExtension on NextcloudClient { - static final Uri defaultNextcloudUri = Uri.parse('https://nc.saber.adil.hanney.org'); + static final Uri defaultNextcloudUri = + Uri.parse('https://nc.saber.adil.hanney.org'); - static const String appRootDirectoryPrefix = FileManager.appRootDirectoryPrefix; - static final PathUri configFileUri = PathUri.parse('$appRootDirectoryPrefix/config.sbc'); + static const String appRootDirectoryPrefix = + FileManager.appRootDirectoryPrefix; + static final PathUri configFileUri = + PathUri.parse('$appRootDirectoryPrefix/config.sbc'); static const _utf8Decoder = Utf8Decoder(allowMalformed: true); @@ -45,12 +48,13 @@ extension NextcloudClientExtension on NextcloudClient { Map bytes = jsonDecode(_utf8Decoder.convert(file)); return bytes.cast(); } + /// Generates a config using known values (i.e. from [Prefs]), /// updating the given [config] in place. - /// + /// /// This is usually preceded by a call to [getConfig] /// and followed by a call to [setConfig]. - /// + /// /// Returns the existing [config] for convenience. Future> generateConfig({ required Map config, @@ -67,6 +71,7 @@ extension NextcloudClientExtension on NextcloudClient { return config; } + /// Uploads the given [config] to Nextcloud Future setConfig(Map config) async { String json = jsonEncode(config); @@ -124,7 +129,8 @@ extension NextcloudClientExtension on NextcloudClient { } Future get encrypter async { - final List encodedPassword = utf8.encode(Prefs.encPassword.value + reproducibleSalt); + final List encodedPassword = + utf8.encode(Prefs.encPassword.value + reproducibleSalt); final List hashedPasswordBytes = sha256.convert(encodedPassword).bytes; final Key passwordKey = Key(hashedPasswordBytes as Uint8List); return Encrypter(AES(passwordKey)); diff --git a/lib/data/prefs.dart b/lib/data/prefs.dart index bc3fda0a7..575009b5b 100644 --- a/lib/data/prefs.dart +++ b/lib/data/prefs.dart @@ -26,10 +26,10 @@ abstract class Prefs { static bool testingMode = false; /// The current Android version. - /// + /// /// If the user is on Android 9 or older, we can't use /// platform views (e.g. ads) performantly. - /// + /// /// If the device is not an Android device, this will be 9999. static int androidVersion = 9999; @@ -44,8 +44,10 @@ abstract class Prefs { static late final EncPref allowInsecureConnections; static late final EncPref url; static late final EncPref username; + /// the password used to login to Nextcloud static late final EncPref ncPassword; + /// the password used to encrypt/decrypt notes static late final EncPref encPassword; @@ -55,9 +57,11 @@ abstract class Prefs { static late final PlainPref pfp; static late final PlainPref appTheme; + /// The type of platform to theme. Default value is [defaultTargetPlatform]. static late final PlainPref platform; static late final PlainPref layoutSize; + /// The accent color of the app. If 0, the system accent color will be used. static late final PlainPref accentColor; static late final PlainPref hyperlegibleFont; @@ -68,7 +72,8 @@ abstract class Prefs { static late final PlainPref editorAutoInvert; static late final PlainPref editorOpaqueBackgrounds; static late final PlainPref preferGreyscale; - @Deprecated('Straight line detection now only happens with ShapePen (and happens immediately)') + @Deprecated( + 'Straight line detection now only happens with ShapePen (and happens immediately)') static late final PlainPref editorStraightenDelay; static late final PlainPref editorPromptRename; static late final PlainPref autosaveDelay; @@ -91,8 +96,7 @@ abstract class Prefs { static late final PlainPref recentColorsLength; static late final PlainPref lastTool; - static late final PlainPref - lastFountainPenProperties, + static late final PlainPref lastFountainPenProperties, lastBallpointPenProperties, lastHighlighterProperties, lastPencilProperties, @@ -109,14 +113,18 @@ abstract class Prefs { /// File paths that need to be uploaded to Nextcloud static late final PlainPref> fileSyncUploadQueue; + /// File paths that have been deleted locally static late final PlainPref> fileSyncAlreadyDeleted; + /// File paths that are known to be corrupted on Nextcloud static late final PlainPref> fileSyncCorruptFiles; + /// Set when we want to resync everything. /// Files on the server older than this date will be /// reuploaded with the local version. static late final PlainPref fileSyncResyncEverythingDate; + /// The last storage quota that was fetched from Nextcloud static late final PlainPref lastStorageQuota; @@ -128,7 +136,8 @@ abstract class Prefs { static void init() { final disableAdsDefault = androidVersion < 10; if (disableAdsDefault) { - log.info('Disabling ads because Android version ($androidVersion) is < 10'); + log.info( + 'Disabling ads because Android version ($androidVersion) is < 10'); } disableAds = PlainPref('disableAds', disableAdsDefault); @@ -149,14 +158,18 @@ abstract class Prefs { accentColor = PlainPref('accentColor', 0); hyperlegibleFont = PlainPref('hyperlegibleFont', false); - editorToolbarAlignment = PlainPref('editorToolbarAlignment', AxisDirection.down); - editorToolbarShowInFullscreen = PlainPref('editorToolbarShowInFullscreen', true); + editorToolbarAlignment = + PlainPref('editorToolbarAlignment', AxisDirection.down); + editorToolbarShowInFullscreen = + PlainPref('editorToolbarShowInFullscreen', true); editorFingerDrawing = PlainPref('editorFingerDrawing', true); - editorAutoInvert = PlainPref('editorAutoInvert', true, historicalKeys: const ['editorAutoDarken']); + editorAutoInvert = PlainPref('editorAutoInvert', true, + historicalKeys: const ['editorAutoDarken']); editorOpaqueBackgrounds = PlainPref('editorOpaqueBackgrounds', true); preferGreyscale = PlainPref('preferGreyscale', false); // ignore: deprecated_member_use_from_same_package - editorStraightenDelay = PlainPref('__editorStraightenDelay', 500, deprecatedKeys: const ['editorStraightenDelay']); + editorStraightenDelay = PlainPref('__editorStraightenDelay', 500, + deprecatedKeys: const ['editorStraightenDelay']); editorPromptRename = PlainPref('editorPromptRename', isDesktop); autosaveDelay = PlainPref('autosaveDelay', 10000); shapeRecognitionDelay = PlainPref('shapeRecognitionDelay', 500); @@ -172,45 +185,61 @@ abstract class Prefs { hideFingerDrawingToggle = PlainPref('hideFingerDrawingToggle', false); recentColorsChronological = PlainPref('recentColorsChronological', []); - recentColorsPositioned = PlainPref('recentColorsPositioned', [], historicalKeys: const ['recentColors']); + recentColorsPositioned = PlainPref('recentColorsPositioned', [], + historicalKeys: const ['recentColors']); pinnedColors = PlainPref('pinnedColors', []); recentColorsDontSavePresets = PlainPref('dontSavePresetColors', false); recentColorsLength = PlainPref('recentColorsLength', 5) - ..addListener(() { - // truncate if needed - while (recentColorsLength.value < recentColorsPositioned.value.length) { - // remove oldest color - final removed = recentColorsChronological.value.removeAt(0); - recentColorsPositioned.value.remove(removed); - } - }); + ..addListener(() { + // truncate if needed + while (recentColorsLength.value < recentColorsPositioned.value.length) { + // remove oldest color + final removed = recentColorsChronological.value.removeAt(0); + recentColorsPositioned.value.remove(removed); + } + }); lastTool = PlainPref('lastTool', ToolId.fountainPen); - lastFountainPenProperties = PlainPref('lastFountainPenProperties', StrokeProperties.fountainPen, deprecatedKeys: const ['lastPenColor']); - lastBallpointPenProperties = PlainPref('lastBallpointPenProperties', StrokeProperties.ballpointPen); - lastHighlighterProperties = PlainPref('lastHighlighterProperties', StrokeProperties.highlighter, deprecatedKeys: const ['lastHighlighterColor']); - lastPencilProperties = PlainPref('lastPencilProperties', StrokeProperties.pencil); - lastShapePenProperties = PlainPref('lastShapePenProperties', StrokeProperties.shapePen); - - lastBackgroundPattern = PlainPref('lastBackgroundPattern', CanvasBackgroundPattern.none); + lastFountainPenProperties = PlainPref( + 'lastFountainPenProperties', StrokeProperties.fountainPen, + deprecatedKeys: const ['lastPenColor']); + lastBallpointPenProperties = + PlainPref('lastBallpointPenProperties', StrokeProperties.ballpointPen); + lastHighlighterProperties = PlainPref( + 'lastHighlighterProperties', StrokeProperties.highlighter, + deprecatedKeys: const ['lastHighlighterColor']); + lastPencilProperties = + PlainPref('lastPencilProperties', StrokeProperties.pencil); + lastShapePenProperties = + PlainPref('lastShapePenProperties', StrokeProperties.shapePen); + + lastBackgroundPattern = + PlainPref('lastBackgroundPattern', CanvasBackgroundPattern.none); lastLineHeight = PlainPref('lastLineHeight', 40); lastZoomLock = PlainPref('lastZoomLock', false); - lastSingleFingerPanLock = PlainPref('lastSingleFingerPanLock', false, historicalKeys: const ['lastPanLock']); + lastSingleFingerPanLock = PlainPref('lastSingleFingerPanLock', false, + historicalKeys: const ['lastPanLock']); lastAxisAlignedPanLock = PlainPref('lastAxisAlignedPanLock', false); - hasDraggedSizeIndicatorBefore = PlainPref('hasDraggedSizeIndicatorBefore', false); + hasDraggedSizeIndicatorBefore = + PlainPref('hasDraggedSizeIndicatorBefore', false); - recentFiles = PlainPref('recentFiles', [], historicalKeys: const ['recentlyAccessed']); + recentFiles = PlainPref('recentFiles', [], + historicalKeys: const ['recentlyAccessed']); fileSyncUploadQueue = PlainPref('fileSyncUploadQueue', Queue()); fileSyncAlreadyDeleted = PlainPref('fileSyncAlreadyDeleted', {}); fileSyncCorruptFiles = PlainPref('fileSyncCorruptFiles', {}); // By default, we resync everything uploaded before v0.18.4, since uploads before then resulted in 0B files. - fileSyncResyncEverythingDate = PlainPref('fileSyncResyncEverythingDate', DateTime.parse('2023-12-10T10:06:31.000Z')); + fileSyncResyncEverythingDate = PlainPref('fileSyncResyncEverythingDate', + DateTime.parse('2023-12-10T10:06:31.000Z')); lastStorageQuota = PlainPref('lastStorageQuota', null); - shouldCheckForUpdates = PlainPref('shouldCheckForUpdates', FlavorConfig.shouldCheckForUpdatesByDefault && !Platform.isLinux); - shouldAlwaysAlertForUpdates = PlainPref('shouldAlwaysAlertForUpdates', (kDebugMode || FlavorConfig.dirty) ? true : false, deprecatedKeys: const ['updatesToIgnore']); + shouldCheckForUpdates = PlainPref('shouldCheckForUpdates', + FlavorConfig.shouldCheckForUpdatesByDefault && !Platform.isLinux); + shouldAlwaysAlertForUpdates = PlainPref('shouldAlwaysAlertForUpdates', + (kDebugMode || FlavorConfig.dirty) ? true : false, + deprecatedKeys: const ['updatesToIgnore']); locale = PlainPref('locale', ''); @@ -229,13 +258,16 @@ abstract class Prefs { username.value = await client.getUsername(); } - static bool get isDesktop => Platform.isLinux || Platform.isWindows || Platform.isMacOS; + static bool get isDesktop => + Platform.isLinux || Platform.isWindows || Platform.isMacOS; } abstract class IPref extends ValueNotifier { final String key; + /// The keys that were used in the past for this Pref. If one of these keys is found, the value will be migrated to the current key. final List historicalKeys; + /// The keys that were used in the past for a similar Pref. If one of these keys is found, it will be deleted. final List deprecatedKeys; @@ -247,10 +279,12 @@ abstract class IPref extends ValueNotifier { @protected bool _saved = true; - IPref(this.key, this.defaultValue, { + IPref( + this.key, + this.defaultValue, { List? historicalKeys, List? deprecatedKeys, - }) : historicalKeys = historicalKeys ?? [], + }) : historicalKeys = historicalKeys ?? [], deprecatedKeys = deprecatedKeys ?? [], super(defaultValue) { if (Prefs.testingMode) { @@ -285,6 +319,7 @@ abstract class IPref extends ValueNotifier { } return super.value; } + bool get loaded => _loaded; bool get saved => _saved; @@ -309,22 +344,30 @@ abstract class IPref extends ValueNotifier { @override void notifyListeners() => super.notifyListeners(); } + class PlainPref extends IPref { SharedPreferences? _prefs; - PlainPref(super.key, super.defaultValue, {super.historicalKeys, super.deprecatedKeys}) { + PlainPref(super.key, super.defaultValue, + {super.historicalKeys, super.deprecatedKeys}) { // Accepted types - assert( - T == bool || T == int || T == double || T == String - || T == typeOf() - || T == typeOf>() || T == typeOf>() - || T == typeOf>() - || T == StrokeProperties || T == typeOf() - || T == AxisDirection || T == ThemeMode || T == TargetPlatform - || T == LayoutSize - || T == ToolId || T == CanvasBackgroundPattern - || T == DateTime - ); + assert(T == bool || + T == int || + T == double || + T == String || + T == typeOf() || + T == typeOf>() || + T == typeOf>() || + T == typeOf>() || + T == StrokeProperties || + T == typeOf() || + T == AxisDirection || + T == ThemeMode || + T == TargetPlatform || + T == LayoutSize || + T == ToolId || + T == CanvasBackgroundPattern || + T == DateTime); } @override @@ -351,6 +394,7 @@ class PlainPref extends IPref { return null; } + @override Future _afterLoad() async { _prefs = null; @@ -378,9 +422,11 @@ class PlainPref extends IPref { } else if (T == typeOf>()) { return await _prefs!.setStringList(key, value as List); } else if (T == typeOf>()) { - return await _prefs!.setStringList(key, (value as Set).toList()); + return await _prefs! + .setStringList(key, (value as Set).toList()); } else if (T == typeOf>()) { - return await _prefs!.setStringList(key, (value as Queue).toList()); + return await _prefs! + .setStringList(key, (value as Queue).toList()); } else if (T == StrokeProperties) { return await _prefs!.setString(key, jsonEncode(value)); } else if (T == typeOf()) { @@ -388,7 +434,8 @@ class PlainPref extends IPref { if (quota == null) { return await _prefs!.remove(key); } else { - return await _prefs!.setStringList(key, [quota.used.toString(), quota.total.toString()]); + return await _prefs!.setStringList( + key, [quota.used.toString(), quota.total.toString()]); } } else if (T == AxisDirection) { return await _prefs!.setInt(key, (value as AxisDirection).index); @@ -401,7 +448,8 @@ class PlainPref extends IPref { } else if (T == ToolId) { return await _prefs!.setString(key, (value as ToolId).id); } else if (T == CanvasBackgroundPattern) { - return await _prefs!.setString(key, (value as CanvasBackgroundPattern).name); + return await _prefs! + .setString(key, (value as CanvasBackgroundPattern).name); } else if (T == DateTime) { final date = value as DateTime; if (date.millisecondsSinceEpoch == 0) { @@ -434,7 +482,8 @@ class PlainPref extends IPref { List? list = _prefs!.getStringList(key); return list != null ? Queue.from(list) as T : null; } else if (T == StrokeProperties) { - return StrokeProperties.fromJson(jsonDecode(_prefs!.getString(key)!)) as T?; + return StrokeProperties.fromJson(jsonDecode(_prefs!.getString(key)!)) + as T?; } else if (T == typeOf()) { List? list = _prefs!.getStringList(key); if (list == null || list.length != 2) return null; @@ -445,7 +494,8 @@ class PlainPref extends IPref { 'used': used, 'total': total, 'relative': used / total * 100, - 'quota': total, // I don't know what this [quota] field is for, but I don't use it + 'quota': + total, // I don't know what this [quota] field is for, but I don't use it }) as T; } else if (T == AxisDirection) { final index = _prefs!.getInt(key); @@ -466,14 +516,13 @@ class PlainPref extends IPref { String id = _prefs!.getString(key)!; return ToolId.values .cast() - .firstWhere((toolId) => toolId?.id == id, orElse: () => null) - as T?; + .firstWhere((toolId) => toolId?.id == id, orElse: () => null) as T?; } else if (T == CanvasBackgroundPattern) { String name = _prefs!.getString(key)!; return CanvasBackgroundPattern.values .cast() - .firstWhere((pattern) => pattern!.name == name, orElse: () => null) - as T?; + .firstWhere((pattern) => pattern!.name == name, + orElse: () => null) as T?; } else if (T == DateTime) { String? iso8601 = _prefs!.getString(key); if (iso8601 == null) return null; @@ -497,7 +546,8 @@ class PlainPref extends IPref { class EncPref extends IPref { FlutterSecureStorage? _storage; - EncPref(super.key, super.defaultValue, {super.historicalKeys, super.deprecatedKeys}) { + EncPref(super.key, super.defaultValue, + {super.historicalKeys, super.deprecatedKeys}) { assert(T == String || T == typeOf>() || T == bool); } @@ -525,6 +575,7 @@ class EncPref extends IPref { return null; } + @override Future _afterLoad() async { _storage = null; @@ -535,8 +586,10 @@ class EncPref extends IPref { _saved = false; try { _storage ??= const FlutterSecureStorage(); - if (T == String) return await _storage!.write(key: key, value: value as String); - if (T == bool) return await _storage!.write(key: key, value: jsonEncode(value)); + if (T == String) + return await _storage!.write(key: key, value: value as String); + if (T == bool) + return await _storage!.write(key: key, value: jsonEncode(value)); return await _storage!.write(key: key, value: jsonEncode(value)); } finally { _saved = true; diff --git a/lib/data/routes.dart b/lib/data/routes.dart index c8cf4fc7a..3ad2c6cb7 100644 --- a/lib/data/routes.dart +++ b/lib/data/routes.dart @@ -17,6 +17,7 @@ abstract class RoutePaths { static String editFilePath(String filePath) { return '$edit?path=${Uri.encodeQueryComponent(filePath)}'; } + static String editImportPdf(String filePath, String pdfPath) { return '$edit' '?path=${Uri.encodeQueryComponent(filePath)}' @@ -32,50 +33,50 @@ abstract class HomeRoutes { static final PathFunction _homeFunction = pathToFunction(RoutePaths.home); static List<_Route> get _routes => <_Route>[ - _Route( - routePath: _homeFunction({'subpage': HomePage.recentSubpage}), - label: t.home.tabs.home, - icon: const AdaptiveIcon( - icon: Icons.home, - cupertinoIcon: CupertinoIcons.house_fill, - ), - ), - _Route( - routePath: _homeFunction({'subpage': HomePage.browseSubpage}), - label: t.home.tabs.browse, - icon: const AdaptiveIcon( - icon: Icons.folder, - cupertinoIcon: CupertinoIcons.folder_fill, - ), - ), - _Route( - routePath: _homeFunction({'subpage': HomePage.whiteboardSubpage}), - label: t.home.tabs.whiteboard, - icon: const AdaptiveIcon( - icon: Icons.draw, - cupertinoIcon: CupertinoIcons.pencil_outline, - ), - ), - _Route( - routePath: _homeFunction({'subpage': HomePage.settingsSubpage}), - label: t.home.tabs.settings, - icon: const AdaptiveIcon( - icon: Icons.settings, - cupertinoIcon: CupertinoIcons.settings_solid, - ), - ), - ]; + _Route( + routePath: _homeFunction({'subpage': HomePage.recentSubpage}), + label: t.home.tabs.home, + icon: const AdaptiveIcon( + icon: Icons.home, + cupertinoIcon: CupertinoIcons.house_fill, + ), + ), + _Route( + routePath: _homeFunction({'subpage': HomePage.browseSubpage}), + label: t.home.tabs.browse, + icon: const AdaptiveIcon( + icon: Icons.folder, + cupertinoIcon: CupertinoIcons.folder_fill, + ), + ), + _Route( + routePath: _homeFunction({'subpage': HomePage.whiteboardSubpage}), + label: t.home.tabs.whiteboard, + icon: const AdaptiveIcon( + icon: Icons.draw, + cupertinoIcon: CupertinoIcons.pencil_outline, + ), + ), + _Route( + routePath: _homeFunction({'subpage': HomePage.settingsSubpage}), + label: t.home.tabs.settings, + icon: const AdaptiveIcon( + icon: Icons.settings, + cupertinoIcon: CupertinoIcons.settings_solid, + ), + ), + ]; static String getRoute(int index) { return _routes[index].routePath; } - static List get navigationDestinations => _routes - .map((e) => e.toNavigationDestination()) - .toList(growable: false); - static List get navigationRailDestinations => _routes - .map((e) => e.toNavigationRailDestination()) - .toList(growable: false); + static List get navigationDestinations => + _routes.map((e) => e.toNavigationDestination()).toList(growable: false); + static List get navigationRailDestinations => + _routes + .map((e) => e.toNavigationRailDestination()) + .toList(growable: false); } class _Route { @@ -90,11 +91,12 @@ class _Route { }); NavigationDestination toNavigationDestination() => NavigationDestination( - label: label, - icon: icon, - ); - NavigationRailDestination toNavigationRailDestination() => NavigationRailDestination( - label: Text(label), - icon: icon, - ); + label: label, + icon: icon, + ); + NavigationRailDestination toNavigationRailDestination() => + NavigationRailDestination( + label: Text(label), + icon: icon, + ); } diff --git a/lib/data/tools/eraser.dart b/lib/data/tools/eraser.dart index 321c7bf9d..2cc359001 100644 --- a/lib/data/tools/eraser.dart +++ b/lib/data/tools/eraser.dart @@ -1,4 +1,3 @@ - import 'dart:ui'; import 'package:saber/components/canvas/_stroke.dart'; @@ -6,7 +5,8 @@ import 'package:saber/components/canvas/_stroke.dart'; import 'package:saber/data/tools/_tool.dart'; double square(double x) => x * x; -double sqrDistanceBetween(Offset p1, Offset p2) => square(p1.dx - p2.dx) + square(p1.dy - p2.dy); +double sqrDistanceBetween(Offset p1, Offset p2) => + square(p1.dx - p2.dx) + square(p1.dy - p2.dy); class Eraser extends Tool { final double size; @@ -14,15 +14,14 @@ class Eraser extends Tool { List _erased = []; - Eraser({ - this.size = 10 - }); + Eraser({this.size = 10}); @override ToolId get toolId => ToolId.eraser; /// Returns any [strokes] that are close to the given [eraserPos]. - List checkForOverlappingStrokes(Offset eraserPos, List strokes) { + List checkForOverlappingStrokes( + Offset eraserPos, List strokes) { final List overlapping = []; for (int i = 0; i < strokes.length; i++) { final Stroke stroke = strokes[i]; @@ -41,7 +40,8 @@ class Eraser extends Tool { return erased; } - static bool _shouldStrokeBeErased(Offset eraserPos, Stroke stroke, double sqrSize) { + static bool _shouldStrokeBeErased( + Offset eraserPos, Stroke stroke, double sqrSize) { if (stroke.length <= 3) { if (stroke.path.contains(eraserPos)) return true; } diff --git a/lib/data/tools/highlighter.dart b/lib/data/tools/highlighter.dart index b9614c077..ed60a1e22 100644 --- a/lib/data/tools/highlighter.dart +++ b/lib/data/tools/highlighter.dart @@ -6,14 +6,15 @@ import 'package:saber/data/tools/pen.dart'; import 'package:saber/i18n/strings.g.dart'; class Highlighter extends Pen { - Highlighter() : super( - name: t.editor.pens.highlighter, - sizeMin: 10, - sizeMax: 100, - sizeStep: 10, - icon: highlighterIcon, - toolId: ToolId.highlighter, - ) { + Highlighter() + : super( + name: t.editor.pens.highlighter, + sizeMin: 10, + sizeMax: 100, + sizeStep: 10, + icon: highlighterIcon, + toolId: ToolId.highlighter, + ) { strokeProperties = Prefs.lastHighlighterProperties.value; } diff --git a/lib/data/tools/laser_pointer.dart b/lib/data/tools/laser_pointer.dart index 8db062a00..f15222a57 100644 --- a/lib/data/tools/laser_pointer.dart +++ b/lib/data/tools/laser_pointer.dart @@ -24,9 +24,10 @@ class LaserPointer extends Tool { /// List of timings that correspond to the delay between each point /// in the stroke. The first point has a delay of 0. - /// + /// /// This is used to fade out each point in the stroke one by one. List strokePointDelays = []; + /// Stopwatch used to find the time elapsed since the last point. final Stopwatch _stopwatch = Stopwatch(); @@ -49,7 +50,8 @@ class LaserPointer extends Tool { _stopwatch.reset(); } - Stroke onDragEnd(VoidCallback redrawPage, void Function(Stroke) deleteStroke) { + Stroke onDragEnd( + VoidCallback redrawPage, void Function(Stroke) deleteStroke) { fadeOutStroke( stroke: Pen.currentStroke!, strokePointDelays: strokePointDelays, @@ -64,12 +66,11 @@ class LaserPointer extends Tool { static const Duration _fadeOutDelay = Duration(milliseconds: 500); @visibleForTesting - static void fadeOutStroke({ - required Stroke stroke, - required List strokePointDelays, - required VoidCallback redrawPage, - required void Function(Stroke) deleteStroke - }) async { + static void fadeOutStroke( + {required Stroke stroke, + required List strokePointDelays, + required VoidCallback redrawPage, + required void Function(Stroke) deleteStroke}) async { await Future.delayed(_fadeOutDelay); for (Duration delay in strokePointDelays) { diff --git a/lib/data/tools/pen.dart b/lib/data/tools/pen.dart index 3da71f989..c10c9bbe8 100644 --- a/lib/data/tools/pen.dart +++ b/lib/data/tools/pen.dart @@ -20,8 +20,8 @@ class Pen extends Tool { required this.toolId, }); - Pen.fountainPen() : - name = t.editor.pens.fountainPen, + Pen.fountainPen() + : name = t.editor.pens.fountainPen, sizeMin = 1, sizeMax = 25, sizeStep = 1, @@ -29,8 +29,8 @@ class Pen extends Tool { strokeProperties = Prefs.lastFountainPenProperties.value, toolId = ToolId.fountainPen; - Pen.ballpointPen() : - name = t.editor.pens.ballpointPen, + Pen.ballpointPen() + : name = t.editor.pens.ballpointPen, sizeMin = 1, sizeMax = 25, sizeStep = 1, @@ -56,8 +56,7 @@ class Pen extends Tool { static set currentPen(Pen currentPen) { assert(currentPen is! Highlighter, 'Use Highlighter.currentHighlighter instead'); - assert(currentPen is! Pencil, - 'Use Pencil.currentPencil instead'); + assert(currentPen is! Pencil, 'Use Pencil.currentPencil instead'); _currentPen = currentPen; } diff --git a/lib/data/tools/pencil.dart b/lib/data/tools/pencil.dart index f6177b47c..c223c72a6 100644 --- a/lib/data/tools/pencil.dart +++ b/lib/data/tools/pencil.dart @@ -6,14 +6,15 @@ import 'package:saber/data/tools/pen.dart'; import 'package:saber/i18n/strings.g.dart'; class Pencil extends Pen { - Pencil() : super( - name: t.editor.pens.pencil, - sizeMin: 1, - sizeMax: 25, - sizeStep: 1, - icon: pencilIcon, - toolId: ToolId.pencil, - ) { + Pencil() + : super( + name: t.editor.pens.pencil, + sizeMin: 1, + sizeMax: 25, + sizeStep: 1, + icon: pencilIcon, + toolId: ToolId.pencil, + ) { strokeProperties = Prefs.lastPencilProperties.value; } diff --git a/lib/data/tools/select.dart b/lib/data/tools/select.dart index a1a42c2e0..6a67a9109 100644 --- a/lib/data/tools/select.dart +++ b/lib/data/tools/select.dart @@ -69,13 +69,13 @@ class Select extends Tool { /// Adds the indices of any [strokes] that are inside the selection area /// to [selectResult.indices]. void onDragEnd(List strokes, List images) { - selectResult.path.close(); doneSelecting = true; for (int i = 0; i < strokes.length; i++) { final stroke = strokes[i]; - final percentInside = polygonPercentInside(selectResult.path, stroke.polygon); + final percentInside = + polygonPercentInside(selectResult.path, stroke.polygon); if (percentInside > minPercentInside) { selectResult.strokes.add(stroke); } @@ -110,6 +110,7 @@ class Select extends Tool { // times 1.25 because the grid is not very accurate return pointsInside / (gridSize * gridSize) * 1.25; } + static double polygonPercentInside(Path selection, List polygon) { int pointsInside = 0; for (Offset point in polygon) { diff --git a/lib/data/tools/shape_pen.dart b/lib/data/tools/shape_pen.dart index 015537002..3739efccb 100644 --- a/lib/data/tools/shape_pen.dart +++ b/lib/data/tools/shape_pen.dart @@ -14,14 +14,15 @@ import 'package:saber/data/tools/pen.dart'; import 'package:saber/i18n/strings.g.dart'; class ShapePen extends Pen { - ShapePen(): super( - name: t.editor.pens.shapePen, - sizeMin: 1, - sizeMax: 25, - sizeStep: 1, - icon: shapePenIcon, - toolId: ToolId.shapePen, - ) { + ShapePen() + : super( + name: t.editor.pens.shapePen, + sizeMin: 1, + sizeMax: 25, + sizeStep: 1, + icon: shapePenIcon, + toolId: ToolId.shapePen, + ) { strokeProperties = Prefs.lastShapePenProperties.value; } diff --git a/lib/data/tools/stroke_properties.dart b/lib/data/tools/stroke_properties.dart index e8f777ebe..43aa87130 100644 --- a/lib/data/tools/stroke_properties.dart +++ b/lib/data/tools/stroke_properties.dart @@ -49,7 +49,8 @@ class StrokeProperties { case null: color = defaultColor; default: - throw Exception('Invalid color value: (${json['c'].runtimeType}) ${json['c']}'); + throw Exception( + 'Invalid color value: (${json['c'].runtimeType}) ${json['c']}'); } size = json['s'] ?? defaultSize; thinning = json['t'] ?? defaultThinning; @@ -63,36 +64,36 @@ class StrokeProperties { simulatePressure = json['sp'] ?? defaultSimulatePressure; } Map toJson() => { - if (color != defaultColor) 'c': color.value, - if (size != defaultSize) 's': size, - if (thinning != defaultThinning) 't': thinning, - if (smoothing != defaultSmoothing) 'sm': smoothing, - if (streamline != defaultStreamline) 'sl': streamline, - if (taperStart != defaultTaperStart) 'ts': taperStart, - if (taperEnd != defaultTaperEnd) 'te': taperEnd, - if (capStart != defaultCapStart) 'cs': capStart, - if (capEnd != defaultCapEnd) 'ce': capEnd, - if (pressureEnabled != defaultPressureEnabled) 'pe': pressureEnabled, - if (simulatePressure != defaultSimulatePressure) 'sp': simulatePressure, - }; + if (color != defaultColor) 'c': color.value, + if (size != defaultSize) 's': size, + if (thinning != defaultThinning) 't': thinning, + if (smoothing != defaultSmoothing) 'sm': smoothing, + if (streamline != defaultStreamline) 'sl': streamline, + if (taperStart != defaultTaperStart) 'ts': taperStart, + if (taperEnd != defaultTaperEnd) 'te': taperEnd, + if (capStart != defaultCapStart) 'cs': capStart, + if (capEnd != defaultCapEnd) 'ce': capEnd, + if (pressureEnabled != defaultPressureEnabled) 'pe': pressureEnabled, + if (simulatePressure != defaultSimulatePressure) 'sp': simulatePressure, + }; StrokeProperties copy() => StrokeProperties.fromJson(toJson()); static StrokeProperties get fountainPen => StrokeProperties(); static StrokeProperties get ballpointPen => StrokeProperties( - pressureEnabled: false, - ); + pressureEnabled: false, + ); static StrokeProperties get shapePen => StrokeProperties( - pressureEnabled: false, - ); + pressureEnabled: false, + ); static StrokeProperties get highlighter => StrokeProperties( - size: defaultSize * 5, - color: Colors.yellow.withAlpha(Highlighter.alpha), - pressureEnabled: false, - ); + size: defaultSize * 5, + color: Colors.yellow.withAlpha(Highlighter.alpha), + pressureEnabled: false, + ); static StrokeProperties get pencil => StrokeProperties( - streamline: 0.1, - taperStart: 1, - taperEnd: 1, - ); + streamline: 0.1, + taperStart: 1, + taperEnd: 1, + ); } diff --git a/lib/main.dart b/lib/main.dart index 0d9571c31..26ef60947 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,7 +4,6 @@ import 'package:saber/data/flavor_config.dart'; import 'package:saber/main_common.dart' as common; Future main() async { - /// To set the flavor config e.g. for the Play Store, use: /// flutter build \ /// --dart-define=FLAVOR="Google Play" \ @@ -14,11 +13,13 @@ Future main() async { FlavorConfig.setup( flavor: const String.fromEnvironment('FLAVOR'), appStore: const String.fromEnvironment('APP_STORE'), - shouldCheckForUpdatesByDefault: const bool.fromEnvironment('UPDATE_CHECK', defaultValue: true), + shouldCheckForUpdatesByDefault: + const bool.fromEnvironment('UPDATE_CHECK', defaultValue: true), dirty: const bool.fromEnvironment('DIRTY', defaultValue: false), ); - if (const bool.fromEnvironment('OFFLINE_FONTS_ONLY', defaultValue: kDebugMode)) { + if (const bool.fromEnvironment('OFFLINE_FONTS_ONLY', + defaultValue: kDebugMode)) { // All fonts should already be included (offline) in the app, but in case // I've forgot to add one, it'll be fetched from the internet. // This prevents the app from fetching fonts from Google Servers, diff --git a/lib/main_common.dart b/lib/main_common.dart index 8be69dd6d..9f6ff0e27 100644 --- a/lib/main_common.dart +++ b/lib/main_common.dart @@ -32,9 +32,7 @@ import 'package:worker_manager/worker_manager.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); - Logger.root.level = kDebugMode - ? Level.INFO - : Level.WARNING; + Logger.root.level = kDebugMode ? Level.INFO : Level.WARNING; Logger.root.onRecord.listen((record) { // ignore: avoid_print print('${record.level.name}: ${record.loggerName}: ${record.message}'); @@ -43,8 +41,10 @@ Future main() async { if (Platform.isAndroid) { final deviceInfo = DeviceInfoPlugin(); final androidInfo = await deviceInfo.androidInfo; - Logger.root.fine('androidInfo.version.release: ${androidInfo.version.release}'); - Prefs.androidVersion = int.tryParse(androidInfo.version.release) ?? Prefs.androidVersion; + Logger.root + .fine('androidInfo.version.release: ${androidInfo.version.release}'); + Prefs.androidVersion = + int.tryParse(androidInfo.version.release) ?? Prefs.androidVersion; } Prefs.init(); @@ -85,7 +85,8 @@ void startSyncAfterUsernameLoaded() async { await Prefs.username.waitUntilLoaded(); Prefs.username.removeListener(startSyncAfterUsernameLoaded); - if (Prefs.username.value.isEmpty) { // try again when logged in + if (Prefs.username.value.isEmpty) { + // try again when logged in return Prefs.username.addListener(startSyncAfterUsernameLoaded); } @@ -97,7 +98,8 @@ void startSyncAfterUsernameLoaded() async { } void setLocale() { - if (Prefs.locale.value.isNotEmpty && AppLocaleUtils.supportedLocalesRaw.contains(Prefs.locale.value)) { + if (Prefs.locale.value.isNotEmpty && + AppLocaleUtils.supportedLocalesRaw.contains(Prefs.locale.value)) { LocaleSettings.setLocaleRaw(Prefs.locale.value); } else { LocaleSettings.useDeviceLocale(); @@ -109,7 +111,8 @@ class App extends StatefulWidget { static final log = Logger('App'); - static String initialLocation = pathToFunction(RoutePaths.home)({'subpage': HomePage.recentSubpage}); + static String initialLocation = + pathToFunction(RoutePaths.home)({'subpage': HomePage.recentSubpage}); static final GoRouter _router = GoRouter( initialLocation: initialLocation, routes: [ @@ -168,8 +171,10 @@ class App extends StatefulWidget { _router.push(RoutePaths.editFilePath(path)); } else if (extension == 'pdf' && Editor.canRasterPdf) { final fileNameWithoutExtension = file.path - .split('/').last - .split('\\').last + .split('/') + .last + .split('\\') + .last .substring(0, file.path.length - '.pdf'.length); final sbnFilePath = await FileManager.suffixFilePathToMakeItUnique( '/$fileNameWithoutExtension', @@ -196,7 +201,8 @@ class _AppState extends State { void setupSharingIntent() { if (Platform.isAndroid || Platform.isIOS) { // for files opened while the app is closed - ReceiveSharingIntent.getInitialMedia().then((List files) { + ReceiveSharingIntent.getInitialMedia() + .then((List files) { for (final file in files) { App.openFile(file); } @@ -204,7 +210,8 @@ class _AppState extends State { // for files opened while the app is open final stream = ReceiveSharingIntent.getMediaStream(); - _intentDataStreamSubscription = stream.listen((List files) { + _intentDataStreamSubscription = + stream.listen((List files) { for (final file in files) { App.openFile(file); } @@ -215,7 +222,8 @@ class _AppState extends State { // this only works for files opened while the app is closed OpenAsDefault.getFileIntent.then((File? file) { if (file == null) return; - App.openFile(SharedMediaFile(file.path, null, null, SharedMediaType.FILE)); + App.openFile( + SharedMediaFile(file.path, null, null, SharedMediaType.FILE)); }); } } diff --git a/lib/pages/editor/editor.dart b/lib/pages/editor/editor.dart index 1ad3e6491..df1cf24a5 100644 --- a/lib/pages/editor/editor.dart +++ b/lib/pages/editor/editor.dart @@ -55,7 +55,8 @@ class Editor extends StatefulWidget { String? path, this.customTitle, this.pdfPath, - }) : initialPath = path != null ? Future.value(path) : FileManager.newFilePath('/'), + }) : initialPath = + path != null ? Future.value(path) : FileManager.newFilePath('/'), needsNaming = path == null; final Future initialPath; @@ -68,6 +69,7 @@ class Editor extends StatefulWidget { /// Files with this extension are /// encoded in BSON format. static const String extension = '.sbn2'; + /// The old file extension used by the app. /// Files with this extension are /// encoded in JSON format. @@ -80,6 +82,7 @@ class Editor extends StatefulWidget { static bool isReservedPath(String path) { return _reservedFilePaths.any((regex) => regex.hasMatch(path)); } + static final List _reservedFilePaths = [ RegExp(RegExp.escape(Whiteboard.filePath)), ]; @@ -97,15 +100,18 @@ class EditorState extends State { late EditorCoreInfo coreInfo = EditorCoreInfo(filePath: ''); final _canvasGestureDetectorKey = GlobalKey(); - late final TransformationController _transformationController = TransformationController() - ..addListener(() { - PdfEditorImage.checkIfHighDpiNeeded( - getZoom: () => _transformationController.value.getMaxScaleOnAxis(), - getScrollY: () => scrollY, - pages: coreInfo.pages, - screenWidth: _canvasGestureDetectorKey.currentState?.containerBounds.maxWidth ?? double.infinity, - ); - }); + late final TransformationController _transformationController = + TransformationController() + ..addListener(() { + PdfEditorImage.checkIfHighDpiNeeded( + getZoom: () => _transformationController.value.getMaxScaleOnAxis(), + getScrollY: () => scrollY, + pages: coreInfo.pages, + screenWidth: _canvasGestureDetectorKey + .currentState?.containerBounds.maxWidth ?? + double.infinity, + ); + }); double get scrollY { final transformation = _transformationController.value; final scale = transformation.getMaxScaleOnAxis(); @@ -173,6 +179,7 @@ class EditorState extends State { /// The tool that was used before switching to the eraser. Tool? tmpTool; + /// If the stylus button is pressed, or was pressed during the current draw gesture. bool stylusButtonPressed = false; @@ -185,6 +192,7 @@ class EditorState extends State { super.initState(); } + void _initAsync() async { coreInfo = PreviewCard.getCachedCoreInfo(await widget.initialPath); filenameTextEditingController.text = coreInfo.fileName; @@ -202,6 +210,7 @@ class EditorState extends State { await importPdfFromFilePath(widget.pdfPath!); } } + Future _initStrokes() async { coreInfo = await EditorCoreInfo.loadFromFilePath(coreInfo.filePath); if (coreInfo.readOnly) { @@ -237,10 +246,12 @@ class EditorState extends State { assert(pageIndex < coreInfo.pages.length); quillFocus.value = coreInfo.pages[pageIndex].quill - ..focusNode.requestFocus(); + ..focusNode.requestFocus(); } - if (coreInfo.filePath == Whiteboard.filePath && Prefs.autoClearWhiteboardOnExit.value && Whiteboard.needsToAutoClearWhiteboard) { + if (coreInfo.filePath == Whiteboard.filePath && + Prefs.autoClearWhiteboardOnExit.value && + Whiteboard.needsToAutoClearWhiteboard) { // clear whiteboard (and add to history) clearAllPages(); @@ -256,13 +267,18 @@ class EditorState extends State { Keybinding? _ctrlZ, _ctrlY, _ctrlShiftZ; void _assignKeybindings() { - _ctrlZ = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyZ)], inclusive: true); - _ctrlY = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyY)], inclusive: true); - _ctrlShiftZ = Keybinding([KeyCode.ctrl, KeyCode.shift, KeyCode.from(LogicalKeyboardKey.keyZ)], inclusive: true); + _ctrlZ = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyZ)], + inclusive: true); + _ctrlY = Keybinding([KeyCode.ctrl, KeyCode.from(LogicalKeyboardKey.keyY)], + inclusive: true); + _ctrlShiftZ = Keybinding( + [KeyCode.ctrl, KeyCode.shift, KeyCode.from(LogicalKeyboardKey.keyZ)], + inclusive: true); Keybinder.bind(_ctrlZ!, undo); Keybinder.bind(_ctrlY!, redo); Keybinder.bind(_ctrlShiftZ!, redo); } + void _removeKeybindings() { if (_ctrlZ != null) Keybinder.remove(_ctrlZ!); if (_ctrlY != null) Keybinder.remove(_ctrlY!); @@ -278,6 +294,7 @@ class EditorState extends State { listenToQuillChanges(page.quill, coreInfo.pages.length - 1); } } + void removeExcessPages() { bool removedAPage = false; @@ -450,7 +467,8 @@ class EditorState extends State { case EditorHistoryItemType.insertPage: undo(item.copyWith(type: EditorHistoryItemType.deletePage)); case EditorHistoryItemType.move: - undo(item.copyWith(offset: Rect.fromLTRB( + undo(item.copyWith( + offset: Rect.fromLTRB( -item.offset!.left, -item.offset!.top, -item.offset!.right, @@ -461,7 +479,9 @@ class EditorState extends State { case EditorHistoryItemType.quillUndoneChange: // this will never happen throw Exception('history should not contain quillUndoneChange items'); case EditorHistoryItemType.changeColor: - undo(item.copyWith(colorChange: item.colorChange!.map((key, value) => MapEntry(key, value.swap())))); + undo(item.copyWith( + colorChange: item.colorChange! + .map((key, value) => MapEntry(key, value.swap())))); } } @@ -469,7 +489,8 @@ class EditorState extends State { for (int i = 0; i < coreInfo.pages.length; ++i) { if (coreInfo.pages[i].renderBox == null) continue; Rect pageBounds = Offset.zero & coreInfo.pages[i].size; - if (pageBounds.contains(coreInfo.pages[i].renderBox!.globalToLocal(focalPoint))) return i; + if (pageBounds.contains( + coreInfo.pages[i].renderBox!.globalToLocal(focalPoint))) return i; } return null; } @@ -477,6 +498,7 @@ class EditorState extends State { /// The position of the previous draw gesture event. /// Used to move a selection. Offset previousPosition = Offset.zero; + /// The total offset of the current move gesture. /// Used to record a move in the history. Offset moveOffset = Offset.zero; @@ -486,22 +508,26 @@ class EditorState extends State { bool isDrawGesture(ScaleStartDetails details) { if (coreInfo.readOnly) return false; - CanvasImage.activeListener.notifyListenersPlease(); // un-select active image + CanvasImage.activeListener + .notifyListenersPlease(); // un-select active image _lastSeenPointerCountTimer?.cancel(); - if (lastSeenPointerCount >= 2) { // was a zoom gesture, ignore + if (lastSeenPointerCount >= 2) { + // was a zoom gesture, ignore lastSeenPointerCount = lastSeenPointerCount; return false; - } else if (details.pointerCount >= 2) { // is a zoom gesture, remove accidental stroke - if (lastSeenPointerCount == 1 - && Prefs.editorFingerDrawing.value - && (currentTool is Pen || currentTool is Eraser)) { + } else if (details.pointerCount >= 2) { + // is a zoom gesture, remove accidental stroke + if (lastSeenPointerCount == 1 && + Prefs.editorFingerDrawing.value && + (currentTool is Pen || currentTool is Eraser)) { EditorHistoryItem? item = history.removeAccidentalStroke(); if (item != null) undo(item); } lastSeenPointerCount = details.pointerCount; return false; - } else { // is a stroke + } else { + // is a stroke lastSeenPointerCount = details.pointerCount; } @@ -517,23 +543,26 @@ class EditorState extends State { return false; } } + void onDrawStart(ScaleStartDetails details) { final page = coreInfo.pages[dragPageIndex!]; final position = page.renderBox!.globalToLocal(details.focalPoint); history.canRedo = false; if (currentTool is Pen) { - (currentTool as Pen).onDragStart(position, dragPageIndex!, currentPressure); + (currentTool as Pen) + .onDragStart(position, dragPageIndex!, currentPressure); } else if (currentTool is Eraser) { - for (Stroke stroke in (currentTool as Eraser).checkForOverlappingStrokes(position, page.strokes)) { + for (Stroke stroke in (currentTool as Eraser) + .checkForOverlappingStrokes(position, page.strokes)) { page.strokes.remove(stroke); } removeExcessPages(); } else if (currentTool is Select) { Select select = currentTool as Select; - if (select.doneSelecting - && select.selectResult.pageIndex == dragPageIndex! - && select.selectResult.path.contains(position)) { + if (select.doneSelecting && + select.selectResult.pageIndex == dragPageIndex! && + select.selectResult.path.contains(position)) { // drag selection in onDrawUpdate } else { select.onDragStart(position, dragPageIndex!); @@ -553,6 +582,7 @@ class EditorState extends State { // setState to let canvas know about currentStroke setState(() {}); } + void onDrawUpdate(ScaleUpdateDetails details) { final page = coreInfo.pages[dragPageIndex!]; final position = page.renderBox!.globalToLocal(details.focalPoint); @@ -561,7 +591,8 @@ class EditorState extends State { (currentTool as Pen).onDragUpdate(position, currentPressure); page.redrawStrokes(); } else if (currentTool is Eraser) { - for (Stroke stroke in (currentTool as Eraser).checkForOverlappingStrokes(position, page.strokes)) { + for (Stroke stroke in (currentTool as Eraser) + .checkForOverlappingStrokes(position, page.strokes)) { page.strokes.remove(stroke); } page.redrawStrokes(); @@ -587,6 +618,7 @@ class EditorState extends State { previousPosition = position; moveOffset += offset; } + void onDrawEnd(ScaleEndDetails details) { final page = coreInfo.pages[dragPageIndex!]; setState(() { @@ -603,7 +635,8 @@ class EditorState extends State { )); } else if (currentTool is Eraser) { final erased = (currentTool as Eraser).onDragEnd(); - if (stylusButtonPressed || Prefs.disableEraserAfterUse.value) { // restore previous tool + if (stylusButtonPressed || Prefs.disableEraserAfterUse.value) { + // restore previous tool stylusButtonPressed = false; currentTool = tmpTool!; tmpTool = null; @@ -650,6 +683,7 @@ class EditorState extends State { }); autosaveAfterDelay(); } + void onInteractionEnd(ScaleEndDetails details) { // reset after 1ms to keep track of the same gesture only _lastSeenPointerCountTimer?.cancel(); @@ -661,6 +695,7 @@ class EditorState extends State { void onPressureChanged(double? pressure) { currentPressure = pressure == 0 ? null : pressure; } + void onStylusButtonChanged(bool buttonPressed) { // whether the stylus button is or was pressed stylusButtonPressed = stylusButtonPressed || buttonPressed; @@ -689,6 +724,7 @@ class EditorState extends State { setState(() {}); autosaveAfterDelay(); } + void onDeleteImage(EditorImage image) { history.recordChange(EditorHistoryItem( type: EditorHistoryItemType.erase, @@ -719,12 +755,14 @@ class EditorState extends State { }); quill.focusNode.addListener(_onQuillFocusChange); } + void _onQuillFocusChange() { for (EditorPage page in coreInfo.pages) { if (!page.quill.focusNode.hasFocus) continue; quillFocus.value = page.quill; } } + void _addQuillChangeToHistory({ required QuillStruct quill, required int pageIndex, @@ -757,7 +795,8 @@ class EditorState extends State { savingState.value = SavingState.waitingToSave; _delayedSaveTimer?.cancel(); if (Prefs.autosaveDelay.value < 0) return; - _delayedSaveTimer = Timer(Duration(milliseconds: Prefs.autosaveDelay.value), () { + _delayedSaveTimer = + Timer(Duration(milliseconds: Prefs.autosaveDelay.value), () { saveToFile(); }); } @@ -787,12 +826,11 @@ class EditorState extends State { await Future.wait([ FileManager.writeFile(filePath, bson, awaitWrite: true), for (int i = 0; i < assets.length; ++i) - assets.getBytes(i) - .then((bytes) => FileManager.writeFile( - '$filePath.$i', - bytes, - awaitWrite: true, - )), + assets.getBytes(i).then((bytes) => FileManager.writeFile( + '$filePath.$i', + bytes, + awaitWrite: true, + )), FileManager.removeUnusedAssets( filePath, numAssets: assets.length, @@ -806,7 +844,6 @@ class EditorState extends State { } } - late final _filenameFormKey = GlobalKey(); late final filenameTextEditingController = TextEditingController(); Timer? _renameTimer; @@ -814,25 +851,32 @@ class EditorState extends State { _renameTimer?.cancel(); _renameTimer = Timer(const Duration(seconds: 5), _renameFileNow); } + Future _renameFileNow() async { final newName = filenameTextEditingController.text; if (newName == coreInfo.fileName) return; if (_filenameFormKey.currentState?.validate() ?? true) { - coreInfo.filePath = await FileManager.moveFile(coreInfo.filePath + Editor.extension, newName + Editor.extension); - coreInfo.filePath = coreInfo.filePath.substring(0, coreInfo.filePath.lastIndexOf(Editor.extension)); + coreInfo.filePath = await FileManager.moveFile( + coreInfo.filePath + Editor.extension, newName + Editor.extension); + coreInfo.filePath = coreInfo.filePath + .substring(0, coreInfo.filePath.lastIndexOf(Editor.extension)); needsNaming = false; } final actualName = coreInfo.fileName; - if (actualName != newName) { // update text field if renamed differently - filenameTextEditingController.value = filenameTextEditingController.value.copyWith( + if (actualName != newName) { + // update text field if renamed differently + filenameTextEditingController.value = + filenameTextEditingController.value.copyWith( text: actualName, - selection: TextSelection.fromPosition(TextPosition(offset: actualName.length)), + selection: + TextSelection.fromPosition(TextPosition(offset: actualName.length)), composing: TextRange.empty, ); } } + String? _validateFilenameTextField(String? newName) { if (newName == null) return null; if (newName.isEmpty) return t.home.renameNote.noteNameEmpty; @@ -842,7 +886,8 @@ class EditorState extends State { void updateColorBar(Color color) { if (Prefs.recentColorsDontSavePresets.value) { - if (ColorBar.colorPresets.any((colorPreset) => colorPreset.color == color)) { + if (ColorBar.colorPresets + .any((colorPreset) => colorPreset.color == color)) { return; } } @@ -850,9 +895,12 @@ class EditorState extends State { final String newColorString = color.value.toString(); // migrate from old pref format - if (Prefs.recentColorsChronological.value.length != Prefs.recentColorsPositioned.value.length) { - log.info('MIGRATING recentColors: ${Prefs.recentColorsChronological.value.length} vs ${Prefs.recentColorsPositioned.value.length}'); - Prefs.recentColorsChronological.value = List.of(Prefs.recentColorsPositioned.value); + if (Prefs.recentColorsChronological.value.length != + Prefs.recentColorsPositioned.value.length) { + log.info( + 'MIGRATING recentColors: ${Prefs.recentColorsChronological.value.length} vs ${Prefs.recentColorsPositioned.value.length}'); + Prefs.recentColorsChronological.value = + List.of(Prefs.recentColorsPositioned.value); } if (Prefs.pinnedColors.value.contains(newColorString)) { @@ -863,12 +911,16 @@ class EditorState extends State { Prefs.recentColorsChronological.value.add(newColorString); Prefs.recentColorsChronological.notifyListeners(); } else { - if (Prefs.recentColorsPositioned.value.length >= Prefs.recentColorsLength.value) { + if (Prefs.recentColorsPositioned.value.length >= + Prefs.recentColorsLength.value) { // if full, replace the oldest color with the new one - final String removedColorString = Prefs.recentColorsChronological.value.removeAt(0); + final String removedColorString = + Prefs.recentColorsChronological.value.removeAt(0); Prefs.recentColorsChronological.value.add(newColorString); - final int removedColorPosition = Prefs.recentColorsPositioned.value.indexOf(removedColorString); - Prefs.recentColorsPositioned.value[removedColorPosition] = newColorString; + final int removedColorPosition = + Prefs.recentColorsPositioned.value.indexOf(removedColorString); + Prefs.recentColorsPositioned.value[removedColorPosition] = + newColorString; } else { // if not full, add the new color to the end Prefs.recentColorsChronological.value.add(newColorString); @@ -944,13 +996,19 @@ class EditorState extends State { // https://github.com/brendan-duncan/image/blob/main/doc/formats.md // (plus .svg) allowedExtensions: [ - 'jpg', 'jpeg', 'png', - 'gif', 'tiff', 'bmp', - 'tga', 'ico', 'pvrtc', - + 'jpg', + 'jpeg', + 'png', + 'gif', + 'tiff', + 'bmp', + 'tga', + 'ico', + 'pvrtc', 'svg', - - 'webp', 'psd', 'exr', + 'webp', + 'psd', + 'exr', ], allowMultiple: true, withData: true, @@ -985,7 +1043,7 @@ class EditorState extends State { return importPdfFromFilePath(file.path!); } - Future importPdfFromFilePath(String path) async{ + Future importPdfFromFilePath(String path) async { final pdfFile = File(path); final Uint8List pdfBytes; try { @@ -1065,9 +1123,7 @@ class EditorState extends State { Formats.tiff: '.tiff', Formats.bmp: '.bmp', Formats.ico: '.ico', - Formats.svg: '.svg', - Formats.webp: '.webp', }; @@ -1092,7 +1148,8 @@ class EditorState extends State { String extension; if (file.fileName != null) { - extension = file.fileName!.substring(file.fileName!.lastIndexOf('.')); + extension = + file.fileName!.substring(file.fileName!.lastIndexOf('.')); } else { extension = formats[format]!; } @@ -1118,6 +1175,7 @@ class EditorState extends State { final pdf = await EditorExporter.generatePdf(coreInfo, context); await FileManager.exportFile('${coreInfo.fileName}.pdf', await pdf.save()); } + /// Exports the current note as an SBA (Saber Archive) file. Future exportAsSba() async { final sba = await coreInfo.saveToSba( @@ -1135,7 +1193,8 @@ class EditorState extends State { final theme = Theme.of(context); // whiteboard on mobile should keep home screen navbar color - if (coreInfo.filePath == Whiteboard.filePath && !ResponsiveNavbar.isLargeScreen) { + if (coreInfo.filePath == Whiteboard.filePath && + !ResponsiveNavbar.isLargeScreen) { return ResponsiveNavbar.setAndroidNavBarColor(theme); } @@ -1143,9 +1202,8 @@ class EditorState extends State { if (!mounted) return; final brightness = theme.brightness; - final otherBrightness = brightness == Brightness.dark - ? Brightness.light - : Brightness.dark; + final otherBrightness = + brightness == Brightness.dark ? Brightness.light : Brightness.dark; final overlayStyle = brightness == Brightness.dark ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light; @@ -1160,17 +1218,17 @@ class EditorState extends State { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final platform = Theme.of(context).platform; - final cupertino = platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; - final isToolbarVertical = Prefs.editorToolbarAlignment.value == AxisDirection.left - || Prefs.editorToolbarAlignment.value == AxisDirection.right; + final cupertino = + platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; + final isToolbarVertical = + Prefs.editorToolbarAlignment.value == AxisDirection.left || + Prefs.editorToolbarAlignment.value == AxisDirection.right; setAndroidNavBarColor(); final Widget canvas = CanvasGestureDetector( key: _canvasGestureDetectorKey, - filePath: coreInfo.filePath, - isDrawGesture: isDrawGesture, onInteractionEnd: onInteractionEnd, onDrawStart: onDrawStart, @@ -1178,10 +1236,8 @@ class EditorState extends State { onDrawEnd: onDrawEnd, onStylusButtonChanged: onStylusButtonChanged, onPressureChanged: onPressureChanged, - undo: undo, redo: redo, - pages: coreInfo.pages, initialPageIndex: coreInfo.initialPageIndex, pageBuilder: (BuildContext context, int pageIndex) { @@ -1196,9 +1252,10 @@ class EditorState extends State { textEditing: currentTool == Tool.textEditing, coreInfo: coreInfo, currentStroke: currentStroke, - currentStrokeDetectedShape: currentTool is ShapePen && currentStroke != null - ? ShapePen.detectedShape - : null, + currentStrokeDetectedShape: + currentTool is ShapePen && currentStroke != null + ? ShapePen.detectedShape + : null, currentSelection: () { if (currentTool is! Select) return null; final selectResult = (currentTool as Select).selectResult; @@ -1213,7 +1270,8 @@ class EditorState extends State { page.images.remove(image); page.backgroundImage = image; - CanvasImage.activeListener.notifyListenersPlease(); // un-select active image + CanvasImage.activeListener + .notifyListenersPlease(); // un-select active image autosaveAfterDelay(); setState(() {}); @@ -1236,32 +1294,36 @@ class EditorState extends State { currentToolIsSelect: currentTool is Select, ); }, - transformationController: _transformationController, ); - final Widget? readonlyBanner = coreInfo.readOnlyBecauseOfVersion ? Collapsible( - collapsed: !(coreInfo.readOnly && coreInfo.readOnlyBecauseOfVersion), - axis: CollapsibleAxis.vertical, - child: SafeArea( - child: ListTile( - onTap: askUserToDisableReadOnly, - title: Text(t.editor.newerFileFormat.readOnlyMode), - subtitle: Text(t.editor.newerFileFormat.title), - trailing: const Icon(Icons.edit_off), - ), - ), - ) : null; + final Widget? readonlyBanner = coreInfo.readOnlyBecauseOfVersion + ? Collapsible( + collapsed: + !(coreInfo.readOnly && coreInfo.readOnlyBecauseOfVersion), + axis: CollapsibleAxis.vertical, + child: SafeArea( + child: ListTile( + onTap: askUserToDisableReadOnly, + title: Text(t.editor.newerFileFormat.readOnlyMode), + subtitle: Text(t.editor.newerFileFormat.title), + trailing: const Icon(Icons.edit_off), + ), + ), + ) + : null; final Widget toolbar = Collapsible( - axis: isToolbarVertical ? CollapsibleAxis.horizontal : CollapsibleAxis.vertical, - collapsed: DynamicMaterialApp.isFullscreen && !Prefs.editorToolbarShowInFullscreen.value, + axis: isToolbarVertical + ? CollapsibleAxis.horizontal + : CollapsibleAxis.vertical, + collapsed: DynamicMaterialApp.isFullscreen && + !Prefs.editorToolbarShowInFullscreen.value, maintainState: true, child: SafeArea( bottom: Prefs.editorToolbarAlignment.value != AxisDirection.up, child: Toolbar( readOnly: coreInfo.readOnly, - setTool: (tool) { setState(() { if (tool is Eraser) { @@ -1299,20 +1361,15 @@ class EditorState extends State { const duplicationFeedbackOffset = Offset(25, -25); - final duplicatedStrokes = strokes - .map((stroke) { - return stroke.copy() - ..shift(duplicationFeedbackOffset); - }) - .toList(); - - final duplicatedImages = images - .map((image) { - return image.copy() - ..id = coreInfo.nextImageId++ - ..dstRect.shift(duplicationFeedbackOffset); - }) - .toList(); + final duplicatedStrokes = strokes.map((stroke) { + return stroke.copy()..shift(duplicationFeedbackOffset); + }).toList(); + + final duplicatedImages = images.map((image) { + return image.copy() + ..id = coreInfo.nextImageId++ + ..dstRect.shift(duplicationFeedbackOffset); + }).toList(); page.strokes.addAll(duplicatedStrokes); page.images.addAll(duplicatedImages); @@ -1366,17 +1423,21 @@ class EditorState extends State { updateColorBar(color); if (currentTool is Highlighter) { - (currentTool as Highlighter).strokeProperties.color = color.withAlpha(Highlighter.alpha); + (currentTool as Highlighter).strokeProperties.color = + color.withAlpha(Highlighter.alpha); } else if (currentTool is Pen) { (currentTool as Pen).strokeProperties.color = color; - } else if (currentTool is Select) { // Changes color of selected strokes + } else if (currentTool is Select) { + // Changes color of selected strokes final select = currentTool as Select; if (select.doneSelecting) { final strokes = select.selectResult.strokes; - + Map colorChange = {}; for (Stroke stroke in strokes) { - colorChange[stroke] = ColorChange(previous: stroke.strokeProperties.color, current: color); + colorChange[stroke] = ColorChange( + previous: stroke.strokeProperties.color, + current: color); stroke.strokeProperties.color = color; } @@ -1392,7 +1453,6 @@ class EditorState extends State { } }); }, - quillFocus: quillFocus, textEditing: currentTool == Tool.textEditing, toggleTextEditing: () => setState(() { @@ -1400,30 +1460,29 @@ class EditorState extends State { currentTool = Pen.currentPen; for (EditorPage page in coreInfo.pages) { // unselect text, but maintain cursor position - page.quill.controller.moveCursorToPosition(page.quill.controller.selection.extentOffset); + page.quill.controller.moveCursorToPosition( + page.quill.controller.selection.extentOffset); page.quill.focusNode.unfocus(); } } else { currentTool = Tool.textEditing; quillFocus.value = coreInfo.pages[currentPageIndex].quill - ..focusNode.requestFocus(); + ..focusNode.requestFocus(); } }), - undo: undo, isUndoPossible: history.canUndo, redo: redo, isRedoPossible: history.canRedo, toggleFingerDrawing: () { setState(() { - Prefs.editorFingerDrawing.value = !Prefs.editorFingerDrawing.value; + Prefs.editorFingerDrawing.value = + !Prefs.editorFingerDrawing.value; lastSeenPointerCount = 0; }); }, - pickPhoto: _pickPhotos, paste: paste, - exportAsSba: exportAsSba, exportAsPdf: exportAsPdf, exportAsPng: null, @@ -1434,10 +1493,13 @@ class EditorState extends State { final Widget body; if (isToolbarVertical) { body = Row( - textDirection: Prefs.editorToolbarAlignment.value == AxisDirection.left ? TextDirection.ltr : TextDirection.rtl, + textDirection: Prefs.editorToolbarAlignment.value == AxisDirection.left + ? TextDirection.ltr + : TextDirection.rtl, children: [ toolbar, - Expanded(child: Column( + Expanded( + child: Column( children: [ Expanded(child: canvas), if (readonlyBanner != null) readonlyBanner, @@ -1447,7 +1509,10 @@ class EditorState extends State { ); } else { body = Column( - verticalDirection: Prefs.editorToolbarAlignment.value == AxisDirection.up ? VerticalDirection.up : VerticalDirection.down, + verticalDirection: + Prefs.editorToolbarAlignment.value == AxisDirection.up + ? VerticalDirection.up + : VerticalDirection.down, children: [ Expanded(child: canvas), toolbar, @@ -1479,88 +1544,95 @@ class EditorState extends State { ); }, child: Scaffold( - appBar: DynamicMaterialApp.isFullscreen ? null : AppBar( - toolbarHeight: kToolbarHeight, - title: widget.customTitle != null ? Text(widget.customTitle!) : Form( - key: _filenameFormKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: TextFormField( - decoration: const InputDecoration( - border: InputBorder.none, - ), - controller: filenameTextEditingController, - onChanged: renameFile, - autofocus: needsNaming, - validator: _validateFilenameTextField, - ), - ), - leading: SaveIndicator( - savingState: savingState, - triggerSave: saveToFile, - ), - actions: [ - IconButton( - icon: const AdaptiveIcon( - icon: Icons.insert_page_break, - cupertinoIcon: CupertinoIcons.add, - ), - tooltip: t.editor.menu.insertPage, - onPressed: () => setState(() { - final currentPageIndex = this.currentPageIndex; - insertPageAfter(currentPageIndex); - CanvasGestureDetector.scrollToPage( - pageIndex: currentPageIndex + 1, - pages: coreInfo.pages, - screenWidth: MediaQuery.of(context).size.width, - transformationController: _transformationController, - ); - }), - ), - IconButton( - icon: const AdaptiveIcon( - icon: Icons.grid_view, - cupertinoIcon: CupertinoIcons.rectangle_grid_2x2, - ), - tooltip: t.editor.pages, - onPressed: () { - showDialog( - context: context, - builder: (context) => AdaptiveAlertDialog( - title: Text(t.editor.pages), - content: pageManager(context), - actions: const [], + appBar: DynamicMaterialApp.isFullscreen + ? null + : AppBar( + toolbarHeight: kToolbarHeight, + title: widget.customTitle != null + ? Text(widget.customTitle!) + : Form( + key: _filenameFormKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: TextFormField( + decoration: const InputDecoration( + border: InputBorder.none, + ), + controller: filenameTextEditingController, + onChanged: renameFile, + autofocus: needsNaming, + validator: _validateFilenameTextField, + ), + ), + leading: SaveIndicator( + savingState: savingState, + triggerSave: saveToFile, + ), + actions: [ + IconButton( + icon: const AdaptiveIcon( + icon: Icons.insert_page_break, + cupertinoIcon: CupertinoIcons.add, + ), + tooltip: t.editor.menu.insertPage, + onPressed: () => setState(() { + final currentPageIndex = this.currentPageIndex; + insertPageAfter(currentPageIndex); + CanvasGestureDetector.scrollToPage( + pageIndex: currentPageIndex + 1, + pages: coreInfo.pages, + screenWidth: MediaQuery.of(context).size.width, + transformationController: _transformationController, + ); + }), ), - ); - }, - ), - IconButton( - icon: const AdaptiveIcon( - icon: Icons.more_vert, - cupertinoIcon: CupertinoIcons.ellipsis_vertical, - ), - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => bottomSheet(context), - isScrollControlled: true, - showDragHandle: true, - backgroundColor: colorScheme.surface, - constraints: const BoxConstraints( - maxWidth: 500, + IconButton( + icon: const AdaptiveIcon( + icon: Icons.grid_view, + cupertinoIcon: CupertinoIcons.rectangle_grid_2x2, + ), + tooltip: t.editor.pages, + onPressed: () { + showDialog( + context: context, + builder: (context) => AdaptiveAlertDialog( + title: Text(t.editor.pages), + content: pageManager(context), + actions: const [], + ), + ); + }, ), - ); - }, - ) - ], - ), + IconButton( + icon: const AdaptiveIcon( + icon: Icons.more_vert, + cupertinoIcon: CupertinoIcons.ellipsis_vertical, + ), + onPressed: () { + showModalBottomSheet( + context: context, + builder: (context) => bottomSheet(context), + isScrollControlled: true, + showDragHandle: true, + backgroundColor: colorScheme.surface, + constraints: const BoxConstraints( + maxWidth: 500, + ), + ); + }, + ) + ], + ), body: body, - floatingActionButton: (DynamicMaterialApp.isFullscreen && !Prefs.editorToolbarShowInFullscreen.value) ? FloatingActionButton( - shape: cupertino ? const CircleBorder() : null, - onPressed: () { - DynamicMaterialApp.setFullscreen(false, updateSystem: true); - }, - child: const Icon(Icons.fullscreen_exit), - ) : null, + floatingActionButton: (DynamicMaterialApp.isFullscreen && + !Prefs.editorToolbarShowInFullscreen.value) + ? FloatingActionButton( + shape: cupertino ? const CircleBorder() : null, + onPressed: () { + DynamicMaterialApp.setFullscreen(false, updateSystem: true); + }, + child: const Icon(Icons.fullscreen_exit), + ) + : null, ), ); } @@ -1574,7 +1646,8 @@ class EditorState extends State { Widget bottomSheet(BuildContext context) { final Brightness brightness = Theme.of(context).brightness; - final bool invert = Prefs.editorAutoInvert.value && brightness == Brightness.dark; + final bool invert = + Prefs.editorAutoInvert.value && brightness == Brightness.dark; final int currentPageIndex = this.currentPageIndex; return EditorBottomSheet( @@ -1607,14 +1680,11 @@ class EditorState extends State { clearPage: () { clearPage(currentPageIndex); }, - clearAllPages: clearAllPages, - redrawAndSave: () => setState(() { if (coreInfo.readOnly) return; autosaveAfterDelay(); }), - pickPhotos: _pickPhotos, importPdf: importPdf, canRasterPdf: Editor.canRasterPdf, @@ -1637,14 +1707,12 @@ class EditorState extends State { strokes: page.strokes .map((stroke) => stroke.copy()..pageIndex += 1) .toList(), - images: page.images - .map((image) => image.copy()..pageIndex += 1) - .toList(), + images: + page.images.map((image) => image.copy()..pageIndex += 1).toList(), quill: QuillStruct( controller: flutter_quill.QuillController( document: flutter_quill.Document.fromDelta( - page.quill.controller.document.toDelta() - ), + page.quill.controller.document.toDelta()), selection: const TextSelection.collapsed(offset: 0), ), focusNode: FocusNode(debugLabel: 'Quill Focus Node'), @@ -1681,19 +1749,19 @@ class EditorState extends State { } void insertPageAfter(int pageIndex) => setState(() { - if (coreInfo.readOnly) return; - final page = EditorPage(); - coreInfo.pages.insert(pageIndex + 1, page); - listenToQuillChanges(page.quill, pageIndex + 1); - history.recordChange(EditorHistoryItem( - type: EditorHistoryItemType.insertPage, - pageIndex: pageIndex + 1, - strokes: const [], - images: const [], - page: page, - )); - autosaveAfterDelay(); - }); + if (coreInfo.readOnly) return; + final page = EditorPage(); + coreInfo.pages.insert(pageIndex + 1, page); + listenToQuillChanges(page.quill, pageIndex + 1); + history.recordChange(EditorHistoryItem( + type: EditorHistoryItemType.insertPage, + pageIndex: pageIndex + 1, + strokes: const [], + images: const [], + page: page, + )); + autosaveAfterDelay(); + }); void clearPage(int pageIndex) { if (coreInfo.readOnly) return; @@ -1713,6 +1781,7 @@ class EditorState extends State { autosaveAfterDelay(); }); } + void clearAllPages() { if (coreInfo.readOnly) return; setState(() { @@ -1737,22 +1806,23 @@ class EditorState extends State { Future askUserToDisableReadOnly() async { bool disableReadOnly = await showDialog( - context: context, - builder: (context) => AdaptiveAlertDialog( - title: Text(t.editor.newerFileFormat.title), - content: Text(t.editor.newerFileFormat.subtitle), - actions: [ - CupertinoDialogAction( - child: Text(t.editor.newerFileFormat.cancel), - onPressed: () => Navigator.pop(context, false), - ), - CupertinoDialogAction( - child: Text(t.editor.newerFileFormat.allowEditing), - onPressed: () => Navigator.pop(context, true), + context: context, + builder: (context) => AdaptiveAlertDialog( + title: Text(t.editor.newerFileFormat.title), + content: Text(t.editor.newerFileFormat.subtitle), + actions: [ + CupertinoDialogAction( + child: Text(t.editor.newerFileFormat.cancel), + onPressed: () => Navigator.pop(context, false), + ), + CupertinoDialogAction( + child: Text(t.editor.newerFileFormat.allowEditing), + onPressed: () => Navigator.pop(context, true), + ), + ], ), - ], - ), - ) ?? false; + ) ?? + false; if (!mounted) return; if (!disableReadOnly) return; @@ -1763,6 +1833,7 @@ class EditorState extends State { } late int _lastCurrentPageIndex = coreInfo.initialPageIndex ?? 0; + /// The index of the page that is currently centered on screen. int get currentPageIndex { if (!mounted) return _lastCurrentPageIndex; @@ -1775,6 +1846,7 @@ class EditorState extends State { pages: coreInfo.pages, ); } + @visibleForTesting static int getPageIndexFromScrollPosition({ required double scrollY, diff --git a/lib/pages/home/browse.dart b/lib/pages/home/browse.dart index 2d49bc2bd..aa7060a8c 100644 --- a/lib/pages/home/browse.dart +++ b/lib/pages/home/browse.dart @@ -27,6 +27,7 @@ class BrowsePage extends StatefulWidget { @override State createState() => _BrowsePageState(); } + class _BrowsePageState extends State { DirectoryChildren? children; @@ -40,11 +41,13 @@ class _BrowsePageState extends State { path = widget.initialPath; findChildrenOfPath(); - fileWriteSubscription = FileManager.fileWriteStream.stream.listen(fileWriteListener); + fileWriteSubscription = + FileManager.fileWriteStream.stream.listen(fileWriteListener); selectedFiles.addListener(_setState); super.initState(); } + @override void dispose() { selectedFiles.removeListener(_setState); @@ -96,8 +99,8 @@ class _BrowsePageState extends State { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final platform = Theme.of(context).platform; - final cupertino = platform == TargetPlatform.iOS - || platform == TargetPlatform.macOS; + final cupertino = + platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; String title = t.home.titles.browse; if (path?.isNotEmpty ?? false) { @@ -130,9 +133,7 @@ class _BrowsePageState extends State { ), centerTitle: cupertino, titlePadding: EdgeInsetsDirectional.only( - start: cupertino ? 0 : 16, - bottom: 16 - ), + start: cupertino ? 0 : 16, bottom: 16), ), actions: const [ SyncingButton(), @@ -154,7 +155,8 @@ class _BrowsePageState extends State { }, isFolderEmpty: (String folderName) async { final folderPath = '${path ?? ''}/$folderName'; - final children = await FileManager.getChildrenOfDirectory(folderPath); + final children = + await FileManager.getChildrenOfDirectory(folderPath); return children?.isEmpty ?? true; }, deleteFolder: (String folderName) async { @@ -167,7 +169,6 @@ class _BrowsePageState extends State { directoryPath, ], ), - if (children == null) ...[ // loading ] else if (children!.isEmpty) ...[ @@ -199,37 +200,41 @@ class _BrowsePageState extends State { cupertino: cupertino, path: path, ), - persistentFooterButtons: selectedFiles.value.isEmpty ? null : [ - Collapsible( - axis: CollapsibleAxis.vertical, - collapsed: selectedFiles.value.length != 1, - child: RenameNoteButton( - existingPath: selectedFiles.value.isEmpty - ? '' - : selectedFiles.value.first, - ) - ), - MoveNoteButton( - filesToMove: selectedFiles.value, - ), - IconButton( - padding: EdgeInsets.zero, - tooltip: t.home.deleteNote, - onPressed: () async { - await Future.wait([ - for (String filePath in selectedFiles.value) - FileManager.doesFileExist(filePath + Editor.extensionOldJson) - .then((oldExtension) => FileManager.deleteFile( - filePath + (oldExtension ? Editor.extensionOldJson : Editor.extension) + persistentFooterButtons: selectedFiles.value.isEmpty + ? null + : [ + Collapsible( + axis: CollapsibleAxis.vertical, + collapsed: selectedFiles.value.length != 1, + child: RenameNoteButton( + existingPath: selectedFiles.value.isEmpty + ? '' + : selectedFiles.value.first, )), - ]); - }, - icon: const Icon(Icons.delete_forever), - ), - ExportNoteButton( - selectedFiles: selectedFiles.value, - ), - ], + MoveNoteButton( + filesToMove: selectedFiles.value, + ), + IconButton( + padding: EdgeInsets.zero, + tooltip: t.home.deleteNote, + onPressed: () async { + await Future.wait([ + for (String filePath in selectedFiles.value) + FileManager.doesFileExist( + filePath + Editor.extensionOldJson) + .then((oldExtension) => FileManager.deleteFile( + filePath + + (oldExtension + ? Editor.extensionOldJson + : Editor.extension))), + ]); + }, + icon: const Icon(Icons.delete_forever), + ), + ExportNoteButton( + selectedFiles: selectedFiles.value, + ), + ], ); } } diff --git a/lib/pages/home/home.dart b/lib/pages/home/home.dart index e6b3ad038..f21f88e0d 100644 --- a/lib/pages/home/home.dart +++ b/lib/pages/home/home.dart @@ -25,7 +25,12 @@ class HomePage extends StatefulWidget { static const String browseSubpage = 'browse'; static const String whiteboardSubpage = 'whiteboard'; static const String settingsSubpage = 'settings'; - static const List subpages = [recentSubpage, browseSubpage, whiteboardSubpage, settingsSubpage]; + static const List subpages = [ + recentSubpage, + browseSubpage, + whiteboardSubpage, + settingsSubpage + ]; } class _HomePageState extends State { @@ -54,7 +59,8 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { // hide navbar in fullscreen whiteboard - if (widget.subpage == HomePage.whiteboardSubpage && DynamicMaterialApp.isFullscreen) { + if (widget.subpage == HomePage.whiteboardSubpage && + DynamicMaterialApp.isFullscreen) { return body; } diff --git a/lib/pages/home/recent_notes.dart b/lib/pages/home/recent_notes.dart index a3494c6ed..75bc15fda 100644 --- a/lib/pages/home/recent_notes.dart +++ b/lib/pages/home/recent_notes.dart @@ -35,7 +35,7 @@ class _RecentPageState extends State { /// Mitigates a bug where files got imported starting with `null/` instead of `/`. /// /// This caused them to be written to `Documents/Sabernull/...` instead of `Documents/Saber/...`. - /// + /// /// See https://github.com/saber-notes/saber/issues/996 /// and https://github.com/saber-notes/saber/pull/977. void moveIncorrectlyImportedFiles() async { @@ -44,12 +44,15 @@ class _RecentPageState extends State { final String newFilePath; if (filePath.startsWith('null/')) { - newFilePath = await FileManager.suffixFilePathToMakeItUnique(filePath.substring('null'.length)); + newFilePath = await FileManager.suffixFilePathToMakeItUnique( + filePath.substring('null'.length)); } else { - newFilePath = await FileManager.suffixFilePathToMakeItUnique('/$filePath'); + newFilePath = + await FileManager.suffixFilePathToMakeItUnique('/$filePath'); } - - log.warning('Found incorrectly imported file at `$filePath`; moving to `$newFilePath`'); + + log.warning( + 'Found incorrectly imported file at `$filePath`; moving to `$newFilePath`'); await FileManager.moveFile(filePath, newFilePath); } } @@ -57,12 +60,14 @@ class _RecentPageState extends State { @override void initState() { findRecentlyAccessedNotes(); - fileWriteSubscription = FileManager.fileWriteStream.stream.listen(fileWriteListener); + fileWriteSubscription = + FileManager.fileWriteStream.stream.listen(fileWriteListener); selectedFiles.addListener(_setState); super.initState(); moveIncorrectlyImportedFiles(); } + @override void dispose() { selectedFiles.removeListener(_setState); @@ -102,8 +107,8 @@ class _RecentPageState extends State { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final platform = Theme.of(context).platform; - final cupertino = platform == TargetPlatform.iOS - || platform == TargetPlatform.macOS; + final cupertino = + platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; final crossAxisCount = MediaQuery.of(context).size.width ~/ 300 + 1; return Scaffold( body: RefreshIndicator( @@ -129,9 +134,7 @@ class _RecentPageState extends State { ), centerTitle: cupertino, titlePadding: EdgeInsetsDirectional.only( - start: cupertino ? 0 : 16, - bottom: 16 - ), + start: cupertino ? 0 : 16, bottom: 16), ), actions: const [ SyncingButton(), @@ -165,38 +168,43 @@ class _RecentPageState extends State { floatingActionButton: NewNoteButton( cupertino: cupertino, ), - persistentFooterButtons: selectedFiles.value.isEmpty ? null : [ - Collapsible( - axis: CollapsibleAxis.vertical, - collapsed: selectedFiles.value.length != 1, - child: RenameNoteButton( - existingPath: selectedFiles.value.isEmpty - ? '' - : selectedFiles.value.first, - ), - ), - MoveNoteButton( - filesToMove: selectedFiles.value, - ), - IconButton( - padding: EdgeInsets.zero, - tooltip: t.home.deleteNote, - onPressed: () async { - await Future.wait([ - for (String filePath in selectedFiles.value) - FileManager.doesFileExist(filePath + Editor.extensionOldJson) - .then((oldExtension) => FileManager.deleteFile( - filePath + (oldExtension ? Editor.extensionOldJson : Editor.extension) - )), - ]); - selectedFiles.value = []; - }, - icon: const Icon(Icons.delete_forever), - ), - ExportNoteButton( - selectedFiles: selectedFiles.value, - ), - ], + persistentFooterButtons: selectedFiles.value.isEmpty + ? null + : [ + Collapsible( + axis: CollapsibleAxis.vertical, + collapsed: selectedFiles.value.length != 1, + child: RenameNoteButton( + existingPath: selectedFiles.value.isEmpty + ? '' + : selectedFiles.value.first, + ), + ), + MoveNoteButton( + filesToMove: selectedFiles.value, + ), + IconButton( + padding: EdgeInsets.zero, + tooltip: t.home.deleteNote, + onPressed: () async { + await Future.wait([ + for (String filePath in selectedFiles.value) + FileManager.doesFileExist( + filePath + Editor.extensionOldJson) + .then((oldExtension) => FileManager.deleteFile( + filePath + + (oldExtension + ? Editor.extensionOldJson + : Editor.extension))), + ]); + selectedFiles.value = []; + }, + icon: const Icon(Icons.delete_forever), + ), + ExportNoteButton( + selectedFiles: selectedFiles.value, + ), + ], ); } } diff --git a/lib/pages/home/settings.dart b/lib/pages/home/settings.dart index 0d4bb59e5..dcc0eacae 100644 --- a/lib/pages/home/settings.dart +++ b/lib/pages/home/settings.dart @@ -107,7 +107,8 @@ class _SettingsPageState extends State { TargetPlatform.linux => true, _ => false, }; - static final bool usesMaterialByDefault = !usesCupertinoByDefault && !usesYaruByDefault; + static final bool usesMaterialByDefault = + !usesCupertinoByDefault && !usesYaruByDefault; static const cupertinoDirectionIcons = [ CupertinoIcons.arrow_up_to_line, @@ -126,8 +127,8 @@ class _SettingsPageState extends State { Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final platform = Theme.of(context).platform; - final cupertino = platform == TargetPlatform.iOS - || platform == TargetPlatform.macOS; + final cupertino = + platform == TargetPlatform.iOS || platform == TargetPlatform.macOS; final bool requiresManualUpdates = FlavorConfig.appStore.isEmpty; @@ -160,17 +161,20 @@ class _SettingsPageState extends State { ), ), actions: [ - if (UpdateManager.status.value != UpdateStatus.upToDate) IconButton( - tooltip: t.home.tooltips.showUpdateDialog, - icon: const Icon(Icons.system_update), - onPressed: () { - UpdateManager.showUpdateDialog(context, userTriggered: true); - }, - ), + if (UpdateManager.status.value != UpdateStatus.upToDate) + IconButton( + tooltip: t.home.tooltips.showUpdateDialog, + icon: const Icon(Icons.system_update), + onPressed: () { + UpdateManager.showUpdateDialog(context, + userTriggered: true); + }, + ), ], ), ), - SliverSafeArea(sliver: SliverList.list( + SliverSafeArea( + sliver: SliverList.list( children: [ const NextcloudProfile(), const Padding( @@ -189,7 +193,8 @@ class _SettingsPageState extends State { ...AppLocaleUtils.supportedLocales.map((locale) { final String localeCode = locale.toLanguageTag(); String? localeName = localeNames[localeCode]; - assert(localeName != null, 'Missing locale name for $localeCode'); + assert(localeName != null, + 'Missing locale name for $localeCode'); return ToggleButtonsOption( localeCode, Text(localeName ?? localeCode), @@ -208,9 +213,18 @@ class _SettingsPageState extends State { pref: _SettingsPrefs.appTheme, optionsWidth: 60, options: [ - ToggleButtonsOption(ThemeMode.system.index, Icon(Icons.brightness_auto, semanticLabel: t.settings.themeModes.system)), - ToggleButtonsOption(ThemeMode.light.index, Icon(Icons.light_mode, semanticLabel: t.settings.themeModes.light)), - ToggleButtonsOption(ThemeMode.dark.index, Icon(Icons.dark_mode, semanticLabel: t.settings.themeModes.dark)), + ToggleButtonsOption( + ThemeMode.system.index, + Icon(Icons.brightness_auto, + semanticLabel: t.settings.themeModes.system)), + ToggleButtonsOption( + ThemeMode.light.index, + Icon(Icons.light_mode, + semanticLabel: t.settings.themeModes.light)), + ToggleButtonsOption( + ThemeMode.dark.index, + Icon(Icons.dark_mode, + semanticLabel: t.settings.themeModes.dark)), ], ), SettingsSelection( @@ -225,14 +239,16 @@ class _SettingsPageState extends State { options: [ ToggleButtonsOption( () { - if (usesMaterialByDefault) return defaultTargetPlatform.index; + if (usesMaterialByDefault) + return defaultTargetPlatform.index; return TargetPlatform.android.index; }(), Icon(materialIcon, semanticLabel: 'Material'), ), ToggleButtonsOption( () { - if (usesCupertinoByDefault) return defaultTargetPlatform.index; + if (usesCupertinoByDefault) + return defaultTargetPlatform.index; return TargetPlatform.iOS.index; }(), const Icon(Icons.apple, semanticLabel: 'Cupertino'), @@ -262,9 +278,18 @@ class _SettingsPageState extends State { pref: _SettingsPrefs.layoutSize, optionsWidth: 60, options: [ - ToggleButtonsOption(LayoutSize.auto.index, Icon(Icons.aspect_ratio, semanticLabel: t.settings.layoutSizes.auto)), - ToggleButtonsOption(LayoutSize.phone.index, Icon(Icons.smartphone, semanticLabel: t.settings.layoutSizes.phone)), - ToggleButtonsOption(LayoutSize.tablet.index, Icon(Icons.tablet, semanticLabel: t.settings.layoutSizes.tablet)), + ToggleButtonsOption( + LayoutSize.auto.index, + Icon(Icons.aspect_ratio, + semanticLabel: t.settings.layoutSizes.auto)), + ToggleButtonsOption( + LayoutSize.phone.index, + Icon(Icons.smartphone, + semanticLabel: t.settings.layoutSizes.phone)), + ToggleButtonsOption( + LayoutSize.tablet.index, + Icon(Icons.tablet, + semanticLabel: t.settings.layoutSizes.tablet)), ], ), SettingsColor( @@ -276,18 +301,24 @@ class _SettingsPageState extends State { title: t.settings.prefLabels.hyperlegibleFont, subtitle: t.settings.prefDescriptions.hyperlegibleFont, iconBuilder: (b) { - if (b) return cupertino ? CupertinoIcons.textformat : Icons.font_download; - return cupertino ? CupertinoIcons.textformat_alt : Icons.font_download_off; + if (b) + return cupertino + ? CupertinoIcons.textformat + : Icons.font_download; + return cupertino + ? CupertinoIcons.textformat_alt + : Icons.font_download_off; }, pref: Prefs.hyperlegibleFont, ), - SettingsSubtitle(subtitle: t.settings.prefCategories.writing), SettingsSwitch( title: t.settings.prefLabels.preferGreyscale, subtitle: t.settings.prefDescriptions.preferGreyscale, iconBuilder: (b) { - return b ? Icons.monochrome_photos : Icons.enhance_photo_translate; + return b + ? Icons.monochrome_photos + : Icons.enhance_photo_translate; }, pref: Prefs.preferGreyscale, ), @@ -309,23 +340,28 @@ class _SettingsPageState extends State { if (!Prefs.hideFingerDrawingToggle.value) { return t.settings.prefDescriptions.hideFingerDrawing.shown; } else if (Prefs.editorFingerDrawing.value) { - return t.settings.prefDescriptions.hideFingerDrawing.fixedOn; + return t + .settings.prefDescriptions.hideFingerDrawing.fixedOn; } else { - return t.settings.prefDescriptions.hideFingerDrawing.fixedOff; + return t + .settings.prefDescriptions.hideFingerDrawing.fixedOff; } }(), icon: CupertinoIcons.hand_draw, pref: Prefs.hideFingerDrawingToggle, afterChange: (_) => setState(() {}), ), - SettingsSubtitle(subtitle: t.settings.prefCategories.editor), SettingsSelection( title: t.settings.prefLabels.editorToolbarAlignment, - subtitle: t.settings.axisDirections[_SettingsPrefs.editorToolbarAlignment.value], + subtitle: t.settings.axisDirections[ + _SettingsPrefs.editorToolbarAlignment.value], iconBuilder: (num i) { - if (i is! int || i >= materialDirectionIcons.length) return null; - return cupertino ? cupertinoDirectionIcons[i] : materialDirectionIcons[i]; + if (i is! int || i >= materialDirectionIcons.length) + return null; + return cupertino + ? cupertinoDirectionIcons[i] + : materialDirectionIcons[i]; }, pref: _SettingsPrefs.editorToolbarAlignment, optionsWidth: 60, @@ -334,8 +370,11 @@ class _SettingsPageState extends State { ToggleButtonsOption( direction.index, Icon( - cupertino ? cupertinoDirectionIcons[direction.index] : materialDirectionIcons[direction.index], - semanticLabel: t.settings.axisDirections[direction.index], + cupertino + ? cupertinoDirectionIcons[direction.index] + : materialDirectionIcons[direction.index], + semanticLabel: + t.settings.axisDirections[direction.index], ), ), ], @@ -363,8 +402,11 @@ class _SettingsPageState extends State { title: t.settings.prefLabels.editorPromptRename, subtitle: t.settings.prefDescriptions.editorPromptRename, iconBuilder: (b) { - if (b) return cupertino ? CupertinoIcons.keyboard : Icons.keyboard; - return cupertino ? CupertinoIcons.keyboard_chevron_compact_down : Icons.keyboard_hide; + if (b) + return cupertino ? CupertinoIcons.keyboard : Icons.keyboard; + return cupertino + ? CupertinoIcons.keyboard_chevron_compact_down + : Icons.keyboard_hide; }, pref: Prefs.editorPromptRename, ), @@ -373,7 +415,9 @@ class _SettingsPageState extends State { subtitle: t.settings.prefDescriptions.hideHomeBackgrounds, iconBuilder: (b) { if (b) return cupertino ? CupertinoIcons.photo : Icons.photo; - return cupertino ? CupertinoIcons.photo_fill : Icons.photo_library; + return cupertino + ? CupertinoIcons.photo_fill + : Icons.photo_library; }, pref: Prefs.hideHomeBackgrounds, ), @@ -397,7 +441,6 @@ class _SettingsPageState extends State { icon: Icons.numbers, pref: Prefs.printPageIndicators, ), - SettingsSubtitle(subtitle: t.settings.prefCategories.performance), SettingsSelection( title: t.settings.prefLabels.maxImageSize, @@ -435,9 +478,10 @@ class _SettingsPageState extends State { ShapePen.debounceDuration = ShapePen.getDebounceFromPref(); }, ), - SettingsSubtitle(subtitle: t.settings.prefCategories.advanced), - if (requiresManualUpdates || Prefs.shouldCheckForUpdates.value != Prefs.shouldCheckForUpdates.defaultValue) ...[ + if (requiresManualUpdates || + Prefs.shouldCheckForUpdates.value != + Prefs.shouldCheckForUpdates.defaultValue) ...[ SettingsSwitch( title: t.settings.prefLabels.shouldCheckForUpdates, icon: Icons.system_update, @@ -449,7 +493,8 @@ class _SettingsPageState extends State { axis: CollapsibleAxis.vertical, child: SettingsSwitch( title: t.settings.prefLabels.shouldAlwaysAlertForUpdates, - subtitle: t.settings.prefDescriptions.shouldAlwaysAlertForUpdates, + subtitle: + t.settings.prefDescriptions.shouldAlwaysAlertForUpdates, icon: Icons.system_security_update_warning, pref: Prefs.shouldAlwaysAlertForUpdates, ), diff --git a/lib/pages/home/whiteboard.dart b/lib/pages/home/whiteboard.dart index de384f30b..a2d04a730 100644 --- a/lib/pages/home/whiteboard.dart +++ b/lib/pages/home/whiteboard.dart @@ -9,11 +9,14 @@ class Whiteboard extends StatelessWidget { static const String filePath = '/_whiteboard'; - static bool needsToAutoClearWhiteboard = Prefs.autoClearWhiteboardOnExit.value; + static bool needsToAutoClearWhiteboard = + Prefs.autoClearWhiteboardOnExit.value; - static final _whiteboardKey = GlobalKey(debugLabel: 'whiteboard'); + static final _whiteboardKey = + GlobalKey(debugLabel: 'whiteboard'); - static SavingState? get savingState => _whiteboardKey.currentState?.savingState.value; + static SavingState? get savingState => + _whiteboardKey.currentState?.savingState.value; static void triggerSave() { final editorState = _whiteboardKey.currentState; if (editorState == null) return; diff --git a/lib/pages/user/login.dart b/lib/pages/user/login.dart index 7ce7729d0..8069f074b 100644 --- a/lib/pages/user/login.dart +++ b/lib/pages/user/login.dart @@ -16,7 +16,8 @@ import 'package:saber/i18n/strings.g.dart'; class NcLoginPage extends StatefulWidget { const NcLoginPage({super.key}); - static final Uri signupUrl = Uri.parse('https://nc.saber.adil.hanney.org/index.php/apps/registration/'); + static final Uri signupUrl = Uri.parse( + 'https://nc.saber.adil.hanney.org/index.php/apps/registration/'); @override State createState() => _NcLoginPageState(); @@ -36,10 +37,12 @@ class _NcLoginPageState extends State { final OcsGetCapabilitiesResponseApplicationJson_Ocs_Data capabilities; final VersionCheck versionCheck; try { - capabilities = await client.core.ocs.getCapabilities() - .then((capabilities) => capabilities.body.ocs.data); + capabilities = await client.core.ocs + .getCapabilities() + .then((capabilities) => capabilities.body.ocs.data); versionCheck = client.core.getVersionCheck(capabilities); - log.info('versionCheck: isSupported=${versionCheck.isSupported}, minimumVersion=${versionCheck.minimumVersion}'); + log.info( + 'versionCheck: isSupported=${versionCheck.isSupported}, minimumVersion=${versionCheck.minimumVersion}'); } catch (e) { log.severe('Failed to get capabilities: $e', e); throw NcLoginFailure(); @@ -77,7 +80,8 @@ class _NcLoginPageState extends State { log.severe('Failed to load encryption key, wrong encryption password'); Prefs.encPassword.value = previousEncPassword; rethrow; - } catch (e) { // Probably a webdav error + } catch (e) { + // Probably a webdav error log.severe('Failed to load encryption key: $e', e); Prefs.encPassword.value = previousEncPassword; if (kDebugMode) rethrow; @@ -89,11 +93,12 @@ class _NcLoginPageState extends State { Prefs.ncPassword.value = loginDetails.ncPassword; Prefs.pfp.value = null; - client.core.avatar.getAvatar(userId: username, size: 512) + client.core.avatar + .getAvatar(userId: username, size: 512) .then((response) => response.body) .then((Uint8List pfp) { - Prefs.pfp.value = pfp; - }); + Prefs.pfp.value = pfp; + }); Prefs.lastStorageQuota.value = null; @@ -120,7 +125,6 @@ class _NcLoginPageState extends State { height: 240, excludeFromSemantics: true, ), - const SizedBox(height: 64), LoginInputGroup( tryLogin: _tryLogin, diff --git a/lib/pages/user/profile_page.dart b/lib/pages/user/profile_page.dart index 54d7624b0..077a281f5 100644 --- a/lib/pages/user/profile_page.dart +++ b/lib/pages/user/profile_page.dart @@ -75,7 +75,8 @@ class ProfilePage extends StatelessWidget { TextButton( onPressed: () { launchUrl( - Uri.parse('${Prefs.url.value}/index.php/settings/user/drop_account'), + Uri.parse( + '${Prefs.url.value}/index.php/settings/user/drop_account'), ); }, child: Text(t.profile.quickLinks.deleteAccount), diff --git a/packages/onyxsdk_pen/lib/onyxsdk_pen_area.dart b/packages/onyxsdk_pen/lib/onyxsdk_pen_area.dart index cfc1083d5..76e4f9be5 100644 --- a/packages/onyxsdk_pen/lib/onyxsdk_pen_area.dart +++ b/packages/onyxsdk_pen/lib/onyxsdk_pen_area.dart @@ -28,10 +28,10 @@ class OnyxSdkPenArea extends StatefulWidget { State createState() => _OnyxSdkPenAreaState(); /// Optional method to initialize the onyxsdk_pen package. - /// + /// /// This method should be called in the main() method before runApp(), /// or before the first OnyxSdkPenArea widget is created. - /// + /// /// Returns true if the device is an Onyx device, false otherwise. static Future init() async { return await _OnyxSdkPenAreaState._findIsOnyxDevice(); diff --git a/packages/onyxsdk_pen/test/onyxsdk_pen_method_channel_test.dart b/packages/onyxsdk_pen/test/onyxsdk_pen_method_channel_test.dart index 452890d74..3dc285a15 100644 --- a/packages/onyxsdk_pen/test/onyxsdk_pen_method_channel_test.dart +++ b/packages/onyxsdk_pen/test/onyxsdk_pen_method_channel_test.dart @@ -9,13 +9,15 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, (MethodCall methodCall) async { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { return false; }); }); tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); }); test('isOnyxDevice', () async { diff --git a/scripts/translate_changelogs.dart b/scripts/translate_changelogs.dart index 6beafa2bb..19c5cb455 100644 --- a/scripts/translate_changelogs.dart +++ b/scripts/translate_changelogs.dart @@ -21,9 +21,11 @@ Future getEnglishChangelog() async { final changelog = await file.readAsString(); return changelog; } + Future symlinkChangelog(String localeCode) async { final fileNormal = File('metadata/$localeCode/changelogs/$buildNumber.txt'); - final fileFDroid = Link('metadata/$localeCode/changelogs/${buildNumber}3.txt'); + final fileFDroid = + Link('metadata/$localeCode/changelogs/${buildNumber}3.txt'); if (fileFDroid.existsSync()) return; fileFDroid.create(fileNormal.path); } @@ -33,14 +35,16 @@ void main() async { final useLibreEngine = random.nextBool(); print('Using ${useLibreEngine ? 'Libre' : 'Google'} translation engine...\n'); - final translator = SimplyTranslator(useLibreEngine ? EngineType.libre : EngineType.google); + final translator = + SimplyTranslator(useLibreEngine ? EngineType.libre : EngineType.google); final englishChangelog = await getEnglishChangelog(); print('English changelog for $buildName ($buildNumber):'); print(englishChangelog); if (englishChangelog.length > 500) { - print('Warning: The English changelog has length ${englishChangelog.length}, ' + print( + 'Warning: The English changelog has length ${englishChangelog.length}, ' 'but Google Play only allows 500 characters.'); print('Please shorten the changelog and try again.'); return; @@ -67,7 +71,8 @@ void main() async { final file = File('metadata/$localeCode/changelogs/$buildNumber.txt'); if (file.existsSync()) { - print('$stepPrefix. Skipped $localeCode ($localeName) because it already exists'); + print( + '$stepPrefix. Skipped $localeCode ($localeName) because it already exists'); continue; } else { print('$stepPrefix. Translating to $localeCode ($localeName)...'); @@ -78,10 +83,12 @@ void main() async { nearestLocaleCode = localeCode; } else if (nearestLocaleCodes.containsKey(localeCode)) { nearestLocaleCode = nearestLocaleCodes[localeCode]!; - } else if (LanguageList.contains(localeCode.substring(0, localeCode.indexOf('-')))) { + } else if (LanguageList.contains( + localeCode.substring(0, localeCode.indexOf('-')))) { nearestLocaleCode = localeCode.substring(0, localeCode.indexOf('-')); } else { - print('${' ' * stepPrefix.length} ! Language not supported, skipping...'); + print( + '${' ' * stepPrefix.length} ! Language not supported, skipping...'); someTranslationsFailed = true; continue; } @@ -91,12 +98,14 @@ void main() async { Translation translation; try { - translation = await translator.translateSimply( - englishChangelog, - from: 'en', - to: nearestLocaleCode, - retries: 3, - ).timeout(const Duration(seconds: 5)); + translation = await translator + .translateSimply( + englishChangelog, + from: 'en', + to: nearestLocaleCode, + retries: 3, + ) + .timeout(const Duration(seconds: 5)); } catch (e) { print('${' ' * stepPrefix.length} ! Translation failed, skipping...'); someTranslationsFailed = true; @@ -122,7 +131,8 @@ void main() async { var linesRemoved = -1; // -1 to account for removing the trailing newline while (translatedChangelog.length > 500 - suffix.length) { final lastNewlineIndex = translatedChangelog.lastIndexOf('\n'); - translatedChangelog = translatedChangelog.substring(0, lastNewlineIndex); + translatedChangelog = + translatedChangelog.substring(0, lastNewlineIndex); linesRemoved++; } translatedChangelog += suffix; diff --git a/scripts/translate_missing_translations.dart b/scripts/translate_missing_translations.dart index 2d73acff0..5280e9d56 100644 --- a/scripts/translate_missing_translations.dart +++ b/scripts/translate_missing_translations.dart @@ -40,7 +40,8 @@ String _nearestLocaleCode(String localeCode) { } /// Translate the given tree of strings in place. Note that the tree can contain lists, maps, and strings. -Future translateTree(String languageCode, YamlMap tree, List pathOfKeys) async { +Future translateTree( + String languageCode, YamlMap tree, List pathOfKeys) async { // first translate all direct descendants that are strings for (final key in tree.keys) { if (key.endsWith('(OUTDATED)')) continue; @@ -52,9 +53,11 @@ Future translateTree(String languageCode, YamlMap tree, List pathO if (value is! String) continue; final translated = await translateString(translator, languageCode, value); - if (translated == null || translated == value) continue; // error occured in translation, so skip for now + if (translated == null || translated == value) + continue; // error occured in translation, so skip for now try { - await shell.run('dart run slang add $languageCode $pathToKey "${translated.replaceAll('"', '\\"')}"'); + await shell.run( + 'dart run slang add $languageCode $pathToKey "${translated.replaceAll('"', '\\"')}"'); } catch (e) { print(' Adding translation failed: $e'); errorOccurredInTranslatingTree = true; @@ -79,8 +82,10 @@ Future translateTree(String languageCode, YamlMap tree, List pathO } } } + /// Translates the given list of strings in place. Note that the list can contain lists, maps, and strings. -Future translateList(String languageCode, YamlList list, List pathOfKeys) async { +Future translateList( + String languageCode, YamlList list, List pathOfKeys) async { // first translate all direct descendants that are strings for (int i = 0; i < list.length; ++i) { final pathToKey = [...pathOfKeys, i].join('.'); @@ -90,13 +95,16 @@ Future translateList(String languageCode, YamlList list, List path if (value is! String) continue; final translated = await translateString(translator, languageCode, value); - if (translated == null || translated == value) continue; // error occurred in translation, so skip for now - await shell.run('dart run slang add $languageCode $pathToKey "${translated.replaceAll('"', '\\"')}"'); + if (translated == null || translated == value) + continue; // error occurred in translation, so skip for now + await shell.run( + 'dart run slang add $languageCode $pathToKey "${translated.replaceAll('"', '\\"')}"'); newlyTranslatedPaths.add('$languageCode/$pathToKey'); } // then recurse - for (int i = 0; i < list.length; ++i) { // then recurse + for (int i = 0; i < list.length; ++i) { + // then recurse final value = list[i]; if (value is String) { // already done @@ -109,15 +117,20 @@ Future translateList(String languageCode, YamlList list, List path } } } -Future translateString(SimplyTranslator translator, String languageCode, String english) async { - print(' Translating into $languageCode: ${english.length > 20 ? '${english.substring(0, 20)}...' : english}'); + +Future translateString( + SimplyTranslator translator, String languageCode, String english) async { + print( + ' Translating into $languageCode: ${english.length > 20 ? '${english.substring(0, 20)}...' : english}'); Translation translation; try { - translation = await translator.translateSimply( - english, - from: 'en', - to: _nearestLocaleCode(languageCode), - ).timeout(const Duration(seconds: 3)); + translation = await translator + .translateSimply( + english, + from: 'en', + to: _nearestLocaleCode(languageCode), + ) + .timeout(const Duration(seconds: 3)); } catch (e) { print(' Translation failed: $e'); errorOccurredInTranslatingTree = true; @@ -145,18 +158,22 @@ void main() async { final missingTranslations = await _getMissingTranslations(); final missingLanguageCodes = missingTranslations.keys - .where((languageCode) => missingTranslations[languageCode]?.isNotEmpty ?? false) + .where((languageCode) => + missingTranslations[languageCode]?.isNotEmpty ?? false) .where((languageCode) => !languageCode.startsWith('@@')) .toList(); - print('Found missing translations for ${missingLanguageCodes.length} languages.'); + print( + 'Found missing translations for ${missingLanguageCodes.length} languages.'); errorOccurredInTranslatingTree = true; while (errorOccurredInTranslatingTree) { errorOccurredInTranslatingTree = false; final useLibreEngine = random.nextBool(); - print('Using ${useLibreEngine ? 'Libre' : 'Google'} translation engine...\n'); - translator = SimplyTranslator(useLibreEngine ? EngineType.libre : EngineType.google); + print( + 'Using ${useLibreEngine ? 'Libre' : 'Google'} translation engine...\n'); + translator = + SimplyTranslator(useLibreEngine ? EngineType.libre : EngineType.google); for (final languageCode in missingLanguageCodes) { print('Translating $languageCode...'); @@ -172,9 +189,8 @@ void main() async { // mark all newly translated paths as outdated // for a human to review - final pathsWithoutLanguageCode = newlyTranslatedPaths - .map((e) => e.substring(e.indexOf('/') + 1)) - .toSet(); + final pathsWithoutLanguageCode = + newlyTranslatedPaths.map((e) => e.substring(e.indexOf('/') + 1)).toSet(); for (final path in pathsWithoutLanguageCode) { print('Marking $path as outdated...'); await shell.run('dart run slang outdated $path'); diff --git a/test/canvas_backgrounds_test.dart b/test/canvas_backgrounds_test.dart index e4e1dea05..079356e3a 100644 --- a/test/canvas_backgrounds_test.dart +++ b/test/canvas_backgrounds_test.dart @@ -13,9 +13,11 @@ void main() { }); } -void _testPatternWithLineHeight(final CanvasBackgroundPattern pattern, final int lineHeight) { +void _testPatternWithLineHeight( + final CanvasBackgroundPattern pattern, final int lineHeight) { test("'$pattern' with line height $lineHeight", () { const Size size = Size(1000, 1000); + /// We can't directly compare doubles, so check if they're within a small range (±epsilon) final double epsilon = lineHeight / 100; @@ -26,18 +28,25 @@ void _testPatternWithLineHeight(final CanvasBackgroundPattern pattern, final int ).toList(); if (pattern == CanvasBackgroundPattern.none) { - expect(elements.isEmpty, true, reason: 'No elements should be returned for the none pattern'); + expect(elements.isEmpty, true, + reason: 'No elements should be returned for the none pattern'); return; } else { - expect(elements.isEmpty, false, reason: 'Elements should be returned for patterns other than the none pattern'); + expect(elements.isEmpty, false, + reason: + 'Elements should be returned for patterns other than the none pattern'); } // Check that the elements are within the bounds of the canvas for (PatternElement element in elements) { - expect(element.start.dx, lessThanOrEqualTo(size.width), reason: 'element.start not within canvas bounds'); - expect(element.start.dy, lessThanOrEqualTo(size.height), reason: 'element.start not within canvas bounds'); - expect(element.end.dx, lessThanOrEqualTo(size.width), reason: 'element.end not within canvas bounds'); - expect(element.end.dy, lessThanOrEqualTo(size.height), reason: 'element.end not within canvas bounds'); + expect(element.start.dx, lessThanOrEqualTo(size.width), + reason: 'element.start not within canvas bounds'); + expect(element.start.dy, lessThanOrEqualTo(size.height), + reason: 'element.start not within canvas bounds'); + expect(element.end.dx, lessThanOrEqualTo(size.width), + reason: 'element.end not within canvas bounds'); + expect(element.end.dy, lessThanOrEqualTo(size.height), + reason: 'element.end not within canvas bounds'); } // Check we have 2 lineHeights of space at the top @@ -46,19 +55,22 @@ void _testPatternWithLineHeight(final CanvasBackgroundPattern pattern, final int expect( element.start.dy, greaterThan(lineHeight * 2 - 1), - reason: 'Elements should leave 2 lineHeights of space at the top, but element.start.dy was ${element.start.dy}', + reason: + 'Elements should leave 2 lineHeights of space at the top, but element.start.dy was ${element.start.dy}', ); expect( element.end.dy, greaterThan(lineHeight * 2 - 1), - reason: 'Elements should leave 2 lineHeights of space at the top, but element.end.dy was ${element.end.dy}', + reason: + 'Elements should leave 2 lineHeights of space at the top, but element.end.dy was ${element.end.dy}', ); } // Check all elements are lines or all elements are dots bool allLines = elements.every((element) => element.isLine); bool allDots = elements.every((element) => !element.isLine); - expect(allLines || allDots, true, reason: 'All elements should be lines or all elements should be dots'); + expect(allLines || allDots, true, + reason: 'All elements should be lines or all elements should be dots'); if (allLines) { // Check spacing @@ -67,7 +79,8 @@ void _testPatternWithLineHeight(final CanvasBackgroundPattern pattern, final int for (PatternElement element in elements) { bool isHorizontal = element.start.dy == element.end.dy; bool isVertical = element.start.dx == element.end.dx; - expect(isHorizontal || isVertical, true, reason: 'Lines should be horizontal or vertical'); + expect(isHorizontal || isVertical, true, + reason: 'Lines should be horizontal or vertical'); double position = isHorizontal ? element.start.dy : element.start.dx; @@ -81,7 +94,8 @@ void _testPatternWithLineHeight(final CanvasBackgroundPattern pattern, final int if (pattern != CanvasBackgroundPattern.cornell) { // Cornell has two lines on the same row, so they shouldn't be spaced apart - expect(position != lastPosition, true, reason: 'Lines should be spaced apart'); + expect(position != lastPosition, true, + reason: 'Lines should be spaced apart'); } double spacing = (position - lastPosition).abs(); @@ -89,8 +103,10 @@ void _testPatternWithLineHeight(final CanvasBackgroundPattern pattern, final int if (diffFromALine > lineHeight / 2) { diffFromALine = lineHeight - diffFromALine; } - printOnFailure('spacing: $spacing, lineHeight: $lineHeight, diffFromALine: $diffFromALine'); - expect(diffFromALine, lessThan(epsilon), reason: 'Lines should be spaced in intervals of lineHeight'); + printOnFailure( + 'spacing: $spacing, lineHeight: $lineHeight, diffFromALine: $diffFromALine'); + expect(diffFromALine, lessThan(epsilon), + reason: 'Lines should be spaced in intervals of lineHeight'); lastPosition = position; } @@ -125,9 +141,10 @@ void _testRtlPattern(final CanvasBackgroundPattern pattern) { final rtl = pattern.name.contains('rtl'); final isCorrectlyRtl = rtl - ? linesOnRight >= linesOnLeft * 0.9 - : linesOnLeft >= linesOnRight * 0.9; + ? linesOnRight >= linesOnLeft * 0.9 + : linesOnLeft >= linesOnRight * 0.9; printOnFailure('linesOnLeft: $linesOnLeft, linesOnRight: $linesOnRight'); - expect(isCorrectlyRtl, true, reason: 'Lines should be on the left in ltr and on the right in rtl'); + expect(isCorrectlyRtl, true, + reason: 'Lines should be on the left in ltr and on the right in rtl'); }); } diff --git a/test/canvas_transform_cache_test.dart b/test/canvas_transform_cache_test.dart index eb3004abf..0ebf44b05 100644 --- a/test/canvas_transform_cache_test.dart +++ b/test/canvas_transform_cache_test.dart @@ -21,12 +21,14 @@ void main() { // add first item CanvasTransformCache.add(samples[0].filePath, samples[0].transform); expect(CanvasTransformCache.get(samples[0].filePath), isNotNull); - expect(CanvasTransformCache.get(samples[0].filePath)!.transform, samples[0].transform); + expect(CanvasTransformCache.get(samples[0].filePath)!.transform, + samples[0].transform); // update first item CanvasTransformCache.add(samples[0].filePath, samples[1].transform); expect(CanvasTransformCache.get(samples[0].filePath), isNotNull); - expect(CanvasTransformCache.get(samples[0].filePath)!.transform, samples[1].transform); + expect(CanvasTransformCache.get(samples[0].filePath)!.transform, + samples[1].transform); // add the rest of the items for (final sample in samples.skip(1)) { @@ -39,7 +41,8 @@ void main() { // the rest of the items should be in the cache for (final sample in samples.skip(1)) { expect(CanvasTransformCache.get(sample.filePath), isNotNull); - expect(CanvasTransformCache.get(sample.filePath)!.transform, sample.transform); + expect(CanvasTransformCache.get(sample.filePath)!.transform, + sample.transform); } // clear items diff --git a/test/describe_color_test.dart b/test/describe_color_test.dart index 1ba8812a1..e6b436f8c 100644 --- a/test/describe_color_test.dart +++ b/test/describe_color_test.dart @@ -8,10 +8,12 @@ void main() { expect(ColorBar.describeColor(Colors.red), 'Custom red'); }); test('dark blue', () { - expect(ColorBar.describeColor(const Color(0xFF00145c)), 'Custom dark blue'); + expect( + ColorBar.describeColor(const Color(0xFF00145c)), 'Custom dark blue'); }); test('light blue', () { - expect(ColorBar.describeColor(const Color(0xFF8fa7ff)), 'Custom light blue'); + expect( + ColorBar.describeColor(const Color(0xFF8fa7ff)), 'Custom light blue'); }); test('grey', () { expect(ColorBar.describeColor(Colors.grey), 'Custom grey'); @@ -23,7 +25,8 @@ void main() { expect(ColorBar.describeColor(Colors.white), 'Custom light grey'); }); test('light pink', () { - expect(ColorBar.describeColor(const Color(0xFFffcff0)), 'Custom light pink'); + expect( + ColorBar.describeColor(const Color(0xFFffcff0)), 'Custom light pink'); }); }); } diff --git a/test/editor_current_page_test.dart b/test/editor_current_page_test.dart index 3323d4dce..22b432e29 100644 --- a/test/editor_current_page_test.dart +++ b/test/editor_current_page_test.dart @@ -66,7 +66,8 @@ void main() { ); }); - test('returns last page index when scroll position is beyond last page', () { + test('returns last page index when scroll position is beyond last page', + () { expect( EditorState.getPageIndexFromScrollPosition( scrollY: 100000, diff --git a/test/editor_history_test.dart b/test/editor_history_test.dart index 33612f16c..80a6ff66e 100644 --- a/test/editor_history_test.dart +++ b/test/editor_history_test.dart @@ -21,8 +21,10 @@ void main() { expect(history.canUndo, false, reason: 'History should be empty'); expect(history.canRedo, false, reason: 'History should be empty'); - expect(() => history.undo(), throwsA(anything), reason: 'Undo should throw an exception if history is empty'); - expect(() => history.redo(), throwsA(anything), reason: 'Redo should throw an exception if history is empty'); + expect(() => history.undo(), throwsA(anything), + reason: 'Undo should throw an exception if history is empty'); + expect(() => history.redo(), throwsA(anything), + reason: 'Redo should throw an exception if history is empty'); history.recordChange(item1); expect(history.canUndo, true); diff --git a/test/editor_undo_redo_test.dart b/test/editor_undo_redo_test.dart index 1f8d4ad54..df1938021 100644 --- a/test/editor_undo_redo_test.dart +++ b/test/editor_undo_redo_test.dart @@ -54,48 +54,60 @@ void main() { final editorState = tester.state(find.byType(Editor)); for (int i = 0; i < 10; i++) { if (!editorState.coreInfo.readOnly) break; - await tester.runAsync(() => Future.delayed(const Duration(milliseconds: 10))); + await tester + .runAsync(() => Future.delayed(const Duration(milliseconds: 10))); } - expect(editorState.coreInfo.readOnly, isFalse, reason: 'Editor is still read-only'); + expect(editorState.coreInfo.readOnly, isFalse, + reason: 'Editor is still read-only'); printOnFailure('Editor core info is loaded'); IconButton getUndoBtn() => tester.widget(find.ancestor( - of: find.byIcon(Icons.undo), - matching: find.byType(IconButton), - )); + of: find.byIcon(Icons.undo), + matching: find.byType(IconButton), + )); IconButton getRedoBtn() => tester.widget(find.ancestor( - of: find.byIcon(Icons.redo), - matching: find.byType(IconButton), - )); + of: find.byIcon(Icons.redo), + matching: find.byType(IconButton), + )); - expect(getUndoBtn().onPressed, isNull, reason: 'Undo button should be disabled initially'); - expect(getRedoBtn().onPressed, isNull, reason: 'Redo button should be disabled initially'); + expect(getUndoBtn().onPressed, isNull, + reason: 'Undo button should be disabled initially'); + expect(getRedoBtn().onPressed, isNull, + reason: 'Redo button should be disabled initially'); // draw something await drawOnEditor(tester); await tester.pumpAndSettle(); - expect(getUndoBtn().onPressed, isNotNull, reason: 'Undo button should be enabled after first draw'); - expect(getRedoBtn().onPressed, isNull, reason: 'Redo button should be disabled after first draw'); + expect(getUndoBtn().onPressed, isNotNull, + reason: 'Undo button should be enabled after first draw'); + expect(getRedoBtn().onPressed, isNull, + reason: 'Redo button should be disabled after first draw'); // undo await tester.tap(find.byIcon(Icons.undo)); await tester.pump(); - expect(getUndoBtn().onPressed, isNull, reason: 'Undo button should be disabled after undo'); - expect(getRedoBtn().onPressed, isNotNull, reason: 'Redo button should be enabled after undo'); + expect(getUndoBtn().onPressed, isNull, + reason: 'Undo button should be disabled after undo'); + expect(getRedoBtn().onPressed, isNotNull, + reason: 'Redo button should be enabled after undo'); // redo await tester.tap(find.byIcon(Icons.redo)); await tester.pump(); - expect(getUndoBtn().onPressed, isNotNull, reason: 'Undo button should be enabled after redo'); - expect(getRedoBtn().onPressed, isNull, reason: 'Redo button should be disabled after redo'); + expect(getUndoBtn().onPressed, isNotNull, + reason: 'Undo button should be enabled after redo'); + expect(getRedoBtn().onPressed, isNull, + reason: 'Redo button should be disabled after redo'); // undo, then draw again await tester.tap(find.byIcon(Icons.undo)); await tester.pump(); await drawOnEditor(tester); await tester.pump(); - expect(getUndoBtn().onPressed, isNotNull, reason: 'Undo button should be enabled after undo and draw'); - expect(getRedoBtn().onPressed, isNull, reason: 'Redo button should be disabled after undo and draw'); + expect(getUndoBtn().onPressed, isNotNull, + reason: 'Undo button should be enabled after undo and draw'); + expect(getRedoBtn().onPressed, isNull, + reason: 'Redo button should be disabled after undo and draw'); // save file now to supersede the save timer (which would run after the test is finished) printOnFailure('Saving file: $filePath${Editor.extension}'); @@ -108,7 +120,7 @@ void main() { } Future drawOnEditor(WidgetTester tester) => tester.timedDrag( - find.byType(Editor), - const Offset(50, 0), - const Duration(milliseconds: 100), -); + find.byType(Editor), + const Offset(50, 0), + const Duration(milliseconds: 100), + ); diff --git a/test/editor_zoom_test.dart b/test/editor_zoom_test.dart index 7fb96504f..1778818f6 100644 --- a/test/editor_zoom_test.dart +++ b/test/editor_zoom_test.dart @@ -57,7 +57,7 @@ void main() { test('Zoom in with Ctrl + above max zoom', () { final oldMatrix = Matrix4.identity() - ..scale(CanvasGestureDetector.kMaxScale); + ..scale(CanvasGestureDetector.kMaxScale); final newMatrix = CanvasGestureDetectorState.setZoom( scaleDelta: 0.1, transformation: oldMatrix, @@ -68,7 +68,7 @@ void main() { test('Zoom out with Ctrl - below min zoom', () { final oldMatrix = Matrix4.identity() - ..scale(CanvasGestureDetector.kMinScale); + ..scale(CanvasGestureDetector.kMinScale); final newMatrix = CanvasGestureDetectorState.setZoom( scaleDelta: -0.1, transformation: oldMatrix, diff --git a/test/flutter_submodule_test.dart b/test/flutter_submodule_test.dart index e7f6a3edb..ad757ab95 100644 --- a/test/flutter_submodule_test.dart +++ b/test/flutter_submodule_test.dart @@ -3,7 +3,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:process_run/shell.dart'; void main() { - const maintenanceChecksEnabled = bool.fromEnvironment('maintenanceChecksEnabled'); + const maintenanceChecksEnabled = + bool.fromEnvironment('maintenanceChecksEnabled'); if (maintenanceChecksEnabled) { test('Test that flutter submodule is up to date', () async { @@ -15,22 +16,25 @@ void main() { /// This doesn't require the flutter submodule to be cloned. /// /// e.g. b06b8b2710 - final submoduleCommit = await shell.run('git rev-parse @:./submodules/flutter') - .then((value) => value.outText.substring(0, 10)); + final submoduleCommit = await shell + .run('git rev-parse @:./submodules/flutter') + .then((value) => value.outText.substring(0, 10)); expect(submoduleCommit.length, 10); /// The output of `flutter --version`. /// /// Contains a line like this: /// Framework • revision b06b8b2710 (3 days ago) • 2023-01-23 16:55:55 -0800 - final localFlutterVersion = await shell.run('flutter --version') - .then((value) => value.outText); + final localFlutterVersion = + await shell.run('flutter --version').then((value) => value.outText); expect(localFlutterVersion.length, greaterThan(10)); printOnFailure('Flutter submodule commit: $submoduleCommit'); printOnFailure('Local Flutter version: $localFlutterVersion'); - expect(localFlutterVersion.contains(submoduleCommit), true, reason: 'Flutter submodule does not match local version. Please run `./scripts/update_flutter_submodule.sh` to update the submodule.'); + expect(localFlutterVersion.contains(submoduleCommit), true, + reason: + 'Flutter submodule does not match local version. Please run `./scripts/update_flutter_submodule.sh` to update the submodule.'); }); } } diff --git a/test/fm_test.dart b/test/fm_test.dart index becb9e004..9a4841e87 100644 --- a/test/fm_test.dart +++ b/test/fm_test.dart @@ -46,7 +46,8 @@ void main() { const content = 'test content for $filePath'; // write file - await FileManager.writeFile(filePath, utf8.encode(content), awaitWrite: true); + await FileManager.writeFile(filePath, utf8.encode(content), + awaitWrite: true); // read file final file = File('$rootDir$filePath'); @@ -62,7 +63,8 @@ void main() { const content = 'test content for $filePath'; // write file - await FileManager.writeFile(filePath, utf8.encode(content), awaitWrite: true); + await FileManager.writeFile(filePath, utf8.encode(content), + awaitWrite: true); // read file final readBytes = await FileManager.readFile(filePath); @@ -80,12 +82,14 @@ void main() { const content = 'test content for $filePathBefore'; // write file - await FileManager.writeFile(filePathBefore, utf8.encode(content), awaitWrite: true); + await FileManager.writeFile(filePathBefore, utf8.encode(content), + awaitWrite: true); // ensure file does not exist (in case of previous test failure await FileManager.deleteFile(filePathAfter); // move file - final filePathActual = await FileManager.moveFile(filePathBefore, filePathAfter); + final filePathActual = + await FileManager.moveFile(filePathBefore, filePathAfter); expect(filePathActual, filePathAfter); // verify filePathBefore does not exist, but filePathAfter does @@ -108,7 +112,8 @@ void main() { const content = 'test content for $filePath'; // write file - await FileManager.writeFile(filePath, utf8.encode(content), awaitWrite: true); + await FileManager.writeFile(filePath, utf8.encode(content), + awaitWrite: true); // delete file await FileManager.deleteFile(filePath); diff --git a/test/fm_unique_suffix_test.dart b/test/fm_unique_suffix_test.dart index 1b5f61ade..0e7d19eaf 100644 --- a/test/fm_unique_suffix_test.dart +++ b/test/fm_unique_suffix_test.dart @@ -28,17 +28,24 @@ void main() { ]); suffixedPath = await FileManager.suffixFilePathToMakeItUnique(filePath); - expect(suffixedPath == filePath, true, reason: "filePath doesn't exist, so it should be returned as is"); + expect(suffixedPath == filePath, true, + reason: "filePath doesn't exist, so it should be returned as is"); - await FileManager.writeFile(suffixedPath, [1,2,3], awaitWrite: true, alsoUpload: false); + await FileManager.writeFile(suffixedPath, [1, 2, 3], + awaitWrite: true, alsoUpload: false); suffixedPath = await FileManager.suffixFilePathToMakeItUnique(filePath); - expect(suffixedPath == filePath2, true, reason: "filePath exists, but filePath2 doesn't, so filePath2 should be returned"); + expect(suffixedPath == filePath2, true, + reason: + "filePath exists, but filePath2 doesn't, so filePath2 should be returned"); - await FileManager.writeFile(suffixedPath, [1,2,3], awaitWrite: true, alsoUpload: false); + await FileManager.writeFile(suffixedPath, [1, 2, 3], + awaitWrite: true, alsoUpload: false); suffixedPath = await FileManager.suffixFilePathToMakeItUnique(filePath); - expect(suffixedPath == filePath3, true, reason: "filePath and filePath2 exist, but filePath3 doesn't, so filePath3 should be returned"); + expect(suffixedPath == filePath3, true, + reason: + "filePath and filePath2 exist, but filePath3 doesn't, so filePath3 should be returned"); // cleanup await Future.wait([ diff --git a/test/fm_write_stream_test.dart b/test/fm_write_stream_test.dart index 82161f36e..b17380638 100644 --- a/test/fm_write_stream_test.dart +++ b/test/fm_write_stream_test.dart @@ -14,10 +14,10 @@ void main() { setUp(() async { events.clear(); await subscription?.cancel(); - subscription = FileManager.fileWriteStream.stream - .listen((FileOperation event) { - events.add(event); - }); + subscription = + FileManager.fileWriteStream.stream.listen((FileOperation event) { + events.add(event); + }); }); tearDown(() async { await subscription?.cancel(); diff --git a/test/i18n_misspellings_test.dart b/test/i18n_misspellings_test.dart index 25d554c05..130f36a5a 100644 --- a/test/i18n_misspellings_test.dart +++ b/test/i18n_misspellings_test.dart @@ -9,7 +9,8 @@ void main() { ...Directory('lib/i18n/community').listSync(), // All files of the format metadata/XX/full_description.txt - ...Directory('metadata').listSync() + ...Directory('metadata') + .listSync() .whereType() .expand((dir) => dir.listSync()) .whereType() @@ -19,7 +20,8 @@ void main() { for (final file in files) { test('in ${file.path}', () async { final contents = await File(file.path).readAsString(); - expect(contents, isNot(contains('Sabre')), reason: 'Saber is misspelled as Sabre in ${file.path}'); + expect(contents, isNot(contains('Sabre')), + reason: 'Saber is misspelled as Sabre in ${file.path}'); }); } }); diff --git a/test/isolate_message_test.dart b/test/isolate_message_test.dart index 1bcf6ee23..f3aa7922a 100644 --- a/test/isolate_message_test.dart +++ b/test/isolate_message_test.dart @@ -46,8 +46,8 @@ void main() { }); test('With small PNG image', () async { - final imageBytes = await File('assets/icon/resized/icon-16x16.png') - .readAsBytes(); + final imageBytes = + await File('assets/icon/resized/icon-16x16.png').readAsBytes(); final coreInfo = EditorCoreInfo(filePath: '', readOnly: false); coreInfo.pages = [ EditorPage( @@ -70,8 +70,7 @@ void main() { }); test('With large PNG image', () async { - final imageBytes = await File('assets/icon/icon.png') - .readAsBytes(); + final imageBytes = await File('assets/icon/icon.png').readAsBytes(); final coreInfo = EditorCoreInfo(filePath: '', readOnly: false); coreInfo.pages = [ EditorPage( diff --git a/test/locale_names_test.dart b/test/locale_names_test.dart index 308828010..e428d353b 100644 --- a/test/locale_names_test.dart +++ b/test/locale_names_test.dart @@ -7,7 +7,8 @@ void main() { test('Test that all supported languages have a localised name', () { for (Locale locale in AppLocaleUtils.supportedLocales) { final String localeCode = locale.toLanguageTag(); - expect(localeNames.containsKey(localeCode), true, reason: 'Missing locale name for $localeCode'); + expect(localeNames.containsKey(localeCode), true, + reason: 'Missing locale name for $localeCode'); } }); } diff --git a/test/login_validation_test.dart b/test/login_validation_test.dart index e5ff4b506..fd664ad79 100644 --- a/test/login_validation_test.dart +++ b/test/login_validation_test.dart @@ -3,84 +3,97 @@ import 'package:saber/components/nextcloud/login_group.dart'; import 'package:saber/data/nextcloud/nextcloud_client_extension.dart'; void main() => group('Test login validation:', () { - group('Email:', () { - test('Valid username should pass', () { - expect(LoginInputGroup.validateUsername('username'), isNull); - }); - test('Valid email should pass', () { - expect(LoginInputGroup.validateUsername('user@example.com'), isNull); - }); - test('Empty email should fail', () { - expect(LoginInputGroup.validateUsername(''), isNotNull); - }); - test('Invalid email should fail', () { - expect(LoginInputGroup.validateUsername('invalid email @'), isNotNull); - }); - }); + group('Email:', () { + test('Valid username should pass', () { + expect(LoginInputGroup.validateUsername('username'), isNull); + }); + test('Valid email should pass', () { + expect(LoginInputGroup.validateUsername('user@example.com'), isNull); + }); + test('Empty email should fail', () { + expect(LoginInputGroup.validateUsername(''), isNotNull); + }); + test('Invalid email should fail', () { + expect( + LoginInputGroup.validateUsername('invalid email @'), isNotNull); + }); + }); - group('Nextcloud password:', () { - test('Empty password should fail', () { - expect(LoginInputGroup.validateNcPassword(''), isNotNull); - }); - test('Non-empty password should pass', () { - expect(LoginInputGroup.validateNcPassword('p'), isNull); - }); - }); + group('Nextcloud password:', () { + test('Empty password should fail', () { + expect(LoginInputGroup.validateNcPassword(''), isNotNull); + }); + test('Non-empty password should pass', () { + expect(LoginInputGroup.validateNcPassword('p'), isNull); + }); + }); - group('Encryption password:', () { - test('Empty password should fail', () { - expect(LoginInputGroup.validateEncPassword(''), isNotNull); - }); - test('Non-empty password should pass', () { - expect(LoginInputGroup.validateEncPassword('p'), isNull); - }); - }); + group('Encryption password:', () { + test('Empty password should fail', () { + expect(LoginInputGroup.validateEncPassword(''), isNotNull); + }); + test('Non-empty password should pass', () { + expect(LoginInputGroup.validateEncPassword('p'), isNull); + }); + }); - group('URL:', () { - test('null URL should fail', () { - expect(LoginInputGroup.validateCustomServer(null), isNotNull); - }); - test('Empty URL should fail', () { - expect(LoginInputGroup.validateCustomServer(''), isNotNull); - }); - test('Default URL should pass', () { - expect(LoginInputGroup.validateCustomServer(NextcloudClientExtension.defaultNextcloudUri.toString()), isNull); - }); - test('Invalid URL should fail', () { - expect(LoginInputGroup.validateCustomServer('invalid url'), isNotNull); - }); - test('URL with /nextcloud should pass', () { - expect(LoginInputGroup.validateCustomServer('https://example.com/nextcloud'), isNull); - }); - test('URL with http (not https) should pass', () { - expect(LoginInputGroup.validateCustomServer('http://example.com'), isNull); - }); - test('URL with ftp protocol should fail', () { - expect(LoginInputGroup.validateCustomServer('ftp://example.com'), isNotNull); - }); - test('IP address without port should pass', () { - expect(LoginInputGroup.validateCustomServer('http://192.168.0.1'), isNull); - }); - test('IP address with port should pass', () { - expect(LoginInputGroup.validateCustomServer('http://192.168.0.1:8080'), isNull); - }); - }); + group('URL:', () { + test('null URL should fail', () { + expect(LoginInputGroup.validateCustomServer(null), isNotNull); + }); + test('Empty URL should fail', () { + expect(LoginInputGroup.validateCustomServer(''), isNotNull); + }); + test('Default URL should pass', () { + expect( + LoginInputGroup.validateCustomServer( + NextcloudClientExtension.defaultNextcloudUri.toString()), + isNull); + }); + test('Invalid URL should fail', () { + expect( + LoginInputGroup.validateCustomServer('invalid url'), isNotNull); + }); + test('URL with /nextcloud should pass', () { + expect( + LoginInputGroup.validateCustomServer( + 'https://example.com/nextcloud'), + isNull); + }); + test('URL with http (not https) should pass', () { + expect(LoginInputGroup.validateCustomServer('http://example.com'), + isNull); + }); + test('URL with ftp protocol should fail', () { + expect(LoginInputGroup.validateCustomServer('ftp://example.com'), + isNotNull); + }); + test('IP address without port should pass', () { + expect(LoginInputGroup.validateCustomServer('http://192.168.0.1'), + isNull); + }); + test('IP address with port should pass', () { + expect( + LoginInputGroup.validateCustomServer('http://192.168.0.1:8080'), + isNull); + }); + }); - group('URL prefixing:', () { - test("URL starting with 'http' should stay the same", () { - String url = 'http://example.com'; - String prefixed = LoginInputGroup.prefixUrlWithHttps(url); - expect(prefixed, url); - }); - test("URL starting with 'https' should stay the same", () { - String url = 'https://example.com'; - String prefixed = LoginInputGroup.prefixUrlWithHttps(url); - expect(prefixed, url); - }); - test("URL without 'https?' should be prefixed", () { - String url = 'example.com'; - String prefixed = LoginInputGroup.prefixUrlWithHttps(url); - expect(prefixed, 'https://$url'); + group('URL prefixing:', () { + test("URL starting with 'http' should stay the same", () { + String url = 'http://example.com'; + String prefixed = LoginInputGroup.prefixUrlWithHttps(url); + expect(prefixed, url); + }); + test("URL starting with 'https' should stay the same", () { + String url = 'https://example.com'; + String prefixed = LoginInputGroup.prefixUrlWithHttps(url); + expect(prefixed, url); + }); + test("URL without 'https?' should be prefixed", () { + String url = 'example.com'; + String prefixed = LoginInputGroup.prefixUrlWithHttps(url); + expect(prefixed, 'https://$url'); + }); + }); }); - }); -}); diff --git a/test/nc_deletion_test.dart b/test/nc_deletion_test.dart index 3ea4eb122..85c9916da 100644 --- a/test/nc_deletion_test.dart +++ b/test/nc_deletion_test.dart @@ -48,7 +48,8 @@ void main() { ); // Create a file (to delete later) - await FileManager.writeFile(filePathLocal, fileContent, awaitWrite: true, alsoUpload: false); + await FileManager.writeFile(filePathLocal, fileContent, + awaitWrite: true, alsoUpload: false); // Upload file to Nextcloud Prefs.fileSyncUploadQueue.value = Queue.from([filePathLocal]); @@ -56,7 +57,8 @@ void main() { // Check that the file exists on Nextcloud printOnFailure('Checking if ${syncFile.remotePath} exists on Nextcloud'); - final webDavFiles = await webdav.propfind(PathUri.parse(syncFile.remotePath), depth: WebDavDepth.zero) + final webDavFiles = await webdav + .propfind(PathUri.parse(syncFile.remotePath), depth: WebDavDepth.zero) .then((multistatus) => multistatus.toWebDavFiles()); expect(webDavFiles.length, 1, reason: 'File should exist on Nextcloud'); @@ -68,9 +70,13 @@ void main() { await FileSyncer.uploadFileFromQueue(); // Check that the file is empty on Nextcloud - final webDavFile = await webdav.propfind(PathUri.parse(syncFile.remotePath), depth: WebDavDepth.zero, prop: WebDavPropWithoutValues.fromBools( - davgetcontentlength: true, - )).then((multistatus) => multistatus.toWebDavFiles().single); + final webDavFile = await webdav + .propfind(PathUri.parse(syncFile.remotePath), + depth: WebDavDepth.zero, + prop: WebDavPropWithoutValues.fromBools( + davgetcontentlength: true, + )) + .then((multistatus) => multistatus.toWebDavFiles().single); expect(webDavFile.size, 0, reason: 'File should be empty on Nextcloud'); // Sync the file from Nextcloud diff --git a/test/nc_encryption_test.dart b/test/nc_encryption_test.dart index aef28010c..0805c8387 100644 --- a/test/nc_encryption_test.dart +++ b/test/nc_encryption_test.dart @@ -32,13 +32,17 @@ void main() { final Encrypter encrypter = await client.encrypter; final IV iv = IV.fromBase64(Prefs.iv.value); - final String filePathEncrypted = encrypter.encrypt(filePathUnencrypted, iv: iv).base16; - expect(filePathEncrypted.isNotEmpty, true, reason: 'Encrypted file path is empty'); + final String filePathEncrypted = + encrypter.encrypt(filePathUnencrypted, iv: iv).base16; + expect(filePathEncrypted.isNotEmpty, true, + reason: 'Encrypted file path is empty'); final key2 = await client.loadEncryptionKey(); expect(key1 == key2, true, reason: 'Key changed'); - final String filePathDecrypted = encrypter.decrypt(Encrypted.fromBase16(filePathEncrypted), iv: iv); - expect(filePathDecrypted == filePathUnencrypted, true, reason: 'Decrypted file path is not the same as the original'); + final String filePathDecrypted = + encrypter.decrypt(Encrypted.fromBase16(filePathEncrypted), iv: iv); + expect(filePathDecrypted == filePathUnencrypted, true, + reason: 'Decrypted file path is not the same as the original'); }); } diff --git a/test/prefs_test.dart b/test/prefs_test.dart index ea92f4266..62fe76ba0 100644 --- a/test/prefs_test.dart +++ b/test/prefs_test.dart @@ -26,7 +26,8 @@ void main() { test('PlainPref', () async { final defaultValue = StrokeProperties(); await testPref( - prefBuilder: () => PlainPref('testPlainPrefStrokeProperties', defaultValue), + prefBuilder: () => + PlainPref('testPlainPrefStrokeProperties', defaultValue), defaultValue: defaultValue, alteredValue: defaultValue.copy() ..size = StrokeProperties.defaultSize / 2, diff --git a/test/sbn_test.dart b/test/sbn_test.dart index b63365de0..e37ae8819 100644 --- a/test/sbn_test.dart +++ b/test/sbn_test.dart @@ -32,14 +32,14 @@ void main() { FileManager.init(); setUpAll(() => Future.wait([ - InvertShader.init(), - PencilShader.init(), - GoogleFonts.pendingFonts([ - GoogleFonts.neucha(), - GoogleFonts.dekko(), - GoogleFonts.firaMono(), - ]), - ])); + InvertShader.init(), + PencilShader.init(), + GoogleFonts.pendingFonts([ + GoogleFonts.neucha(), + GoogleFonts.dekko(), + GoogleFonts.firaMono(), + ]), + ])); final sbnExamples = Directory('test/sbn_examples/') .listSync() @@ -84,9 +84,9 @@ void main() { testWidgets('(Light)', (tester) async { await tester.runAsync(() => _precacheImages( - context: tester.binding.rootElement!, - page: page, - )); + context: tester.binding.rootElement!, + page: page, + )); await tester.pumpWidget(_buildCanvas( brightness: Brightness.light, path: path, @@ -103,9 +103,9 @@ void main() { testWidgets('(Dark)', (tester) async { await tester.runAsync(() => _precacheImages( - context: tester.binding.rootElement!, - page: page, - )); + context: tester.binding.rootElement!, + page: page, + )); await tester.pumpWidget(_buildCanvas( brightness: Brightness.dark, path: path, @@ -135,7 +135,8 @@ void main() { // Convert PDF to PNG with Ghostscript final shell = Shell(verbose: false); - await tester.runAsync(() => shell.run('gs -sDEVICE=pngalpha -o ${pngFile.path} ${pdfFile.path}')); + await tester.runAsync(() => shell + .run('gs -sDEVICE=pngalpha -o ${pngFile.path} ${pdfFile.path}')); // Load PNG from disk final pdfImage = await tester.runAsync(() => pngFile.readAsBytes()); @@ -166,22 +167,23 @@ void main() { // copy the file to the temporary directory await tester.runAsync(() => Future.wait([ - FileManager.getFile('/$path') - .create(recursive: true) - .then((file) => File(path).copy(file.path)), - FileManager.getFile('/$path.0') - .create(recursive: true) - .then((file) => File('$path.0').copy(file.path)), - ])); - - final coreInfo = await tester.runAsync(() => EditorCoreInfo.loadFromFilePath( - '/$pathWithoutExtension', - )); + FileManager.getFile('/$path') + .create(recursive: true) + .then((file) => File(path).copy(file.path)), + FileManager.getFile('/$path.0') + .create(recursive: true) + .then((file) => File('$path.0').copy(file.path)), + ])); + + final coreInfo = + await tester.runAsync(() => EditorCoreInfo.loadFromFilePath( + '/$pathWithoutExtension', + )); if (coreInfo == null) fail('Failed to load core info'); final sba = await tester.runAsync(() => coreInfo.saveToSba( - currentPageIndex: null, - )); + currentPageIndex: null, + )); if (sba == null) fail('Failed to save SBA'); final sbaFile = File('$pathWithoutExtension.sba'); @@ -189,20 +191,21 @@ void main() { addTearDown(sbaFile.delete); final importedPath = await tester.runAsync(() => FileManager.importFile( - sbaFile.path, - null, - )); + sbaFile.path, + null, + )); if (importedPath == null) fail('Failed to import SBA'); - final importedCoreInfo = await tester.runAsync(() => EditorCoreInfo.loadFromFilePath( - importedPath, - )); + final importedCoreInfo = + await tester.runAsync(() => EditorCoreInfo.loadFromFilePath( + importedPath, + )); if (importedCoreInfo == null) fail('Failed to load imported core info'); await tester.runAsync(() => _precacheImages( - context: tester.binding.rootElement!, - page: importedCoreInfo.pages.first, - )); + context: tester.binding.rootElement!, + page: importedCoreInfo.pages.first, + )); await tester.pumpWidget(_buildCanvas( brightness: Brightness.light, path: importedPath, @@ -220,7 +223,8 @@ void main() { } /// Provides a [BuildContext] with the necessary inherited widgets -Future _getBuildContext(WidgetTester tester, Size pageSize) async { +Future _getBuildContext( + WidgetTester tester, Size pageSize) async { final completer = Completer(); await tester.pumpWidget(TranslationProvider( @@ -292,20 +296,20 @@ Future _precacheImages({ for (final image in page.images) if (image is PngEditorImage) if (image.imageProvider is FileImage) - (image.imageProvider as FileImage).file.readAsBytes() - .then((bytes) => image.imageProvider = MemoryImage(bytes)), + (image.imageProvider as FileImage) + .file + .readAsBytes() + .then((bytes) => image.imageProvider = MemoryImage(bytes)), if (backgroundImage is PngEditorImage) if (backgroundImage.imageProvider is FileImage) - (backgroundImage.imageProvider as FileImage).file.readAsBytes() - .then((bytes) => (page.backgroundImage as PngEditorImage) - .imageProvider = MemoryImage(bytes)), + (backgroundImage.imageProvider as FileImage).file.readAsBytes().then( + (bytes) => (page.backgroundImage as PngEditorImage).imageProvider = + MemoryImage(bytes)), ]); // Precache images await Future.wait([ - for (final image in page.images) - image.precache(context), - if (page.backgroundImage != null) - page.backgroundImage!.precache(context), + for (final image in page.images) image.precache(context), + if (page.backgroundImage != null) page.backgroundImage!.precache(context), ]); } diff --git a/test/stroke_svg_test.dart b/test/stroke_svg_test.dart index 0b7da10eb..710ca42ff 100644 --- a/test/stroke_svg_test.dart +++ b/test/stroke_svg_test.dart @@ -23,18 +23,19 @@ void main() { } Stroke _stroke(Offset point) => Stroke( - strokeProperties: StrokeProperties(size: _penSize), - pageIndex: 0, - penType: 'testingPen', -)..addPoint(point); + strokeProperties: StrokeProperties(size: _penSize), + pageIndex: 0, + penType: 'testingPen', + )..addPoint(point); void _testStrokeSvg(Stroke stroke) { final svgPath = stroke.toSvgPath(_pageSize); - final svgPoints = svgPath.split(RegExp(r'[ML]')) - .where((e) => e.isNotEmpty) - .map((e) => e.split(' ').map((e) => double.parse(e)).toList()) - .map((e) => Offset(e[0], e[1])) - .toList(); + final svgPoints = svgPath + .split(RegExp(r'[ML]')) + .where((e) => e.isNotEmpty) + .map((e) => e.split(' ').map((e) => double.parse(e)).toList()) + .map((e) => Offset(e[0], e[1])) + .toList(); final center = stroke.points.first; for (int i = 0; i < stroke.points.length; i++) { final svgPoint = svgPoints[i]; @@ -42,7 +43,9 @@ void _testStrokeSvg(Stroke stroke) { expect(svgPoint.dx, greaterThanOrEqualTo(center.x - _penSize)); expect(svgPoint.dx, lessThanOrEqualTo(center.x + _penSize)); - expect(svgPoint.dy, greaterThanOrEqualTo(_pageSize.height - center.y - _penSize)); - expect(svgPoint.dy, lessThanOrEqualTo(_pageSize.height - center.y + _penSize)); + expect(svgPoint.dy, + greaterThanOrEqualTo(_pageSize.height - center.y - _penSize)); + expect( + svgPoint.dy, lessThanOrEqualTo(_pageSize.height - center.y + _penSize)); } } diff --git a/test/tools_eraser_test.dart b/test/tools_eraser_test.dart index fade1c56e..6c63dc114 100644 --- a/test/tools_eraser_test.dart +++ b/test/tools_eraser_test.dart @@ -57,24 +57,30 @@ void main() { ]; List strokes = [...strokesToErase, ...strokesToKeep]; - List erased = eraser.checkForOverlappingStrokes(_eraserPos, strokes); + List erased = + eraser.checkForOverlappingStrokes(_eraserPos, strokes); for (Stroke stroke in strokesToErase) { - expect(erased.contains(stroke), true, reason: 'Stroke should be erased: $stroke'); + expect(erased.contains(stroke), true, + reason: 'Stroke should be erased: $stroke'); } for (Stroke stroke in strokesToKeep) { - expect(erased.contains(stroke), false, reason: 'Stroke should not be erased: $stroke'); + expect(erased.contains(stroke), false, + reason: 'Stroke should not be erased: $stroke'); } List erasedStrokes = eraser.onDragEnd(); - expect(erasedStrokes.length, strokesToErase.length, reason: 'The correct number of strokes should have been erased'); - expect(erasedStrokes.every((stroke) => strokesToErase.contains(stroke)), true, reason: 'The correct strokes should have been erased'); + expect(erasedStrokes.length, strokesToErase.length, + reason: 'The correct number of strokes should have been erased'); + expect( + erasedStrokes.every((stroke) => strokesToErase.contains(stroke)), true, + reason: 'The correct strokes should have been erased'); }); } Stroke _strokeWithPoint(Offset point) => Stroke( - strokeProperties: _strokeProperties, - pageIndex: 0, - penType: _penType, -)..addPoint(point); + strokeProperties: _strokeProperties, + pageIndex: 0, + penType: _penType, + )..addPoint(point); diff --git a/test/tools_select_test.dart b/test/tools_select_test.dart index e497ff5a8..d1eb65c6d 100644 --- a/test/tools_select_test.dart +++ b/test/tools_select_test.dart @@ -19,7 +19,8 @@ void main() { select.onDragUpdate(const Offset(10, 10)); select.onDragUpdate(const Offset(10, 0)); - expect(select.selectResult.pageIndex, 0, reason: 'The page index should be 0'); + expect(select.selectResult.pageIndex, 0, + reason: 'The page index should be 0'); List strokes = [ // index 0 is inside @@ -38,9 +39,12 @@ void main() { select.onDragEnd(strokes, const []); - expect(select.selectResult.strokes.length, 1, reason: 'Only one stroke should be selected'); - expect(select.selectResult.strokes.first, strokes[0], reason: 'The first stroke should be selected'); - expect(select.selectResult.images.isEmpty, true, reason: 'No images should be selected'); + expect(select.selectResult.strokes.length, 1, + reason: 'Only one stroke should be selected'); + expect(select.selectResult.strokes.first, strokes[0], + reason: 'The first stroke should be selected'); + expect(select.selectResult.images.isEmpty, true, + reason: 'No images should be selected'); }); test('Test that the select tool selects the right images', () async { @@ -54,7 +58,8 @@ void main() { select.onDragUpdate(const Offset(10, 10)); select.onDragUpdate(const Offset(10, 0)); - expect(select.selectResult.pageIndex, 0, reason: 'The page index should be 0'); + expect(select.selectResult.pageIndex, 0, + reason: 'The page index should be 0'); List images = [ // index 0 is inside (100% in the selection) @@ -73,10 +78,14 @@ void main() { select.onDragEnd(const [], images); - expect(select.selectResult.images.length, 2, reason: 'Two images should be selected'); - expect(select.selectResult.images.contains(images[0]), true, reason: 'The first image should be selected'); - expect(select.selectResult.images.contains(images[1]), true, reason: 'The second image should be selected'); - expect(select.selectResult.strokes.length, 0, reason: 'No strokes should be selected'); + expect(select.selectResult.images.length, 2, + reason: 'Two images should be selected'); + expect(select.selectResult.images.contains(images[0]), true, + reason: 'The first image should be selected'); + expect(select.selectResult.images.contains(images[1]), true, + reason: 'The second image should be selected'); + expect(select.selectResult.strokes.length, 0, + reason: 'No strokes should be selected'); }); } @@ -86,7 +95,7 @@ class TestImage extends PngEditorImage { TestImage({ required super.dstRect, - }) : super( + }) : super( id: -1, extension: '.png', imageProvider: null, diff --git a/test/tools_stroke_properties_test.dart b/test/tools_stroke_properties_test.dart index d0808aebb..16d9b0efc 100644 --- a/test/tools_stroke_properties_test.dart +++ b/test/tools_stroke_properties_test.dart @@ -4,7 +4,8 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:saber/data/tools/stroke_properties.dart'; void main() { - test('Test strokeProperties copy function', () { // also tests json serialization + test('Test strokeProperties copy function', () { + // also tests json serialization /// StrokeProperties with non-default values final StrokeProperties strokeProperties = StrokeProperties( color: const Color(0xFF123456), diff --git a/test/update_download_link_test.dart b/test/update_download_link_test.dart index faf970031..f02f9486a 100644 --- a/test/update_download_link_test.dart +++ b/test/update_download_link_test.dart @@ -15,29 +15,34 @@ void main() { }); test('on iOS', () async { - final url = await UpdateManager.getLatestDownloadUrl(apiResponse, TargetPlatform.iOS); + final url = await UpdateManager.getLatestDownloadUrl( + apiResponse, TargetPlatform.iOS); expect(url, isNull); }); test('on macOS', () async { - final url = await UpdateManager.getLatestDownloadUrl(apiResponse, TargetPlatform.macOS); + final url = await UpdateManager.getLatestDownloadUrl( + apiResponse, TargetPlatform.macOS); expect(url, isNull); }); test('on Windows', () async { - final url = await UpdateManager.getLatestDownloadUrl(apiResponse, TargetPlatform.windows); + final url = await UpdateManager.getLatestDownloadUrl( + apiResponse, TargetPlatform.windows); expect(url, isNotNull); expect(url, startsWith('http')); expect(url, endsWith('.exe')); }); test('on Linux', () async { - final url = await UpdateManager.getLatestDownloadUrl(apiResponse, TargetPlatform.linux); + final url = await UpdateManager.getLatestDownloadUrl( + apiResponse, TargetPlatform.linux); expect(url, isNull); }); test('on Android', () async { - final url = await UpdateManager.getLatestDownloadUrl(apiResponse, TargetPlatform.android); + final url = await UpdateManager.getLatestDownloadUrl( + apiResponse, TargetPlatform.android); expect(url, isNotNull); expect(url, startsWith('http')); expect(url, endsWith('.apk')); diff --git a/test/update_version_comparison_test.dart b/test/update_version_comparison_test.dart index 7c371fe70..167e1df6c 100644 --- a/test/update_version_comparison_test.dart +++ b/test/update_version_comparison_test.dart @@ -11,56 +11,70 @@ import 'package:saber/data/version.dart'; const int v = 5000; void main() => group('Update manager:', () { - FlavorConfig.setup(); - Prefs.testingMode = true; - Prefs.init(); + FlavorConfig.setup(); + Prefs.testingMode = true; + Prefs.init(); - test('Test version comparison (release mode)', () { - Prefs.shouldAlwaysAlertForUpdates.value = false; + test('Test version comparison (release mode)', () { + Prefs.shouldAlwaysAlertForUpdates.value = false; - expect(UpdateManager.getUpdateStatus(v, v - 10), UpdateStatus.upToDate); - expect(UpdateManager.getUpdateStatus(v, v - 1), UpdateStatus.upToDate); - expect(UpdateManager.getUpdateStatus(v, v), UpdateStatus.upToDate); - expect(UpdateManager.getUpdateStatus(v, v + 1), UpdateStatus.upToDate); - expect(UpdateManager.getUpdateStatus(v, v + 9), UpdateStatus.upToDate); + expect(UpdateManager.getUpdateStatus(v, v - 10), UpdateStatus.upToDate); + expect(UpdateManager.getUpdateStatus(v, v - 1), UpdateStatus.upToDate); + expect(UpdateManager.getUpdateStatus(v, v), UpdateStatus.upToDate); + expect(UpdateManager.getUpdateStatus(v, v + 1), UpdateStatus.upToDate); + expect(UpdateManager.getUpdateStatus(v, v + 9), UpdateStatus.upToDate); - expect(UpdateManager.getUpdateStatus(v, v + 10), UpdateStatus.updateOptional); - expect(UpdateManager.getUpdateStatus(v, v + 11), UpdateStatus.updateOptional); - expect(UpdateManager.getUpdateStatus(v, v + 19), UpdateStatus.updateOptional); + expect(UpdateManager.getUpdateStatus(v, v + 10), + UpdateStatus.updateOptional); + expect(UpdateManager.getUpdateStatus(v, v + 11), + UpdateStatus.updateOptional); + expect(UpdateManager.getUpdateStatus(v, v + 19), + UpdateStatus.updateOptional); - expect(UpdateManager.getUpdateStatus(v, v + 20), UpdateStatus.updateRecommended); - expect(UpdateManager.getUpdateStatus(v, v + 100), UpdateStatus.updateRecommended); - }); + expect(UpdateManager.getUpdateStatus(v, v + 20), + UpdateStatus.updateRecommended); + expect(UpdateManager.getUpdateStatus(v, v + 100), + UpdateStatus.updateRecommended); + }); - test('Test version comparison (debug mode)', () { - Prefs.shouldAlwaysAlertForUpdates.value = true; + test('Test version comparison (debug mode)', () { + Prefs.shouldAlwaysAlertForUpdates.value = true; - expect(UpdateManager.getUpdateStatus(v, v), UpdateStatus.upToDate); - expect(UpdateManager.getUpdateStatus(v, v + 1), UpdateStatus.upToDate); - expect(UpdateManager.getUpdateStatus(v, v + 9), UpdateStatus.upToDate); - expect(UpdateManager.getUpdateStatus(v, v + 10), UpdateStatus.updateRecommended); - }); + expect(UpdateManager.getUpdateStatus(v, v), UpdateStatus.upToDate); + expect(UpdateManager.getUpdateStatus(v, v + 1), UpdateStatus.upToDate); + expect(UpdateManager.getUpdateStatus(v, v + 9), UpdateStatus.upToDate); + expect(UpdateManager.getUpdateStatus(v, v + 10), + UpdateStatus.updateRecommended); + }); - test('Test that the latest version can be parsed from version.dart', () async { - // load local file from lib/data/version.dart - String latestVersionFile = await File('lib/data/version.dart').readAsString(); - expect(latestVersionFile.isNotEmpty, true, reason: 'Failed to load local version.dart file'); + test('Test that the latest version can be parsed from version.dart', + () async { + // load local file from lib/data/version.dart + String latestVersionFile = + await File('lib/data/version.dart').readAsString(); + expect(latestVersionFile.isNotEmpty, true, + reason: 'Failed to load local version.dart file'); - final int? parsedVersion = await UpdateManager.getNewestVersion(latestVersionFile); - expect(parsedVersion, isNotNull, reason: 'Could not parse version number from version.dart file'); - expect(parsedVersion, buildNumber, reason: 'Incorrect version number parsed from version.dart file'); - }); + final int? parsedVersion = + await UpdateManager.getNewestVersion(latestVersionFile); + expect(parsedVersion, isNotNull, + reason: 'Could not parse version number from version.dart file'); + expect(parsedVersion, buildNumber, + reason: 'Incorrect version number parsed from version.dart file'); + }); - test('Test that the latest version can be parsed from GitHub', () async { - final int? newestVersion; - try { - newestVersion = await UpdateManager.getNewestVersion(); - } on SocketException { - fail('Failed to download newest version from GitHub'); - } + test('Test that the latest version can be parsed from GitHub', () async { + final int? newestVersion; + try { + newestVersion = await UpdateManager.getNewestVersion(); + } on SocketException { + fail('Failed to download newest version from GitHub'); + } - expect(newestVersion, isNotNull, reason: 'Could not parse version number from GitHub'); - // at the time of writing, the latest version is 5050 - expect(newestVersion, greaterThan(5000), reason: 'Incorrect version number parsed from GitHub'); - }); -}); + expect(newestVersion, isNotNull, + reason: 'Could not parse version number from GitHub'); + // at the time of writing, the latest version is 5050 + expect(newestVersion, greaterThan(5000), + reason: 'Incorrect version number parsed from GitHub'); + }); + }); diff --git a/test/utils/test_mock_channel_handlers.dart b/test/utils/test_mock_channel_handlers.dart index 86497d265..f6bafd3b5 100644 --- a/test/utils/test_mock_channel_handlers.dart +++ b/test/utils/test_mock_channel_handlers.dart @@ -10,7 +10,8 @@ void setupMockFlutterSecureStorage() { if (methodCall.method == 'delete') { _mockSecureStorage.remove(methodCall.arguments['key'] as String); } else if (methodCall.method == 'write') { - _mockSecureStorage[methodCall.arguments['key'] as String] = methodCall.arguments['value'] as String; + _mockSecureStorage[methodCall.arguments['key'] as String] = + methodCall.arguments['value'] as String; } else if (methodCall.method == 'read') { return _mockSecureStorage[methodCall.arguments['key'] as String]; } diff --git a/test/utils/test_random.dart b/test/utils/test_random.dart index 454ebd619..5027a45cd 100644 --- a/test/utils/test_random.dart +++ b/test/utils/test_random.dart @@ -8,5 +8,6 @@ final _r = Random(); String randomString(int len) { final start = 'a'.codeUnitAt(0); final end = 'z'.codeUnitAt(0); - return String.fromCharCodes(List.generate(len, (_) => _r.nextInt(end - start) + start)); + return String.fromCharCodes( + List.generate(len, (_) => _r.nextInt(end - start) + start)); } diff --git a/test/version_test.dart b/test/version_test.dart index f65a3a90c..2e45e3b93 100644 --- a/test/version_test.dart +++ b/test/version_test.dart @@ -12,28 +12,36 @@ const String dummyChangelog = 'Release notes will be added here.'; void main() { test('Check for dummy text in changelogs', () async { - final File androidMetadata = File('metadata/en-US/changelogs/$buildNumber.txt'); + final File androidMetadata = + File('metadata/en-US/changelogs/$buildNumber.txt'); expect(androidMetadata.existsSync(), true); final String androidMetadataContents = await androidMetadata.readAsString(); - expect(androidMetadataContents.contains(dummyChangelog), false, reason: 'Dummy text found in Android changelog'); + expect(androidMetadataContents.contains(dummyChangelog), false, + reason: 'Dummy text found in Android changelog'); - final File flatpakMetadata = File('flatpak/com.adilhanney.saber.metainfo.xml'); + final File flatpakMetadata = + File('flatpak/com.adilhanney.saber.metainfo.xml'); expect(flatpakMetadata.existsSync(), true); final String flatpakMetadataContents = await flatpakMetadata.readAsString(); - expect(flatpakMetadataContents.contains(dummyChangelog), false, reason: 'Dummy text found in Flatpak changelog'); + expect(flatpakMetadataContents.contains(dummyChangelog), false, + reason: 'Dummy text found in Flatpak changelog'); }); test('Check that metainfo tags are in the right place', () async { - final File flatpakMetadata = File('flatpak/com.adilhanney.saber.metainfo.xml'); + final File flatpakMetadata = + File('flatpak/com.adilhanney.saber.metainfo.xml'); expect(flatpakMetadata.existsSync(), true); final String flatpakMetadataContents = await flatpakMetadata.readAsString(); - + final releasesTag = flatpakMetadataContents.indexOf(' tag found in Flatpak metainfo'); + expect(releasesTag, isNot(-1), + reason: 'No tag found in Flatpak metainfo'); final releaseTag = flatpakMetadataContents.indexOf(' tag found in Flatpak metainfo'); + expect(releaseTag, isNot(-1), + reason: 'No tag found in Flatpak metainfo'); - expect(releaseTag > releasesTag, true, reason: ' tag is not inside tag'); + expect(releaseTag > releasesTag, true, + reason: ' tag is not inside tag'); }); test('Test that buildNumber parses to buildName', () { @@ -70,34 +78,42 @@ void main() { // expect git to be clean final before = await shell.run('git status --porcelain'); - expect(before.outText.isEmpty, true, reason: 'Git status is not initially clean'); + expect(before.outText.isEmpty, true, + reason: 'Git status is not initially clean'); // Run `./scripts/apply_version.sh` to update the version in code... - const command = 'bash ./scripts/apply_version.sh $buildName $buildNumber -q'; + const command = + 'bash ./scripts/apply_version.sh $buildName $buildNumber -q'; printOnFailure('Running: $command'); await shell.run(command); // expect that script didn't need to change anything final after = await shell.run('git diff -w'); // ignore whitespace printOnFailure('Git diff after running $command:\n ${after.outText}'); - expect(after.outText.isEmpty, true, reason: './scripts/apply_version.sh found inconsistencies'); + expect(after.outText.isEmpty, true, + reason: './scripts/apply_version.sh found inconsistencies'); }); test('Test that changelog can be downloaded from GitHub', () async { final changelog = await UpdateManager.getChangelog( newestVersion: buildNumber, ); - expect(changelog, isNotNull, reason: 'Changelog can\'t be found on GitHub. Please ignore this test if you haven\'t pushed the latest version yet.'); + expect(changelog, isNotNull, + reason: + 'Changelog can\'t be found on GitHub. Please ignore this test if you haven\'t pushed the latest version yet.'); expect(changelog, isNotEmpty); - expect(changelog!.contains(dummyChangelog), false, reason: 'Dummy text found in changelog downloaded from GitHub'); + expect(changelog!.contains(dummyChangelog), false, + reason: 'Dummy text found in changelog downloaded from GitHub'); }); test('Test that changelog has been translated', () { for (final localeCode in localeNames.keys) { if (localeCode == 'en') continue; - final File file = File('metadata/$localeCode/changelogs/$buildNumber.txt'); - expect(file.existsSync(), true, reason: 'Changelog for $localeCode does not exist'); + final File file = + File('metadata/$localeCode/changelogs/$buildNumber.txt'); + expect(file.existsSync(), true, + reason: 'Changelog for $localeCode does not exist'); } }); }