From 09a0cd04e96f10398260bf43e4277cc944fd2925 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Fri, 8 Dec 2023 17:17:29 +0100 Subject: [PATCH] Fix some html element not supported (map) + init puzzle widget --- lib/Screens/Map/google_map_view.dart | 4 +- lib/Screens/Map/marker_view.dart | 17 +- lib/Screens/Menu/menu_view.dart | 2 +- lib/Screens/Puzzle/correct_overlay.dart | 68 +++++++ lib/Screens/Puzzle/puzzle_piece.dart | 245 ++++++++++++++++++++++++ lib/Screens/Puzzle/puzzle_view.dart | 149 ++++++++++++++ lib/Screens/Puzzle/score_widget.dart | 14 ++ 7 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 lib/Screens/Puzzle/correct_overlay.dart create mode 100644 lib/Screens/Puzzle/puzzle_piece.dart create mode 100644 lib/Screens/Puzzle/puzzle_view.dart create mode 100644 lib/Screens/Puzzle/score_widget.dart diff --git a/lib/Screens/Map/google_map_view.dart b/lib/Screens/Map/google_map_view.dart index 1f101e7..a97d4e2 100644 --- a/lib/Screens/Map/google_map_view.dart +++ b/lib/Screens/Map/google_map_view.dart @@ -8,6 +8,7 @@ import 'package:manager_api/api.dart'; import 'package:provider/provider.dart'; import 'package:tablet_app/Models/map-marker.dart'; import 'package:tablet_app/Screens/Map/map_context.dart'; +import 'package:html/parser.dart' show parse; class GoogleMapView extends StatefulWidget { final MapDTO? mapDTO; @@ -34,9 +35,10 @@ class _GoogleMapViewState extends State { markers = {}; widget.mapDTO!.points!.forEach((point) { + var textSansHTML = parse(point.title!.firstWhere((translation) => translation.language == language).value); var mapMarker = new MapMarker( id: point.id, - title: point.title!.firstWhere((translation) => translation.language == language).value, + title: parse(textSansHTML.body!.text).documentElement!.text, description: point.description!.firstWhere((translation) => translation.language == language).value, longitude: point.longitude, latitude: point.latitude, diff --git a/lib/Screens/Map/marker_view.dart b/lib/Screens/Map/marker_view.dart index 7a2e795..517ce6c 100644 --- a/lib/Screens/Map/marker_view.dart +++ b/lib/Screens/Map/marker_view.dart @@ -1,6 +1,7 @@ import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:photo_view/photo_view.dart'; import 'package:provider/provider.dart'; import 'package:tablet_app/Models/map-marker.dart'; @@ -99,7 +100,13 @@ class _MarkerInfoWidget extends State { alignment: Alignment.topCenter, child: Padding( padding: const EdgeInsets.only(top: 20), - child: Text(mapContext.getSelectedMarker().title, style: TextStyle(fontWeight: FontWeight.w600, fontSize: kIsWeb ? kWebTitleSize : kTitleSize)), + child: HtmlWidget( + mapContext.getSelectedMarker().title, + customStylesBuilder: (element) { + return {'text-align': 'center'}; + }, + textStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: kIsWeb ? kWebTitleSize : kTitleSize) + ), ), ), Padding( @@ -193,7 +200,13 @@ class _MarkerInfoWidget extends State { child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(15.0), - child: Text(mapContext.getSelectedMarker().description, textAlign: TextAlign.center, style: TextStyle(fontSize: kIsWeb ? kWebDescriptionSize : kDescriptionSize)), + child: HtmlWidget( + mapContext.getSelectedMarker().description, + customStylesBuilder: (element) { + return {'text-align': 'center'}; + }, + textStyle: TextStyle(fontSize: kIsWeb ? kWebDescriptionSize : kDescriptionSize) + ), ), ), ), diff --git a/lib/Screens/Menu/menu_view.dart b/lib/Screens/Menu/menu_view.dart index cb9d34b..b0fba58 100644 --- a/lib/Screens/Menu/menu_view.dart +++ b/lib/Screens/Menu/menu_view.dart @@ -204,7 +204,7 @@ boxDecoration(SectionDTO section, bool isSelected) { borderRadius: BorderRadius.circular(30.0), image: section.imageSource != null ? new DecorationImage( fit: kIsWeb ? BoxFit.cover : BoxFit.contain, - colorFilter: !isSelected? new ColorFilter.mode(Colors.black.withOpacity(kIsWeb ? 0.3 : 0.5), BlendMode.dstATop) : null, + colorFilter: !isSelected? new ColorFilter.mode(Colors.black.withOpacity(0.3), BlendMode.dstATop) : null, image: new NetworkImage( section.imageSource!, ), diff --git a/lib/Screens/Puzzle/correct_overlay.dart b/lib/Screens/Puzzle/correct_overlay.dart new file mode 100644 index 0000000..be33c18 --- /dev/null +++ b/lib/Screens/Puzzle/correct_overlay.dart @@ -0,0 +1,68 @@ +import 'dart:math' as math; +import 'package:flutter/material.dart'; + +class CorrectOverlay extends StatefulWidget { + final bool _isCorrect; + final VoidCallback _onTap; + + CorrectOverlay(this._isCorrect, this._onTap); + + @override + State createState() => new CorrectOverlayState(); +} + +class CorrectOverlayState extends State + with SingleTickerProviderStateMixin { + late Animation _iconAnimation; + late AnimationController _iconAnimationController; + + @override + void initState() { + super.initState(); + _iconAnimationController = new AnimationController( + duration: new Duration(seconds: 2), vsync: this); + _iconAnimation = new CurvedAnimation( + parent: _iconAnimationController, curve: Curves.elasticOut); + _iconAnimation.addListener(() => this.setState(() {})); + _iconAnimationController.forward(); + } + + @override + void dispose() { + _iconAnimationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return new Material( + color: Colors.black54, + child: new InkWell( + onTap: () => widget._onTap(), + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Container( + decoration: new BoxDecoration( + color: Colors.white, shape: BoxShape.circle), + child: new Transform.rotate( + angle: _iconAnimation.value * 2 * math.pi, + child: new Icon( + widget._isCorrect == true ? Icons.done : Icons.clear, + size: _iconAnimation.value * 80.0, + ), + ), + ), + new Padding( + padding: new EdgeInsets.only(bottom: 20.0), + ), + new Text( + widget._isCorrect == true ? "Correct!" : "Wrong!", + style: new TextStyle(color: Colors.white, fontSize: 30.0), + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/Screens/Puzzle/puzzle_piece.dart b/lib/Screens/Puzzle/puzzle_piece.dart new file mode 100644 index 0000000..9ee7ea1 --- /dev/null +++ b/lib/Screens/Puzzle/puzzle_piece.dart @@ -0,0 +1,245 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +class PuzzlePiece extends StatefulWidget { + final Image image; + final Size imageSize; + final int row; + final int col; + final int maxRow; + final int maxCol; + final Function bringToTop; + final Function sendToBack; + + static PuzzlePiece fromMap(Map map) { + return PuzzlePiece( + image: map['image'], + imageSize: map['imageSize'], + row: map['row'], + col: map['col'], + maxRow: map['maxRow'], + maxCol: map['maxCol'], + bringToTop: map['bringToTop'], + sendToBack: map['SendToBack'], + ); + } + + PuzzlePiece( + {Key? key, + required this.image, + required this.imageSize, + required this.row, + required this.col, + required this.maxRow, + required this.maxCol, + required this.bringToTop, + required this.sendToBack}) + : super(key: key); + + @override + _PuzzlePieceState createState() => _PuzzlePieceState(); +} + +class _PuzzlePieceState extends State { + // the piece initial top offset + double? top; + // the piece initial left offset + double? left; + // can we move the piece ? + bool isMovable = true; + + @override + Widget build(BuildContext context) { + // the image width + final imageWidth = MediaQuery.of(context).size.width; + // the image height + final imageHeight = MediaQuery.of(context).size.height * + MediaQuery.of(context).size.width / + widget.imageSize.width; + final pieceWidth = imageWidth / widget.maxCol; + final pieceHeight = imageHeight / widget.maxRow; + + if (top == null) { + top = Random().nextInt((imageHeight - pieceHeight).ceil()).toDouble(); + var test = top!; + test -= widget.row * pieceHeight; + top = test; + } + if (left == null) { + left = Random().nextInt((imageWidth - pieceWidth).ceil()).toDouble(); + var test = left!; + test -= widget.col * pieceWidth; + left = test; + } + + return Positioned( + top: top, + left: left, + width: imageWidth, + child: GestureDetector( + onTap: () { + if (isMovable) { + widget.bringToTop(widget); + } + }, + onPanStart: (_) { + if (isMovable) { + widget.bringToTop(widget); + } + }, + onPanUpdate: (dragUpdateDetails) { + if (isMovable) { + setState(() { + var testTop = top!; + var testLeft = left!; + testTop = top!; + testLeft = left!; + testTop += dragUpdateDetails.delta.dy; + testLeft += dragUpdateDetails.delta.dx; + top = testTop!; + left = testLeft!; + + if (-10 < top! && top! < 10 && -10 < left! && left! < 10) { + top = 0; + left = 0; + isMovable = false; + widget.sendToBack(widget); + + //ScoreWidget.of(context).allInPlaceCount++; + } + }); + } + }, + child: ClipPath( + child: CustomPaint( + foregroundPainter: PuzzlePiecePainter( + widget.row, widget.col, widget.maxRow, widget.maxCol), + child: widget.image), + clipper: PuzzlePieceClipper( + widget.row, widget.col, widget.maxRow, widget.maxCol), + ), + )); + } +} + +// this class is used to clip the image to the puzzle piece path +class PuzzlePieceClipper extends CustomClipper { + final int row; + final int col; + final int maxRow; + final int maxCol; + + PuzzlePieceClipper(this.row, this.col, this.maxRow, this.maxCol); + + @override + Path getClip(Size size) { + return getPiecePath(size, row, col, maxRow, maxCol); + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} + +// this class is used to draw a border around the clipped image +class PuzzlePiecePainter extends CustomPainter { + final int row; + final int col; + final int maxRow; + final int maxCol; + + PuzzlePiecePainter(this.row, this.col, this.maxRow, this.maxCol); + + @override + void paint(Canvas canvas, Size size) { + final Paint paint = Paint() + ..color = Color(0x80FFFFFF) + ..style = PaintingStyle.stroke + ..strokeWidth = 1.0; + + canvas.drawPath(getPiecePath(size, row, col, maxRow, maxCol), paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } +} + +// this is the path used to clip the image and, then, to draw a border around it; here we actually draw the puzzle piece +Path getPiecePath(Size size, int row, int col, int maxRow, int maxCol) { + final width = size.width / maxCol; + final height = size.height / maxRow; + final offsetX = col * width; + final offsetY = row * height; + final bumpSize = height / 4; + + var path = Path(); + path.moveTo(offsetX, offsetY); + + if (row == 0) { + // top side piece + path.lineTo(offsetX + width, offsetY); + } else { + // top bump + path.lineTo(offsetX + width / 3, offsetY); + path.cubicTo( + offsetX + width / 6, + offsetY - bumpSize, + offsetX + width / 6 * 5, + offsetY - bumpSize, + offsetX + width / 3 * 2, + offsetY); + path.lineTo(offsetX + width, offsetY); + } + + if (col == maxCol - 1) { + // right side piece + path.lineTo(offsetX + width, offsetY + height); + } else { + // right bump + path.lineTo(offsetX + width, offsetY + height / 3); + path.cubicTo( + offsetX + width - bumpSize, + offsetY + height / 6, + offsetX + width - bumpSize, + offsetY + height / 6 * 5, + offsetX + width, + offsetY + height / 3 * 2); + path.lineTo(offsetX + width, offsetY + height); + } + + if (row == maxRow - 1) { + // bottom side piece + path.lineTo(offsetX, offsetY + height); + } else { + // bottom bump + path.lineTo(offsetX + width / 3 * 2, offsetY + height); + path.cubicTo( + offsetX + width / 6 * 5, + offsetY + height - bumpSize, + offsetX + width / 6, + offsetY + height - bumpSize, + offsetX + width / 3, + offsetY + height); + path.lineTo(offsetX, offsetY + height); + } + + if (col == 0) { + // left side piece + path.close(); + } else { + // left bump + path.lineTo(offsetX, offsetY + height / 3 * 2); + path.cubicTo( + offsetX - bumpSize, + offsetY + height / 6 * 5, + offsetX - bumpSize, + offsetY + height / 6, + offsetX, + offsetY + height / 3); + path.close(); + } + + return path; +} \ No newline at end of file diff --git a/lib/Screens/Puzzle/puzzle_view.dart b/lib/Screens/Puzzle/puzzle_view.dart new file mode 100644 index 0000000..c638b00 --- /dev/null +++ b/lib/Screens/Puzzle/puzzle_view.dart @@ -0,0 +1,149 @@ +import 'dart:convert'; +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:manager_api/api.dart'; +import 'puzzle_piece.dart'; +import 'score_widget.dart'; + +const IMAGE_PATH = 'image_path'; + +class PuzzleView extends StatefulWidget { + final SectionDTO? section; + PuzzleView({this.section}); + + @override + _PuzzleViewWidget createState() => _PuzzleViewWidget(); +} + +class _PuzzleViewWidget extends State { + SliderDTO sliderDTO = SliderDTO(); + final int rows = 3; + final int cols = 3; + + //File? _image; + String? _imagePath; + List pieces = []; + + bool _overlayVisible = true; + + @override + void initState() { + sliderDTO = SliderDTO.fromJson(jsonDecode(widget.section!.data!))!; + + sliderDTO.images!.sort((a, b) => a.order!.compareTo(b.order!)); + super.initState(); + splitImage(Image.network(sliderDTO.images![1].source_!)); + } + + /*void savePrefs() async { + await prefs!.setString(IMAGE_PATH, _imagePath!); + }*/ + + /*Future getImage(ImageSource source) async { + var image = await ImagePicker.platform.(source: source); + + if (image != null) { + setState(() { + _image = image; + _imagePath = _image!.path; + pieces.clear(); + ScoreWidget + .of(context) + .allInPlaceCount = 0; + }); + } + splitImage(Image.file(image)); + savePrefs(); + }*/ + + // we need to find out the image size, to be used in the PuzzlePiece widget + Future getImageSize(Image image) async { + final Completer completer = Completer(); + + image.image + .resolve(const ImageConfiguration()) + .addListener(ImageStreamListener((ImageInfo info, bool _) { + completer.complete( + Size(info.image.width.toDouble(), info.image.height.toDouble())); + })); + + final Size imageSize = await completer.future; + + return imageSize; + } + + // here we will split the image into small pieces + // using the rows and columns defined above; each piece will be added to a stack + void splitImage(Image image) async { + Size imageSize = await getImageSize(image); + + for (int x = 0; x < rows; x++) { + for (int y = 0; y < cols; y++) { + setState(() { + pieces.add( + PuzzlePiece( + key: GlobalKey(), + image: image, + imageSize: imageSize, + row: x, + col: y, + maxRow: rows, + maxCol: cols, + bringToTop: this.bringToTop, + sendToBack: this.sendToBack, + ), + ); + }); + } + } + } + + // when the pan of a piece starts, we need to bring it to the front of the stack + void bringToTop(Widget widget) { + setState(() { + pieces.remove(widget); + pieces.add(widget); + }); + } + +// when a piece reaches its final position, +// it will be sent to the back of the stack to not get in the way of other, still movable, pieces + void sendToBack(Widget widget) { + setState(() { + pieces.remove(widget); + pieces.insert(0, widget); + }); + } + + @override + Widget build(BuildContext context) { + //savePrefs(); + + return SafeArea( + child: sliderDTO.images![0].source_! == null + ? Center(child: Text('No image selected.')) + : Stack( + children: pieces, + ), + ); /*ScoreWidget + .of(context) + .allInPlaceCount == + rows * cols + ? Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return CorrectOverlay(true, () { + setState(() { + ScoreWidget + .of(context) + .allInPlaceCount = 0; + }); + }); + }) + ], + ) + :*/ + } +} \ No newline at end of file diff --git a/lib/Screens/Puzzle/score_widget.dart b/lib/Screens/Puzzle/score_widget.dart new file mode 100644 index 0000000..d637992 --- /dev/null +++ b/lib/Screens/Puzzle/score_widget.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +class ScoreWidget extends InheritedWidget { + ScoreWidget({Key? key, required Widget child}) : super(key: key, child: child); + + int allInPlaceCount = 0; + + static ScoreWidget of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType() as ScoreWidget; + } + + @override + bool updateShouldNotify(ScoreWidget oldWidget) => false; +} \ No newline at end of file