diff --git a/lib/Components/audio_player.dart b/lib/Components/audio_player.dart new file mode 100644 index 0000000..baa3cfc --- /dev/null +++ b/lib/Components/audio_player.dart @@ -0,0 +1,265 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/app_context.dart'; +import 'package:provider/provider.dart'; + +import 'package:just_audio/just_audio.dart'; +import 'package:just_audio_cache/just_audio_cache.dart'; + + +class AudioPlayerFloatingContainer extends StatefulWidget { + const AudioPlayerFloatingContainer({Key? key, required this.file, required this.audioBytes, required this.resourceURl, required this.isAuto}) : super(key: key); + + final File? file; + final Uint8List? audioBytes; + final String resourceURl; + final bool isAuto; + + @override + State createState() => _AudioPlayerFloatingContainerState(); +} + +class _AudioPlayerFloatingContainerState extends State { + AudioPlayer player = AudioPlayer(); + Uint8List? audiobytes = null; + bool isplaying = false; + bool audioplayed = false; + int currentpos = 0; + int maxduration = 100; + Duration? durationAudio; + String currentpostlabel = "00:00"; + + @override + void initState() { + //print("IN INITSTATE AUDDDIOOOO"); + Future.delayed(Duration.zero, () async { + if(widget.audioBytes != null) { + audiobytes = widget.audioBytes!; + } + + if(widget.file != null) { + audiobytes = await fileToUint8List(widget.file!); + } + + player.durationStream.listen((Duration? d) { //get the duration of audio + if(d != null) { + maxduration = d.inSeconds; + durationAudio = d; + } + }); + + //player.bufferedPositionStream + + player.positionStream.listen((event) { + if(durationAudio != null) { + + currentpos = event.inMilliseconds; //get the current position of playing audio + + //generating the duration label + int shours = Duration(milliseconds:durationAudio!.inMilliseconds - currentpos).inHours; + int sminutes = Duration(milliseconds:durationAudio!.inMilliseconds - currentpos).inMinutes; + int sseconds = Duration(milliseconds:durationAudio!.inMilliseconds - currentpos).inSeconds; + + int rminutes = sminutes - (shours * 60); + int rseconds = sseconds - (sminutes * 60 + shours * 60 * 60); + + String minutesToShow = rminutes < 10 ? '0$rminutes': rminutes.toString(); + String secondsToShow = rseconds < 10 ? '0$rseconds': rseconds.toString(); + + currentpostlabel = "$minutesToShow:$secondsToShow"; + + setState(() { + //refresh the UI + if(currentpos > player.duration!.inMilliseconds) { + print("RESET ALL"); + player.stop(); + player.seek(const Duration(seconds: 0)); + isplaying = false; + audioplayed = false; + currentpostlabel = "00:00"; + } + }); + } + }); + + + + /*player.onPositionChanged.listen((Duration p){ + currentpos = p.inMilliseconds; //get the current position of playing audio + + //generating the duration label + int shours = Duration(milliseconds:currentpos).inHours; + int sminutes = Duration(milliseconds:currentpos).inMinutes; + int sseconds = Duration(milliseconds:currentpos).inSeconds; + + int rminutes = sminutes - (shours * 60); + int rseconds = sseconds - (sminutes * 60 + shours * 60 * 60); + + String minutesToShow = rminutes < 10 ? '0$rminutes': rminutes.toString(); + String secondsToShow = rseconds < 10 ? '0$rseconds': rseconds.toString(); + + currentpostlabel = "$minutesToShow:$secondsToShow"; + + setState(() { + //refresh the UI + }); + });*/ + + if(audiobytes != null) { + print("GOT AUDIOBYYYTES - LOCALLY SOSO"); + await player.setAudioSource(LoadedSource(audiobytes!)); + } else { + print("GET SOUND BY URL"); + await player.dynamicSet(url: widget.resourceURl); + } + + if(widget.isAuto) { + //player.play(BytesSource(audiobytes)); + // + player.play(); + setState(() { + isplaying = true; + audioplayed = true; + }); + } + }); + super.initState(); + } + + @override + void dispose() { + player.stop(); + player.dispose(); + super.dispose(); + } + + Future fileToUint8List(File file) async { + List bytes = await file.readAsBytes(); + return Uint8List.fromList(bytes); + } + + @override + Widget build(BuildContext context) { + final appContext = Provider.of(context); + VisitAppContext visitAppContext = appContext.getContext(); + + return FloatingActionButton( + backgroundColor: Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)).withValues(alpha: 0.7), + onPressed: () async { + if(!isplaying && !audioplayed){ + //player.play(BytesSource(audiobytes)); + //await player.setUrl(widget.resourceURl); + player.play(); + setState(() { + isplaying = true; + audioplayed = true; + }); + }else if(audioplayed && !isplaying){ + //player.resume(); + player.play(); + setState(() { + isplaying = true; + audioplayed = true; + }); + }else{ + player.pause(); + setState(() { + isplaying = false; + }); + } + }, + child: isplaying ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.pause), + Text(currentpostlabel), + ], + ) : audioplayed ? Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.play_arrow), + Text(currentpostlabel), + ], + ): const Icon(Icons.play_arrow), + + /*Column( + children: [ + //Text(currentpostlabel, style: const TextStyle(fontSize: 25)), + Wrap( + spacing: 10, + children: [ + ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: kSecondColor, // Background color + ), + onPressed: () async { + if(!isplaying && !audioplayed){ + //player.play(BytesSource(audiobytes)); + await player.setAudioSource(LoadedSource(audiobytes)); + player.play(); + setState(() { + isplaying = true; + audioplayed = true; + }); + }else if(audioplayed && !isplaying){ + //player.resume(); + player.play(); + setState(() { + isplaying = true; + audioplayed = true; + }); + }else{ + player.pause(); + setState(() { + isplaying = false; + }); + } + }, + icon: Icon(isplaying?Icons.pause:Icons.play_arrow), + //label:Text(isplaying?TranslationHelper.getFromLocale("pause", appContext.getContext()):TranslationHelper.getFromLocale("play", appContext.getContext())) + ), + + /*ElevatedButton.icon( + style: ElevatedButton.styleFrom( + backgroundColor: kSecondColor, // Background color + ), + onPressed: () async { + player.stop(); + player.seek(const Duration(seconds: 0)); + setState(() { + isplaying = false; + audioplayed = false; + currentpostlabel = "00:00"; + }); + }, + icon: const Icon(Icons.stop), + //label: Text(TranslationHelper.getFromLocale("stop", appContext.getContext())) + ),*/ + ], + ) + ], + ),*/ + ); + } +} + +// Feed your own stream of bytes into the player +class LoadedSource extends StreamAudioSource { + final List bytes; + LoadedSource(this.bytes); + + @override + Future request([int? start, int? end]) async { + start ??= 0; + end ??= bytes.length; + return StreamAudioResponse( + sourceLength: bytes.length, + contentLength: end - start, + offset: start, + stream: Stream.value(bytes.sublist(start, end)), + contentType: 'audio/mpeg', + ); + } +} diff --git a/lib/Components/cached_custom_resource.dart b/lib/Components/cached_custom_resource.dart new file mode 100644 index 0000000..7ac7594 --- /dev/null +++ b/lib/Components/cached_custom_resource.dart @@ -0,0 +1,126 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/Components/audio_player.dart'; +import 'package:mymuseum_visitapp/Components/video_viewer.dart'; +import 'package:mymuseum_visitapp/Components/video_viewer_youtube.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/app_context.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; +class CachedCustomResource extends StatelessWidget { + final ResourceDTO resourceDTO; + final bool isAuto; + final bool webView; + final BoxFit fit; + + CachedCustomResource({ + required this.resourceDTO, + required this.isAuto, + required this.webView, + this.fit = BoxFit.cover, + }); + + @override + Widget build(BuildContext context) { + final appContext = Provider.of(context); + VisitAppContext visitAppContext = appContext.getContext(); + Size size = MediaQuery.of(context).size; + + Color primaryColor = Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)); + + if(resourceDTO.type == ResourceType.ImageUrl || resourceDTO.type == ResourceType.VideoUrl) + { + // Image Url or Video Url don't care, just get resource + if(resourceDTO.type == ResourceType.ImageUrl) { + return CachedNetworkImage( + imageUrl: resourceDTO.url!, + fit: BoxFit.fill, + progressIndicatorBuilder: (context, url, downloadProgress) => + CircularProgressIndicator(value: downloadProgress.progress, color: primaryColor), + errorWidget: (context, url, error) => Icon(Icons.error), + ); + } else { + if(resourceDTO.url == null) { + return const Center(child: Text("Error loading video")); + } else { + return VideoViewerYoutube(videoUrl: resourceDTO.url!, isAuto: isAuto, webView: webView); + } + } + } else { + // Check if exist on local storage, if no, just show it via url + print("Check local storage in cached custom resource"); + return FutureBuilder( + future: _checkIfLocalResourceExists(visitAppContext), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + // Loader ou indicateur de chargement pendant la vérification + return const CircularProgressIndicator(); + } else if (snapshot.hasError || snapshot.data == null) { + // Si la ressource locale n'existe pas ou s'il y a une erreur + + switch(resourceDTO.type) { + case ResourceType.Image : + return CachedNetworkImage( + imageUrl: resourceDTO.url!, + fit: fit, + placeholder: (context, url) => const CircularProgressIndicator(), + errorWidget: (context, url, error) => const Icon(Icons.error), + ); + case ResourceType.Video : + return VideoViewer(file: null, videoUrl: resourceDTO.url!); + case ResourceType.Audio : + return AudioPlayerFloatingContainer(file: null, audioBytes: null, resourceURl: resourceDTO.url!, isAuto: isAuto); + default: + return const Text("Not supported type"); + } + } else { + + switch(resourceDTO.type) { + case ResourceType.Image : + return Image.file( + snapshot.data!, + fit: fit, + ); + case ResourceType.Video : + return VideoViewer(file: snapshot.data!, videoUrl: resourceDTO.url!); + case ResourceType.Audio : + return AudioPlayerFloatingContainer(file: snapshot.data!, audioBytes: null, resourceURl: resourceDTO.url!, isAuto: isAuto); + default: + return const Text("Not supported type"); + } + // Utilisation de l'image locale + + } + }, + ); + } + } + + Future _checkIfLocalResourceExists(VisitAppContext visitAppContext) async { + try { + Directory? appDocumentsDirectory = Platform.isIOS ? await getApplicationDocumentsDirectory() : await getDownloadsDirectory(); + String localPath = appDocumentsDirectory!.path; + Directory configurationDirectory = Directory('$localPath/${visitAppContext.configuration!.id}'); + List fileList = configurationDirectory.listSync(); + + if(fileList.any((fileL) => fileL.uri.pathSegments.last.contains(resourceDTO.id!))) { + File file = File(fileList.firstWhere((fileL) => fileL.uri.pathSegments.last.contains(resourceDTO.id!)).path); + return file; + } + } catch(e) { + print("ERROR _checkIfLocalResourceExists CachedCustomResource"); + print(e); + } + + return null; + } + + Future get localPath async { + Directory? appDocumentsDirectory = Platform.isIOS ? await getApplicationDocumentsDirectory() : await getDownloadsDirectory(); + return appDocumentsDirectory!.path; + } +} diff --git a/lib/Components/show_element_for_resource.dart b/lib/Components/show_element_for_resource.dart new file mode 100644 index 0000000..f5166d6 --- /dev/null +++ b/lib/Components/show_element_for_resource.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/Components/audio_player.dart'; +import 'package:mymuseum_visitapp/Components/video_viewer.dart'; +import 'package:mymuseum_visitapp/Components/video_viewer_youtube.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/app_context.dart'; + +import 'cached_custom_resource.dart'; + +showElementForResource(ResourceDTO resourceDTO, AppContext appContext, bool isAuto, bool webView) { + VisitAppContext visitAppContext = appContext.getContext(); + Color primaryColor = Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)); + + return CachedCustomResource(resourceDTO: resourceDTO, isAuto: isAuto, webView: webView); + + switch(resourceDTO.type) { + case ResourceType.Image: + case ResourceType.ImageUrl: + return CachedNetworkImage( + imageUrl: resourceDTO.url!, + fit: BoxFit.fill, + progressIndicatorBuilder: (context, url, downloadProgress) => + CircularProgressIndicator(value: downloadProgress.progress, color: primaryColor), + errorWidget: (context, url, error) => Icon(Icons.error), + ); + /*return Image.network( + resourceDTO.url!, + fit:BoxFit.fill, + loadingBuilder: (BuildContext context, Widget child, + ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } + return Center( + child: CircularProgressIndicator( + color: primaryColor, + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + );*/ + case ResourceType.Audio: + return AudioPlayerFloatingContainer(file: null, audioBytes: null, resourceURl: resourceDTO.url!, isAuto: isAuto); + /*return FutureBuilder( + future: getAudio(resourceDTO.url, appContext), + builder: (context, AsyncSnapshot snapshot) { + Size size = MediaQuery.of(context).size; + if (snapshot.connectionState == ConnectionState.done) { + var audioBytes; + if(snapshot.data != null) { + print("snapshot.data"); + print(snapshot.data); + audioBytes = snapshot.data; + //this.player.playBytes(audiobytes); + } + return AudioPlayerFloatingContainer(audioBytes: audioBytes, resourceURl: resourceDTO.url, isAuto: true); + } else if (snapshot.connectionState == ConnectionState.none) { + return Text("No data"); + } else { + return Center( + child: Container( + //height: size.height * 0.2, + width: size.width * 0.2, + child: LoadingCommon() + ) + ); + } + } + );*/ + case ResourceType.Video: + if(resourceDTO.url == null) { + return Center(child: Text("Error loading video")); + } else { + return VideoViewer(file: null, videoUrl: resourceDTO.url!); + } + case ResourceType.VideoUrl: + if(resourceDTO.url == null) { + return Center(child: Text("Error loading video")); + } else { + return VideoViewerYoutube(videoUrl: resourceDTO.url!, isAuto: isAuto, webView: webView); + } + } +} diff --git a/lib/Components/video_viewer.dart b/lib/Components/video_viewer.dart new file mode 100644 index 0000000..c1cc3ef --- /dev/null +++ b/lib/Components/video_viewer.dart @@ -0,0 +1,94 @@ +import 'dart:io'; + +//import 'package:cached_video_player/cached_video_player.dart'; +import 'package:flutter/material.dart'; +import 'package:mymuseum_visitapp/Components/loading_common.dart'; +import 'package:video_player/video_player.dart'; +import '../../constants.dart'; + +class VideoViewer extends StatefulWidget { + final String videoUrl; + final File? file; + VideoViewer({required this.videoUrl, required this.file}); + + @override + _VideoViewer createState() => _VideoViewer(); +} + +class _VideoViewer extends State { + late VideoPlayerController _controller; // Cached + + @override + void initState() { + super.initState(); + if(widget.file != null) { + _controller = VideoPlayerController.file(widget.file!) // Uri.parse() // Cached + ..initialize().then((_) { + // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. + setState(() {}); + }); + } else { + _controller = VideoPlayerController.networkUrl(Uri.parse(widget.videoUrl)) // Uri.parse() + ..initialize().then((_) { + // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. + setState(() {}); + }); + } + } + + @override + void dispose() { + super.dispose(); + _controller.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Center( + child: InkWell( + onTap: () { + setState(() { + if(_controller.value.isInitialized) { + if(_controller.value.isPlaying) { + _controller.pause(); + } else { + _controller.play(); + } + } + }); + }, + child: _controller.value.isInitialized + ? AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ) + : Center( + child: Container( + child: LoadingCommon() + ) + ), + ), + ), + if(!_controller.value.isPlaying && _controller.value.isInitialized) + Center( + child: FloatingActionButton( + backgroundColor: kMainColor.withValues(alpha: 0.8), + onPressed: () { + setState(() { + _controller.value.isPlaying + ? _controller.pause() + : _controller.play(); + }); + }, + child: Icon( + _controller.value.isPlaying ? Icons.pause : Icons.play_arrow, + color: Colors.white), + ), + ) + ], + ); + } +} + diff --git a/lib/Components/video_viewer_youtube.dart b/lib/Components/video_viewer_youtube.dart new file mode 100644 index 0000000..5fc7028 --- /dev/null +++ b/lib/Components/video_viewer_youtube.dart @@ -0,0 +1,99 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/constants.dart'; +import 'package:youtube_player_iframe/youtube_player_iframe.dart' as iframe; +//import 'package:youtube_player_flutter/youtube_player_flutter.dart'; + +class VideoViewerYoutube extends StatefulWidget { + final String videoUrl; + final bool isAuto; + final bool webView; + VideoViewerYoutube({required this.videoUrl, required this.isAuto, this.webView = false}); + + @override + _VideoViewerYoutube createState() => _VideoViewerYoutube(); +} + +class _VideoViewerYoutube extends State { + iframe.YoutubePlayer? _videoViewWeb; + //YoutubePlayer? _videoView; + + @override + void initState() { + String? videoId; + if (widget.videoUrl.isNotEmpty ) { + //videoId = YoutubePlayer.convertUrlToId(widget.videoUrl); + + if (true) { + final _controllerWeb = iframe.YoutubePlayerController( + params: iframe.YoutubePlayerParams( + mute: false, + showControls: false, + showFullscreenButton: false, + loop: false, + showVideoAnnotations: false, + strictRelatedVideos: false, + enableKeyboard: false, + enableCaption: false, + pointerEvents: iframe.PointerEvents.auto + ), + ); + + _controllerWeb.loadVideo(widget.videoUrl); + if(!widget.isAuto) { + _controllerWeb.stopVideo(); + } + + _videoViewWeb = iframe.YoutubePlayer( + controller: _controllerWeb, + //showVideoProgressIndicator: false, + /*progressIndicatorColor: Colors.amber, + progressColors: ProgressBarColors( + playedColor: Colors.amber, + handleColor: Colors.amberAccent, + ),*/ + ); + } else /*{ + // Cause memory issue on tablet + videoId = YoutubePlayer.convertUrlToId(widget.videoUrl); + YoutubePlayerController _controller = YoutubePlayerController( + initialVideoId: videoId!, + flags: YoutubePlayerFlags( + autoPlay: widget.isAuto, + controlsVisibleAtStart: false, + loop: true, + hideControls: false, + hideThumbnail: false, + ), + ); + + + _videoView = YoutubePlayer( + controller: _controller, + //showVideoProgressIndicator: false, + progressIndicatorColor: Colors.amber, + progressColors: ProgressBarColors( + playedColor: Colors.amber, + handleColor: Colors.amberAccent, + ), + ); + }*/ + super.initState(); + } + } + + @override + void dispose() { + //_videoView = null; + _videoViewWeb = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget.videoUrl.isNotEmpty ? + _videoViewWeb!: //(widget.webView ? _videoViewWeb! : _videoView!) + const Center(child: Text("La vidéo ne peut pas être affichée, l'url est incorrecte", style: TextStyle(fontSize: kNoneInfoOrIncorrect))); +} \ No newline at end of file diff --git a/lib/Helpers/ImageCustomProvider.dart b/lib/Helpers/ImageCustomProvider.dart new file mode 100644 index 0000000..2d3c2bf --- /dev/null +++ b/lib/Helpers/ImageCustomProvider.dart @@ -0,0 +1,35 @@ +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/app_context.dart'; + +class ImageCustomProvider { + static ImageProvider getImageProvider(AppContext appContext, String? imageId, String imageSource) { + VisitAppContext visitAppContext = appContext.getContext(); + try { + if(appContext.getContext().localPath != null && visitAppContext.configuration != null) { + Directory configurationDirectory = Directory('${visitAppContext.localPath!}/${visitAppContext.configuration!.id!}'); + List fileList = configurationDirectory.listSync(); + + if(imageId != null && fileList.any((fileL) => fileL.uri.pathSegments.last.contains(imageId))) { + File file = File(fileList.firstWhere((fileL) => fileL.uri.pathSegments.last.contains(imageId)).path); + print("FILE EXISTT"); + return FileImage(file); + } + + } + } catch(e) { + print("Error getImageProvider"); + print(e.toString()); + } + + + // If localpath not found or file missing + print("MISSINGG FILE"); + print(imageId); + return CachedNetworkImageProvider(imageSource); + } +} + diff --git a/lib/Models/visitContext.dart b/lib/Models/visitContext.dart index 0199625..013cf3f 100644 --- a/lib/Models/visitContext.dart +++ b/lib/Models/visitContext.dart @@ -23,6 +23,8 @@ class VisitAppContext with ChangeNotifier { bool isScanBeaconAlreadyAllowed = false; bool isMaximizeTextSize = false; + Size? puzzleSize; + List audiosNotWorking = []; bool? isAdmin = false; diff --git a/lib/Screens/Sections/Puzzle/correct_overlay.dart b/lib/Screens/Sections/Puzzle/correct_overlay.dart new file mode 100644 index 0000000..712e7a8 --- /dev/null +++ b/lib/Screens/Sections/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 Container( + color: Colors.black54, + child: new InkWell( + onTap: () => widget._onTap(), + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Container( + decoration: new BoxDecoration( + color: Colors.blueAccent, 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/Sections/Puzzle/message_dialog.dart b/lib/Screens/Sections/Puzzle/message_dialog.dart new file mode 100644 index 0000000..5317aac --- /dev/null +++ b/lib/Screens/Sections/Puzzle/message_dialog.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/Components/show_element_for_resource.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/app_context.dart'; +import 'package:mymuseum_visitapp/constants.dart'; + +void showMessage(TranslationAndResourceDTO translationAndResourceDTO, AppContext appContext, BuildContext context, Size size) { + print("translationAndResourceDTO"); + print(translationAndResourceDTO); + + VisitAppContext visitAppContext = appContext.getContext(); + + showDialog( + builder: (BuildContext context) => AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(visitAppContext.configuration!.roundedValue?.toDouble() ?? 20.0)) + ), + content: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if(translationAndResourceDTO.resourceId != null) + Container( + //color: Colors.cyan, + height: size.height *0.45, + width: size.width *0.5, + child: Center( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(visitAppContext.configuration!.roundedValue?.toDouble() ?? 30), + //border: Border.all(width: 3, color: Colors.black) + ), + child: showElementForResource(ResourceDTO(id: translationAndResourceDTO.resourceId, type: translationAndResourceDTO.resource!.type, url: translationAndResourceDTO.resource!.url), appContext, true, false), + ), + ), + ), + Container( + //color: Colors.green, + height: size.height *0.3, + width: size.width *0.5, + child: Center( + child: Align( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: HtmlWidget( + translationAndResourceDTO.value!, + customStylesBuilder: (element) { + return {'text-align': 'center', 'font-family': "Roboto"}; + }, + textStyle: const TextStyle(fontSize: kDescriptionSize), + ),/*Text( + resourceDTO.label == null ? "" : resourceDTO.label, + style: new TextStyle(fontSize: 25, fontWeight: FontWeight.w400)),*/ + ), + ), + ), + ), + ], + ), + ), + /*actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Align( + alignment: AlignmentDirectional.bottomEnd, + child: Container( + width: 175, + height: 70, + child: RoundedButton( + text: "Merci", + icon: Icons.undo, + color: kSecondGrey, + press: () { + Navigator.of(context).pop(); + }, + fontSize: 20, + ), + ), + ), + ), + ], + ), + ],*/ + ), context: context + ); +} diff --git a/lib/Screens/Sections/Puzzle/puzzle_page.dart b/lib/Screens/Sections/Puzzle/puzzle_page.dart new file mode 100644 index 0000000..3808885 --- /dev/null +++ b/lib/Screens/Sections/Puzzle/puzzle_page.dart @@ -0,0 +1,357 @@ +import 'dart:convert'; +import 'dart:async'; +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/Components/loading_common.dart'; +import 'package:mymuseum_visitapp/Helpers/translationHelper.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/Screens/Sections/Puzzle/message_dialog.dart'; +import 'package:mymuseum_visitapp/app_context.dart'; +import 'package:mymuseum_visitapp/constants.dart'; +import 'package:provider/provider.dart'; +import 'puzzle_piece.dart'; + +const IMAGE_PATH = 'image_path'; + +class PuzzlePage extends StatefulWidget { + final PuzzleDTO section; + PuzzlePage({required this.section}); + + @override + _PuzzlePage createState() => _PuzzlePage(); +} + +class _PuzzlePage extends State { + PuzzleDTO puzzleDTO = PuzzleDTO(); + + int allInPlaceCount = 0; + bool isFinished = false; + GlobalKey _widgetKey = GlobalKey(); + Size? realWidgetSize; + List pieces = []; + + bool isSplittingImage = true; + + @override + void initState() { + //puzzleDTO = PuzzleDTO.fromJson(jsonDecode(widget.section!.data!))!; + puzzleDTO = widget.section; + puzzleDTO.rows = puzzleDTO.rows ?? 3; + puzzleDTO.cols = puzzleDTO.cols ?? 3; + + WidgetsBinding.instance.addPostFrameCallback((_) async { + Size size = MediaQuery.of(context).size; + final appContext = Provider.of(context, listen: false); + VisitAppContext visitAppContext = appContext.getContext(); + + print(puzzleDTO.messageDebut); + TranslationAndResourceDTO? messageDebut = puzzleDTO.messageDebut != null && puzzleDTO.messageDebut!.isNotEmpty ? puzzleDTO.messageDebut!.where((message) => message.language!.toUpperCase() == visitAppContext.language!.toUpperCase()).firstOrNull : null; + + //await Future.delayed(const Duration(milliseconds: 50)); + + await WidgetsBinding.instance.endOfFrame; + getRealWidgetSize(); + + if(puzzleDTO.puzzleImage != null && puzzleDTO.puzzleImage!.url != null) { + //splitImage(Image.network(puzzleDTO.image!.resourceUrl!)); + splitImage(CachedNetworkImage( + imageUrl: puzzleDTO.puzzleImage!.url!, + fit: BoxFit.fill, + errorWidget: (context, url, error) => Icon(Icons.error), + )); + } else { + setState(() { + isSplittingImage = false; + }); + } + + if(messageDebut != null) { + showMessage(messageDebut, appContext, context, size); + } + }); + + super.initState(); + } + + Future getRealWidgetSize() async { + RenderBox renderBox = _widgetKey.currentContext?.findRenderObject() as RenderBox; + Size size = renderBox.size; + + setState(() { + realWidgetSize = size; + }); + print("Taille réelle du widget : $size"); + } + + // we need to find out the image size, to be used in the PuzzlePiece widget + /*Future getImageSize(CachedNetworkImage image) async { + Completer completer = Completer(); + + /*image.image + .resolve(const ImageConfiguration()) + .addListener(ImageStreamListener((ImageInfo info, bool _) { + completer.complete( + Size(info.image.width.toDouble(), info.image.height.toDouble())); + }));*/ + + CachedNetworkImage( + imageUrl: 'https://example.com/image.jpg', + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => Icon(Icons.error), + imageBuilder: (BuildContext context, ImageProvider imageProvider) { + Completer completer = Completer(); + + imageProvider + .resolve(const ImageConfiguration()) + .addListener(ImageStreamListener((ImageInfo info, bool _) { + completer.complete( + Size(info.image.width.toDouble(), info.image.height.toDouble())); + })); + + return CachedNetworkImage( + imageUrl: 'https://example.com/image.jpg', + placeholder: (context, url) => CircularProgressIndicator(), + errorWidget: (context, url, error) => Icon(Icons.error), + imageBuilder: (context, imageProvider) { + return Image( + image: imageProvider, + loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { + if (loadingProgress == null) { + return child; + } else { + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / (loadingProgress.expectedTotalBytes ?? 1) + : null, + ), + ); + } + }, + ); + }, + ); + }, + ); + + 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(CachedNetworkImage image) async { + //Size imageSize = await getImageSize(image); + //imageSize = realWidgetSize!; + Size imageSize = Size(realWidgetSize!.width * 1.25, realWidgetSize!.height * 1.25); + + for (int x = 0; x < puzzleDTO.rows!; x++) { + for (int y = 0; y < puzzleDTO.cols!; y++) { + setState(() { + pieces.add( + PuzzlePiece( + key: GlobalKey(), + image: image, + imageSize: imageSize, + row: x, + col: y, + maxRow: puzzleDTO.rows!, + maxCol: puzzleDTO.cols!, + bringToTop: bringToTop, + sendToBack: sendToBack, + ), + ); + }); + } + } + + setState(() { + isSplittingImage = false; + }); + } + + // 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(() { + allInPlaceCount++; + isFinished = allInPlaceCount == puzzleDTO.rows! * puzzleDTO.cols!; + pieces.remove(widget); + pieces.insert(0, widget); + + if(isFinished) { + Size size = MediaQuery.of(context).size; + final appContext = Provider.of(context, listen: false); + VisitAppContext visitAppContext = appContext.getContext(); + TranslationAndResourceDTO? messageFin = puzzleDTO.messageFin != null && puzzleDTO.messageFin!.isNotEmpty ? puzzleDTO.messageFin!.where((message) => message.language!.toUpperCase() == visitAppContext.language!.toUpperCase()).firstOrNull : null; + + if(messageFin != null) { + showMessage(messageFin, appContext, context, size); + } + } + }); + } + + @override + Widget build(BuildContext context) { + final appContext = Provider.of(context); + VisitAppContext visitAppContext = appContext.getContext(); + Size size = MediaQuery.of(context).size; + var title = TranslationHelper.get(widget.section.title, appContext.getContext()); + String cleanedTitle = title.replaceAll('\n', ' ').replaceAll('
', ' '); + + return Stack( + children: [ + Container( + height: size.height * 0.28, + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + color: kMainGrey, + spreadRadius: 0.5, + blurRadius: 5, + offset: Offset(0, 1), // changes position of shadow + ), + ], + gradient: const LinearGradient( + begin: Alignment.centerRight, + end: Alignment.centerLeft, + colors: [ + /*Color(0xFFDD79C2), + Color(0xFFB65FBE), + Color(0xFF9146BA), + Color(0xFF7633B8), + Color(0xFF6528B6), + Color(0xFF6025B6)*/ + kMainColor0, //Color(0xFFf6b3c4) + kMainColor1, + kMainColor2, + + ], + ), + image: widget.section.imageSource != null ? DecorationImage( + fit: BoxFit.cover, + opacity: 0.65, + image: NetworkImage( + widget.section.imageSource!, + ), + ): null, + ), + ), + Column( + children: [ + SizedBox( + height: size.height * 0.11, + width: size.width, + child: Stack( + fit: StackFit.expand, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.only(top: 22.0), + child: SizedBox( + width: size.width *0.7, + child: HtmlWidget( + cleanedTitle, + textStyle: const TextStyle(color: Colors.white, fontFamily: 'Roboto', fontSize: 20), + customStylesBuilder: (element) + { + return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"}; + }, + ), + ), + ), + ), + Positioned( + top: 35, + left: 10, + child: SizedBox( + width: 50, + height: 50, + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + decoration: const BoxDecoration( + color: kMainColor, + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_back, size: 23, color: Colors.white) + ), + ) + ), + ), + ], + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.only(top: 0), + decoration: const BoxDecoration( + boxShadow: [ + BoxShadow( + color: kMainGrey, + spreadRadius: 0.5, + blurRadius: 2, + offset: Offset(0, 1), // changes position of shadow + ), + ], + color: kBackgroundColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + ), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30), + topRight: Radius.circular(30), + ), + child: Center( + //color: Colors.green, + child: Container( + color: Colors.green, + child: Padding( + key: _widgetKey, + padding: const EdgeInsets.all(0.0), + child: isSplittingImage ? Center(child: LoadingCommon()) : + puzzleDTO.puzzleImage == null || puzzleDTO.puzzleImage!.url == null || realWidgetSize == null + ? Center(child: Text("Aucune image à afficher", style: TextStyle(fontSize: kNoneInfoOrIncorrect))) + : Center( + child: Padding( + padding: const EdgeInsets.all(0.0), + child: Container( + width: visitAppContext.puzzleSize != null && visitAppContext.puzzleSize!.width > 0 ? visitAppContext.puzzleSize!.width : realWidgetSize!.width * 0.8, + height: visitAppContext.puzzleSize != null && visitAppContext.puzzleSize!.height > 0 ? visitAppContext.puzzleSize!.height +1.5 : realWidgetSize!.height * 0.85, + child: Stack( + children: pieces, + ), + ), + ), + ), + ), + ), + ) + ) + ), + ), + ], + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/Screens/Sections/Puzzle/puzzle_piece.dart b/lib/Screens/Sections/Puzzle/puzzle_piece.dart new file mode 100644 index 0000000..e03f5f3 --- /dev/null +++ b/lib/Screens/Sections/Puzzle/puzzle_piece.dart @@ -0,0 +1,287 @@ +import 'dart:math'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/app_context.dart'; +import 'package:provider/provider.dart'; + +class PuzzlePiece extends StatefulWidget { + final CachedNetworkImage 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; + + GlobalKey _widgetPieceKey = GlobalKey(); + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + + RenderBox renderBox = _widgetPieceKey.currentContext?.findRenderObject() as RenderBox; + Size size = renderBox.size; + + final appContext = Provider.of(context, listen: false); + VisitAppContext visitAppContext = appContext.getContext(); + visitAppContext.puzzleSize = size; // do it another way + appContext.setContext(visitAppContext); + + if(widget.row == 0 && widget.col == 0) { + widget.sendToBack(widget); + } + }); + }); + } + + @override + Widget build(BuildContext context) { + + var isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; + + var imageHeight = isPortrait ? widget.imageSize.width/1.55 : widget.imageSize.width; + var imageWidth = isPortrait ? widget.imageSize.width/1.55 : widget.imageSize.height; + + 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 /7; // TODO change ? + } + + if (left == null) { + left = Random().nextInt((imageWidth - pieceWidth).ceil()).toDouble(); + var test = left!; + test -= widget.col * pieceWidth; + left = test /7; // TODO change ? + } + + if(widget.row == 0 && widget.col == 0) { + top = 0; + left = 0; + isMovable = false; + } + + return Positioned( + top: top, + left: left, + width: imageWidth, + child: Container( + key: _widgetPieceKey, + decoration: widget.col == 0 && widget.row == 0 ? BoxDecoration( + border: Border.all( + color: Colors.black, + width: 0.5, + ), + ) : null, + 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); + } + }); + } + }, + 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 = Colors.black//Color(0x80FFFFFF) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.5; + + 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/Sections/Puzzle/score_widget.dart b/lib/Screens/Sections/Puzzle/score_widget.dart new file mode 100644 index 0000000..d637992 --- /dev/null +++ b/lib/Screens/Sections/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 diff --git a/lib/Screens/Sections/Slider/slider_view.dart b/lib/Screens/Sections/Slider/slider_view.dart new file mode 100644 index 0000000..2fdf16b --- /dev/null +++ b/lib/Screens/Sections/Slider/slider_view.dart @@ -0,0 +1,336 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:manager_api_new/api.dart'; +import 'package:mymuseum_visitapp/Components/show_element_for_resource.dart'; +import 'package:mymuseum_visitapp/Helpers/ImageCustomProvider.dart'; +import 'package:mymuseum_visitapp/Models/visitContext.dart'; +import 'package:mymuseum_visitapp/app_context.dart'; +import 'package:mymuseum_visitapp/constants.dart'; +import 'package:provider/provider.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; + +class SliderPage extends StatefulWidget { + final SliderDTO section; + SliderPage({required this.section}); + + @override + _SliderPage createState() => _SliderPage(); +} + +class _SliderPage extends State { + SliderDTO sliderDTO = SliderDTO(); + CarouselSliderController? sliderController; + ValueNotifier currentIndex = ValueNotifier(1); + + late ConfigurationDTO configurationDTO; + + @override + void initState() { + sliderController = CarouselSliderController(); + sliderDTO = widget.section; + sliderDTO.contents!.sort((a, b) => a.order!.compareTo(b.order!)); + + super.initState(); + } + + @override + void dispose() { + sliderController = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Size size = MediaQuery.of(context).size; + final appContext = Provider.of(context); + VisitAppContext visitAppContex = appContext.getContext() as VisitAppContext; + Color? primaryColor = visitAppContex.configuration!.primaryColor != null ? Color(int.parse(visitAppContex.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : null; + + configurationDTO = appContext.getContext().configuration; + + return Stack( + children: [ + if(sliderDTO.contents != null && sliderDTO.contents!.isNotEmpty) + CarouselSlider( + carouselController: sliderController, + options: CarouselOptions( + onPageChanged: (int index, CarouselPageChangedReason reason) { + currentIndex.value = index + 1; + }, + height: MediaQuery.of(context).size.height * 1.0, + enlargeCenterPage: false, + reverse: false, + ), + items: sliderDTO.contents!.map((i) { + return Builder( + builder: (BuildContext context) { + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + margin: const EdgeInsets.symmetric(horizontal: 5.0), + decoration: BoxDecoration( + color: Colors.green, + //color: configurationDTO.imageId == null ? configurationDTO.secondaryColor != null ? new Color(int.parse(configurationDTO.secondaryColor!.split('(0x')[1].split(')')[0], radix: 16)): kBackgroundGrey : null, + borderRadius: BorderRadius.circular(visitAppContex.configuration!.roundedValue?.toDouble() ?? 10.0), + //border: Border.all(width: 0.3, color: kSecondGrey), + ), + child: Column( + //crossAxisAlignment: CrossAxisAlignment.center, + //mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Padding( + padding: const EdgeInsets.all(10.0), + child: Container( + color: Colors.orange, + height: MediaQuery.of(context).size.height * 0.6, + width: MediaQuery.of(context).size.width * 0.72, + /*decoration: BoxDecoration( + color: kBackgroundLight, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(20.0), + /*image: i.source_ != null ? new DecorationImage( + fit: BoxFit.cover, + image: new NetworkImage( + i.source_, + ), + ): null,*/ + boxShadow: [ + BoxShadow( + color: kBackgroundSecondGrey, + spreadRadius: 0.5, + blurRadius: 5, + offset: Offset(0, 1.5), // changes position of shadow + ), + ], + ),*/ + child: Stack( + children: [ + getElementForResource(appContext, i), + Positioned( + bottom: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.all(15.0), + child: HtmlWidget( + i.title!.where((translation) => translation.language == appContext.getContext().language).firstOrNull?.value != null ? i.title!.firstWhere((translation) => translation.language == appContext.getContext().language).value! : "", + textStyle: const TextStyle(fontSize: kTitleSize, color: kBackgroundLight), + ), + ) + ) + ] + ),/**/ + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Container( + height: MediaQuery.of(context).size.height *0.25, + width: MediaQuery.of(context).size.width *0.7, + decoration: BoxDecoration( + color: Colors.blue,// kBackgroundLight, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(visitAppContex.configuration!.roundedValue?.toDouble() ?? 10.0), + boxShadow: const [ + BoxShadow( + color: kBackgroundSecondGrey, + spreadRadius: 0.3, + blurRadius: 4, + offset: Offset(0, 2), // changes position of shadow + ), + ], + ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(15.0), + child: HtmlWidget( + i.description!.where((translation) => translation.language == appContext.getContext().language).firstOrNull?.value != null ? i.description!.firstWhere((translation) => translation.language == appContext.getContext().language).value! : "", + textStyle: const TextStyle(fontSize: kDescriptionSize), + customStylesBuilder: (element) { + return {'text-align': 'center', 'font-family': "Roboto"}; + }, + ), + ), + ), + ), + ), + ), + ], + ) + ); + }, + ); + }).toList(), + ), + /*if(sliderDTO.contents != null && sliderDTO.contents!.length > 1) + Positioned( + top: MediaQuery.of(context).size.height * 0.35, + right: 60, + child: InkWell( + onTap: () { + if (sliderDTO.contents!.length > 0) + sliderController!.nextPage(duration: new Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); + }, + child: Icon( + Icons.chevron_right, + size: 90, + color: primaryColor ?? kMainColor, + ), + ) + ),*/ + /*if(sliderDTO.contents != null && sliderDTO.contents!.length > 1) + Positioned( + top: MediaQuery.of(context).size.height * 0.35, + left: 60, + child: InkWell( + onTap: () { + if (sliderDTO.contents!.length > 0) + sliderController!.previousPage(duration: new Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); + }, + child: Icon( + Icons.chevron_left, + size: 90, + color: primaryColor ?? kMainColor, + ), + ) + ),*/ + if(sliderDTO.contents != null && sliderDTO.contents!.isNotEmpty) // Todo replace by dot ? + Padding( + padding: widget.section.parentId == null ? const EdgeInsets.only(bottom: 20) : const EdgeInsets.only(left: 15, bottom: 20), + child: Align( + alignment: widget.section.parentId == null ? Alignment.bottomCenter : Alignment.bottomLeft, + child: InkWell( + onTap: () { + sliderController!.previousPage(duration: const Duration(milliseconds: 500), curve: Curves.fastOutSlowIn); + }, + child: ValueListenableBuilder( + valueListenable: currentIndex, + builder: (context, value, _) { + return AnimatedSmoothIndicator( + activeIndex: value -1, + count: sliderDTO.contents!.length, + effect: const ExpandingDotsEffect(activeDotColor: kMainColor), + ); + + /*Text( + value.toString()+'/'+sliderDTO.contents!.length.toString(), + style: const TextStyle(fontSize: 25, fontWeight: FontWeight.w500), + );*/ + } + ), + ) + ), + ), + if(sliderDTO.contents == null || sliderDTO.contents!.isEmpty) + const Center(child: Text("Aucun contenu à afficher", style: TextStyle(fontSize: kNoneInfoOrIncorrect))), + Positioned( + top: 35, + left: 10, + child: SizedBox( + width: 50, + height: 50, + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + decoration: const BoxDecoration( + color: kMainColor, + shape: BoxShape.circle, + ), + child: const Icon(Icons.arrow_back, size: 23, color: Colors.white) + ), + ) + ), + ), + // Description + /*Container( + height: sliderDTO.images != null && sliderDTO.images.length > 0 ? size.height *0.3 : size.height *0.6, + width: MediaQuery.of(context).size.width *0.35, + decoration: BoxDecoration( + color: kBackgroundLight, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(10.0), + boxShadow: [ + BoxShadow( + color: kBackgroundSecondGrey, + spreadRadius: 0.5, + blurRadius: 1.1, + offset: Offset(0, 1.1), // changes position of shadow + ), + ], + ), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Text(sliderDTO., textAlign: TextAlign.center, style: TextStyle(fontSize: 15)), + ), + ), + ),*/ + ] + ); + } + + getElementForResource(AppContext appContext, ContentDTO i) { + var widgetToInclude; + VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext; + + switch(i.resource!.type) { + case ResourceType.Image: + widgetToInclude = PhotoView( + imageProvider: ImageCustomProvider.getImageProvider(appContext, i.resourceId!, i.resource!.url!), + minScale: PhotoViewComputedScale.contained * 0.8, + maxScale: PhotoViewComputedScale.contained * 3.0, + backgroundDecoration: BoxDecoration( + color: kBackgroundSecondGrey, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(visitAppContext.configuration!.roundedValue?.toDouble() ?? 15.0), + ), + ); + break; + case ResourceType.ImageUrl: + widgetToInclude = PhotoView( + imageProvider: CachedNetworkImageProvider(i.resource!.url!), + minScale: PhotoViewComputedScale.contained * 0.8, + maxScale: PhotoViewComputedScale.contained * 3.0, + backgroundDecoration: BoxDecoration( + color: kBackgroundSecondGrey, + shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(visitAppContext.configuration!.roundedValue?.toDouble() ?? 15.0), + ), + ); + break; + case ResourceType.Video: + case ResourceType.VideoUrl: + case ResourceType.Audio: + widgetToInclude = Container( + decoration: BoxDecoration( + //color: kBackgroundSecondGrey, + //shape: BoxShape.rectangle, + borderRadius: BorderRadius.circular(visitAppContext.configuration!.roundedValue?.toDouble() ?? 15.0), + ), + child: showElementForResource(ResourceDTO(id: i.resourceId, url: i.resource!.url, type: i.resource!.type), appContext, false, true), + ); + break; + } + + return Center( + child: Container( + height: MediaQuery.of(context).size.height * 0.6, + width: MediaQuery.of(context).size.width * 0.72, + color: Colors.yellow, + child: AspectRatio( + aspectRatio: 16 / 9, + child: ClipRect( + child: widgetToInclude, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/Screens/Sections/Video/video_view.dart b/lib/Screens/Sections/Video/video_page.dart similarity index 100% rename from lib/Screens/Sections/Video/video_view.dart rename to lib/Screens/Sections/Video/video_page.dart diff --git a/lib/Screens/section_page.dart b/lib/Screens/section_page.dart index dda86e7..3f8f454 100644 --- a/lib/Screens/section_page.dart +++ b/lib/Screens/section_page.dart @@ -15,8 +15,10 @@ import 'package:mymuseum_visitapp/Models/resourceModel.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Article/article_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/PDF/pdf_view.dart'; +import 'package:mymuseum_visitapp/Screens/Sections/Puzzle/puzzle_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Quiz/quizz_page.dart'; -import 'package:mymuseum_visitapp/Screens/Sections/Video/video_view.dart'; +import 'package:mymuseum_visitapp/Screens/Sections/Slider/slider_view.dart'; +import 'package:mymuseum_visitapp/Screens/Sections/Video/video_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Web/web_page.dart'; import 'package:mymuseum_visitapp/app_context.dart'; import 'package:mymuseum_visitapp/client.dart'; @@ -64,7 +66,7 @@ class _SectionPageState extends State { return Scaffold( key: _scaffoldKey, - appBar: test!.type != SectionType.Quiz && test.type != SectionType.Article && test.type != SectionType.Web && test.type != SectionType.Pdf && test.type != SectionType.Video ? CustomAppBar( + appBar: test!.type != SectionType.Quiz && test.type != SectionType.Article && test.type != SectionType.Web && test.type != SectionType.Pdf && test.type != SectionType.Video && test.type != SectionType.Puzzle && test.type != SectionType.Slider ? CustomAppBar( title: sectionDTO != null ? TranslationHelper.get(sectionDTO!.title, visitAppContext) : "", isHomeButton: false, ) : null, @@ -91,6 +93,12 @@ class _SectionPageState extends State { case SectionType.Video: VideoDTO videoDTO = VideoDTO.fromJson(sectionResult)!; return VideoPage(section: videoDTO); + case SectionType.Puzzle: + PuzzleDTO puzzleDTO = PuzzleDTO.fromJson(sectionResult)!; + return PuzzlePage(section: puzzleDTO); + case SectionType.Slider: + SliderDTO sliderDTO = SliderDTO.fromJson(sectionResult)!; + return SliderPage(section: sliderDTO); default: return const Center(child: Text("Unsupported type")); } diff --git a/pubspec.lock b/pubspec.lock index 0c00b40..ec169d5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1081,6 +1081,14 @@ packages: description: flutter source: sdk version: "0.0.0" + smooth_page_indicator: + dependency: "direct main" + description: + name: smooth_page_indicator + sha256: b21ebb8bc39cf72d11c7cfd809162a48c3800668ced1c9da3aade13a32cf6c1c + url: "https://pub.dev" + source: hosted + version: "1.2.1" source_gen: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b3fd34a..ed66398 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,8 @@ dependencies: flutter_beacon: ^0.5.1 #not in web flutter_staggered_grid_view: ^0.7.0 + smooth_page_indicator: ^1.2.1 + manager_api_new: path: manager_api_new # The following adds the Cupertino Icons font to your application.