Puzzle working + slider wip + misc
This commit is contained in:
parent
bddde86974
commit
7aca0638ce
265
lib/Components/audio_player.dart
Normal file
265
lib/Components/audio_player.dart
Normal file
@ -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<AudioPlayerFloatingContainer> createState() => _AudioPlayerFloatingContainerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AudioPlayerFloatingContainerState extends State<AudioPlayerFloatingContainer> {
|
||||||
|
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<Uint8List> fileToUint8List(File file) async {
|
||||||
|
List<int> bytes = await file.readAsBytes();
|
||||||
|
return Uint8List.fromList(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final appContext = Provider.of<AppContext>(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<int> bytes;
|
||||||
|
LoadedSource(this.bytes);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StreamAudioResponse> 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',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
126
lib/Components/cached_custom_resource.dart
Normal file
126
lib/Components/cached_custom_resource.dart
Normal file
@ -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<AppContext>(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<File?>(
|
||||||
|
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<File?> _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<FileSystemEntity> 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<String> get localPath async {
|
||||||
|
Directory? appDocumentsDirectory = Platform.isIOS ? await getApplicationDocumentsDirectory() : await getDownloadsDirectory();
|
||||||
|
return appDocumentsDirectory!.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
92
lib/Components/show_element_for_resource.dart
Normal file
92
lib/Components/show_element_for_resource.dart
Normal file
@ -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<dynamic> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
lib/Components/video_viewer.dart
Normal file
94
lib/Components/video_viewer.dart
Normal file
@ -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<VideoViewer> {
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
99
lib/Components/video_viewer_youtube.dart
Normal file
99
lib/Components/video_viewer_youtube.dart
Normal file
@ -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<VideoViewerYoutube> {
|
||||||
|
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)));
|
||||||
|
}
|
||||||
35
lib/Helpers/ImageCustomProvider.dart
Normal file
35
lib/Helpers/ImageCustomProvider.dart
Normal file
@ -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<Object> 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<FileSystemEntity> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -23,6 +23,8 @@ class VisitAppContext with ChangeNotifier {
|
|||||||
bool isScanBeaconAlreadyAllowed = false;
|
bool isScanBeaconAlreadyAllowed = false;
|
||||||
bool isMaximizeTextSize = false;
|
bool isMaximizeTextSize = false;
|
||||||
|
|
||||||
|
Size? puzzleSize;
|
||||||
|
|
||||||
List<ResourceModel> audiosNotWorking = [];
|
List<ResourceModel> audiosNotWorking = [];
|
||||||
|
|
||||||
bool? isAdmin = false;
|
bool? isAdmin = false;
|
||||||
|
|||||||
68
lib/Screens/Sections/Puzzle/correct_overlay.dart
Normal file
68
lib/Screens/Sections/Puzzle/correct_overlay.dart
Normal file
@ -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<CorrectOverlay>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late Animation<double> _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: <Widget>[
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
lib/Screens/Sections/Puzzle/message_dialog.dart
Normal file
93
lib/Screens/Sections/Puzzle/message_dialog.dart
Normal file
@ -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: <Widget>[
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
357
lib/Screens/Sections/Puzzle/puzzle_page.dart
Normal file
357
lib/Screens/Sections/Puzzle/puzzle_page.dart
Normal file
@ -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<PuzzlePage> {
|
||||||
|
PuzzleDTO puzzleDTO = PuzzleDTO();
|
||||||
|
|
||||||
|
int allInPlaceCount = 0;
|
||||||
|
bool isFinished = false;
|
||||||
|
GlobalKey _widgetKey = GlobalKey();
|
||||||
|
Size? realWidgetSize;
|
||||||
|
List<Widget> 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<AppContext>(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<void> 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<Size> getImageSize(CachedNetworkImage image) async {
|
||||||
|
Completer<Size> completer = Completer<Size>();
|
||||||
|
|
||||||
|
/*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<Size> completer = Completer<Size>();
|
||||||
|
|
||||||
|
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<AppContext>(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<AppContext>(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('<br>', ' ');
|
||||||
|
|
||||||
|
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: <Widget>[
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
287
lib/Screens/Sections/Puzzle/puzzle_piece.dart
Normal file
287
lib/Screens/Sections/Puzzle/puzzle_piece.dart
Normal file
@ -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<String, dynamic> 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<PuzzlePiece> {
|
||||||
|
// 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<AppContext>(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<Path> {
|
||||||
|
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<Path> 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;
|
||||||
|
}
|
||||||
14
lib/Screens/Sections/Puzzle/score_widget.dart
Normal file
14
lib/Screens/Sections/Puzzle/score_widget.dart
Normal file
@ -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<ScoreWidget>() as ScoreWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(ScoreWidget oldWidget) => false;
|
||||||
|
}
|
||||||
336
lib/Screens/Sections/Slider/slider_view.dart
Normal file
336
lib/Screens/Sections/Slider/slider_view.dart
Normal file
@ -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<SliderPage> {
|
||||||
|
SliderDTO sliderDTO = SliderDTO();
|
||||||
|
CarouselSliderController? sliderController;
|
||||||
|
ValueNotifier<int> currentIndex = ValueNotifier<int>(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<AppContext>(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<Widget>((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<int>(
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,8 +15,10 @@ import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
|||||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||||
import 'package:mymuseum_visitapp/Screens/Sections/Article/article_page.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/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/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/Screens/Sections/Web/web_page.dart';
|
||||||
import 'package:mymuseum_visitapp/app_context.dart';
|
import 'package:mymuseum_visitapp/app_context.dart';
|
||||||
import 'package:mymuseum_visitapp/client.dart';
|
import 'package:mymuseum_visitapp/client.dart';
|
||||||
@ -64,7 +66,7 @@ class _SectionPageState extends State<SectionPage> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: _scaffoldKey,
|
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) : "",
|
title: sectionDTO != null ? TranslationHelper.get(sectionDTO!.title, visitAppContext) : "",
|
||||||
isHomeButton: false,
|
isHomeButton: false,
|
||||||
) : null,
|
) : null,
|
||||||
@ -91,6 +93,12 @@ class _SectionPageState extends State<SectionPage> {
|
|||||||
case SectionType.Video:
|
case SectionType.Video:
|
||||||
VideoDTO videoDTO = VideoDTO.fromJson(sectionResult)!;
|
VideoDTO videoDTO = VideoDTO.fromJson(sectionResult)!;
|
||||||
return VideoPage(section: videoDTO);
|
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:
|
default:
|
||||||
return const Center(child: Text("Unsupported type"));
|
return const Center(child: Text("Unsupported type"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1081,6 +1081,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -61,6 +61,8 @@ dependencies:
|
|||||||
flutter_beacon: ^0.5.1 #not in web
|
flutter_beacon: ^0.5.1 #not in web
|
||||||
flutter_staggered_grid_view: ^0.7.0
|
flutter_staggered_grid_view: ^0.7.0
|
||||||
|
|
||||||
|
smooth_page_indicator: ^1.2.1
|
||||||
|
|
||||||
manager_api_new:
|
manager_api_new:
|
||||||
path: manager_api_new
|
path: manager_api_new
|
||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user