Compare commits
19 Commits
master
...
IA-Assista
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8ef78d0e2 | ||
|
|
538e677993 | ||
|
|
c6599e13c9 | ||
|
|
959b494b12 | ||
|
|
f0e41adace | ||
|
|
303e50a255 | ||
|
|
c50083b19f | ||
|
|
f07570d8ee | ||
|
|
5e5d92a510 | ||
|
|
b20a112eb2 | ||
|
|
4e9dc59df9 | ||
|
|
7aca0638ce | ||
|
|
bddde86974 | ||
|
|
4946247812 | ||
|
|
9c5ae56549 | ||
|
|
b7ca69162c | ||
|
|
878a2e4cf0 | ||
|
|
53dff98388 | ||
|
|
42d3e257b2 |
@ -3,10 +3,10 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/CustomAppBar.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Article/article_page.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/section_page.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
|
||||
// NOT USED ANYMORE
|
||||
@ -180,7 +180,7 @@ class _ScannerPageState extends State<ScannerPage> {
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ArticlePage(articleId: code, visitAppContextIn: VisitAppContext()); // will not work..
|
||||
return SectionPage(configuration: ConfigurationDTO(), rawSection: null, sectionId: code, visitAppContextIn: VisitAppContext()); // will not work..
|
||||
},
|
||||
),
|
||||
);
|
||||
117
AR-TEST/Tests/DebugOptionsWidget.dart
Normal file
117
AR-TEST/Tests/DebugOptionsWidget.dart
Normal file
@ -0,0 +1,117 @@
|
||||
import 'package:ar_flutter_plugin/managers/ar_location_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_session_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_object_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ar_flutter_plugin/ar_flutter_plugin.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart';
|
||||
|
||||
class DebugOptionsWidget extends StatefulWidget {
|
||||
DebugOptionsWidget({Key? key}) : super(key: key);
|
||||
@override
|
||||
_DebugOptionsWidgetState createState() => _DebugOptionsWidgetState();
|
||||
}
|
||||
|
||||
class _DebugOptionsWidgetState extends State<DebugOptionsWidget> {
|
||||
ARSessionManager? arSessionManager;
|
||||
ARObjectManager? arObjectManager;
|
||||
bool _showFeaturePoints = false;
|
||||
bool _showPlanes = false;
|
||||
bool _showWorldOrigin = false;
|
||||
bool _showAnimatedGuide = true;
|
||||
String _planeTexturePath = "Images/triangle.png";
|
||||
bool _handleTaps = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
arSessionManager!.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Debug Options'),
|
||||
),
|
||||
body: Container(
|
||||
child: Stack(children: [
|
||||
ARView(
|
||||
onARViewCreated: onARViewCreated,
|
||||
planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical,
|
||||
showPlatformType: true,
|
||||
),
|
||||
Align(
|
||||
alignment: FractionalOffset.bottomRight,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width * 0.5,
|
||||
color: Color(0xFFFFFFF).withOpacity(0.5),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Feature Points'),
|
||||
value: _showFeaturePoints,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_showFeaturePoints = value;
|
||||
updateSessionSettings();
|
||||
});
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('Planes'),
|
||||
value: _showPlanes,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_showPlanes = value;
|
||||
updateSessionSettings();
|
||||
});
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: const Text('World Origin'),
|
||||
value: _showWorldOrigin,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_showWorldOrigin = value;
|
||||
updateSessionSettings();
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
])));
|
||||
}
|
||||
|
||||
void onARViewCreated(
|
||||
ARSessionManager arSessionManager,
|
||||
ARObjectManager arObjectManager,
|
||||
ARAnchorManager arAnchorManager,
|
||||
ARLocationManager arLocationManager) {
|
||||
this.arSessionManager = arSessionManager;
|
||||
this.arObjectManager = arObjectManager;
|
||||
|
||||
this.arSessionManager!.onInitialize(
|
||||
showFeaturePoints: _showFeaturePoints,
|
||||
showPlanes: _showPlanes,
|
||||
customPlaneTexturePath: _planeTexturePath,
|
||||
showWorldOrigin: _showWorldOrigin,
|
||||
showAnimatedGuide: _showAnimatedGuide,
|
||||
handleTaps: _handleTaps,
|
||||
);
|
||||
this.arObjectManager!.onInitialize();
|
||||
}
|
||||
|
||||
void updateSessionSettings() {
|
||||
this.arSessionManager!.onInitialize(
|
||||
showFeaturePoints: _showFeaturePoints,
|
||||
showPlanes: _showPlanes,
|
||||
customPlaneTexturePath: _planeTexturePath,
|
||||
showWorldOrigin: _showWorldOrigin,
|
||||
);
|
||||
}
|
||||
}
|
||||
144
AR-TEST/Tests/TestAR.dart
Normal file
144
AR-TEST/Tests/TestAR.dart
Normal file
@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:ar_flutter_plugin/ar_flutter_plugin.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Tests/DebugOptionsWidget.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Tests/cloudtest.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Tests/testother.dart';
|
||||
import 'localtest.dart';
|
||||
|
||||
class TestAR extends StatefulWidget {
|
||||
@override
|
||||
_MyAppState createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<TestAR> {
|
||||
String _platformVersion = 'Unknown';
|
||||
static const String _title = 'AR Plugin Demo';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
initPlatformState();
|
||||
}
|
||||
|
||||
// Platform messages are asynchronous, so we initialize in an async method.
|
||||
Future<void> initPlatformState() async {
|
||||
String platformVersion;
|
||||
// Platform messages may fail, so we use a try/catch PlatformException.
|
||||
try {
|
||||
platformVersion = await ArFlutterPlugin.platformVersion;
|
||||
} on PlatformException {
|
||||
platformVersion = 'Failed to get platform version.';
|
||||
}
|
||||
|
||||
// If the widget was removed from the tree while the asynchronous platform
|
||||
// message was in flight, we want to discard the reply rather than calling
|
||||
// setState to update our non-existent appearance.
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_platformVersion = platformVersion;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(_title),
|
||||
),
|
||||
body: Column(children: [
|
||||
Text('Running on: $_platformVersion\n'),
|
||||
Expanded(
|
||||
child: ExampleList(),
|
||||
),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExampleList extends StatelessWidget {
|
||||
ExampleList({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final examples = [
|
||||
Example(
|
||||
'Debug Options',
|
||||
'Visualize feature points, planes and world coordinate system',
|
||||
() => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => DebugOptionsWidget()))),
|
||||
/*Example(
|
||||
'Local & Online Objects',
|
||||
'Place 3D objects from Flutter assets and the web into the scene',
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => LocalAndWebObjectsWidget()))),*/
|
||||
Example(
|
||||
'Anchors & Objects on Planes',
|
||||
'Place 3D objects on detected planes using anchors',
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ObjectsOnPlanesWidget()))),
|
||||
Example(
|
||||
'Object Transformation Gestures',
|
||||
'Rotate and Pan Objects',
|
||||
() => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => ObjectGesturesWidget()))),
|
||||
Example(
|
||||
'Screenshots',
|
||||
'Place 3D objects on planes and take screenshots',
|
||||
() => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => ScreenshotWidget()))),
|
||||
/*Example(
|
||||
'Cloud Anchors',
|
||||
'Place and retrieve 3D objects using the Google Cloud Anchor API',
|
||||
() => Navigator.push(context,
|
||||
MaterialPageRoute(builder: (context) => CloudAnchorWidget()))),
|
||||
Example(
|
||||
'External Model Management',
|
||||
'Similar to Cloud Anchors example, but uses external database to choose from available 3D models',
|
||||
() => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ExternalModelManagementWidget())))*/
|
||||
];
|
||||
return ListView(
|
||||
children:
|
||||
examples.map((example) => ExampleCard(example: example)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExampleCard extends StatelessWidget {
|
||||
ExampleCard({Key? key, required this.example}) : super(key: key);
|
||||
final Example example;
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
return Card(
|
||||
child: InkWell(
|
||||
splashColor: Colors.blue.withAlpha(30),
|
||||
onTap: () {
|
||||
example.onTap();
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(example.name),
|
||||
subtitle: Text(example.description),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Example {
|
||||
const Example(this.name, this.description, this.onTap);
|
||||
final String name;
|
||||
final String description;
|
||||
final Function onTap;
|
||||
}
|
||||
72
AR-TEST/Tests/XRTest.dart
Normal file
72
AR-TEST/Tests/XRTest.dart
Normal file
@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
class XRWithQRScannerPage extends StatefulWidget {
|
||||
@override
|
||||
_XRWithQRScannerPageState createState() => _XRWithQRScannerPageState();
|
||||
}
|
||||
|
||||
class _XRWithQRScannerPageState extends State<XRWithQRScannerPage> {
|
||||
String qrCode = "";
|
||||
late final WebViewController _webViewController;
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_webViewController = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..loadRequest(Uri.parse("https://immersive-web.github.io/webxr-samples/immersive-ar-session.html"))
|
||||
;
|
||||
//..loadFlutterAsset('assets/files/xr_environment.html'); // Charge le fichier local
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("XR avec Scanner QR Code")),
|
||||
body: Stack(
|
||||
children: [
|
||||
// La couche QR code scanner (superposée au-dessus de l'XR)
|
||||
Positioned.fill(
|
||||
child: QRView(
|
||||
onQRViewCreated: (controller) {
|
||||
controller.scannedDataStream.listen((scanData) {
|
||||
setState(() {
|
||||
qrCode = scanData.code!;
|
||||
print('QR Code détecté : $qrCode');
|
||||
fetchData(qrCode);
|
||||
});
|
||||
});
|
||||
}, key: qrKey,
|
||||
),
|
||||
),
|
||||
// La couche XR (WebXR avec Three.js ou autre moteur)
|
||||
WebViewWidget(
|
||||
controller: _webViewController
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(Color(0x00000000))
|
||||
..addJavaScriptChannel(
|
||||
'WebViewChannel',
|
||||
onMessageReceived: (message) {
|
||||
// Message reçu de JavaScript
|
||||
print(message.message);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: Text(message.message),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> fetchData(String? qrCode) async {
|
||||
print('Fetching data for QR Code: $qrCode');
|
||||
}
|
||||
}
|
||||
170
AR-TEST/Tests/cloudtest.dart
Normal file
170
AR-TEST/Tests/cloudtest.dart
Normal file
@ -0,0 +1,170 @@
|
||||
import 'package:ar_flutter_plugin/managers/ar_location_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_session_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_object_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart';
|
||||
import 'package:ar_flutter_plugin/models/ar_anchor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ar_flutter_plugin/ar_flutter_plugin.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/node_types.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/hittest_result_types.dart';
|
||||
import 'package:ar_flutter_plugin/models/ar_node.dart';
|
||||
import 'package:ar_flutter_plugin/models/ar_hittest_result.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
import 'dart:math';
|
||||
|
||||
class ObjectGesturesWidget extends StatefulWidget {
|
||||
ObjectGesturesWidget({Key? key}) : super(key: key);
|
||||
@override
|
||||
_ObjectGesturesWidgetState createState() => _ObjectGesturesWidgetState();
|
||||
}
|
||||
|
||||
class _ObjectGesturesWidgetState extends State<ObjectGesturesWidget> {
|
||||
ARSessionManager? arSessionManager;
|
||||
ARObjectManager? arObjectManager;
|
||||
ARAnchorManager? arAnchorManager;
|
||||
|
||||
List<ARNode> nodes = [];
|
||||
List<ARAnchor> anchors = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
arSessionManager!.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Object Transformation Gestures'),
|
||||
),
|
||||
body: Container(
|
||||
child: Stack(children: [
|
||||
ARView(
|
||||
onARViewCreated: onARViewCreated,
|
||||
planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical,
|
||||
),
|
||||
Align(
|
||||
alignment: FractionalOffset.bottomCenter,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: onRemoveEverything,
|
||||
child: Text("Remove Everything")),
|
||||
]),
|
||||
)
|
||||
])));
|
||||
}
|
||||
|
||||
void onARViewCreated(
|
||||
ARSessionManager arSessionManager,
|
||||
ARObjectManager arObjectManager,
|
||||
ARAnchorManager arAnchorManager,
|
||||
ARLocationManager arLocationManager) {
|
||||
this.arSessionManager = arSessionManager;
|
||||
this.arObjectManager = arObjectManager;
|
||||
this.arAnchorManager = arAnchorManager;
|
||||
|
||||
this.arSessionManager!.onInitialize(
|
||||
showFeaturePoints: false,
|
||||
showPlanes: true,
|
||||
customPlaneTexturePath: "Images/triangle.png",
|
||||
showWorldOrigin: true,
|
||||
handlePans: true,
|
||||
handleRotation: true,
|
||||
);
|
||||
this.arObjectManager!.onInitialize();
|
||||
|
||||
this.arSessionManager!.onPlaneOrPointTap = onPlaneOrPointTapped;
|
||||
this.arObjectManager!.onPanStart = onPanStarted;
|
||||
this.arObjectManager!.onPanChange = onPanChanged;
|
||||
this.arObjectManager!.onPanEnd = onPanEnded;
|
||||
this.arObjectManager!.onRotationStart = onRotationStarted;
|
||||
this.arObjectManager!.onRotationChange = onRotationChanged;
|
||||
this.arObjectManager!.onRotationEnd = onRotationEnded;
|
||||
}
|
||||
|
||||
Future<void> onRemoveEverything() async {
|
||||
/*nodes.forEach((node) {
|
||||
this.arObjectManager.removeNode(node);
|
||||
});*/
|
||||
anchors.forEach((anchor) {
|
||||
this.arAnchorManager!.removeAnchor(anchor);
|
||||
});
|
||||
anchors = [];
|
||||
}
|
||||
|
||||
Future<void> onPlaneOrPointTapped(
|
||||
List<ARHitTestResult> hitTestResults) async {
|
||||
var singleHitTestResult = hitTestResults.firstWhere(
|
||||
(hitTestResult) => hitTestResult.type == ARHitTestResultType.plane);
|
||||
if (singleHitTestResult != null) {
|
||||
var newAnchor =
|
||||
ARPlaneAnchor(transformation: singleHitTestResult.worldTransform);
|
||||
bool? didAddAnchor = await this.arAnchorManager!.addAnchor(newAnchor);
|
||||
if (didAddAnchor!) {
|
||||
this.anchors.add(newAnchor);
|
||||
// Add note to anchor
|
||||
var newNode = ARNode(
|
||||
type: NodeType.webGLB,
|
||||
uri:
|
||||
"assets/files/Duck.glb",
|
||||
scale: Vector3(0.2, 0.2, 0.2),
|
||||
position: Vector3(0.0, 0.0, 0.0),
|
||||
rotation: Vector4(1.0, 0.0, 0.0, 0.0));
|
||||
bool? didAddNodeToAnchor =
|
||||
await this.arObjectManager!.addNode(newNode, planeAnchor: newAnchor);
|
||||
if (didAddNodeToAnchor!) {
|
||||
this.nodes.add(newNode);
|
||||
} else {
|
||||
this.arSessionManager!.onError("Adding Node to Anchor failed");
|
||||
}
|
||||
} else {
|
||||
this.arSessionManager!.onError("Adding Anchor failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPanStarted(String nodeName) {
|
||||
print("Started panning node " + nodeName);
|
||||
}
|
||||
|
||||
onPanChanged(String nodeName) {
|
||||
print("Continued panning node " + nodeName);
|
||||
}
|
||||
|
||||
onPanEnded(String nodeName, Matrix4 newTransform) {
|
||||
print("Ended panning node " + nodeName);
|
||||
final pannedNode =
|
||||
this.nodes.firstWhere((element) => element.name == nodeName);
|
||||
|
||||
/*
|
||||
* Uncomment the following command if you want to keep the transformations of the Flutter representations of the nodes up to date
|
||||
* (e.g. if you intend to share the nodes through the cloud)
|
||||
*/
|
||||
//pannedNode.transform = newTransform;
|
||||
}
|
||||
|
||||
onRotationStarted(String nodeName) {
|
||||
print("Started rotating node " + nodeName);
|
||||
}
|
||||
|
||||
onRotationChanged(String nodeName) {
|
||||
print("Continued rotating node " + nodeName);
|
||||
}
|
||||
|
||||
onRotationEnded(String nodeName, Matrix4 newTransform) {
|
||||
print("Ended rotating node " + nodeName);
|
||||
final rotatedNode =
|
||||
this.nodes.firstWhere((element) => element.name == nodeName);
|
||||
|
||||
/*
|
||||
* Uncomment the following command if you want to keep the transformations of the Flutter representations of the nodes up to date
|
||||
* (e.g. if you intend to share the nodes through the cloud)
|
||||
*/
|
||||
//rotatedNode.transform = newTransform;
|
||||
}
|
||||
}
|
||||
157
AR-TEST/Tests/localtest.dart
Normal file
157
AR-TEST/Tests/localtest.dart
Normal file
@ -0,0 +1,157 @@
|
||||
import 'package:ar_flutter_plugin/managers/ar_location_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_session_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_object_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart';
|
||||
import 'package:ar_flutter_plugin/models/ar_anchor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ar_flutter_plugin/ar_flutter_plugin.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/node_types.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/hittest_result_types.dart';
|
||||
import 'package:ar_flutter_plugin/models/ar_node.dart';
|
||||
import 'package:ar_flutter_plugin/models/ar_hittest_result.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
class ScreenshotWidget extends StatefulWidget {
|
||||
const ScreenshotWidget({Key? key}) : super(key: key);
|
||||
@override
|
||||
_ScreenshotWidgetState createState() => _ScreenshotWidgetState();
|
||||
}
|
||||
|
||||
class _ScreenshotWidgetState extends State<ScreenshotWidget> {
|
||||
ARSessionManager? arSessionManager;
|
||||
ARObjectManager? arObjectManager;
|
||||
ARAnchorManager? arAnchorManager;
|
||||
|
||||
List<ARNode> nodes = [];
|
||||
List<ARAnchor> anchors = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
arSessionManager!.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Screenshots'),
|
||||
),
|
||||
body:
|
||||
Container(
|
||||
child:
|
||||
Stack(children: [
|
||||
ARView(
|
||||
onARViewCreated: onARViewCreated,
|
||||
planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical,
|
||||
),
|
||||
Align(
|
||||
alignment: FractionalOffset.bottomCenter,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: onRemoveEverything,
|
||||
child: const Text("Remove Everything")),
|
||||
ElevatedButton(
|
||||
onPressed: onTakeScreenshot,
|
||||
child: const Text("Take Screenshot")),
|
||||
]),
|
||||
)
|
||||
])));
|
||||
}
|
||||
|
||||
void onARViewCreated(
|
||||
ARSessionManager arSessionManager,
|
||||
ARObjectManager arObjectManager,
|
||||
ARAnchorManager arAnchorManager,
|
||||
ARLocationManager arLocationManager) {
|
||||
this.arSessionManager = arSessionManager;
|
||||
this.arObjectManager = arObjectManager;
|
||||
this.arAnchorManager = arAnchorManager;
|
||||
|
||||
this.arSessionManager!.onInitialize(
|
||||
showFeaturePoints: false,
|
||||
showPlanes: true,
|
||||
customPlaneTexturePath: "Images/triangle.png",
|
||||
showWorldOrigin: true,
|
||||
);
|
||||
this.arObjectManager!.onInitialize();
|
||||
|
||||
this.arSessionManager!.onPlaneOrPointTap = onPlaneOrPointTapped;
|
||||
this.arObjectManager!.onNodeTap = onNodeTapped;
|
||||
}
|
||||
|
||||
Future<void> onRemoveEverything() async {
|
||||
/*nodes.forEach((node) {
|
||||
this.arObjectManager.removeNode(node);
|
||||
});*/
|
||||
// anchors.forEach((anchor)
|
||||
for (var anchor in anchors)
|
||||
{
|
||||
arAnchorManager!.removeAnchor(anchor);
|
||||
};
|
||||
anchors = [];
|
||||
}
|
||||
|
||||
Future<void> onTakeScreenshot() async {
|
||||
var image = await arSessionManager!.snapshot();
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(image: image, fit: BoxFit.cover)),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> onNodeTapped(List<String> nodes) async {
|
||||
var number = nodes.length;
|
||||
arSessionManager!.onError("Tapped $number node(s)");
|
||||
}
|
||||
|
||||
Future<void> onPlaneOrPointTapped(
|
||||
List<ARHitTestResult> hitTestResults) async {
|
||||
var singleHitTestResult = hitTestResults.firstWhere(
|
||||
(hitTestResult) => hitTestResult.type == ARHitTestResultType.plane);
|
||||
if (singleHitTestResult != null) {
|
||||
var newAnchor =
|
||||
ARPlaneAnchor(transformation: singleHitTestResult.worldTransform);
|
||||
bool? didAddAnchor = await arAnchorManager!.addAnchor(newAnchor);
|
||||
if (didAddAnchor != null && didAddAnchor) {
|
||||
anchors.add(newAnchor);
|
||||
// Add note to anchor
|
||||
var newNode = ARNode(
|
||||
type: NodeType.webGLB,
|
||||
uri:
|
||||
"https://github.com/KhronosGroup/glTF-Sample-Models/blob/main/2.0/Duck/glTF-Binary/Duck.glb",
|
||||
scale: Vector3(0.2, 0.2, 0.2),
|
||||
position: Vector3(0.0, 0.0, 0.0),
|
||||
rotation: Vector4(1.0, 0.0, 0.0, 0.0));
|
||||
bool? didAddNodeToAnchor =
|
||||
await arObjectManager!.addNode(newNode, planeAnchor: newAnchor);
|
||||
|
||||
if (didAddNodeToAnchor != null && didAddNodeToAnchor) {
|
||||
nodes.add(newNode);
|
||||
} else {
|
||||
arSessionManager!.onError("Adding Node to Anchor failed");
|
||||
}
|
||||
} else {
|
||||
arSessionManager!.onError("Adding Anchor failed");
|
||||
}
|
||||
/*
|
||||
// To add a node to the tapped position without creating an anchor, use the following code (Please mind: the function onRemoveEverything has to be adapted accordingly!):
|
||||
var newNode = ARNode(
|
||||
type: NodeType.localGLTF2,
|
||||
uri: "Models/Chicken_01/Chicken_01.gltf",
|
||||
scale: Vector3(0.2, 0.2, 0.2),
|
||||
transformation: singleHitTestResult.worldTransform);
|
||||
bool didAddWebNode = await this.arObjectManager.addNode(newNode);
|
||||
if (didAddWebNode) {
|
||||
this.nodes.add(newNode);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
139
AR-TEST/Tests/testother.dart
Normal file
139
AR-TEST/Tests/testother.dart
Normal file
@ -0,0 +1,139 @@
|
||||
import 'package:ar_flutter_plugin/managers/ar_location_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_session_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_object_manager.dart';
|
||||
import 'package:ar_flutter_plugin/managers/ar_anchor_manager.dart';
|
||||
import 'package:ar_flutter_plugin/models/ar_anchor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ar_flutter_plugin/ar_flutter_plugin.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/config_planedetection.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/node_types.dart';
|
||||
import 'package:ar_flutter_plugin/datatypes/hittest_result_types.dart';
|
||||
import 'package:ar_flutter_plugin/models/ar_node.dart';
|
||||
import 'package:ar_flutter_plugin/models/ar_hittest_result.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
import 'dart:math';
|
||||
|
||||
class ObjectsOnPlanesWidget extends StatefulWidget {
|
||||
ObjectsOnPlanesWidget({Key? key}) : super(key: key);
|
||||
@override
|
||||
_ObjectsOnPlanesWidgetState createState() => _ObjectsOnPlanesWidgetState();
|
||||
}
|
||||
|
||||
class _ObjectsOnPlanesWidgetState extends State<ObjectsOnPlanesWidget> {
|
||||
ARSessionManager? arSessionManager;
|
||||
ARObjectManager? arObjectManager;
|
||||
ARAnchorManager? arAnchorManager;
|
||||
|
||||
List<ARNode> nodes = [];
|
||||
List<ARAnchor> anchors = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
arSessionManager!.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Anchors & Objects on Planes'),
|
||||
),
|
||||
body: Container(
|
||||
child: Stack(children: [
|
||||
ARView(
|
||||
onARViewCreated: onARViewCreated,
|
||||
planeDetectionConfig: PlaneDetectionConfig.horizontalAndVertical,
|
||||
),
|
||||
Align(
|
||||
alignment: FractionalOffset.bottomCenter,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: onRemoveEverything,
|
||||
child: Text("Remove Everything")),
|
||||
]),
|
||||
)
|
||||
])));
|
||||
}
|
||||
|
||||
void onARViewCreated(
|
||||
ARSessionManager arSessionManager,
|
||||
ARObjectManager arObjectManager,
|
||||
ARAnchorManager arAnchorManager,
|
||||
ARLocationManager arLocationManager) {
|
||||
this.arSessionManager = arSessionManager;
|
||||
this.arObjectManager = arObjectManager;
|
||||
this.arAnchorManager = arAnchorManager;
|
||||
|
||||
this.arSessionManager!.onInitialize(
|
||||
showFeaturePoints: false,
|
||||
showPlanes: true,
|
||||
customPlaneTexturePath: "Images/triangle.png",
|
||||
showWorldOrigin: true,
|
||||
);
|
||||
this.arObjectManager!.onInitialize();
|
||||
|
||||
this.arSessionManager!.onPlaneOrPointTap = onPlaneOrPointTapped;
|
||||
this.arObjectManager!.onNodeTap = onNodeTapped;
|
||||
}
|
||||
|
||||
Future<void> onRemoveEverything() async {
|
||||
/*nodes.forEach((node) {
|
||||
this.arObjectManager.removeNode(node);
|
||||
});*/
|
||||
anchors.forEach((anchor) {
|
||||
this.arAnchorManager!.removeAnchor(anchor);
|
||||
});
|
||||
anchors = [];
|
||||
}
|
||||
|
||||
Future<void> onNodeTapped(List<String> nodes) async {
|
||||
var number = nodes.length;
|
||||
this.arSessionManager!.onError("Tapped $number node(s)");
|
||||
}
|
||||
|
||||
Future<void> onPlaneOrPointTapped(
|
||||
List<ARHitTestResult> hitTestResults) async {
|
||||
var singleHitTestResult = hitTestResults.firstWhere(
|
||||
(hitTestResult) => hitTestResult.type == ARHitTestResultType.plane);
|
||||
if (singleHitTestResult != null) {
|
||||
var newAnchor =
|
||||
ARPlaneAnchor(transformation: singleHitTestResult.worldTransform);
|
||||
bool? didAddAnchor = await this.arAnchorManager!.addAnchor(newAnchor);
|
||||
if (didAddAnchor!) {
|
||||
this.anchors.add(newAnchor);
|
||||
// Add note to anchor
|
||||
var newNode = ARNode(
|
||||
type: NodeType.webGLB,
|
||||
uri:
|
||||
"https://github.com/KhronosGroup/glTF-Sample-Models/blob/main/2.0/Duck/glTF-Binary/Duck.glb",
|
||||
scale: Vector3(0.2, 0.2, 0.2),
|
||||
position: Vector3(0.0, 0.0, 0.0),
|
||||
rotation: Vector4(1.0, 0.0, 0.0, 0.0));
|
||||
bool? didAddNodeToAnchor =
|
||||
await this.arObjectManager!.addNode(newNode, planeAnchor: newAnchor);
|
||||
if (didAddNodeToAnchor!) {
|
||||
this.nodes.add(newNode);
|
||||
} else {
|
||||
this.arSessionManager!.onError("Adding Node to Anchor failed");
|
||||
}
|
||||
} else {
|
||||
this.arSessionManager!.onError("Adding Anchor failed");
|
||||
}
|
||||
/*
|
||||
// To add a node to the tapped position without creating an anchor, use the following code (Please mind: the function onRemoveEverything has to be adapted accordingly!):
|
||||
var newNode = ARNode(
|
||||
type: NodeType.localGLTF2,
|
||||
uri: "Models/Chicken_01/Chicken_01.gltf",
|
||||
scale: Vector3(0.2, 0.2, 0.2),
|
||||
transformation: singleHitTestResult.worldTransform);
|
||||
bool didAddWebNode = await this.arObjectManager.addNode(newNode);
|
||||
if (didAddWebNode) {
|
||||
this.nodes.add(newNode);
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -39,13 +39,22 @@ apply plugin: 'kotlin-android'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"*/
|
||||
|
||||
android {
|
||||
compileSdkVersion 34
|
||||
namespace = "be.unov.mymuseum.fortsaintheribert"
|
||||
compileSdkVersion 36
|
||||
ndkVersion "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||
pickFirst 'lib/x86_64/libc++_shared.so'
|
||||
pickFirst 'lib/x86/libc++_shared.so'
|
||||
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
@ -57,11 +66,15 @@ android {
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "be.unov.mymuseum.fortsaintheribert" // Update for mdlf and other clients -- "be.unov.mymuseum.fortsaintheribert" // be.unov.myinfomate.mdlf
|
||||
minSdkVersion flutter.minSdkVersion
|
||||
minSdkVersion 24// flutter.minSdkVersion
|
||||
targetSdkVersion flutter.targetSdkVersion
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
||||
/*ndk {
|
||||
abiFilters "arm64-v8a"
|
||||
}*/
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
|
||||
@ -51,7 +51,8 @@
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<meta-data android:name="com.google.android.geo.API_KEY"
|
||||
android:value="AIzaSyDg6ApuZb6TRsauIyHJ9-XVwGYeh7MsWXE"/>
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/logo" />
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.experimental.enable16kApk=true
|
||||
android.useNewNativePlugin=true
|
||||
@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
|
||||
|
||||
|
||||
@ -31,8 +31,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "7.2.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "1.9.0" apply false
|
||||
id "com.android.application" version "8.9.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
|
||||
}
|
||||
|
||||
include ":app"
|
||||
BIN
assets/files/Duck.glb
Normal file
BIN
assets/files/Duck.glb
Normal file
Binary file not shown.
202
assets/files/xr_environment.html
Normal file
202
assets/files/xr_environment.html
Normal file
@ -0,0 +1,202 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Copyright 2018 The Immersive Web Community Group
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
-->
|
||||
<html>
|
||||
<head>
|
||||
<meta charset='utf-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no'>
|
||||
<meta name='mobile-web-app-capable' content='yes'>
|
||||
<meta name='apple-mobile-web-app-capable' content='yes'>
|
||||
<link rel='icon' type='image/png' sizes='32x32' href='favicon-32x32.png'>
|
||||
<link rel='icon' type='image/png' sizes='96x96' href='favicon-96x96.png'>
|
||||
<link rel='stylesheet' href='css/common.css'>
|
||||
|
||||
<title>Immersive AR Session</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<details open>
|
||||
<summary>Immersive AR Session</summary>
|
||||
<p>
|
||||
This sample demonstrates how to use an 'immersive-ar' XRSession to
|
||||
present a simple WebGL scene to a transparent or passthrough XR
|
||||
device. The logic is largely the same as the corresponding VR sample,
|
||||
with the primary difference being that no background is rendered and
|
||||
the model is scaled down for easier viewing in a real-world space.
|
||||
<a class="back" href="./">Back</a>
|
||||
</p>
|
||||
</details>
|
||||
</header>
|
||||
<script type="module">
|
||||
import {WebXRButton} from './js/util/webxr-button.js';
|
||||
import {Scene} from './js/render/scenes/scene.js';
|
||||
import {Renderer, createWebGLContext} from './js/render/core/renderer.js';
|
||||
import {SkyboxNode} from './js/render/nodes/skybox.js';
|
||||
import {InlineViewerHelper} from './js/util/inline-viewer-helper.js';
|
||||
import {Gltf2Node} from './js/render/nodes/gltf2.js';
|
||||
import {QueryArgs} from './js/util/query-args.js';
|
||||
|
||||
// If requested, use the polyfill to provide support for mobile devices
|
||||
// and devices which only support WebVR.
|
||||
import WebXRPolyfill from './js/third-party/webxr-polyfill/build/webxr-polyfill.module.js';
|
||||
if (QueryArgs.getBool('usePolyfill', true)) {
|
||||
let polyfill = new WebXRPolyfill();
|
||||
}
|
||||
|
||||
// XR globals.
|
||||
let xrButton = null;
|
||||
let xrImmersiveRefSpace = null;
|
||||
let inlineViewerHelper = null;
|
||||
|
||||
// WebGL scene globals.
|
||||
let gl = null;
|
||||
let renderer = null;
|
||||
let scene = new Scene();
|
||||
let solarSystem = new Gltf2Node({url: 'media/gltf/space/space.gltf'});
|
||||
// The solar system is big (citation needed). Scale it down so that users
|
||||
// can move around the planets more easily.
|
||||
solarSystem.scale = [0.01, 0.01, 0.1];
|
||||
scene.addNode(solarSystem);
|
||||
// Still adding a skybox, but only for the benefit of the inline view.
|
||||
let skybox = new SkyboxNode({url: 'media/textures/milky-way-4k.png'});
|
||||
scene.addNode(skybox);
|
||||
|
||||
function initXR() {
|
||||
xrButton = new WebXRButton({
|
||||
onRequestSession: onRequestSession,
|
||||
onEndSession: onEndSession,
|
||||
textEnterXRTitle: "START AR Youhou",
|
||||
textXRNotFoundTitle: "AR NOT FOUND",
|
||||
textExitXRTitle: "EXIT AR",
|
||||
});
|
||||
document.querySelector('header').appendChild(xrButton.domElement);
|
||||
|
||||
if (navigator.xr) {
|
||||
// Checks to ensure that 'immersive-ar' mode is available, and only
|
||||
// enables the button if so.
|
||||
navigator.xr.isSessionSupported('immersive-ar').then((supported) => {
|
||||
xrButton.enabled = supported;
|
||||
});
|
||||
|
||||
navigator.xr.requestSession('inline').then(onSessionStarted);
|
||||
}
|
||||
}
|
||||
|
||||
function onRequestSession() {
|
||||
// Requests an 'immersive-ar' session, which ensures that the users
|
||||
// environment will be visible either via video passthrough or a
|
||||
// transparent display. This may be presented either in a headset or
|
||||
// fullscreen on a mobile device.
|
||||
return navigator.xr.requestSession('immersive-ar')
|
||||
.then((session) => {
|
||||
xrButton.setSession(session);
|
||||
session.isImmersive = true;
|
||||
onSessionStarted(session);
|
||||
});
|
||||
}
|
||||
|
||||
function initGL() {
|
||||
if (gl)
|
||||
return;
|
||||
|
||||
gl = createWebGLContext({
|
||||
xrCompatible: true
|
||||
});
|
||||
document.body.appendChild(gl.canvas);
|
||||
|
||||
function onResize() {
|
||||
gl.canvas.width = gl.canvas.clientWidth * window.devicePixelRatio;
|
||||
gl.canvas.height = gl.canvas.clientHeight * window.devicePixelRatio;
|
||||
}
|
||||
window.addEventListener('resize', onResize);
|
||||
onResize();
|
||||
|
||||
renderer = new Renderer(gl);
|
||||
|
||||
scene.setRenderer(renderer);
|
||||
}
|
||||
|
||||
function onSessionStarted(session) {
|
||||
session.addEventListener('end', onSessionEnded);
|
||||
|
||||
if (session.isImmersive) {
|
||||
// When in 'immersive-ar' mode don't draw an opaque background because
|
||||
// we want the real world to show through.
|
||||
skybox.visible = false;
|
||||
}
|
||||
|
||||
initGL();
|
||||
|
||||
session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl) });
|
||||
|
||||
let refSpaceType = session.isImmersive ? 'local' : 'viewer';
|
||||
session.requestReferenceSpace(refSpaceType).then((refSpace) => {
|
||||
if (session.isImmersive) {
|
||||
xrImmersiveRefSpace = refSpace;
|
||||
|
||||
xrImmersiveRefSpace.addEventListener('reset', (evt) => {
|
||||
if (evt.transform) {
|
||||
// AR experiences typically should stay grounded to the real world.
|
||||
// If there's a known origin shift, compensate for it here.
|
||||
xrImmersiveRefSpace = xrImmersiveRefSpace.getOffsetReferenceSpace(evt.transform);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
inlineViewerHelper = new InlineViewerHelper(gl.canvas, refSpace);
|
||||
}
|
||||
session.requestAnimationFrame(onXRFrame);
|
||||
});
|
||||
}
|
||||
|
||||
function onEndSession(session) {
|
||||
session.end();
|
||||
}
|
||||
|
||||
function onSessionEnded(event) {
|
||||
if (event.session.isImmersive) {
|
||||
xrButton.setSession(null);
|
||||
// Turn the background back on when we go back to the inlive view.
|
||||
skybox.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Called every time a XRSession requests that a new frame be drawn.
|
||||
function onXRFrame(t, frame) {
|
||||
let session = frame.session;
|
||||
let refSpace = session.isImmersive ?
|
||||
xrImmersiveRefSpace :
|
||||
inlineViewerHelper.referenceSpace;
|
||||
let pose = frame.getViewerPose(refSpace);
|
||||
|
||||
scene.startFrame();
|
||||
|
||||
session.requestAnimationFrame(onXRFrame);
|
||||
|
||||
scene.drawXRFrame(frame, pose);
|
||||
|
||||
scene.endFrame();
|
||||
}
|
||||
|
||||
// Start the XR application.
|
||||
initXR();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
assets/icons/marker.png
Normal file
BIN
assets/icons/marker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
332
lib/Components/AssistantChatSheet.dart
Normal file
332
lib/Components/AssistantChatSheet.dart
Normal file
@ -0,0 +1,332 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mymuseum_visitapp/Models/AssistantResponse.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Services/assistantService.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
|
||||
class AssistantChatSheet extends StatefulWidget {
|
||||
final VisitAppContext visitAppContext;
|
||||
final String? configurationId; // null = scope instance, fourni = scope configuration
|
||||
final void Function(String sectionId, String sectionTitle)? onNavigateToSection;
|
||||
|
||||
const AssistantChatSheet({
|
||||
Key? key,
|
||||
required this.visitAppContext,
|
||||
this.configurationId,
|
||||
this.onNavigateToSection,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AssistantChatSheet> createState() => _AssistantChatSheetState();
|
||||
}
|
||||
|
||||
class _AssistantChatSheetState extends State<AssistantChatSheet> {
|
||||
late AssistantService _assistantService;
|
||||
final TextEditingController _controller = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final List<Widget> _bubbles = [];
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_assistantService = AssistantService(visitAppContext: widget.visitAppContext);
|
||||
}
|
||||
|
||||
Future<void> _send() async {
|
||||
final text = _controller.text.trim();
|
||||
if (text.isEmpty || _isLoading) return;
|
||||
|
||||
_controller.clear();
|
||||
setState(() {
|
||||
_bubbles.add(_ChatBubble(text: text, isUser: true));
|
||||
_isLoading = true;
|
||||
});
|
||||
_scrollToBottom();
|
||||
|
||||
try {
|
||||
final response = await _assistantService.chat(
|
||||
message: text,
|
||||
configurationId: widget.configurationId,
|
||||
);
|
||||
setState(() {
|
||||
_bubbles.add(_AssistantMessage(
|
||||
response: response,
|
||||
onNavigate: widget.onNavigateToSection,
|
||||
));
|
||||
});
|
||||
} catch (_) {
|
||||
setState(() {
|
||||
_bubbles.add(_ChatBubble(text: "Une erreur est survenue, réessayez.", isUser: false));
|
||||
});
|
||||
} finally {
|
||||
setState(() => _isLoading = false);
|
||||
_scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DraggableScrollableSheet(
|
||||
initialChildSize: 0.6,
|
||||
minChildSize: 0.4,
|
||||
maxChildSize: 0.92,
|
||||
expand: false,
|
||||
builder: (context, scrollController) {
|
||||
return Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Handle
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10),
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.chat_bubble_outline, color: kMainColor1),
|
||||
const SizedBox(width: 8),
|
||||
Text("Assistant",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: kSecondGrey)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
// Messages
|
||||
Expanded(
|
||||
child: _bubbles.isEmpty
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
"Bonjour ! Posez-moi vos questions sur cette visite.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 15),
|
||||
),
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
itemCount: _bubbles.length,
|
||||
itemBuilder: (_, i) => _bubbles[i],
|
||||
),
|
||||
),
|
||||
// Loading indicator
|
||||
if (_isLoading)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20, height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: kMainColor1),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text("...", style: TextStyle(color: Colors.grey[400])),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Input
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 12, right: 12, bottom: MediaQuery.of(context).viewInsets.bottom + 8, top: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Votre question...",
|
||||
filled: true,
|
||||
fillColor: Colors.grey[100],
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
),
|
||||
onSubmitted: (_) => _send(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
CircleAvatar(
|
||||
backgroundColor: kMainColor1,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.send, color: Colors.white, size: 18),
|
||||
onPressed: _send,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatBubble extends StatelessWidget {
|
||||
final String text;
|
||||
final bool isUser;
|
||||
|
||||
const _ChatBubble({required this.text, required this.isUser});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.78),
|
||||
decoration: BoxDecoration(
|
||||
color: isUser ? kMainColor1 : Colors.grey[100],
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: const Radius.circular(16),
|
||||
topRight: const Radius.circular(16),
|
||||
bottomLeft: isUser ? const Radius.circular(16) : const Radius.circular(4),
|
||||
bottomRight: isUser ? const Radius.circular(4) : const Radius.circular(16),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
color: isUser ? Colors.white : kSecondGrey,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssistantMessage extends StatelessWidget {
|
||||
final AssistantResponse response;
|
||||
final void Function(String sectionId, String sectionTitle)? onNavigate;
|
||||
|
||||
const _AssistantMessage({required this.response, this.onNavigate});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Text bubble
|
||||
_ChatBubble(text: response.reply, isUser: false),
|
||||
|
||||
// Cards
|
||||
if (response.cards != null && response.cards!.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6, left: 4, right: 24),
|
||||
child: Column(
|
||||
children: response.cards!
|
||||
.map((card) => _AiCardWidget(card: card))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
|
||||
// Navigation button
|
||||
if (response.navigation != null && onNavigate != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8, left: 4),
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
onNavigate!(
|
||||
response.navigation!.sectionId,
|
||||
response.navigation!.sectionTitle,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.arrow_forward, size: 16),
|
||||
label: Text(response.navigation!.sectionTitle),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: kMainColor1,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
||||
textStyle: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AiCardWidget extends StatelessWidget {
|
||||
final AiCard card;
|
||||
|
||||
const _AiCardWidget({required this.card});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 6),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: Colors.grey[200]!),
|
||||
boxShadow: [
|
||||
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 3, offset: const Offset(0, 1)),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
if (card.icon != null) ...[
|
||||
Text(card.icon!, style: const TextStyle(fontSize: 18)),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
card.title,
|
||||
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 13, color: kSecondGrey),
|
||||
),
|
||||
if (card.subtitle.isNotEmpty)
|
||||
Text(
|
||||
card.subtitle,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,149 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'carousel_options.dart';
|
||||
import 'carousel_state.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
abstract class CarouselController {
|
||||
bool get ready;
|
||||
|
||||
Future<Null> get onReady;
|
||||
|
||||
Future<void> nextPage({Duration? duration, Curve? curve});
|
||||
|
||||
Future<void> previousPage({Duration? duration, Curve? curve});
|
||||
|
||||
void jumpToPage(int page);
|
||||
|
||||
Future<void> animateToPage(int page, {Duration? duration, Curve? curve});
|
||||
|
||||
void startAutoPlay();
|
||||
|
||||
void stopAutoPlay();
|
||||
|
||||
factory CarouselController() => CarouselControllerImpl();
|
||||
}
|
||||
|
||||
class CarouselControllerImpl implements CarouselController {
|
||||
final Completer<Null> _readyCompleter = Completer<Null>();
|
||||
|
||||
CarouselState? _state;
|
||||
|
||||
set state(CarouselState? state) {
|
||||
_state = state;
|
||||
if (!_readyCompleter.isCompleted) {
|
||||
_readyCompleter.complete();
|
||||
}
|
||||
}
|
||||
|
||||
void _setModeController() =>
|
||||
_state!.changeMode(CarouselPageChangedReason.controller);
|
||||
|
||||
@override
|
||||
bool get ready => _state != null;
|
||||
|
||||
@override
|
||||
Future<Null> get onReady => _readyCompleter.future;
|
||||
|
||||
/// Animates the controlled [CarouselSlider] to the next page.
|
||||
///
|
||||
/// The animation lasts for the given duration and follows the given curve.
|
||||
/// The returned [Future] resolves when the animation completes.
|
||||
Future<void> nextPage(
|
||||
{Duration? duration = const Duration(milliseconds: 300),
|
||||
Curve? curve = Curves.linear}) async {
|
||||
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
||||
if (isNeedResetTimer) {
|
||||
_state!.onResetTimer();
|
||||
}
|
||||
_setModeController();
|
||||
await _state!.pageController!.nextPage(duration: duration!, curve: curve!);
|
||||
if (isNeedResetTimer) {
|
||||
_state!.onResumeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/// Animates the controlled [CarouselSlider] to the previous page.
|
||||
///
|
||||
/// The animation lasts for the given duration and follows the given curve.
|
||||
/// The returned [Future] resolves when the animation completes.
|
||||
Future<void> previousPage(
|
||||
{Duration? duration = const Duration(milliseconds: 300),
|
||||
Curve? curve = Curves.linear}) async {
|
||||
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
||||
if (isNeedResetTimer) {
|
||||
_state!.onResetTimer();
|
||||
}
|
||||
_setModeController();
|
||||
await _state!.pageController!
|
||||
.previousPage(duration: duration!, curve: curve!);
|
||||
if (isNeedResetTimer) {
|
||||
_state!.onResumeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes which page is displayed in the controlled [CarouselSlider].
|
||||
///
|
||||
/// Jumps the page position from its current value to the given value,
|
||||
/// without animation, and without checking if the new value is in range.
|
||||
void jumpToPage(int page) {
|
||||
final index = getRealIndex(_state!.pageController!.page!.toInt(),
|
||||
_state!.realPage - _state!.initialPage, _state!.itemCount);
|
||||
|
||||
_setModeController();
|
||||
final int pageToJump = _state!.pageController!.page!.toInt() + page - index;
|
||||
return _state!.pageController!.jumpToPage(pageToJump);
|
||||
}
|
||||
|
||||
/// Animates the controlled [CarouselSlider] from the current page to the given page.
|
||||
///
|
||||
/// The animation lasts for the given duration and follows the given curve.
|
||||
/// The returned [Future] resolves when the animation completes.
|
||||
Future<void> animateToPage(int page,
|
||||
{Duration? duration = const Duration(milliseconds: 300),
|
||||
Curve? curve = Curves.linear}) async {
|
||||
final bool isNeedResetTimer = _state!.options.pauseAutoPlayOnManualNavigate;
|
||||
if (isNeedResetTimer) {
|
||||
_state!.onResetTimer();
|
||||
}
|
||||
final index = getRealIndex(_state!.pageController!.page!.toInt(),
|
||||
_state!.realPage - _state!.initialPage, _state!.itemCount);
|
||||
int smallestMovement = page - index;
|
||||
if (_state!.options.enableInfiniteScroll &&
|
||||
_state!.itemCount != null &&
|
||||
_state!.options.animateToClosest) {
|
||||
if ((page - index).abs() > (page + _state!.itemCount! - index).abs()) {
|
||||
smallestMovement = page + _state!.itemCount! - index;
|
||||
} else if ((page - index).abs() >
|
||||
(page - _state!.itemCount! - index).abs()) {
|
||||
smallestMovement = page - _state!.itemCount! - index;
|
||||
}
|
||||
}
|
||||
_setModeController();
|
||||
await _state!.pageController!.animateToPage(
|
||||
_state!.pageController!.page!.toInt() + smallestMovement,
|
||||
duration: duration!,
|
||||
curve: curve!);
|
||||
if (isNeedResetTimer) {
|
||||
_state!.onResumeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts the controlled [CarouselSlider] autoplay.
|
||||
///
|
||||
/// The carousel will only autoPlay if the [autoPlay] parameter
|
||||
/// in [CarouselOptions] is true.
|
||||
void startAutoPlay() {
|
||||
_state!.onResumeTimer();
|
||||
}
|
||||
|
||||
/// Stops the controlled [CarouselSlider] from autoplaying.
|
||||
///
|
||||
/// This is a more on-demand way of doing this. Use the [autoPlay]
|
||||
/// parameter in [CarouselOptions] to specify the autoPlay behaviour of the carousel.
|
||||
void stopAutoPlay() {
|
||||
_state!.onResetTimer();
|
||||
}
|
||||
}
|
||||
@ -1,223 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum CarouselPageChangedReason { timed, manual, controller }
|
||||
|
||||
enum CenterPageEnlargeStrategy { scale, height, zoom }
|
||||
|
||||
class CarouselOptions {
|
||||
/// Set carousel height and overrides any existing [aspectRatio].
|
||||
final double? height;
|
||||
|
||||
/// Aspect ratio is used if no height have been declared.
|
||||
///
|
||||
/// Defaults to 16:9 aspect ratio.
|
||||
final double aspectRatio;
|
||||
|
||||
/// The fraction of the viewport that each page should occupy.
|
||||
///
|
||||
/// Defaults to 0.8, which means each page fills 80% of the carousel.
|
||||
final double viewportFraction;
|
||||
|
||||
/// The initial page to show when first creating the [CarouselSlider].
|
||||
///
|
||||
/// Defaults to 0.
|
||||
final int initialPage;
|
||||
|
||||
///Determines if carousel should loop infinitely or be limited to item length.
|
||||
///
|
||||
///Defaults to true, i.e. infinite loop.
|
||||
final bool enableInfiniteScroll;
|
||||
|
||||
///Determines if carousel should loop to the closest occurence of requested page.
|
||||
///
|
||||
///Defaults to true.
|
||||
final bool animateToClosest;
|
||||
|
||||
/// Reverse the order of items if set to true.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool reverse;
|
||||
|
||||
/// Enables auto play, sliding one page at a time.
|
||||
///
|
||||
/// Use [autoPlayInterval] to determent the frequency of slides.
|
||||
/// Defaults to false.
|
||||
final bool autoPlay;
|
||||
|
||||
/// Sets Duration to determent the frequency of slides when
|
||||
///
|
||||
/// [autoPlay] is set to true.
|
||||
/// Defaults to 4 seconds.
|
||||
final Duration autoPlayInterval;
|
||||
|
||||
/// The animation duration between two transitioning pages while in auto playback.
|
||||
///
|
||||
/// Defaults to 800 ms.
|
||||
final Duration autoPlayAnimationDuration;
|
||||
|
||||
/// Determines the animation curve physics.
|
||||
///
|
||||
/// Defaults to [Curves.fastOutSlowIn].
|
||||
final Curve autoPlayCurve;
|
||||
|
||||
/// Determines if current page should be larger than the side images,
|
||||
/// creating a feeling of depth in the carousel.
|
||||
///
|
||||
/// Defaults to false.
|
||||
final bool? enlargeCenterPage;
|
||||
|
||||
/// The axis along which the page view scrolls.
|
||||
///
|
||||
/// Defaults to [Axis.horizontal].
|
||||
final Axis scrollDirection;
|
||||
|
||||
/// Called whenever the page in the center of the viewport changes.
|
||||
final Function(int index, CarouselPageChangedReason reason)? onPageChanged;
|
||||
|
||||
/// Called whenever the carousel is scrolled
|
||||
final ValueChanged<double?>? onScrolled;
|
||||
|
||||
/// How the carousel should respond to user input.
|
||||
///
|
||||
/// For example, determines how the items continues to animate after the
|
||||
/// user stops dragging the page view.
|
||||
///
|
||||
/// The physics are modified to snap to page boundaries using
|
||||
/// [PageScrollPhysics] prior to being used.
|
||||
///
|
||||
/// Defaults to matching platform conventions.
|
||||
final ScrollPhysics? scrollPhysics;
|
||||
|
||||
/// Set to false to disable page snapping, useful for custom scroll behavior.
|
||||
///
|
||||
/// Default to `true`.
|
||||
final bool pageSnapping;
|
||||
|
||||
/// If `true`, the auto play function will be paused when user is interacting with
|
||||
/// the carousel, and will be resumed when user finish interacting.
|
||||
/// Default to `true`.
|
||||
final bool pauseAutoPlayOnTouch;
|
||||
|
||||
/// If `true`, the auto play function will be paused when user is calling
|
||||
/// pageController's `nextPage` or `previousPage` or `animateToPage` method.
|
||||
/// And after the animation complete, the auto play will be resumed.
|
||||
/// Default to `true`.
|
||||
final bool pauseAutoPlayOnManualNavigate;
|
||||
|
||||
/// If `enableInfiniteScroll` is `false`, and `autoPlay` is `true`, this option
|
||||
/// decide the carousel should go to the first item when it reach the last item or not.
|
||||
/// If set to `true`, the auto play will be paused when it reach the last item.
|
||||
/// If set to `false`, the auto play function will animate to the first item when it was
|
||||
/// in the last item.
|
||||
final bool pauseAutoPlayInFiniteScroll;
|
||||
|
||||
/// Pass a `PageStoragekey` if you want to keep the pageview's position when it was recreated.
|
||||
final PageStorageKey? pageViewKey;
|
||||
|
||||
/// Use [enlargeStrategy] to determine which method to enlarge the center page.
|
||||
final CenterPageEnlargeStrategy enlargeStrategy;
|
||||
|
||||
/// How much the pages next to the center page will be scaled down.
|
||||
/// If `enlargeCenterPage` is false, this property has no effect.
|
||||
final double enlargeFactor;
|
||||
|
||||
/// Whether or not to disable the `Center` widget for each slide.
|
||||
final bool disableCenter;
|
||||
|
||||
/// Whether to add padding to both ends of the list.
|
||||
/// If this is set to true and [viewportFraction] < 1.0, padding will be added such that the first and last child slivers will be in the center of the viewport when scrolled all the way to the start or end, respectively.
|
||||
/// If [viewportFraction] >= 1.0, this property has no effect.
|
||||
/// This property defaults to true and must not be null.
|
||||
final bool padEnds;
|
||||
|
||||
/// Exposed clipBehavior of PageView
|
||||
final Clip clipBehavior;
|
||||
|
||||
CarouselOptions({
|
||||
this.height,
|
||||
this.aspectRatio = 16 / 9,
|
||||
this.viewportFraction = 0.8,
|
||||
this.initialPage = 0,
|
||||
this.enableInfiniteScroll = true,
|
||||
this.animateToClosest = true,
|
||||
this.reverse = false,
|
||||
this.autoPlay = false,
|
||||
this.autoPlayInterval = const Duration(seconds: 4),
|
||||
this.autoPlayAnimationDuration = const Duration(milliseconds: 800),
|
||||
this.autoPlayCurve = Curves.fastOutSlowIn,
|
||||
this.enlargeCenterPage = false,
|
||||
this.onPageChanged,
|
||||
this.onScrolled,
|
||||
this.scrollPhysics,
|
||||
this.pageSnapping = true,
|
||||
this.scrollDirection = Axis.horizontal,
|
||||
this.pauseAutoPlayOnTouch = true,
|
||||
this.pauseAutoPlayOnManualNavigate = true,
|
||||
this.pauseAutoPlayInFiniteScroll = false,
|
||||
this.pageViewKey,
|
||||
this.enlargeStrategy = CenterPageEnlargeStrategy.scale,
|
||||
this.enlargeFactor = 0.3,
|
||||
this.disableCenter = false,
|
||||
this.padEnds = true,
|
||||
this.clipBehavior = Clip.hardEdge,
|
||||
});
|
||||
|
||||
///Generate new [CarouselOptions] based on old ones.
|
||||
|
||||
CarouselOptions copyWith(
|
||||
{double? height,
|
||||
double? aspectRatio,
|
||||
double? viewportFraction,
|
||||
int? initialPage,
|
||||
bool? enableInfiniteScroll,
|
||||
bool? reverse,
|
||||
bool? autoPlay,
|
||||
Duration? autoPlayInterval,
|
||||
Duration? autoPlayAnimationDuration,
|
||||
Curve? autoPlayCurve,
|
||||
bool? enlargeCenterPage,
|
||||
Function(int index, CarouselPageChangedReason reason)? onPageChanged,
|
||||
ValueChanged<double?>? onScrolled,
|
||||
ScrollPhysics? scrollPhysics,
|
||||
bool? pageSnapping,
|
||||
Axis? scrollDirection,
|
||||
bool? pauseAutoPlayOnTouch,
|
||||
bool? pauseAutoPlayOnManualNavigate,
|
||||
bool? pauseAutoPlayInFiniteScroll,
|
||||
PageStorageKey? pageViewKey,
|
||||
CenterPageEnlargeStrategy? enlargeStrategy,
|
||||
double? enlargeFactor,
|
||||
bool? disableCenter,
|
||||
Clip? clipBehavior,
|
||||
bool? padEnds}) =>
|
||||
CarouselOptions(
|
||||
height: height ?? this.height,
|
||||
aspectRatio: aspectRatio ?? this.aspectRatio,
|
||||
viewportFraction: viewportFraction ?? this.viewportFraction,
|
||||
initialPage: initialPage ?? this.initialPage,
|
||||
enableInfiniteScroll: enableInfiniteScroll ?? this.enableInfiniteScroll,
|
||||
reverse: reverse ?? this.reverse,
|
||||
autoPlay: autoPlay ?? this.autoPlay,
|
||||
autoPlayInterval: autoPlayInterval ?? this.autoPlayInterval,
|
||||
autoPlayAnimationDuration:
|
||||
autoPlayAnimationDuration ?? this.autoPlayAnimationDuration,
|
||||
autoPlayCurve: autoPlayCurve ?? this.autoPlayCurve,
|
||||
enlargeCenterPage: enlargeCenterPage ?? this.enlargeCenterPage,
|
||||
onPageChanged: onPageChanged ?? this.onPageChanged,
|
||||
onScrolled: onScrolled ?? this.onScrolled,
|
||||
scrollPhysics: scrollPhysics ?? this.scrollPhysics,
|
||||
pageSnapping: pageSnapping ?? this.pageSnapping,
|
||||
scrollDirection: scrollDirection ?? this.scrollDirection,
|
||||
pauseAutoPlayOnTouch: pauseAutoPlayOnTouch ?? this.pauseAutoPlayOnTouch,
|
||||
pauseAutoPlayOnManualNavigate:
|
||||
pauseAutoPlayOnManualNavigate ?? this.pauseAutoPlayOnManualNavigate,
|
||||
pauseAutoPlayInFiniteScroll:
|
||||
pauseAutoPlayInFiniteScroll ?? this.pauseAutoPlayInFiniteScroll,
|
||||
pageViewKey: pageViewKey ?? this.pageViewKey,
|
||||
enlargeStrategy: enlargeStrategy ?? this.enlargeStrategy,
|
||||
enlargeFactor: enlargeFactor ?? this.enlargeFactor,
|
||||
disableCenter: disableCenter ?? this.disableCenter,
|
||||
clipBehavior: clipBehavior ?? this.clipBehavior,
|
||||
padEnds: padEnds ?? this.padEnds,
|
||||
);
|
||||
}
|
||||
@ -1,396 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'carousel_controller.dart' as cs;
|
||||
import 'carousel_options.dart';
|
||||
import 'carousel_state.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
export 'carousel_controller.dart';
|
||||
export 'carousel_options.dart';
|
||||
|
||||
typedef Widget ExtendedIndexedWidgetBuilder(
|
||||
BuildContext context, int index, int realIndex);
|
||||
|
||||
class CarouselSlider extends StatefulWidget {
|
||||
/// [CarouselOptions] to create a [CarouselState] with
|
||||
final CarouselOptions options;
|
||||
|
||||
final bool? disableGesture;
|
||||
|
||||
/// The widgets to be shown in the carousel of default constructor
|
||||
final List<Widget>? items;
|
||||
|
||||
/// The widget item builder that will be used to build item on demand
|
||||
/// The third argument is the PageView's real index, can be used to cooperate
|
||||
/// with Hero.
|
||||
final ExtendedIndexedWidgetBuilder? itemBuilder;
|
||||
|
||||
/// A [MapController], used to control the map.
|
||||
final cs.CarouselControllerImpl _carouselController;
|
||||
|
||||
final int? itemCount;
|
||||
|
||||
CarouselSlider(
|
||||
{required this.items,
|
||||
required this.options,
|
||||
this.disableGesture,
|
||||
cs.CarouselController? carouselController,
|
||||
Key? key})
|
||||
: itemBuilder = null,
|
||||
itemCount = items != null ? items.length : 0,
|
||||
_carouselController = carouselController != null
|
||||
? carouselController as cs.CarouselControllerImpl
|
||||
: cs.CarouselController() as cs.CarouselControllerImpl,
|
||||
super(key: key);
|
||||
|
||||
/// The on demand item builder constructor
|
||||
CarouselSlider.builder(
|
||||
{required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
required this.options,
|
||||
this.disableGesture,
|
||||
cs.CarouselController? carouselController,
|
||||
Key? key})
|
||||
: items = null,
|
||||
_carouselController = carouselController != null
|
||||
? carouselController as cs.CarouselControllerImpl
|
||||
: cs.CarouselController() as cs.CarouselControllerImpl,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
CarouselSliderState createState() => CarouselSliderState(_carouselController);
|
||||
}
|
||||
|
||||
class CarouselSliderState extends State<CarouselSlider>
|
||||
with TickerProviderStateMixin {
|
||||
final cs.CarouselControllerImpl carouselController;
|
||||
Timer? timer;
|
||||
|
||||
CarouselOptions get options => widget.options;
|
||||
|
||||
CarouselState? carouselState;
|
||||
|
||||
PageController? pageController;
|
||||
|
||||
/// mode is related to why the page is being changed
|
||||
CarouselPageChangedReason mode = CarouselPageChangedReason.controller;
|
||||
|
||||
CarouselSliderState(this.carouselController);
|
||||
|
||||
void changeMode(CarouselPageChangedReason _mode) {
|
||||
mode = _mode;
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(CarouselSlider oldWidget) {
|
||||
carouselState!.options = options;
|
||||
carouselState!.itemCount = widget.itemCount;
|
||||
|
||||
// pageController needs to be re-initialized to respond to state changes
|
||||
pageController = PageController(
|
||||
viewportFraction: options.viewportFraction,
|
||||
initialPage: carouselState!.realPage,
|
||||
);
|
||||
carouselState!.pageController = pageController;
|
||||
|
||||
// handle autoplay when state changes
|
||||
handleAutoPlay();
|
||||
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
carouselState =
|
||||
CarouselState(this.options, clearTimer, resumeTimer, this.changeMode);
|
||||
|
||||
carouselState!.itemCount = widget.itemCount;
|
||||
carouselController.state = carouselState;
|
||||
carouselState!.initialPage = widget.options.initialPage;
|
||||
carouselState!.realPage = options.enableInfiniteScroll
|
||||
? carouselState!.realPage + carouselState!.initialPage
|
||||
: carouselState!.initialPage;
|
||||
handleAutoPlay();
|
||||
|
||||
pageController = PageController(
|
||||
viewportFraction: options.viewportFraction,
|
||||
initialPage: carouselState!.realPage,
|
||||
);
|
||||
|
||||
carouselState!.pageController = pageController;
|
||||
}
|
||||
|
||||
Timer? getTimer() {
|
||||
return widget.options.autoPlay
|
||||
? Timer.periodic(widget.options.autoPlayInterval, (_) {
|
||||
if (!mounted) {
|
||||
clearTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
final route = ModalRoute.of(context);
|
||||
if (route?.isCurrent == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
CarouselPageChangedReason previousReason = mode;
|
||||
changeMode(CarouselPageChangedReason.timed);
|
||||
int nextPage = carouselState!.pageController!.page!.round() + 1;
|
||||
int itemCount = widget.itemCount ?? widget.items!.length;
|
||||
|
||||
if (nextPage >= itemCount &&
|
||||
widget.options.enableInfiniteScroll == false) {
|
||||
if (widget.options.pauseAutoPlayInFiniteScroll) {
|
||||
clearTimer();
|
||||
return;
|
||||
}
|
||||
nextPage = 0;
|
||||
}
|
||||
|
||||
carouselState!.pageController!
|
||||
.animateToPage(nextPage,
|
||||
duration: widget.options.autoPlayAnimationDuration,
|
||||
curve: widget.options.autoPlayCurve)
|
||||
.then((_) => changeMode(previousReason));
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
void clearTimer() {
|
||||
if (timer != null) {
|
||||
timer?.cancel();
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void resumeTimer() {
|
||||
if (timer == null) {
|
||||
timer = getTimer();
|
||||
}
|
||||
}
|
||||
|
||||
void handleAutoPlay() {
|
||||
bool autoPlayEnabled = widget.options.autoPlay;
|
||||
|
||||
if (autoPlayEnabled && timer != null) return;
|
||||
|
||||
clearTimer();
|
||||
if (autoPlayEnabled) {
|
||||
resumeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
Widget getGestureWrapper(Widget child) {
|
||||
Widget wrapper;
|
||||
if (widget.options.height != null) {
|
||||
wrapper = Container(height: widget.options.height, child: child);
|
||||
} else {
|
||||
wrapper =
|
||||
AspectRatio(aspectRatio: widget.options.aspectRatio, child: child);
|
||||
}
|
||||
|
||||
if (true == widget.disableGesture) {
|
||||
return NotificationListener(
|
||||
onNotification: (Notification notification) {
|
||||
if (widget.options.onScrolled != null &&
|
||||
notification is ScrollUpdateNotification) {
|
||||
widget.options.onScrolled!(carouselState!.pageController!.page);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: wrapper,
|
||||
);
|
||||
}
|
||||
|
||||
return RawGestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
gestures: {
|
||||
_MultipleGestureRecognizer:
|
||||
GestureRecognizerFactoryWithHandlers<_MultipleGestureRecognizer>(
|
||||
() => _MultipleGestureRecognizer(),
|
||||
(_MultipleGestureRecognizer instance) {
|
||||
instance.onStart = (_) {
|
||||
onStart();
|
||||
};
|
||||
instance.onDown = (_) {
|
||||
onPanDown();
|
||||
};
|
||||
instance.onEnd = (_) {
|
||||
onPanUp();
|
||||
};
|
||||
instance.onCancel = () {
|
||||
onPanUp();
|
||||
};
|
||||
}),
|
||||
},
|
||||
child: NotificationListener(
|
||||
onNotification: (Notification notification) {
|
||||
if (widget.options.onScrolled != null &&
|
||||
notification is ScrollUpdateNotification) {
|
||||
widget.options.onScrolled!(carouselState!.pageController!.page);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: wrapper,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget getCenterWrapper(Widget child) {
|
||||
if (widget.options.disableCenter) {
|
||||
return Container(
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return Center(child: child);
|
||||
}
|
||||
|
||||
Widget getEnlargeWrapper(Widget? child,
|
||||
{double? width,
|
||||
double? height,
|
||||
double? scale,
|
||||
required double itemOffset}) {
|
||||
if (widget.options.enlargeStrategy == CenterPageEnlargeStrategy.height) {
|
||||
return SizedBox(child: child, width: width, height: height);
|
||||
}
|
||||
if (widget.options.enlargeStrategy == CenterPageEnlargeStrategy.zoom) {
|
||||
late Alignment alignment;
|
||||
final bool horizontal = options.scrollDirection == Axis.horizontal;
|
||||
if (itemOffset > 0) {
|
||||
alignment = horizontal ? Alignment.centerRight : Alignment.bottomCenter;
|
||||
} else {
|
||||
alignment = horizontal ? Alignment.centerLeft : Alignment.topCenter;
|
||||
}
|
||||
return Transform.scale(child: child, scale: scale!, alignment: alignment);
|
||||
}
|
||||
return Transform.scale(
|
||||
scale: scale!,
|
||||
child: Container(child: child, width: width, height: height));
|
||||
}
|
||||
|
||||
void onStart() {
|
||||
changeMode(CarouselPageChangedReason.manual);
|
||||
}
|
||||
|
||||
void onPanDown() {
|
||||
if (widget.options.pauseAutoPlayOnTouch) {
|
||||
clearTimer();
|
||||
}
|
||||
|
||||
changeMode(CarouselPageChangedReason.manual);
|
||||
}
|
||||
|
||||
void onPanUp() {
|
||||
if (widget.options.pauseAutoPlayOnTouch) {
|
||||
resumeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
clearTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return getGestureWrapper(PageView.builder(
|
||||
padEnds: widget.options.padEnds,
|
||||
scrollBehavior: ScrollConfiguration.of(context).copyWith(
|
||||
scrollbars: false,
|
||||
overscroll: false,
|
||||
dragDevices: {
|
||||
PointerDeviceKind.touch,
|
||||
PointerDeviceKind.mouse,
|
||||
},
|
||||
),
|
||||
clipBehavior: widget.options.clipBehavior,
|
||||
physics: widget.options.scrollPhysics,
|
||||
scrollDirection: widget.options.scrollDirection,
|
||||
pageSnapping: widget.options.pageSnapping,
|
||||
controller: carouselState!.pageController,
|
||||
reverse: widget.options.reverse,
|
||||
itemCount: widget.options.enableInfiniteScroll ? null : widget.itemCount,
|
||||
key: widget.options.pageViewKey,
|
||||
onPageChanged: (int index) {
|
||||
int currentPage = getRealIndex(index + carouselState!.initialPage,
|
||||
carouselState!.realPage, widget.itemCount);
|
||||
if (widget.options.onPageChanged != null) {
|
||||
widget.options.onPageChanged!(currentPage, mode);
|
||||
}
|
||||
},
|
||||
itemBuilder: (BuildContext context, int idx) {
|
||||
final int index = getRealIndex(idx + carouselState!.initialPage,
|
||||
carouselState!.realPage, widget.itemCount);
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: carouselState!.pageController!,
|
||||
child: (widget.items != null)
|
||||
? (widget.items!.length > 0 ? widget.items![index] : Container())
|
||||
: widget.itemBuilder!(context, index, idx),
|
||||
builder: (BuildContext context, child) {
|
||||
double distortionValue = 1.0;
|
||||
// if `enlargeCenterPage` is true, we must calculate the carousel item's height
|
||||
// to display the visual effect
|
||||
double itemOffset = 0;
|
||||
if (widget.options.enlargeCenterPage != null &&
|
||||
widget.options.enlargeCenterPage == true) {
|
||||
// pageController.page can only be accessed after the first build,
|
||||
// so in the first build we calculate the itemoffset manually
|
||||
var position = carouselState?.pageController?.position;
|
||||
if (position != null &&
|
||||
position.hasPixels &&
|
||||
position.hasContentDimensions) {
|
||||
var _page = carouselState?.pageController?.page;
|
||||
if (_page != null) {
|
||||
itemOffset = _page - idx;
|
||||
}
|
||||
} else {
|
||||
BuildContext storageContext = carouselState!
|
||||
.pageController!.position.context.storageContext;
|
||||
final double? previousSavedPosition =
|
||||
PageStorage.of(storageContext)?.readState(storageContext)
|
||||
as double?;
|
||||
if (previousSavedPosition != null) {
|
||||
itemOffset = previousSavedPosition - idx.toDouble();
|
||||
} else {
|
||||
itemOffset =
|
||||
carouselState!.realPage.toDouble() - idx.toDouble();
|
||||
}
|
||||
}
|
||||
|
||||
final double enlargeFactor =
|
||||
options.enlargeFactor.clamp(0.0, 1.0);
|
||||
final num distortionRatio =
|
||||
(1 - (itemOffset.abs() * enlargeFactor)).clamp(0.0, 1.0);
|
||||
distortionValue =
|
||||
Curves.easeOut.transform(distortionRatio as double);
|
||||
}
|
||||
|
||||
final double height = widget.options.height ??
|
||||
MediaQuery.of(context).size.width *
|
||||
(1 / widget.options.aspectRatio);
|
||||
|
||||
if (widget.options.scrollDirection == Axis.horizontal) {
|
||||
return getCenterWrapper(getEnlargeWrapper(child,
|
||||
height: distortionValue * height,
|
||||
scale: distortionValue,
|
||||
itemOffset: itemOffset));
|
||||
} else {
|
||||
return getCenterWrapper(getEnlargeWrapper(child,
|
||||
width: distortionValue * MediaQuery.of(context).size.width,
|
||||
scale: distortionValue,
|
||||
itemOffset: itemOffset));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _MultipleGestureRecognizer extends PanGestureRecognizer {}
|
||||
@ -1,43 +0,0 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'carousel_options.dart';
|
||||
|
||||
class CarouselState {
|
||||
/// The [CarouselOptions] to create this state
|
||||
CarouselOptions options;
|
||||
|
||||
/// [pageController] is created using the properties passed to the constructor
|
||||
/// and can be used to control the [PageView] it is passed to.
|
||||
PageController? pageController;
|
||||
|
||||
/// The actual index of the [PageView].
|
||||
///
|
||||
/// This value can be ignored unless you know the carousel will be scrolled
|
||||
/// backwards more then 10000 pages.
|
||||
/// Defaults to 10000 to simulate infinite backwards scrolling.
|
||||
int realPage = 10000;
|
||||
|
||||
/// The initial index of the [PageView] on [CarouselSlider] init.
|
||||
///
|
||||
int initialPage = 0;
|
||||
|
||||
/// The widgets count that should be shown at carousel
|
||||
int? itemCount;
|
||||
|
||||
/// Will be called when using pageController to go to next page or
|
||||
/// previous page. It will clear the autoPlay timer.
|
||||
/// Internal use only
|
||||
Function onResetTimer;
|
||||
|
||||
/// Will be called when using pageController to go to next page or
|
||||
/// previous page. It will restart the autoPlay timer.
|
||||
/// Internal use only
|
||||
Function onResumeTimer;
|
||||
|
||||
/// The callback to set the Reason Carousel changed
|
||||
Function(CarouselPageChangedReason) changeMode;
|
||||
|
||||
CarouselState(
|
||||
this.options, this.onResetTimer, this.onResumeTimer, this.changeMode);
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
/// Converts an index of a set size to the corresponding index of a collection of another size
|
||||
/// as if they were circular.
|
||||
///
|
||||
/// Takes a [position] from collection Foo, a [base] from where Foo's index originated
|
||||
/// and the [length] of a second collection Baa, for which the correlating index is sought.
|
||||
///
|
||||
/// For example; We have a Carousel of 10000(simulating infinity) but only 6 images.
|
||||
/// We need to repeat the images to give the illusion of a never ending stream.
|
||||
/// By calling _getRealIndex with position and base we get an offset.
|
||||
/// This offset modulo our length, 6, will return a number between 0 and 5, which represent the image
|
||||
/// to be placed in the given position.
|
||||
int getRealIndex(int position, int base, int? length) {
|
||||
final int offset = position - base;
|
||||
return remainder(offset, length);
|
||||
}
|
||||
|
||||
/// Returns the remainder of the modulo operation [input] % [source], and adjust it for
|
||||
/// negative values.
|
||||
int remainder(int input, int? source) {
|
||||
if (source == 0) return 0;
|
||||
final int result = input % source!;
|
||||
return result < 0 ? source + result : result;
|
||||
}
|
||||
@ -4,6 +4,7 @@ import 'package:mymuseum_visitapp/Components/AdminPopup.dart';
|
||||
import 'package:mymuseum_visitapp/Components/LanguageSelection.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Home/home.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Home/home_3.0.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -77,7 +78,7 @@ class _CustomAppBarState extends State<CustomAppBar> {
|
||||
visitAppContext.isScanningBeacons = false;
|
||||
//Navigator.of(context).pop();
|
||||
Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(
|
||||
builder: (context) => const HomePage(),
|
||||
builder: (context) => const HomePage3(),
|
||||
),(route) => false);
|
||||
});
|
||||
}
|
||||
@ -96,14 +97,14 @@ class _CustomAppBarState extends State<CustomAppBar> {
|
||||
child: visitAppContext.isMaximizeTextSize ? const Icon(Icons.text_fields, color: Colors.white) : const Icon(Icons.format_size, color: Colors.white)
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
/*Padding(
|
||||
padding: const EdgeInsets.only(right: 5.0),
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: LanguageSelection()
|
||||
)
|
||||
),
|
||||
),*/
|
||||
],
|
||||
flexibleSpace: Container(
|
||||
decoration: const BoxDecoration(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
//import 'package:flutter_svg_provider/flutter_svg_provider.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
@ -48,9 +48,10 @@ class _LanguageSelection extends State<LanguageSelection> with TickerProviderSta
|
||||
}
|
||||
|
||||
return PopupMenuButton(
|
||||
color: kMainColor.withValues(alpha: 0.65),
|
||||
icon: Container(
|
||||
height: size.height *0.07,
|
||||
width: size.width *0.07,
|
||||
height: size.height *0.08,
|
||||
width: size.width *0.08,
|
||||
decoration: flagDecoration(selectedLanguage!),
|
||||
),
|
||||
itemBuilder: (context){
|
||||
|
||||
@ -26,9 +26,16 @@ class _ScannerBoutonState extends State<ScannerBouton> {
|
||||
return InkWell(
|
||||
onTap: _onItemTapped,
|
||||
child: Container(
|
||||
decoration: const BoxDecoration(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: kMainColor1,
|
||||
color: kMainColor1.withValues(alpha: 0.6),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
offset: const Offset(0, 1.5),
|
||||
blurRadius: 3.5,
|
||||
color: kConfigurationColor.withValues(alpha: 0.6), // Black color with 12% opacity
|
||||
)
|
||||
],
|
||||
),
|
||||
height: 85.0,
|
||||
width: 85.0,
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SlideFromRouteRight.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Article/article_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Quizz/quizz_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/section_page.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:qr_code_scanner/qr_code_scanner.dart';
|
||||
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class ScannerDialog extends StatefulWidget {
|
||||
const ScannerDialog({Key? key, required this.appContext}) : super(key: key);
|
||||
@ -22,19 +20,16 @@ class ScannerDialog extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ScannerDialogState extends State<ScannerDialog> {
|
||||
Barcode? result;
|
||||
QRViewController? controller;
|
||||
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
|
||||
final MobileScannerController controller = MobileScannerController();
|
||||
bool isProcessing = false;
|
||||
|
||||
// In order to get hot reload to work we need to pause the camera if the platform
|
||||
// is android, or resume the camera if the platform is iOS.
|
||||
@override
|
||||
void reassemble() {
|
||||
super.reassemble();
|
||||
if (Platform.isAndroid) {
|
||||
controller!.pauseCamera();
|
||||
controller.stop();
|
||||
}
|
||||
controller!.resumeCamera();
|
||||
controller.start();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -42,208 +37,135 @@ class _ScannerDialogState extends State<ScannerDialog> {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
|
||||
return Container(
|
||||
height: size.height *0.5,
|
||||
width: size.width *0.9,
|
||||
height: size.height * 0.5,
|
||||
width: size.width * 0.9,
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
child: _buildQrView(context),
|
||||
)
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.rectangle,
|
||||
color: kMainColor1,
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await controller?.toggleFlash();
|
||||
setState(() {});
|
||||
},
|
||||
child: FutureBuilder(
|
||||
future: controller?.getFlashStatus(),
|
||||
builder: (context, snapshot) {
|
||||
return const Icon(Icons.flash_on, color: Colors.white);
|
||||
},
|
||||
)),
|
||||
child: MobileScanner(
|
||||
controller: controller,
|
||||
//allowDuplicates: false,
|
||||
onDetect: (barcodes) => _onDetect(barcodes),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.rectangle,
|
||||
color: kMainColor1,
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
margin: const EdgeInsets.all(8),
|
||||
child: InkWell(
|
||||
onTap: () async {
|
||||
await controller?.flipCamera();
|
||||
setState(() {});
|
||||
},
|
||||
child: FutureBuilder(
|
||||
future: controller?.getCameraInfo(),
|
||||
builder: (context, snapshot) {
|
||||
return const Icon(Icons.flip_camera_android, color: Colors.white);
|
||||
},
|
||||
)),
|
||||
_buildControlButton(
|
||||
icon: Icons.flash_on,
|
||||
onTap: () => controller.toggleTorch(),
|
||||
alignment: Alignment.topRight,
|
||||
),
|
||||
_buildControlButton(
|
||||
icon: Icons.flip_camera_android,
|
||||
onTap: () => controller.switchCamera(),
|
||||
alignment: Alignment.bottomRight,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQrView(BuildContext context) {
|
||||
// For this example we check how width or tall the device is and change the scanArea and overlay accordingly.
|
||||
var scanArea = (MediaQuery.of(context).size.width < 400 ||
|
||||
MediaQuery.of(context).size.height < 400)
|
||||
? 225.0
|
||||
: 300.0;
|
||||
|
||||
// To ensure the Scanner view is properly sizes after rotation
|
||||
// we need to listen for Flutter SizeChanged notification and update controller
|
||||
return QRView(
|
||||
key: qrKey,
|
||||
onQRViewCreated: _onQRViewCreated,
|
||||
overlay: QrScannerOverlayShape(
|
||||
borderColor: kMainColor1,
|
||||
borderRadius: 10,
|
||||
borderLength: 25,
|
||||
borderWidth: 5,
|
||||
overlayColor: Colors.black.withValues(alpha: 0.55),
|
||||
cutOutSize: 225.0),
|
||||
onPermissionSet: (ctrl, p) => _onPermissionSet(context, ctrl, p),
|
||||
Widget _buildControlButton({
|
||||
required IconData icon,
|
||||
required VoidCallback onTap,
|
||||
required Alignment alignment,
|
||||
}) {
|
||||
return Align(
|
||||
alignment: alignment,
|
||||
child: Container(
|
||||
width: 45,
|
||||
height: 45,
|
||||
margin: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.rectangle,
|
||||
color: kMainColor1,
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Icon(icon, color: Colors.white),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_onQRViewCreated(QRViewController controller) {
|
||||
setState(() {
|
||||
this.controller = controller;
|
||||
});
|
||||
if (Platform.isAndroid) {
|
||||
controller.pauseCamera();
|
||||
}
|
||||
controller.resumeCamera();
|
||||
controller.scannedDataStream.listen((scanData) {
|
||||
setState(() {
|
||||
result = scanData;
|
||||
void _onDetect(BarcodeCapture capture) {
|
||||
if (isProcessing) return;
|
||||
|
||||
var code = result == null ? "" : result!.code.toString();
|
||||
if(result!.format == BarcodeFormat.qrcode) {
|
||||
controller.pauseCamera();
|
||||
final barcode = capture.barcodes.first;
|
||||
final code = barcode.rawValue ?? "";
|
||||
|
||||
if (barcode.format == BarcodeFormat.qrCode && code.isNotEmpty) {
|
||||
isProcessing = true;
|
||||
|
||||
RegExp regExp = RegExp(r'^(?:https:\/\/web\.myinfomate\.be\/([^\/]+)\/([^\/]+)\/([^\/]+)|([^\/]+))$');
|
||||
RegExp regExp2 = RegExp(r'^(?:https:\/\/web\.mymuseum\.be\/([^\/]+)\/([^\/]+)\/([^\/]+)|([^\/]+))$');
|
||||
var match = regExp.firstMatch(code);
|
||||
var match2 = regExp2.firstMatch(code);
|
||||
String? instanceId;
|
||||
String? configurationId;
|
||||
String? sectionId;
|
||||
|
||||
if (match != null) {
|
||||
if(match == null) {
|
||||
instanceId = match2?.group(1);
|
||||
configurationId = match2?.group(2);
|
||||
sectionId = match2?.group(3) ?? match2?.group(4);
|
||||
} else {
|
||||
instanceId = match.group(1);
|
||||
configurationId = match.group(2);
|
||||
sectionId = match.group(3) ?? match.group(4);
|
||||
|
||||
print('InstanceId: $instanceId');
|
||||
print('ConfigurationId: $configurationId');
|
||||
print('SectionId: $sectionId');
|
||||
} else {
|
||||
print('L\'URL ne correspond pas au format attendu.');
|
||||
}
|
||||
|
||||
|
||||
//print("QR CODE FOUND");
|
||||
/*ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('QR CODE FOUND - ${code.toString()}')),
|
||||
);*/
|
||||
if ((match == null && match2 == null) || sectionId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("L'URL ne correspond pas au format attendu."), backgroundColor: kMainColor2),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
return;
|
||||
}
|
||||
|
||||
VisitAppContext visitAppContext = widget.appContext!.getContext();
|
||||
|
||||
if(!visitAppContext.sectionIds!.contains(sectionId) || sectionId == null) {
|
||||
if (visitAppContext.sectionIds == null || !visitAppContext.sectionIds!.contains(sectionId)) {
|
||||
visitAppContext.statisticsService?.track(VisitEventType.qrScan, metadata: {'valid': false, 'sectionId': sectionId});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(TranslationHelper.getFromLocale('invalidQRCode', widget.appContext!.getContext())), backgroundColor: kMainColor2),
|
||||
SnackBar(content: Text(TranslationHelper.getFromLocale('invalidQRCode', visitAppContext)), backgroundColor: kMainColor2),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
|
||||
} else {
|
||||
SectionDTO section = visitAppContext.currentSections!.firstWhere((cs) => cs!.id == sectionId)!;
|
||||
switch(section.type) {
|
||||
case SectionType.Article:
|
||||
Navigator.pushReplacement(
|
||||
visitAppContext.statisticsService?.track(VisitEventType.qrScan, sectionId: sectionId, metadata: {'valid': true});
|
||||
dynamic rawSection = visitAppContext.currentSections!.firstWhere((cs) => cs!['id'] == sectionId)!;
|
||||
Navigator.of(context).pop();
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return ArticlePage(visitAppContextIn: visitAppContext, articleId: section.id!);
|
||||
},
|
||||
),
|
||||
SlideFromRightRoute(page: SectionPage(
|
||||
configuration: visitAppContext.configuration!,
|
||||
rawSection: rawSection,
|
||||
visitAppContextIn: visitAppContext,
|
||||
sectionId: rawSection['id'],
|
||||
)),
|
||||
);
|
||||
break;
|
||||
case SectionType.Quizz:
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return QuizzPage(visitAppContextIn: visitAppContext, sectionId: section.id!);
|
||||
},
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) async {
|
||||
//log('${DateTime.now().toIso8601String()}_onPermissionSet $p');
|
||||
if (!p) {
|
||||
var status = await Permission.camera.status;
|
||||
if(!status.isGranted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('no Permission')),
|
||||
);
|
||||
|
||||
// You can request multiple permissions at once.
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.camera,
|
||||
].request();
|
||||
print(statuses[Permission.camera]);
|
||||
print(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller?.dispose();
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
showScannerDialog (BuildContext context, AppContext appContext) {
|
||||
showScannerDialog(BuildContext context, AppContext appContext) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0))
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
),
|
||||
content: ScannerDialog(appContext: appContext),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
), context: context
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -8,9 +8,11 @@ class SearchBox extends StatefulWidget {
|
||||
const SearchBox({
|
||||
Key? key,
|
||||
this.onChanged,
|
||||
this.width,
|
||||
}) : super(key: key);
|
||||
|
||||
final ValueChanged? onChanged;
|
||||
final double? width;
|
||||
|
||||
@override
|
||||
State<SearchBox> createState() => _SearchBoxState();
|
||||
@ -25,17 +27,18 @@ class _SearchBoxState extends State<SearchBox> {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
|
||||
return Container(
|
||||
width: size.width*0.65,
|
||||
width: widget.width ?? size.width*0.65,
|
||||
margin: const EdgeInsets.all(kDefaultPadding),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: kDefaultPadding,
|
||||
vertical: kDefaultPadding / 4, // 5 top and bottom
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
color: Colors.white.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TextFormField(
|
||||
cursorColor: kMainColor,
|
||||
controller: _controller,
|
||||
onChanged: widget.onChanged,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
|
||||
@ -26,10 +26,11 @@ class _SearchNumberBoxState extends State<SearchNumberBox> {
|
||||
vertical: kDefaultPadding / 4, // 5 top and bottom
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
color: Colors.white.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TextFormField(
|
||||
cursorColor: kMainColor,
|
||||
controller: _controller,
|
||||
onChanged: widget.onChanged,
|
||||
keyboardType: TextInputType.number,
|
||||
|
||||
@ -3,7 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
|
||||
24
lib/Components/SlideFromRouteRight.dart
Normal file
24
lib/Components/SlideFromRouteRight.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SlideFromRightRoute extends PageRouteBuilder {
|
||||
final Widget page;
|
||||
|
||||
SlideFromRightRoute({required this.page})
|
||||
: super(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => page,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
const begin = Offset(1.0, 0.0); // départ à droite (hors écran)
|
||||
const end = Offset.zero; // arrivée position normale
|
||||
const curve = Curves.easeInOut;
|
||||
|
||||
final tween =
|
||||
Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
|
||||
final offsetAnimation = animation.drive(tween);
|
||||
|
||||
return SlideTransition(
|
||||
position: offsetAnimation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -1,10 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'package:mymuseum_visitapp/Components/Carousel/carousel_slider.dart' as cs;
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/ShowImagePopup.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/marker_view.dart';
|
||||
import 'package:mymuseum_visitapp/Services/apiService.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
@ -22,12 +23,12 @@ class SliderImagesWidget extends StatefulWidget {
|
||||
|
||||
class _SliderImagesWidget extends State<SliderImagesWidget> {
|
||||
List<ResourceModel?> resourcesInWidget = [];
|
||||
late cs.CarouselController? sliderController;
|
||||
late CarouselSliderController? sliderController;
|
||||
final ValueNotifier<int> currentIndex = ValueNotifier<int>(1);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
sliderController = cs.CarouselController();
|
||||
sliderController = CarouselSliderController();
|
||||
resourcesInWidget = widget.resources;
|
||||
super.initState();
|
||||
}
|
||||
@ -53,10 +54,10 @@ class _SliderImagesWidget extends State<SliderImagesWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if(resourcesInWidget.isNotEmpty)
|
||||
cs.CarouselSlider(
|
||||
CarouselSlider(
|
||||
carouselController: sliderController,
|
||||
options: cs.CarouselOptions(
|
||||
onPageChanged: (int index, cs.CarouselPageChangedReason reason) {
|
||||
options: CarouselOptions(
|
||||
onPageChanged: (int index, CarouselPageChangedReason reason) {
|
||||
//setState(() {
|
||||
//print("SET STATE");
|
||||
currentIndex.value = index + 1;
|
||||
@ -69,8 +70,12 @@ class _SliderImagesWidget extends State<SliderImagesWidget> {
|
||||
items: resourcesInWidget.map<Widget>((i) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
AppContext appContext = Provider.of<AppContext>(context);
|
||||
ContentDTO contentDTO = ContentDTO(resourceId: i!.id, resource: ResourceDTO(id: i.id, type: i.type, label: i.label, url: i.source));
|
||||
var resourcetoShow = getElementForResource(context, appContext, contentDTO, true);
|
||||
return resourcetoShow;
|
||||
//print(widget.imagesDTO[currentIndex-1]);
|
||||
return FutureBuilder(
|
||||
/*return FutureBuilder(
|
||||
future: ApiService.getResource(appContext, visitAppContext.configuration!, i!.id!),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
return Padding(
|
||||
@ -118,7 +123,7 @@ class _SliderImagesWidget extends State<SliderImagesWidget> {
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
);*/
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
299
lib/Components/audio_player.dart
Normal file
299
lib/Components/audio_player.dart
Normal file
@ -0,0 +1,299 @@
|
||||
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";
|
||||
bool _isDisposed = false;
|
||||
|
||||
@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();
|
||||
|
||||
Future.microtask(() async {
|
||||
try {
|
||||
if (_isDisposed || !mounted) return;
|
||||
|
||||
if (widget.audioBytes != null) {
|
||||
await player.setAudioSource(LoadedSource(widget.audioBytes!));
|
||||
} else {
|
||||
await player.dynamicSet(url: widget.resourceURl);
|
||||
}
|
||||
|
||||
if (_isDisposed || !mounted) return;
|
||||
|
||||
if (widget.isAuto) {
|
||||
await player.play();
|
||||
setState(() {
|
||||
isplaying = true;
|
||||
audioplayed = true;
|
||||
});
|
||||
}
|
||||
} catch (e, stack) {
|
||||
debugPrint('Audio error: $e');
|
||||
debugPrintStack(stackTrace: stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_isDisposed = true;
|
||||
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();
|
||||
Size size = MediaQuery.of(context).size;
|
||||
|
||||
return InkWell(
|
||||
onTap: () 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: Container(
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)).withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 15.0),
|
||||
),
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FadeRoute extends PageRouteBuilder {
|
||||
final Widget page;
|
||||
|
||||
FadeRoute({required this.page})
|
||||
: super(
|
||||
pageBuilder: (context, animation, secondaryAnimation) => page,
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
const begin = 0.0;
|
||||
const end = 1.0;
|
||||
const curve = Curves.easeInOut;
|
||||
|
||||
final tween = Tween(begin: begin, end: end).chain(CurveTween(curve: curve));
|
||||
final opacityAnimation = animation.drive(tween);
|
||||
|
||||
return FadeTransition(
|
||||
opacity: opacityAnimation,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -2,7 +2,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
|
||||
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)));
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/articleRead.dart';
|
||||
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
@ -31,8 +31,7 @@ class DatabaseHelper {
|
||||
static const columnData = 'data';
|
||||
static const columnType = 'type';
|
||||
static const columnDateCreation = 'dateCreation';
|
||||
static const columnIsMobile = 'isMobile';
|
||||
static const columnIsTablet = 'isTablet';
|
||||
// columnIsMobile/columnIsTablet removed: now in AppConfigurationLink
|
||||
static const columnIsOffline = 'isOffline';
|
||||
|
||||
static const configurationsTable = 'configurations';
|
||||
@ -65,6 +64,7 @@ class DatabaseHelper {
|
||||
|
||||
static const columnIsAdmin = 'isAdmin';
|
||||
static const columnIsAllLanguages = 'isAllLanguages';
|
||||
static const columnApiKey = 'apiKey';
|
||||
|
||||
|
||||
DatabaseHelper._privateConstructor();
|
||||
@ -158,7 +158,8 @@ class DatabaseHelper {
|
||||
$columnLanguage TEXT NOT NULL,
|
||||
$columnInstanceId TEXT NOT NULL,
|
||||
$columnIsAdmin BOOLEAN NOT NULL CHECK ($columnIsAdmin IN (0,1)),
|
||||
$columnIsAllLanguages BOOLEAN CHECK ($columnIsAllLanguages IN (0,1))
|
||||
$columnIsAllLanguages BOOLEAN CHECK ($columnIsAllLanguages IN (0,1)),
|
||||
$columnApiKey TEXT
|
||||
)
|
||||
''');
|
||||
break;
|
||||
@ -175,8 +176,6 @@ class DatabaseHelper {
|
||||
$columnDateCreation TEXT NOT NULL,
|
||||
$columnPrimaryColor TEXT,
|
||||
$columnSecondaryColor TEXT,
|
||||
$columnIsMobile BOOLEAN NOT NULL CHECK ($columnIsMobile IN (0,1)),
|
||||
$columnIsTablet BOOLEAN NOT NULL CHECK ($columnIsTablet IN (0,1)),
|
||||
$columnIsOffline BOOLEAN NOT NULL CHECK ($columnIsOffline IN (0,1))
|
||||
)
|
||||
''');
|
||||
@ -255,6 +254,9 @@ class DatabaseHelper {
|
||||
} else {
|
||||
print("IN columnIsAllLanguages");
|
||||
}
|
||||
if(test.where((e) => e.toString().contains(columnApiKey)).isEmpty) {
|
||||
await db.rawQuery("ALTER TABLE $nameOfTable ADD $columnApiKey TEXT");
|
||||
}
|
||||
DatabaseHelper.instance.insert(DatabaseTableType.main, visitAppContext.toMap());
|
||||
} catch (e) {
|
||||
print("ERROR IN updateTableMain");
|
||||
@ -321,6 +323,7 @@ class DatabaseHelper {
|
||||
language: element["language"],
|
||||
isAdmin: element["isAdmin"] == 1 ? true : false,
|
||||
isAllLanguages: element["isAllLanguages"] == 1 ? true : false,
|
||||
apiKey: element["apiKey"] as String?,
|
||||
);
|
||||
break;
|
||||
case DatabaseTableType.configurations:
|
||||
@ -381,8 +384,6 @@ class DatabaseHelper {
|
||||
secondaryColor: element["secondaryColor"],
|
||||
languages: List<String>.from(jsonDecode(element["languages"])),
|
||||
dateCreation: DateTime.tryParse(element["dateCreation"]),
|
||||
isMobile: element["isMobile"] == 1 ? true : false,
|
||||
isTablet: element["isTablet"] == 1 ? true : false,
|
||||
isOffline: element["isOffline"] == 1 ? true : false
|
||||
);
|
||||
}
|
||||
@ -400,7 +401,7 @@ class DatabaseHelper {
|
||||
imageSource: element["imageSource"],
|
||||
configurationId: element["configurationId"],
|
||||
type: SectionType.values[element["type"]],
|
||||
data: element["data"],
|
||||
// data: element["data"], // TODO section data
|
||||
dateCreation: DateTime.tryParse(element["dateCreation"]),
|
||||
order: int.parse(element["orderOfElement"]),
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
||||
|
||||
class ModelsHelper {
|
||||
@ -16,8 +16,6 @@ class ModelsHelper {
|
||||
'secondaryColor': configuration.secondaryColor,
|
||||
'languages': configuration.languages,
|
||||
'dateCreation': configuration.dateCreation!.toUtc().toIso8601String(),
|
||||
'isMobile': configuration.isMobile,
|
||||
'isTablet': configuration.isTablet,
|
||||
'isOffline': configuration.isOffline
|
||||
};
|
||||
}
|
||||
@ -35,7 +33,7 @@ class ModelsHelper {
|
||||
'isSubSection': section.isSubSection,
|
||||
'parentId': section.parentId,
|
||||
'type': section.type!.value,
|
||||
'data': section.data,
|
||||
//'data': section.data, // TODO section data
|
||||
'dateCreation': section.dateCreation!.toUtc().toIso8601String(),
|
||||
'orderOfElement': section.order,
|
||||
};
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
// TODO // import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class RequirementStateController extends GetxController {
|
||||
var bluetoothState = BluetoothState.stateOff.obs;
|
||||
var authorizationStatus = AuthorizationStatus.notDetermined.obs;
|
||||
var bluetoothState = false; //BluetoothState.stateOff.obs;
|
||||
var authorizationStatus = false; //AuthorizationStatus.notDetermined.obs;
|
||||
var locationService = false.obs;
|
||||
|
||||
var _startBroadcasting = false.obs;
|
||||
var _startScanning = false.obs;
|
||||
var _pauseScanning = false.obs;
|
||||
|
||||
bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn;
|
||||
/*bool get bluetoothEnabled => bluetoothState.value == BluetoothState.stateOn;
|
||||
bool get authorizationStatusOk =>
|
||||
authorizationStatus.value == AuthorizationStatus.allowed ||
|
||||
authorizationStatus.value == AuthorizationStatus.always;
|
||||
@ -22,7 +22,7 @@ class RequirementStateController extends GetxController {
|
||||
|
||||
updateAuthorizationStatus(AuthorizationStatus status) {
|
||||
authorizationStatus.value = status;
|
||||
}
|
||||
}*/
|
||||
|
||||
updateLocationService(bool flag) {
|
||||
locationService.value = flag;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/translations.dart';
|
||||
|
||||
|
||||
56
lib/Models/AssistantResponse.dart
Normal file
56
lib/Models/AssistantResponse.dart
Normal file
@ -0,0 +1,56 @@
|
||||
class AiCard {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String? icon;
|
||||
|
||||
const AiCard({required this.title, required this.subtitle, this.icon});
|
||||
|
||||
factory AiCard.fromJson(Map<String, dynamic> json) => AiCard(
|
||||
title: json['title'] as String? ?? '',
|
||||
subtitle: json['subtitle'] as String? ?? '',
|
||||
icon: json['icon'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
class AssistantNavigationAction {
|
||||
final String sectionId;
|
||||
final String sectionTitle;
|
||||
final String sectionType;
|
||||
|
||||
const AssistantNavigationAction({
|
||||
required this.sectionId,
|
||||
required this.sectionTitle,
|
||||
required this.sectionType,
|
||||
});
|
||||
|
||||
factory AssistantNavigationAction.fromJson(Map<String, dynamic> json) =>
|
||||
AssistantNavigationAction(
|
||||
sectionId: json['sectionId'] as String? ?? '',
|
||||
sectionTitle: json['sectionTitle'] as String? ?? '',
|
||||
sectionType: json['sectionType'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
class AssistantResponse {
|
||||
final String reply;
|
||||
final List<AiCard>? cards;
|
||||
final AssistantNavigationAction? navigation;
|
||||
|
||||
const AssistantResponse({
|
||||
required this.reply,
|
||||
this.cards,
|
||||
this.navigation,
|
||||
});
|
||||
|
||||
factory AssistantResponse.fromJson(Map<String, dynamic> json) =>
|
||||
AssistantResponse(
|
||||
reply: json['reply'] as String? ?? '',
|
||||
cards: (json['cards'] as List<dynamic>?)
|
||||
?.map((e) => AiCard.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
navigation: json['navigation'] != null
|
||||
? AssistantNavigationAction.fromJson(
|
||||
json['navigation'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
|
||||
class ResponseSubDTO {
|
||||
List<TranslationAndResourceDTO>? label;
|
||||
|
||||
128
lib/Models/agenda.dart
Normal file
128
lib/Models/agenda.dart
Normal file
@ -0,0 +1,128 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class Agenda {
|
||||
List<EventAgenda> events;
|
||||
|
||||
Agenda({required this.events});
|
||||
|
||||
factory Agenda.fromJson(String jsonString) {
|
||||
final List<dynamic> jsonList = json.decode(jsonString);
|
||||
List<EventAgenda> events = [];
|
||||
|
||||
for (var eventData in jsonList) {
|
||||
try {
|
||||
events.add(EventAgenda.fromJson(eventData));
|
||||
} catch(e) {
|
||||
print("Erreur lors du parsing du json : ${e.toString()}");
|
||||
}
|
||||
}
|
||||
|
||||
return Agenda(events: events);
|
||||
}
|
||||
}
|
||||
|
||||
class EventAgenda {
|
||||
String? name;
|
||||
String? description;
|
||||
String? type;
|
||||
DateTime? dateAdded;
|
||||
DateTime? dateFrom;
|
||||
DateTime? dateTo;
|
||||
String? dateHour;
|
||||
EventAddress? address;
|
||||
String? website;
|
||||
String? phone;
|
||||
String? idVideoYoutube;
|
||||
String? email;
|
||||
String? image;
|
||||
|
||||
EventAgenda({
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.type,
|
||||
required this.dateAdded,
|
||||
required this.dateFrom,
|
||||
required this.dateTo,
|
||||
required this.dateHour,
|
||||
required this.address,
|
||||
required this.website,
|
||||
required this.phone,
|
||||
required this.idVideoYoutube,
|
||||
required this.email,
|
||||
required this.image,
|
||||
});
|
||||
|
||||
factory EventAgenda.fromJson(Map<String, dynamic> json) {
|
||||
return EventAgenda(
|
||||
name: json['name'],
|
||||
description: json['description'],
|
||||
type: json['type'] is !bool ? json['type'] : null,
|
||||
dateAdded: json['date_added'] != null && json['date_added'].isNotEmpty ? DateTime.parse(json['date_added']) : null,
|
||||
dateFrom: json['date_from'] != null && json['date_from'].isNotEmpty ? DateTime.parse(json['date_from']) : null,
|
||||
dateTo: json['date_to'] != null && json['date_to'].isNotEmpty ? DateTime.parse(json['date_to']) : null,
|
||||
dateHour: json['date_hour'],
|
||||
address: json['address'] is !bool ? EventAddress.fromJson(json['address']) : null,
|
||||
website: json['website'],
|
||||
phone: json['phone'],
|
||||
idVideoYoutube: json['id_video_youtube'],
|
||||
email: json['email'],
|
||||
image: json['image'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EventAddress {
|
||||
String? address;
|
||||
dynamic lat;
|
||||
dynamic lng;
|
||||
int? zoom;
|
||||
String? placeId;
|
||||
String? name;
|
||||
String? streetNumber;
|
||||
String? streetName;
|
||||
String? streetNameShort;
|
||||
String? city;
|
||||
String? state;
|
||||
String? stateShort;
|
||||
String? postCode;
|
||||
String? country;
|
||||
String? countryShort;
|
||||
|
||||
EventAddress({
|
||||
required this.address,
|
||||
required this.lat,
|
||||
required this.lng,
|
||||
required this.zoom,
|
||||
required this.placeId,
|
||||
required this.name,
|
||||
required this.streetNumber,
|
||||
required this.streetName,
|
||||
required this.streetNameShort,
|
||||
required this.city,
|
||||
required this.state,
|
||||
required this.stateShort,
|
||||
required this.postCode,
|
||||
required this.country,
|
||||
required this.countryShort,
|
||||
});
|
||||
|
||||
factory EventAddress.fromJson(Map<String, dynamic> json) {
|
||||
return EventAddress(
|
||||
address: json['address'],
|
||||
lat: json['lat'],
|
||||
lng: json['lng'],
|
||||
zoom: json['zoom'],
|
||||
placeId: json['place_id'],
|
||||
name: json['name'],
|
||||
streetNumber: json['street_number'],
|
||||
streetName: json['street_name'],
|
||||
streetNameShort: json['street_name_short'],
|
||||
city: json['city'],
|
||||
state: json['state'],
|
||||
stateShort: json['state_short'],
|
||||
postCode: json['post_code'],
|
||||
country: json['country'],
|
||||
countryShort: json['country_short'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
|
||||
class SectionRead {
|
||||
String id = "";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
|
||||
class BeaconSection {
|
||||
int? minorBeaconId;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
|
||||
class ResourceModel {
|
||||
String? id = "";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
|
||||
class Translation {
|
||||
String? language = "";
|
||||
|
||||
@ -1,36 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/articleRead.dart';
|
||||
import 'package:mymuseum_visitapp/Services/statisticsService.dart';
|
||||
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
|
||||
class VisitAppContext with ChangeNotifier {
|
||||
Client clientAPI = Client("https://api.myinfomate.be"); // Replace by https://api.mymuseum.be //http://192.168.31.140:8089
|
||||
Client clientAPI = Client(kApiBaseUrl);
|
||||
|
||||
String? id = "";
|
||||
String? language = "";
|
||||
String? instanceId = "633ee379d9405f32f166f047"; // 63514fd67ed8c735aaa4b8f2 MyInfoMate test instance -- Fort de Saint-Héribert Mymuseum instance id : 633ee379d9405f32f166f047 // 63514fd67ed8c735aaa4b8f1 Mymuseum test // MDLF instance 65ccc67265373befd15be511
|
||||
String? instanceId = kInstanceId;
|
||||
String? apiKey;
|
||||
|
||||
List<ConfigurationDTO>? configurations;
|
||||
ConfigurationDTO? configuration;
|
||||
List<String?>? sectionIds; // Use to valid QR code found
|
||||
List<BeaconSection?>? beaconSections;
|
||||
List<SectionDTO?>? currentSections;
|
||||
List<dynamic>? currentSections;
|
||||
List<SectionRead> readSections = [];
|
||||
bool isContentCurrentlyShown = false;
|
||||
bool isScanningBeacons = false;
|
||||
bool isScanBeaconAlreadyAllowed = false;
|
||||
bool isMaximizeTextSize = false;
|
||||
|
||||
Size? puzzleSize;
|
||||
|
||||
List<ResourceModel> audiosNotWorking = [];
|
||||
|
||||
ApplicationInstanceDTO? applicationInstanceDTO; // null = assistant non activé
|
||||
StatisticsService? statisticsService;
|
||||
|
||||
/// Retourne l'AppConfigurationLink de l'instance mobile pour la configuration courante.
|
||||
/// Contient roundedValue, isSectionImageBackground, etc.
|
||||
AppConfigurationLink? get currentAppConfigurationLink {
|
||||
if (applicationInstanceDTO == null || configuration == null) return null;
|
||||
return applicationInstanceDTO!.configurations
|
||||
?.where((c) => c.configurationId == configuration!.id)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
bool? isAdmin = false;
|
||||
bool? isAllLanguages = false;
|
||||
|
||||
String? localPath;
|
||||
|
||||
VisitAppContext({this.language, this.id, this.configuration, this.isAdmin, this.isAllLanguages, this.instanceId});
|
||||
VisitAppContext({this.language, this.id, this.configuration, this.isAdmin, this.isAllLanguages, this.instanceId, this.apiKey});
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
@ -38,7 +55,8 @@ class VisitAppContext with ChangeNotifier {
|
||||
'instanceId': instanceId,
|
||||
'language': language,
|
||||
'isAdmin': isAdmin != null ? isAdmin! ? 1 : 0 : 0,
|
||||
'isAllLanguages': isAllLanguages != null ? isAllLanguages! ? 1 : 0 : 0
|
||||
'isAllLanguages': isAllLanguages != null ? isAllLanguages! ? 1 : 0 : 0,
|
||||
'apiKey': apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
@ -48,6 +66,7 @@ class VisitAppContext with ChangeNotifier {
|
||||
instanceId: json['instanceId'] as String,
|
||||
language: json['language'] as String,
|
||||
configuration: json['configuration'] == null ? null : ConfigurationDTO.fromJson(json['configuration']),
|
||||
apiKey: json['apiKey'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
217
lib/Models/weatherData.dart
Normal file
217
lib/Models/weatherData.dart
Normal file
@ -0,0 +1,217 @@
|
||||
class WeatherData {
|
||||
String? cod;
|
||||
int? message;
|
||||
int? cnt;
|
||||
List<WeatherForecast>? list;
|
||||
City? city;
|
||||
|
||||
WeatherData({this.cod, this.message, this.cnt, this.list, this.city});
|
||||
|
||||
factory WeatherData.fromJson(Map<String, dynamic> json) {
|
||||
return WeatherData(
|
||||
cod: json['cod'],
|
||||
message: json['message'],
|
||||
cnt: json['cnt'],
|
||||
list: (json['list'] as List?)?.map((item) => WeatherForecast.fromJson(item)).toList(),
|
||||
city: json['city'] != null ? City.fromJson(json['city']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WeatherForecast {
|
||||
int? dt;
|
||||
MainWeatherData? main;
|
||||
List<Weather>? weather;
|
||||
Clouds? clouds;
|
||||
Wind? wind;
|
||||
int? visibility;
|
||||
dynamic pop;
|
||||
Rain? rain;
|
||||
Sys? sys;
|
||||
String? dtTxt;
|
||||
|
||||
WeatherForecast({
|
||||
this.dt,
|
||||
this.main,
|
||||
this.weather,
|
||||
this.clouds,
|
||||
this.wind,
|
||||
this.visibility,
|
||||
this.pop,
|
||||
this.rain,
|
||||
this.sys,
|
||||
this.dtTxt,
|
||||
});
|
||||
|
||||
factory WeatherForecast.fromJson(Map<String, dynamic> json) {
|
||||
return WeatherForecast(
|
||||
dt: json['dt'],
|
||||
main: json['main'] != null ? MainWeatherData.fromJson(json['main']) : null,
|
||||
weather: (json['weather'] as List?)?.map((item) => Weather.fromJson(item)).toList(),
|
||||
clouds: json['clouds'] != null ? Clouds.fromJson(json['clouds']) : null,
|
||||
wind: json['wind'] != null ? Wind.fromJson(json['wind']) : null,
|
||||
visibility: json['visibility'],
|
||||
pop: json['pop'],
|
||||
rain: json['rain'] != null ? Rain.fromJson(json['rain']) : null,
|
||||
sys: json['sys'] != null ? Sys.fromJson(json['sys']) : null,
|
||||
dtTxt: json['dt_txt'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MainWeatherData {
|
||||
double? temp;
|
||||
double? feelsLike;
|
||||
double? tempMin;
|
||||
double? tempMax;
|
||||
int? pressure;
|
||||
int? seaLevel;
|
||||
int? grndLevel;
|
||||
int? humidity;
|
||||
double? tempKf;
|
||||
|
||||
MainWeatherData({
|
||||
this.temp,
|
||||
this.feelsLike,
|
||||
this.tempMin,
|
||||
this.tempMax,
|
||||
this.pressure,
|
||||
this.seaLevel,
|
||||
this.grndLevel,
|
||||
this.humidity,
|
||||
this.tempKf,
|
||||
});
|
||||
|
||||
factory MainWeatherData.fromJson(Map<String, dynamic> json) {
|
||||
return MainWeatherData(
|
||||
temp: json['temp']?.toDouble(),
|
||||
feelsLike: json['feels_like']?.toDouble(),
|
||||
tempMin: json['temp_min']?.toDouble(),
|
||||
tempMax: json['temp_max']?.toDouble(),
|
||||
pressure: json['pressure'],
|
||||
seaLevel: json['sea_level'],
|
||||
grndLevel: json['grnd_level'],
|
||||
humidity: json['humidity'],
|
||||
tempKf: json['temp_kf']?.toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Weather {
|
||||
int? id;
|
||||
String? main;
|
||||
String? description;
|
||||
String? icon;
|
||||
|
||||
Weather({this.id, this.main, this.description, this.icon});
|
||||
|
||||
factory Weather.fromJson(Map<String, dynamic> json) {
|
||||
return Weather(
|
||||
id: json['id'],
|
||||
main: json['main'],
|
||||
description: json['description'],
|
||||
icon: json['icon'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Clouds {
|
||||
int? all;
|
||||
|
||||
Clouds({this.all});
|
||||
|
||||
factory Clouds.fromJson(Map<String, dynamic> json) {
|
||||
return Clouds(
|
||||
all: json['all'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Wind {
|
||||
double? speed;
|
||||
int? deg;
|
||||
double? gust;
|
||||
|
||||
Wind({this.speed, this.deg, this.gust});
|
||||
|
||||
factory Wind.fromJson(Map<String, dynamic> json) {
|
||||
return Wind(
|
||||
speed: json['speed']?.toDouble(),
|
||||
deg: json['deg'],
|
||||
gust: json['gust']?.toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Rain {
|
||||
double? h3;
|
||||
|
||||
Rain({this.h3});
|
||||
|
||||
factory Rain.fromJson(Map<String, dynamic> json) {
|
||||
return Rain(
|
||||
h3: json['3h']?.toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Sys {
|
||||
String? pod;
|
||||
|
||||
Sys({this.pod});
|
||||
|
||||
factory Sys.fromJson(Map<String, dynamic> json) {
|
||||
return Sys(
|
||||
pod: json['pod'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class City {
|
||||
int? id;
|
||||
String? name;
|
||||
Coord? coord;
|
||||
String? country;
|
||||
int? population;
|
||||
int? timezone;
|
||||
int? sunrise;
|
||||
int? sunset;
|
||||
|
||||
City({
|
||||
this.id,
|
||||
this.name,
|
||||
this.coord,
|
||||
this.country,
|
||||
this.population,
|
||||
this.timezone,
|
||||
this.sunrise,
|
||||
this.sunset,
|
||||
});
|
||||
|
||||
factory City.fromJson(Map<String, dynamic> json) {
|
||||
return City(
|
||||
id: json['id'],
|
||||
name: json['name'],
|
||||
coord: json['coord'] != null ? Coord.fromJson(json['coord']) : null,
|
||||
country: json['country'],
|
||||
population: json['population'],
|
||||
timezone: json['timezone'],
|
||||
sunrise: json['sunrise'],
|
||||
sunset: json['sunset'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Coord {
|
||||
double? lat;
|
||||
double? lon;
|
||||
|
||||
Coord({this.lat, this.lon});
|
||||
|
||||
factory Coord.fromJson(Map<String, dynamic> json) {
|
||||
return Coord(
|
||||
lat: json['lat']?.toDouble(),
|
||||
lon: json['lon']?.toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/Loading.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SearchBox.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SearchNumberBox.dart';
|
||||
@ -12,7 +12,7 @@ import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Article/article_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Article/article_page.dart';
|
||||
import 'package:mymuseum_visitapp/Services/apiService.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
294
lib/Screens/ConfigurationPage/components/body.dart
Normal file
294
lib/Screens/ConfigurationPage/components/body.dart
Normal file
@ -0,0 +1,294 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:diacritic/diacritic.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SlideFromRouteRight.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SearchBox.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SearchNumberBox.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/section_page.dart';
|
||||
import 'package:mymuseum_visitapp/Services/apiService.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class Body extends StatefulWidget {
|
||||
const Body({Key? key, required this.configuration}) : super(key: key);
|
||||
|
||||
final ConfigurationDTO configuration;
|
||||
|
||||
@override
|
||||
State<Body> createState() => _BodyState();
|
||||
}
|
||||
|
||||
class _BodyState extends State<Body> {
|
||||
late List<SectionDTO> sections;
|
||||
late List<SectionDTO> _allSections;
|
||||
late List<dynamic> rawSections;
|
||||
String? searchValue;
|
||||
int? searchNumberValue;
|
||||
|
||||
final ValueNotifier<List<SectionDTO>> filteredSections = ValueNotifier([]);
|
||||
|
||||
late Future<List<SectionDTO>> _futureSections;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||
_futureSections = getSections(appContext);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
Size size = MediaQuery.of(context).size;
|
||||
|
||||
Color? primaryColor = widget.configuration.primaryColor != null ? Color(int.parse(widget.configuration.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : null;
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
top: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
Hero(
|
||||
tag: widget.configuration.id!,
|
||||
child: 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: widget.configuration.imageSource == null ? 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,
|
||||
|
||||
],
|
||||
) : null,
|
||||
image: widget.configuration.imageSource != null ? DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
opacity: 0.65,
|
||||
image: NetworkImage(
|
||||
widget.configuration.imageSource!,
|
||||
),
|
||||
): null,
|
||||
),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: size.height * 0.11,
|
||||
width: size.width,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned(
|
||||
top: 35,
|
||||
left: 10,
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
//setState(() {
|
||||
/**/
|
||||
Navigator.of(context).pop();
|
||||
visitAppContext.configuration = null;
|
||||
visitAppContext.isScanningBeacons = false;
|
||||
/*Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(
|
||||
builder: (context) => const HomePage3(),
|
||||
),(route) => false);*/
|
||||
//});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.arrow_back, size: 23, color: Colors.white)
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
SearchBox(onChanged: (value) {
|
||||
searchValue = value?.trim();
|
||||
applyFilters(visitAppContext);
|
||||
}),
|
||||
Expanded(
|
||||
child: SearchNumberBox(onChanged: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
searchNumberValue = int.tryParse(value);
|
||||
} else {
|
||||
searchNumberValue = null;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
applyFilters(visitAppContext);
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
//const SizedBox(height: kDefaultPadding / 2),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
// Our background
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _futureSections,
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
/*print("SECTIONTODISPA");
|
||||
print(sectionsToDisplay);*/
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 0),
|
||||
child: RefreshIndicator(
|
||||
color: kMainColor,
|
||||
onRefresh: () async {
|
||||
if(!widget.configuration.isOffline!) {
|
||||
setState(() {
|
||||
_futureSections = getSections(appContext);
|
||||
});
|
||||
} },
|
||||
child: ValueListenableBuilder<List<SectionDTO>>(
|
||||
valueListenable: filteredSections,
|
||||
builder: (context, value, child) {
|
||||
return ListView.builder(
|
||||
itemCount: value.length,
|
||||
itemBuilder: (context, index) => SectionCard(
|
||||
configuration: widget.configuration,
|
||||
itemCount: value.length,
|
||||
itemIndex: index,
|
||||
sectionDTO: value[index],
|
||||
press: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
SlideFromRightRoute(page: SectionPage(
|
||||
configuration: widget.configuration,
|
||||
rawSection: rawSections[index],
|
||||
visitAppContextIn: appContext.getContext(),
|
||||
sectionId: value[index].id!,
|
||||
)),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
)
|
||||
),
|
||||
);
|
||||
} else if (snapshot.connectionState == ConnectionState.none) {
|
||||
return Text(TranslationHelper.getFromLocale("noData", appContext.getContext()));
|
||||
} else {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: size.height * 0.15,
|
||||
child: const LoadingCommon()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<SectionDTO>> getSections(AppContext appContext) async {
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
if(widget.configuration.isOffline!)
|
||||
{
|
||||
// OFFLINE
|
||||
sections = List<SectionDTO>.from(await DatabaseHelper.instance.getData(DatabaseTableType.sections));
|
||||
}
|
||||
else
|
||||
{
|
||||
// ONLINE
|
||||
List<dynamic>? sectionsDownloaded = await ApiService.getAllSections(visitAppContext.clientAPI, widget.configuration.id!);
|
||||
rawSections = jsonDecode(jsonEncode(sectionsDownloaded));
|
||||
var rawToSection = jsonDecode(jsonEncode(rawSections)).map((json) => SectionDTO.fromJson(json)).toList();
|
||||
List<SectionDTO> sectionList = rawToSection.whereType<SectionDTO>().toList();
|
||||
visitAppContext.currentSections = rawSections;
|
||||
|
||||
//print(sectionsDownloaded);
|
||||
if(sectionList.isNotEmpty) {
|
||||
sections = sectionList.toList();
|
||||
//print(sections);
|
||||
}
|
||||
}
|
||||
|
||||
sections = sections.where((s) => s.configurationId == widget.configuration.id!).toList();
|
||||
sections.sort((a,b) => a.order!.compareTo(b.order!));
|
||||
|
||||
_allSections = sections;
|
||||
applyFilters(visitAppContext);
|
||||
|
||||
return _allSections;
|
||||
}
|
||||
|
||||
void applyFilters(VisitAppContext visitAppContext) {
|
||||
List<SectionDTO> result = _allSections;
|
||||
|
||||
if (searchValue != null && searchValue!.isNotEmpty) {
|
||||
result = result.where((s) =>
|
||||
removeDiacritics(TranslationHelper.get(s.title, visitAppContext).toLowerCase())
|
||||
.contains(removeDiacritics(searchValue!.toLowerCase()))
|
||||
).toList();
|
||||
} else if (searchNumberValue != null) {
|
||||
result = result.where((s) => s.order! + 1 == searchNumberValue).toList();
|
||||
}
|
||||
|
||||
filteredSections.value = result;
|
||||
}
|
||||
|
||||
}
|
||||
@ -2,7 +2,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
@ -16,12 +16,14 @@ import 'package:provider/provider.dart';
|
||||
class SectionCard extends StatelessWidget {
|
||||
const SectionCard({
|
||||
Key? key,
|
||||
required this.configuration,
|
||||
required this.itemIndex,
|
||||
required this.itemCount,
|
||||
required this.sectionDTO,
|
||||
required this.press,
|
||||
}) : super(key: key);
|
||||
|
||||
final ConfigurationDTO configuration;
|
||||
final int itemIndex;
|
||||
final int itemCount;
|
||||
final SectionDTO sectionDTO;
|
||||
@ -33,7 +35,7 @@ class SectionCard extends StatelessWidget {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
bool isOffline = (appContext.getContext() as VisitAppContext).configuration!.isOffline!;
|
||||
bool isOffline = configuration.isOffline! ?? false;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(
|
||||
@ -69,7 +71,7 @@ class SectionCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if(sectionDTO.imageId != null)
|
||||
if(sectionDTO.imageId != null ) // && (appContext.getContext() as VisitAppContext).configuration != null
|
||||
// section main image
|
||||
Positioned(
|
||||
top: kDefaultPadding +4.0,
|
||||
@ -82,7 +84,7 @@ class SectionCard extends StatelessWidget {
|
||||
// image is square but we add extra 20 + 20 padding thats why width is 200
|
||||
width: size.width*0.5,
|
||||
child: FutureBuilder(
|
||||
future: ApiService.getResource(appContext, (appContext.getContext() as VisitAppContext).configuration!, sectionDTO.imageId!),
|
||||
future: ApiService.getResource(appContext, configuration, sectionDTO.imageId!),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return ClipRRect(
|
||||
@ -142,10 +144,6 @@ class SectionCard extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: kDefaultPadding),
|
||||
child: HtmlWidget(TranslationHelper.get(sectionDTO.title, appContext.getContext()))
|
||||
/*Text(
|
||||
TranslationHelper.get(sectionDTO.title, appContext.getContext()),
|
||||
style: Theme.of(context).textTheme.button,
|
||||
)*/,
|
||||
),
|
||||
// it use the available space
|
||||
const Spacer(),
|
||||
@ -171,6 +169,16 @@ class SectionCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const Positioned(
|
||||
right: -4,
|
||||
top: (136/2)-18,
|
||||
bottom: 25,
|
||||
child: SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: Icon(Icons.chevron_right, size: 15, color: Colors.white)
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -3,19 +3,21 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
// TODO //import 'package:flutter_beacon/flutter_beacon.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/AssistantChatSheet.dart';
|
||||
import 'package:mymuseum_visitapp/Components/CustomAppBar.dart';
|
||||
import 'package:mymuseum_visitapp/Components/ScannerBouton.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Article/article_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Quizz/quizz_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Visit/beaconArticleFound.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/ConfigurationPage/beaconArticleFound.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Article/article_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Quiz/quizz_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/section_page.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
@ -23,18 +25,18 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import 'components/body.dart';
|
||||
|
||||
class VisitPage extends StatefulWidget {
|
||||
const VisitPage({Key? key, required this.configurationId, required this.isAlreadyAllowed}) : super(key: key);
|
||||
class ConfigurationPage extends StatefulWidget {
|
||||
const ConfigurationPage({Key? key,required this.configuration, required this.isAlreadyAllowed}) : super(key: key);
|
||||
|
||||
final String configurationId;
|
||||
final ConfigurationDTO configuration;
|
||||
final bool isAlreadyAllowed;
|
||||
|
||||
@override
|
||||
State<VisitPage> createState() => _VisitPageState();
|
||||
State<ConfigurationPage> createState() => _ConfigurationPageState();
|
||||
}
|
||||
|
||||
class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
ConfigurationDTO? configuration;
|
||||
class _ConfigurationPageState extends State<ConfigurationPage> with WidgetsBindingObserver {
|
||||
//ConfigurationDTO? configuration;
|
||||
|
||||
int timeBetweenBeaconPopUp = 20000; // 20 sec
|
||||
int meterToBeacon = 100; // 15 meters
|
||||
@ -42,15 +44,15 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
|
||||
// Beacon specific
|
||||
final controller = Get.find<RequirementStateController>();
|
||||
StreamSubscription<BluetoothState>? _streamBluetooth;
|
||||
StreamSubscription<RangingResult>? _streamRanging;
|
||||
/*StreamSubscription<BluetoothState>? _streamBluetooth;
|
||||
StreamSubscription<RangingResult>? _streamRanging;*/
|
||||
/*final _regionBeacons = <Region, List<Beacon>>{};
|
||||
final _beacons = <Beacon>[];*/
|
||||
bool _isDialogShowing = false;
|
||||
DateTime? lastTimePopUpWasClosed;
|
||||
//bool _isArticleOpened = false;
|
||||
StreamSubscription? listener;
|
||||
final List<Region> regions = <Region>[];
|
||||
//final List<Region> regions = <Region>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -58,13 +60,13 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
|
||||
if (Platform.isIOS) {
|
||||
// iOS platform, at least set identifier and proximityUUID for region scanning
|
||||
regions.add(Region(
|
||||
/*regions.add(Region(
|
||||
identifier: 'MyMuseumB',
|
||||
proximityUUID: 'FDA50693-A4E2-4FB1-AFCF-C6EB07647825')
|
||||
);
|
||||
);*/
|
||||
} else {
|
||||
// Android platform, it can ranging out of beacon that filter all of Proximity UUID
|
||||
regions.add(Region(identifier: 'MyMuseumB'));
|
||||
//regions.add(Region(identifier: 'MyMuseumB'));
|
||||
}
|
||||
|
||||
super.initState();
|
||||
@ -72,21 +74,29 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
if(widget.isAlreadyAllowed) {
|
||||
listeningState();
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
visitAppContext.configuration = widget.configuration;
|
||||
visitAppContext.sectionIds = widget.configuration.sectionIds;
|
||||
appContext.setContext(visitAppContext);
|
||||
});
|
||||
//listeningState();
|
||||
}
|
||||
|
||||
listeningState() async {
|
||||
print('Listening to bluetooth state');
|
||||
_streamBluetooth = flutterBeacon
|
||||
/*_streamBluetooth = flutterBeacon
|
||||
.bluetoothStateChanged()
|
||||
.listen((BluetoothState state) async {
|
||||
controller.updateBluetoothState(state);
|
||||
await checkAllRequirements();
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
||||
checkAllRequirements() async {
|
||||
final bluetoothState = await flutterBeacon.bluetoothState;
|
||||
/*final bluetoothState = await flutterBeacon.bluetoothState;
|
||||
controller.updateBluetoothState(bluetoothState);
|
||||
print('BLUETOOTH $bluetoothState');
|
||||
|
||||
@ -96,7 +106,7 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
|
||||
final locationServiceEnabled = await flutterBeacon.checkLocationServicesIfEnabled;
|
||||
controller.updateLocationService(locationServiceEnabled);
|
||||
print('LOCATION SERVICE $locationServiceEnabled');
|
||||
print('LOCATION SERVICE $locationServiceEnabled');*/
|
||||
|
||||
var status = await Permission.bluetoothScan.status;
|
||||
|
||||
@ -114,7 +124,7 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
print(statuses[Permission.bluetoothConnect]);
|
||||
print(status);
|
||||
|
||||
if (controller.bluetoothEnabled &&
|
||||
/*if (controller.bluetoothEnabled &&
|
||||
controller.authorizationStatusOk &&
|
||||
controller.locationServiceEnabled) {
|
||||
print('STATE READY');
|
||||
@ -134,13 +144,13 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
} else {
|
||||
print('STATE NOT READY');
|
||||
controller.pauseScanning();
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
initScanBeacon(VisitAppContext visitAppContext) async {
|
||||
|
||||
await flutterBeacon.initializeScanning;
|
||||
if (!controller.authorizationStatusOk ||
|
||||
//await flutterBeacon.initializeScanning;
|
||||
/*if (!controller.authorizationStatusOk ||
|
||||
!controller.locationServiceEnabled ||
|
||||
!controller.bluetoothEnabled) {
|
||||
print(
|
||||
@ -148,16 +158,16 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
'locationServiceEnabled=${controller.locationServiceEnabled}, '
|
||||
'bluetoothEnabled=${controller.bluetoothEnabled}');
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
|
||||
if (_streamRanging != null) {
|
||||
/*if (_streamRanging != null) {
|
||||
if (_streamRanging!.isPaused) {
|
||||
_streamRanging?.resume();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
_streamRanging =
|
||||
/*_streamRanging =
|
||||
flutterBeacon.ranging(regions).listen((RangingResult result) {
|
||||
//print(result);
|
||||
if (mounted) {
|
||||
@ -220,7 +230,7 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
//});
|
||||
}
|
||||
});
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
/*pauseScanBeacon() async {
|
||||
@ -250,14 +260,14 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) async {
|
||||
print('AppLifecycleState = $state');
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
if (_streamBluetooth != null) {
|
||||
/*if (_streamBluetooth != null) {
|
||||
if (_streamBluetooth!.isPaused) {
|
||||
_streamBluetooth?.resume();
|
||||
}
|
||||
}
|
||||
await checkAllRequirements();
|
||||
await checkAllRequirements();*/
|
||||
} else if (state == AppLifecycleState.paused) {
|
||||
_streamBluetooth?.pause();
|
||||
//_streamBluetooth?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,33 +297,17 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
//visitAppContext.isArticleCurrentlyShown = true;
|
||||
lastTimePopUpWasClosed = DateTime.now();
|
||||
|
||||
switch(beaconSection!.sectionType!) {
|
||||
case SectionType.Article:
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ArticlePage(
|
||||
builder: (context) => SectionPage(
|
||||
configuration: widget.configuration,
|
||||
rawSection: null,
|
||||
visitAppContextIn: visitAppContext,
|
||||
articleId: beaconSection.sectionId!,
|
||||
sectionId: beaconSection!.sectionId!,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case SectionType.Quizz:
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => QuizzPage(
|
||||
visitAppContextIn: visitAppContext,
|
||||
sectionId: beaconSection.sectionId!,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// TODO HANDLE, SHOW NOT SUPPORTED
|
||||
break;
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
@ -337,7 +331,7 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
configuration = visitAppContext.configuration;
|
||||
//configuration = visitAppContext.configuration;
|
||||
|
||||
listener = controller.startStream.listen((flag) async {
|
||||
print(flag);
|
||||
@ -348,19 +342,50 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
canPop: true,
|
||||
child: Scaffold(
|
||||
appBar: CustomAppBar(
|
||||
/*appBar: CustomAppBar(
|
||||
title: TranslationHelper.get(configuration!.title, visitAppContext),
|
||||
isHomeButton: true,
|
||||
),
|
||||
),*/
|
||||
backgroundColor: kBackgroundGrey,
|
||||
body: Body(configurationId: configuration!.id), // TODO handle error..
|
||||
body: Body(configuration: widget.configuration),
|
||||
floatingActionButton: Stack(
|
||||
children: [
|
||||
if (visitAppContext.applicationInstanceDTO?.isAssistant == true)
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 90, bottom: 1),
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'assistant_config',
|
||||
backgroundColor: kMainColor1,
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => AssistantChatSheet(
|
||||
visitAppContext: visitAppContext,
|
||||
configurationId: widget.configuration.id,
|
||||
onNavigateToSection: (sectionId, sectionTitle) {
|
||||
Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) => SectionPage(
|
||||
configuration: widget.configuration,
|
||||
rawSection: null,
|
||||
visitAppContextIn: visitAppContext,
|
||||
sectionId: sectionId,
|
||||
),
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.chat_bubble_outline, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
visitAppContext.beaconSections != null && visitAppContext.beaconSections!.where((bs) => bs!.configurationId == visitAppContext.configuration!.id).isNotEmpty ? Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
@ -369,7 +394,7 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
onTap: () async {
|
||||
bool isCancel = false;
|
||||
|
||||
if(!controller.authorizationStatusOk) {
|
||||
/*if(!controller.authorizationStatusOk) {
|
||||
//await handleOpenLocationSettings();
|
||||
|
||||
await showDialog(
|
||||
@ -478,12 +503,12 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
}
|
||||
if(!visitAppContext.isScanningBeacons) {
|
||||
print("Start Scan");
|
||||
print(_streamRanging);
|
||||
/*print(_streamRanging);
|
||||
if (_streamRanging != null) {
|
||||
_streamRanging?.resume();
|
||||
} else {
|
||||
await initScanBeacon(visitAppContext);
|
||||
}
|
||||
}*/
|
||||
|
||||
controller.startScanning();
|
||||
visitAppContext.isScanningBeacons = true;
|
||||
@ -495,7 +520,7 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
visitAppContext.isScanningBeacons = false;
|
||||
appContext.setContext(visitAppContext);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
@ -521,7 +546,7 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
|
||||
handleOpenLocationSettings() async {
|
||||
if (Platform.isAndroid) {
|
||||
await flutterBeacon.openLocationSettings;
|
||||
//await flutterBeacon.openLocationSettings;
|
||||
} else if (Platform.isIOS) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
@ -546,7 +571,7 @@ class _VisitPageState extends State<VisitPage> with WidgetsBindingObserver {
|
||||
handleOpenBluetooth() async {
|
||||
if (Platform.isAndroid) {
|
||||
try {
|
||||
await flutterBeacon.openBluetoothSettings;
|
||||
//await flutterBeacon.openBluetoothSettings;
|
||||
} on PlatformException catch (e) {
|
||||
print(e);
|
||||
}
|
||||
@ -3,12 +3,12 @@ import 'dart:convert';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/LanguageSelection.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/Visit/visit.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/ConfigurationPage/configuration_page.dart';
|
||||
import 'package:mymuseum_visitapp/Services/apiService.dart';
|
||||
import 'package:mymuseum_visitapp/Services/downloadConfiguration.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
@ -79,7 +79,7 @@ class _ConfigurationsListState extends State<ConfigurationsList> {
|
||||
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
VisitPage(configurationId: configurations[index].id!, isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed),
|
||||
ConfigurationPage(configuration: configurations[index], isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed),
|
||||
));
|
||||
|
||||
}
|
||||
@ -110,7 +110,7 @@ class _ConfigurationsListState extends State<ConfigurationsList> {
|
||||
|
||||
Navigator.of(context).pushReplacement(MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
VisitPage(configurationId: configurations[index].id!, isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed),
|
||||
ConfigurationPage(configuration: configurations[index], isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,9 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/CustomAppBar.dart';
|
||||
import 'package:mymuseum_visitapp/Components/ScannerBouton.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/modelsHelper.dart';
|
||||
@ -14,14 +15,12 @@ import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Visit/visit.dart';
|
||||
import 'package:mymuseum_visitapp/Services/apiService.dart';
|
||||
import 'package:mymuseum_visitapp/Services/downloadConfiguration.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'configurations_list.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
@ -50,14 +49,21 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
isHomeButton: false,
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
child: FutureBuilder(
|
||||
future: getConfigurationsCall(visitAppContext.clientAPI, appContext),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
configurations = List<ConfigurationDTO>.from(snapshot.data).where((configuration) => configuration.isMobile!).toList();
|
||||
// Filtrer par les configurations liées à l'instance Mobile (appType == Mobile)
|
||||
final mobileConfigIds = visitAppContext.applicationInstanceDTO
|
||||
?.configurations?.map((c) => c.configurationId).toSet() ?? {};
|
||||
configurations = List<ConfigurationDTO>.from(snapshot.data)
|
||||
.where((c) => mobileConfigIds.isEmpty || mobileConfigIds.contains(c.id))
|
||||
.toList();
|
||||
return RefreshIndicator (
|
||||
onRefresh: () {
|
||||
setState(() {});
|
||||
@ -84,9 +90,30 @@ class _HomePageState extends State<HomePage> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
),
|
||||
),
|
||||
/*InkWell(
|
||||
onTap: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return TestAR();
|
||||
//return XRWithQRScannerPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
},
|
||||
child: const SizedBox(
|
||||
height: 50,
|
||||
width: 10,
|
||||
child: Text('TEST XR'),
|
||||
),
|
||||
),*/
|
||||
],
|
||||
)
|
||||
),
|
||||
//floatingActionButton: ScannerBouton(appContext: appContext),
|
||||
// floatingActionButton: ScannerBouton(appContext: appContext),
|
||||
//floatingActionButtonLocation: FloatingActionButtonLocation.miniCenterFloat,
|
||||
/*bottomNavigationBar: BottomNavigationBar(
|
||||
currentIndex: currentIndex,
|
||||
|
||||
445
lib/Screens/Home/home_3.0.dart
Normal file
445
lib/Screens/Home/home_3.0.dart
Normal file
@ -0,0 +1,445 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/AssistantChatSheet.dart';
|
||||
import 'package:mymuseum_visitapp/Components/CustomAppBar.dart';
|
||||
import 'package:mymuseum_visitapp/Components/LanguageSelection.dart';
|
||||
import 'package:mymuseum_visitapp/Components/ScannerBouton.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/modelsHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/networkCheck.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/requirement_state_controller.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/beaconSection.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/ConfigurationPage/configuration_page.dart';
|
||||
import 'package:mymuseum_visitapp/Services/apiService.dart';
|
||||
import 'package:mymuseum_visitapp/Services/downloadConfiguration.dart';
|
||||
import 'package:mymuseum_visitapp/Services/statisticsService.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class HomePage3 extends StatefulWidget {
|
||||
const HomePage3({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<HomePage3> createState() => _HomePage3State();
|
||||
}
|
||||
|
||||
class _HomePage3State extends State<HomePage3> with WidgetsBindingObserver {
|
||||
int currentIndex = 0;
|
||||
|
||||
late List<ConfigurationDTO> configurations = [];
|
||||
List<String?> alreadyDownloaded = [];
|
||||
late VisitAppContext visitAppContext;
|
||||
|
||||
late Future<List<ConfigurationDTO>?> _futureConfigurations;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
super.initState();
|
||||
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||
_futureConfigurations = getConfigurationsCall(appContext);
|
||||
}
|
||||
|
||||
Widget _buildCard(BuildContext context, int index) {
|
||||
final lang = visitAppContext.language ?? "FR";
|
||||
final config = configurations[index];
|
||||
final titleEntry = config.title?.firstWhere(
|
||||
(t) => t.language == lang,
|
||||
orElse: () => config.title!.first,
|
||||
);
|
||||
final cleanedTitle = (titleEntry?.value ?? '').replaceAll('\n', ' ').replaceAll('<br>', ' ');
|
||||
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ConfigurationPage(
|
||||
configuration: config,
|
||||
isAlreadyAllowed: visitAppContext.isScanBeaconAlreadyAllowed,
|
||||
),
|
||||
));
|
||||
},
|
||||
child: Hero(
|
||||
tag: config.id!,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: kSecondGrey,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black38,
|
||||
blurRadius: 6,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
image: config.imageSource != null
|
||||
? DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: NetworkImage(config.imageSource!),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Gradient overlay for text readability
|
||||
Positioned.fill(
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withValues(alpha: 0.72),
|
||||
],
|
||||
stops: const [0.4, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Title at bottom-left
|
||||
Positioned(
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
right: 28,
|
||||
child: HtmlWidget(
|
||||
cleanedTitle,
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
customStylesBuilder: (_) => {
|
||||
'font-family': 'Roboto',
|
||||
'-webkit-line-clamp': '2',
|
||||
},
|
||||
),
|
||||
),
|
||||
// Chevron
|
||||
const Positioned(
|
||||
bottom: 10,
|
||||
right: 8,
|
||||
child: Icon(Icons.chevron_right, size: 18, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
visitAppContext = appContext.getContext();
|
||||
|
||||
return Scaffold(
|
||||
extendBody: true,
|
||||
body: FutureBuilder(
|
||||
future: _futureConfigurations,
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
final mobileConfigIds = visitAppContext.applicationInstanceDTO
|
||||
?.configurations?.map((c) => c.configurationId).toSet() ?? {};
|
||||
configurations = List<ConfigurationDTO>.from(snapshot.data)
|
||||
.where((c) => mobileConfigIds.isEmpty || mobileConfigIds.contains(c.id))
|
||||
.toList();
|
||||
|
||||
final layoutType = visitAppContext.applicationInstanceDTO?.layoutMainPage;
|
||||
final isMasonry = layoutType == null ||
|
||||
layoutType.value == LayoutMainPageType.MasonryGrid.value;
|
||||
|
||||
final lang = visitAppContext.language ?? "FR";
|
||||
final headerTitleEntry = configurations.isNotEmpty
|
||||
? configurations[0].title?.firstWhere(
|
||||
(t) => t.language == lang,
|
||||
orElse: () => configurations[0].title!.first,
|
||||
)
|
||||
: null;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
// Dark background
|
||||
const ColoredBox(color: Color(0xFF111111), child: SizedBox.expand()),
|
||||
SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverAppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
pinned: false,
|
||||
expandedHeight: 235.0,
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
collapseMode: CollapseMode.pin,
|
||||
centerTitle: true,
|
||||
background: Container(
|
||||
padding: const EdgeInsets.only(bottom: 25.0),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(25),
|
||||
bottomRight: Radius.circular(25),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black38,
|
||||
spreadRadius: 0.5,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(25),
|
||||
bottomRight: Radius.circular(25),
|
||||
),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (configurations.isNotEmpty && configurations[0].imageSource != null)
|
||||
Image.network(
|
||||
configurations[0].imageSource!,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
// Bottom gradient for title readability
|
||||
const DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Colors.transparent, Colors.black54],
|
||||
stops: [0.5, 1.0],
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 35,
|
||||
right: 10,
|
||||
child: SizedBox(
|
||||
width: 75,
|
||||
height: 75,
|
||||
child: LanguageSelection(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
title: SizedBox(
|
||||
width: size.width * 1.0,
|
||||
height: 120,
|
||||
child: Center(
|
||||
child: headerTitleEntry != null
|
||||
? HtmlWidget(
|
||||
headerTitleEntry.value!,
|
||||
textStyle: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
customStylesBuilder: (_) => {
|
||||
'text-align': 'center',
|
||||
'font-family': 'Roboto',
|
||||
'-webkit-line-clamp': '2',
|
||||
},
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8.0, right: 8.0, top: 8.0, bottom: 20.0),
|
||||
sliver: isMasonry
|
||||
? SliverMasonryGrid.count(
|
||||
crossAxisCount: 2,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childCount: configurations.length,
|
||||
itemBuilder: (context, index) => SizedBox(
|
||||
height: 160 + (index % 3) * 50,
|
||||
child: _buildCard(context, index),
|
||||
),
|
||||
)
|
||||
: SliverGrid(
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisSpacing: 12,
|
||||
childAspectRatio: 0.82,
|
||||
),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
_buildCard,
|
||||
childCount: configurations.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (visitAppContext.applicationInstanceDTO?.isAssistant == true)
|
||||
Positioned(
|
||||
bottom: 24,
|
||||
right: 16,
|
||||
child: FloatingActionButton(
|
||||
heroTag: 'assistant_home',
|
||||
backgroundColor: kMainColor1,
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => AssistantChatSheet(
|
||||
visitAppContext: visitAppContext,
|
||||
onNavigateToSection: (configurationId, _) {
|
||||
final config = configurations
|
||||
.where((c) => c.id == configurationId)
|
||||
.firstOrNull;
|
||||
if (config != null) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ConfigurationPage(
|
||||
configuration: config,
|
||||
isAlreadyAllowed:
|
||||
visitAppContext.isScanBeaconAlreadyAllowed,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.chat_bubble_outline, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (snapshot.connectionState == ConnectionState.none) {
|
||||
return Text(TranslationHelper.getFromLocale("noData", appContext.getContext()));
|
||||
} else {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: size.height * 0.15,
|
||||
child: const LoadingCommon(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ConfigurationDTO>?> getConfigurationsCall(AppContext appContext) async {
|
||||
bool isOnline = await hasNetwork();
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
|
||||
|
||||
isOnline = true; // Todo remove if not local test
|
||||
List<ConfigurationDTO>? configurations;
|
||||
configurations = List<ConfigurationDTO>.from(await DatabaseHelper.instance.getData(DatabaseTableType.configurations));
|
||||
alreadyDownloaded = configurations.map((c) => c.id).toList();
|
||||
print("GOT configurations from LOCAL");
|
||||
print(configurations.length);
|
||||
print(configurations);
|
||||
|
||||
if(!isOnline) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(TranslationHelper.getFromLocale("noInternet", appContext.getContext())), backgroundColor: kMainColor2),
|
||||
);
|
||||
|
||||
// GET ALL SECTIONIDS FOR ALL CONFIGURATION (OFFLINE)
|
||||
for(var configuration in configurations)
|
||||
{
|
||||
var sections = List<SectionDTO>.from(await DatabaseHelper.instance.queryWithConfigurationId(DatabaseTableType.sections, configuration.id!));
|
||||
configuration.sectionIds = sections.map((e) => e.id!).toList();
|
||||
}
|
||||
|
||||
// GET BEACONS FROM LOCAL
|
||||
List<BeaconSection> beaconSections = List<BeaconSection>.from(await DatabaseHelper.instance.getData(DatabaseTableType.beaconSection));
|
||||
print("GOT beaconSection from LOCAL");
|
||||
print(beaconSections);
|
||||
|
||||
visitAppContext.beaconSections = beaconSections;
|
||||
//appContext.setContext(visitAppContext);
|
||||
|
||||
return configurations;
|
||||
}
|
||||
|
||||
if(visitAppContext.beaconSections == null) {
|
||||
List<SectionDTO>? sections = await ApiService.getAllBeacons(visitAppContext.clientAPI, visitAppContext.instanceId!);
|
||||
if(sections != null && sections.isNotEmpty) {
|
||||
List<BeaconSection> beaconSections = sections.map((e) => BeaconSection(minorBeaconId: e.beaconId, orderInConfig: e.order, configurationId: e.configurationId, sectionId: e.id, sectionType: e.type)).toList();
|
||||
visitAppContext.beaconSections = beaconSections;
|
||||
|
||||
try {
|
||||
// Clear all before
|
||||
await DatabaseHelper.instance.clearTable(DatabaseTableType.beaconSection);
|
||||
// Store it locally for offline mode
|
||||
for(var beaconSection in beaconSections) {
|
||||
await DatabaseHelper.instance.insert(DatabaseTableType.beaconSection, ModelsHelper.beaconSectionToMap(beaconSection));
|
||||
}
|
||||
print("STORE beaconSection DONE");
|
||||
} catch(e) {
|
||||
print("Issue during beaconSection insertion");
|
||||
print(e);
|
||||
}
|
||||
|
||||
print("Got some Beacons for you");
|
||||
print(beaconSections);
|
||||
appContext.setContext(visitAppContext);
|
||||
}
|
||||
}
|
||||
|
||||
// Charge l'ApplicationInstance Mobile pour savoir si l'assistant/statistiques sont activés
|
||||
if (visitAppContext.applicationInstanceDTO == null && visitAppContext.instanceId != null) {
|
||||
try {
|
||||
final instances = await visitAppContext.clientAPI.applicationInstanceApi!
|
||||
.applicationInstanceGet(instanceId: visitAppContext.instanceId);
|
||||
final mobileInstance = instances?.where((e) => e.appType == AppType.Mobile).firstOrNull;
|
||||
if (mobileInstance != null) {
|
||||
visitAppContext.applicationInstanceDTO = mobileInstance;
|
||||
if (mobileInstance.isStatistic ?? false) {
|
||||
visitAppContext.statisticsService = StatisticsService(
|
||||
clientAPI: visitAppContext.clientAPI,
|
||||
instanceId: visitAppContext.instanceId,
|
||||
configurationId: visitAppContext.configuration?.id,
|
||||
appType: 'Mobile',
|
||||
language: visitAppContext.language,
|
||||
);
|
||||
}
|
||||
}
|
||||
appContext.setContext(visitAppContext);
|
||||
} catch (e) {
|
||||
print("Could not load applicationInstance: $e");
|
||||
}
|
||||
}
|
||||
|
||||
return await ApiService.getConfigurations(visitAppContext.clientAPI, visitAppContext);
|
||||
}
|
||||
}
|
||||
@ -1,443 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
//import 'package:confetti/confetti.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/CustomAppBar.dart';
|
||||
import 'package:mymuseum_visitapp/Components/Loading.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Components/rounded_button.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/ResponseSubDTO.dart';
|
||||
import 'package:mymuseum_visitapp/Models/articleRead.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
//import 'package:mymuseum_visitapp/Screens/Quizz/drawPath.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Quizz/questions_list.dart';
|
||||
//import 'package:mymuseum_visitapp/Screens/Quizz/showResponses.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class QuizzPage extends StatefulWidget {
|
||||
const QuizzPage({Key? key, required this.visitAppContextIn, required this.sectionId}) : super(key: key);
|
||||
|
||||
final String sectionId;
|
||||
final VisitAppContext visitAppContextIn;
|
||||
|
||||
@override
|
||||
State<QuizzPage> createState() => _QuizzPageState();
|
||||
}
|
||||
|
||||
class _QuizzPageState extends State<QuizzPage> {
|
||||
SectionDTO? sectionDTO;
|
||||
List<ResourceModel?> resourcesModel = <ResourceModel?>[];
|
||||
ResourceModel? audioResourceModel;
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
late Uint8List audiobytes;
|
||||
late VisitAppContext visitAppContext;
|
||||
|
||||
QuizzDTO? quizzDTO;
|
||||
List<QuestionSubDTO> _questionsSubDTO = <QuestionSubDTO>[];
|
||||
//ConfettiController? _controllerCenter;
|
||||
int currentIndex = 1;
|
||||
bool showResult = false;
|
||||
bool showResponses = false;
|
||||
|
||||
bool kIsWeb = false;
|
||||
bool isResultPage = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.visitAppContextIn.isContentCurrentlyShown = true;
|
||||
|
||||
//_controllerCenter = ConfettiController(duration: const Duration(seconds: 10));
|
||||
//_controllerCenter!.play();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
visitAppContext.isContentCurrentlyShown = false;
|
||||
currentIndex = 1;
|
||||
//_controllerCenter!.dispose();
|
||||
if(quizzDTO != null) {
|
||||
if(quizzDTO!.questions != null) {
|
||||
_questionsSubDTO = QuestionSubDTO().fromJSON(quizzDTO!.questions!);
|
||||
}
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
Size size = MediaQuery.of(context).size;
|
||||
visitAppContext = appContext.getContext();
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: CustomAppBar(
|
||||
title: sectionDTO != null ? TranslationHelper.get(sectionDTO!.title, visitAppContext!) : "",
|
||||
isHomeButton: false,
|
||||
),
|
||||
body: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
return FutureBuilder(
|
||||
future: getQuizz(appContext, visitAppContext.clientAPI, widget.sectionId), // MAYBE MOVE THAT TO PARENT ..
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if(quizzDTO != null && sectionDTO != null) {
|
||||
|
||||
if(showResult) {
|
||||
var goodResponses = 0;
|
||||
for (var question in _questionsSubDTO) {
|
||||
if(question.chosen == question.responsesSubDTO!.indexWhere((response) => response.isGood!)) {
|
||||
goodResponses +=1;
|
||||
}
|
||||
}
|
||||
log("goodResponses =" + goodResponses.toString());
|
||||
LevelDTO levelToShow = LevelDTO();
|
||||
var test = goodResponses/quizzDTO!.questions!.length;
|
||||
|
||||
if((0 == test || test < 0.25) && quizzDTO!.badLevel != null) {
|
||||
levelToShow = quizzDTO!.badLevel!;
|
||||
}
|
||||
if((test>=0.25 && test < 0.5) && quizzDTO!.mediumLevel != null) {
|
||||
levelToShow = quizzDTO!.mediumLevel!;
|
||||
}
|
||||
if((test>=0.5 && test < 0.75) && quizzDTO!.goodLevel != null) {
|
||||
levelToShow = quizzDTO!.goodLevel!;
|
||||
}
|
||||
if((test>=0.75 && test <= 1) && quizzDTO!.greatLevel != null) {
|
||||
levelToShow = quizzDTO!.greatLevel!;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
/*Center(
|
||||
child: SizedBox(
|
||||
width: 5,
|
||||
height: 5,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _controllerCenter!,
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
shouldLoop: false, // start again as soon as the animation is finished
|
||||
colors: const [
|
||||
kMainColor,
|
||||
kSecondColor,
|
||||
kConfigurationColor,
|
||||
kMainColor1
|
||||
//Colors.pink,
|
||||
//Colors.orange,
|
||||
//Colors.purple
|
||||
], // manually specify the colors to be used
|
||||
createParticlePath: drawPath, // define a custom shape/path.
|
||||
),
|
||||
),
|
||||
),*/
|
||||
if (orientation == Orientation.portrait)
|
||||
Column(
|
||||
children: [
|
||||
if (!showResponses && levelToShow.label!.firstWhere((label) => label.language == visitAppContext!.language).resourceUrl != null) // TODO SUPPORT OTHER THAN IMAGES
|
||||
resultImage(visitAppContext!, size, levelToShow, orientation),
|
||||
if(!showResponses)
|
||||
// TEXT BOX WITH MAIN SCORE
|
||||
Text('$goodResponses/${quizzDTO!.questions!.length}', textAlign: TextAlign.center, style: TextStyle(fontSize: kIsWeb ? (showResponses ? 60 : 100) : 75, color: kBackgroundSecondGrey)),
|
||||
],
|
||||
),
|
||||
|
||||
if (orientation == Orientation.landscape)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if(!showResponses)
|
||||
// TEXT BOX WITH MAIN SCORE
|
||||
Text('$goodResponses/${quizzDTO!.questions!.length}', textAlign: TextAlign.center, style: TextStyle(fontSize: kIsWeb ? (showResponses ? 60 : 100) : 75, color: kBackgroundSecondGrey)),
|
||||
if (!showResponses && levelToShow.label!.firstWhere((label) => label.language == visitAppContext!.language).resourceUrl != null)
|
||||
resultImage(visitAppContext!, size, levelToShow, orientation),
|
||||
],
|
||||
),
|
||||
|
||||
if(!showResponses)
|
||||
// TEXT BOX WITH LEVEL TEXT RESULT
|
||||
resultText(size, levelToShow, appContext),
|
||||
if(showResponses)
|
||||
QuestionsListWidget(
|
||||
questionsSubDTO: _questionsSubDTO,
|
||||
isShowResponse: true,
|
||||
onShowResponse: () {},
|
||||
orientation: orientation,
|
||||
),
|
||||
// RESPONSE BOX
|
||||
//ShowReponsesWidget(questionsSubDTO: _questionsSubDTO),
|
||||
|
||||
if(orientation == Orientation.portrait && !showResponses)
|
||||
// Buttons
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: resultButtons(size, orientation, visitAppContext!),
|
||||
),
|
||||
if(orientation == Orientation.landscape && !showResponses)
|
||||
// Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: resultButtons(size, orientation, visitAppContext!),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
} else {
|
||||
return QuestionsListWidget(
|
||||
isShowResponse: false,
|
||||
questionsSubDTO: _questionsSubDTO,
|
||||
onShowResponse: () {
|
||||
setState(() {
|
||||
showResult = true;
|
||||
});
|
||||
},
|
||||
orientation: orientation,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const LoadingCommon();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
),
|
||||
floatingActionButton: showResponses ? FloatingActionButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showResult = false;
|
||||
showResponses = false;
|
||||
currentIndex = 1;
|
||||
_questionsSubDTO = QuestionSubDTO().fromJSON(quizzDTO!.questions!);
|
||||
});
|
||||
},
|
||||
backgroundColor: kBackgroundSecondGrey,
|
||||
child: const Icon(Icons.undo),
|
||||
) : null,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.miniEndFloat,
|
||||
);
|
||||
}
|
||||
|
||||
Future<QuizzDTO?> getQuizz(AppContext appContext, Client client, String sectionId) async {
|
||||
try {
|
||||
if(sectionDTO == null || quizzDTO == null) {
|
||||
bool isConfigOffline = (appContext.getContext() as VisitAppContext).configuration!.isOffline!;
|
||||
if(isConfigOffline)
|
||||
{
|
||||
// OFFLINE
|
||||
List<Map<String, dynamic>> sectionTest = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.sections, sectionId);
|
||||
if(sectionTest.isNotEmpty) {
|
||||
sectionDTO = DatabaseHelper.instance.getSectionFromDB(sectionTest.first);
|
||||
try {
|
||||
SectionRead sectionRead = SectionRead(id: sectionDTO!.id!, readTime: DateTime.now().millisecondsSinceEpoch);
|
||||
await DatabaseHelper.instance.insert(DatabaseTableType.articleRead, sectionRead.toMap());
|
||||
visitAppContext!.readSections.add(sectionRead);
|
||||
|
||||
appContext.setContext(visitAppContext!);
|
||||
} catch (e) {
|
||||
print("DATABASE ERROR SECTIONREAD");
|
||||
print(e);
|
||||
}
|
||||
} else {
|
||||
print("EMPTY SECTION");
|
||||
}
|
||||
} else
|
||||
{
|
||||
// ONLINE
|
||||
SectionDTO? sectionOnline = await client.sectionApi!.sectionGetDetail(sectionId);
|
||||
if(sectionOnline != null) {
|
||||
sectionDTO = sectionOnline;
|
||||
} else {
|
||||
print("EMPTY SECTION");
|
||||
}
|
||||
}
|
||||
|
||||
if(sectionDTO!.type == SectionType.Quizz) {
|
||||
quizzDTO = QuizzDTO.fromJson(jsonDecode(sectionDTO!.data!));
|
||||
}
|
||||
if(quizzDTO != null) {
|
||||
quizzDTO!.questions!.sort((a, b) => a.order!.compareTo(b.order!));
|
||||
_questionsSubDTO = QuestionSubDTO().fromJSON(quizzDTO!.questions!);
|
||||
if(quizzDTO!.questions != null && quizzDTO!.questions!.isNotEmpty) {
|
||||
quizzDTO!.questions!.sort((a, b) => a.order!.compareTo(b.order!));
|
||||
for (var question in quizzDTO!.questions!) {
|
||||
if(isConfigOffline)
|
||||
{
|
||||
// OFFLINE
|
||||
if(question.imageBackgroundResourceId != null) {
|
||||
List<Map<String, dynamic>> ressourceQuizz = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.resources, question.imageBackgroundResourceId!);
|
||||
if(ressourceQuizz.isNotEmpty) {
|
||||
resourcesModel.add(DatabaseHelper.instance.getResourceFromDB(ressourceQuizz.first));
|
||||
} else {
|
||||
print("EMPTY resourcesModel - second");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ONLINE
|
||||
if(question.imageBackgroundResourceId != null) {
|
||||
resourcesModel.add(ResourceModel(id: question.imageBackgroundResourceId, source: question.imageBackgroundResourceUrl, type: ResourceType.Image));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
//print(sectionDTO!.title);
|
||||
});
|
||||
} else {
|
||||
return null; // TODO return local list..
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
print("IN CATCH");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
resultImage(VisitAppContext visitAppContext, Size size, LevelDTO levelToShow, Orientation orientation) {
|
||||
return Container(
|
||||
//height: size.height * 0.2,
|
||||
//width: size.width * 0.25,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: size.height * 0.25,
|
||||
maxWidth: kIsWeb ? size.width * 0.20 : orientation == Orientation.portrait ? size.width * 0.85 : size.width * 0.4,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
image: levelToShow.label!.where((label) => label.language == visitAppContext.language).isNotEmpty ? DecorationImage(
|
||||
fit: BoxFit.contain,
|
||||
opacity: 0.85,
|
||||
image: NetworkImage(
|
||||
levelToShow.label!.firstWhere((label) => label.language == visitAppContext.language).resourceUrl!,
|
||||
),
|
||||
): null,
|
||||
borderRadius: const BorderRadius.all( Radius.circular(50.0)),
|
||||
border: Border.all(
|
||||
color: kBackgroundGrey,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
//borderRadius: BorderRadius.all(Radius.circular(25.0)),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff7c94b6),
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(
|
||||
levelToShow.label!.firstWhere((label) => label.language == visitAppContext.language).resourceUrl!, // TODO REDUNDANCY here??
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: const BorderRadius.all( Radius.circular(50.0)),
|
||||
border: Border.all(
|
||||
color: kBackgroundGrey,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
resultText(Size size, LevelDTO levelToShow, AppContext appContext) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Container(
|
||||
width: size.width *0.75,
|
||||
height: kIsWeb ? (showResponses ? size.height *0.10 : size.height *0.20) : size.height *0.25,
|
||||
decoration: BoxDecoration(
|
||||
color: kBackgroundLight, //kBackgroundLight
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: kBackgroundSecondGrey,
|
||||
spreadRadius: 0.3,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: Text(TranslationHelper.getWithResource(levelToShow.label, appContext.getContext() as VisitAppContext), textAlign: TextAlign.center, style: TextStyle(fontSize: kIsWeb ? kDescriptionSize : kDescriptionSize)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
resultButtons(Size size, Orientation orientation, VisitAppContext visitAppContext) {
|
||||
return [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: SizedBox(
|
||||
height: kIsWeb ? 50 : 40,
|
||||
width: orientation == Orientation.portrait ? size.width * 0.6 : size.width * 0.35,
|
||||
child: RoundedButton(
|
||||
text: TranslationHelper.getFromLocale("restart", visitAppContext),
|
||||
color: kBackgroundSecondGrey,
|
||||
textColor: kBackgroundLight,
|
||||
icon: Icons.undo,
|
||||
press: () {
|
||||
setState(() {
|
||||
showResult = false;
|
||||
showResponses = false;
|
||||
currentIndex = 1;
|
||||
_questionsSubDTO = QuestionSubDTO().fromJSON(quizzDTO!.questions!);
|
||||
});
|
||||
},
|
||||
fontSize: 18,
|
||||
horizontal: 20,
|
||||
vertical: 5
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: SizedBox(
|
||||
height: kIsWeb ? 50 : 40,
|
||||
width: orientation == Orientation.portrait ? size.width * 0.6 : size.width * 0.35,
|
||||
child: RoundedButton(
|
||||
text: TranslationHelper.getFromLocale("responses", visitAppContext),
|
||||
color: kBackgroundSecondGrey,
|
||||
textColor: kBackgroundLight,
|
||||
icon: Icons.assignment_turned_in,
|
||||
press: () {
|
||||
setState(() {
|
||||
showResponses = true;
|
||||
});
|
||||
},
|
||||
fontSize: 18,
|
||||
horizontal: 20,
|
||||
vertical: 5
|
||||
),
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
203
lib/Screens/Sections/Agenda/agenda_page.dart
Normal file
203
lib/Screens/Sections/Agenda/agenda_page.dart
Normal file
@ -0,0 +1,203 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
//import 'dart:html';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Models/agenda.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Agenda/event_list_item.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Agenda/event_popup.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Agenda/month_filter.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
||||
class AgendaPage extends StatefulWidget {
|
||||
final AgendaDTO section;
|
||||
AgendaPage({required this.section});
|
||||
|
||||
@override
|
||||
_AgendaPage createState() => _AgendaPage();
|
||||
}
|
||||
|
||||
class _AgendaPage extends State<AgendaPage> {
|
||||
AgendaDTO agendaDTO = AgendaDTO();
|
||||
late Agenda agenda;
|
||||
late ValueNotifier<List<EventAgenda>> filteredAgenda = ValueNotifier<List<EventAgenda>>([]);
|
||||
late Uint8List mapIcon;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
/*print(widget.section!.data);
|
||||
agendaDTO = AgendaDTO.fromJson(jsonDecode(widget.section!.data!))!;
|
||||
print(agendaDTO);*/
|
||||
agendaDTO = widget.section;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<Agenda?> getAndParseJsonInfo(VisitAppContext visitAppContext) async {
|
||||
try {
|
||||
// Récupération du contenu JSON depuis l'URL
|
||||
var httpClient = HttpClient();
|
||||
|
||||
// We need to get detail to get url from resourceId
|
||||
var resourceIdForSelectedLanguage = agendaDTO.resourceIds!.where((ri) => ri.language == visitAppContext.language).first.value;
|
||||
ResourceDTO? resourceDTO = await visitAppContext.clientAPI.resourceApi!.resourceGetDetail(resourceIdForSelectedLanguage!);
|
||||
|
||||
var request = await httpClient.getUrl(Uri.parse(resourceDTO!.url!));
|
||||
var response = await request.close();
|
||||
var jsonString = await response.transform(utf8.decoder).join();
|
||||
|
||||
agenda = Agenda.fromJson(jsonString);
|
||||
agenda.events = agenda.events.where((a) => a.dateFrom != null && a.dateFrom!.isAfter(DateTime.now())).toList();
|
||||
agenda.events.sort((a, b) => a.dateFrom!.compareTo(b.dateFrom!));
|
||||
filteredAgenda.value = agenda.events;
|
||||
|
||||
mapIcon = await getByteIcon();
|
||||
|
||||
return agenda;
|
||||
} catch(e) {
|
||||
print("Erreur lors du parsing du json : ${e.toString()}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getByteIcon() async {
|
||||
final ByteData bytes = await rootBundle.load('assets/icons/marker.png');
|
||||
var icon = await getBytesFromAsset(bytes, 25);
|
||||
return icon;
|
||||
}
|
||||
|
||||
Future<Uint8List> getBytesFromAsset(ByteData data, int width) async {
|
||||
//ByteData data = await rootBundle.load(path);
|
||||
ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(),
|
||||
targetWidth: width);
|
||||
ui.FrameInfo fi = await codec.getNextFrame();
|
||||
return (await fi.image.toByteData(format: ui.ImageByteFormat.png))
|
||||
!.buffer
|
||||
.asUint8List();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final VisitAppContext visitAppContext = Provider.of<AppContext>(context).getContext();
|
||||
|
||||
return FutureBuilder(future: getAndParseJsonInfo(visitAppContext),
|
||||
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return Center(
|
||||
child: Text("Le fichier choisi n'est pas valide")
|
||||
);
|
||||
} else {
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
color: kBackgroundLight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, bottom: 2.0, top: 2.0),
|
||||
child: ValueListenableBuilder<List<EventAgenda>>(
|
||||
valueListenable: filteredAgenda,
|
||||
builder: (context, value, _) {
|
||||
return GridView.builder(
|
||||
scrollDirection: Axis.vertical, // Changer pour horizontal si nécessaire
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2, // Nombre de colonnes dans la grid
|
||||
crossAxisSpacing: 2.0, // Espace entre les colonnes
|
||||
mainAxisSpacing: 2.0, // Espace entre les lignes
|
||||
childAspectRatio: 0.65, // Aspect ratio des enfants de la grid
|
||||
),
|
||||
itemCount: value.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
EventAgenda eventAgenda = value[index];
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
print("${eventAgenda.name}");
|
||||
(Provider.of<AppContext>(context, listen: false).getContext() as VisitAppContext)
|
||||
.statisticsService?.track(
|
||||
VisitEventType.agendaEventTap,
|
||||
metadata: {'eventId': eventAgenda.name, 'eventTitle': eventAgenda.name},
|
||||
);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return EventPopup(eventAgenda: eventAgenda, mapProvider: agendaDTO.agendaMapProvider ?? MapProvider.Google, mapIcon: mapIcon);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: EventListItem(
|
||||
eventAgenda: eventAgenda,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: MonthFilter(
|
||||
events: snapshot.data.events,
|
||||
onMonthSelected: (filteredList) {
|
||||
print('events sélectionné: $filteredList');
|
||||
var result = filteredList != null ? filteredList : <EventAgenda>[];
|
||||
result.sort((a, b) => a.dateFrom!.compareTo(b.dateFrom!));
|
||||
filteredAgenda.value = result;
|
||||
}),
|
||||
),
|
||||
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)
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
} else if (snapshot.connectionState == ConnectionState.none) {
|
||||
return Text("No data");
|
||||
} else {
|
||||
return Center(
|
||||
child: Container(
|
||||
height: size.height * 0.2,
|
||||
child: LoadingCommon()
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
} //_webView
|
||||
163
lib/Screens/Sections/Agenda/event_list_item.dart
Normal file
163
lib/Screens/Sections/Agenda/event_list_item.dart
Normal file
@ -0,0 +1,163 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Models/agenda.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:intl/intl.dart';
|
||||
|
||||
class EventListItem extends StatelessWidget {
|
||||
final EventAgenda eventAgenda;
|
||||
|
||||
EventListItem({super.key, required this.eventAgenda});
|
||||
|
||||
final DateFormat formatter = DateFormat('dd/MM/yyyy');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
var primaryColor = visitAppContext.configuration != null ? visitAppContext.configuration!.primaryColor != null ? new Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : kSecondColor : kSecondColor;
|
||||
|
||||
Size size = MediaQuery
|
||||
.of(context)
|
||||
.size;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(10.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0),
|
||||
//color: Colors.red,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Colors.black26,
|
||||
offset: Offset(0.0, 2.0),
|
||||
blurRadius: 6.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
//width: size.width * 0.5, //210.0,
|
||||
//constraints: const BoxConstraints(maxWidth: 210, maxHeight: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
height: size.height * 0.18, // must be same ref0
|
||||
constraints: const BoxConstraints(maxHeight: 250),
|
||||
width: size.width*1,
|
||||
child: Stack(
|
||||
children: [
|
||||
eventAgenda.image != null ? ClipRRect(
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0), topRight: Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
child: Container(
|
||||
//color: Colors.green,
|
||||
//constraints: const BoxConstraints(maxHeight: 175),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: eventAgenda.image!,
|
||||
width: size.width,
|
||||
height: size.height * 0.2, // must be same ref0
|
||||
fit: BoxFit.cover,
|
||||
progressIndicatorBuilder: (context, url, downloadProgress) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: LoadingCommon(),
|
||||
),
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) => Icon(Icons.error),
|
||||
)
|
||||
),
|
||||
): SizedBox(),
|
||||
Positioned(
|
||||
right: 0.0,
|
||||
bottom: 0.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 10.0,
|
||||
right: 10.0,
|
||||
top: 2.0,
|
||||
bottom: 2.0),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
Icons.calendar_today_rounded,
|
||||
size: 10.0,
|
||||
color: kBackgroundColor,
|
||||
),
|
||||
const SizedBox(width: 5.0),
|
||||
Text(
|
||||
eventAgenda.dateFrom!.isAtSameMomentAs(eventAgenda.dateTo!) ? "${formatter.format(eventAgenda.dateFrom!)}": "${formatter.format(eventAgenda.dateFrom!)} - ${formatter.format(eventAgenda.dateTo!)}",
|
||||
style: TextStyle(
|
||||
color: kBackgroundColor,
|
||||
fontSize: 12
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
/*height: size.height * 0.13,
|
||||
constraints: BoxConstraints(maxHeight: 120),*/
|
||||
child: Container(
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(bottomLeft: Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0), bottomRight: Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
border: Border(top: BorderSide(width: 0.1, color: kMainGrey))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
HtmlWidget(
|
||||
'<div style="text-align: center;">${eventAgenda.name!.length > 75 ? eventAgenda.name!.substring(0, 75) + " ..." : eventAgenda.name}</div>',
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'center', 'font-family': "Roboto"};
|
||||
},
|
||||
textStyle: TextStyle(fontSize: 14.0),
|
||||
),
|
||||
/*AutoSizeText(
|
||||
eventAgenda.type!,
|
||||
maxFontSize: 12.0,
|
||||
style: TextStyle(
|
||||
fontSize: 10.0,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),*/
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
475
lib/Screens/Sections/Agenda/event_popup.dart
Normal file
475
lib/Screens/Sections/Agenda/event_popup.dart
Normal file
@ -0,0 +1,475 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' as mapBox;
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/video_viewer_youtube.dart';
|
||||
import 'package:mymuseum_visitapp/Models/agenda.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:intl/intl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class EventPopup extends StatefulWidget {
|
||||
final EventAgenda eventAgenda;
|
||||
final MapProvider mapProvider;
|
||||
final Uint8List mapIcon;
|
||||
|
||||
EventPopup({Key? key, required this.eventAgenda, required this.mapProvider, required this.mapIcon}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<EventPopup> createState() => _EventPopupState();
|
||||
}
|
||||
|
||||
class _EventPopupState extends State<EventPopup> {
|
||||
final DateFormat formatter = DateFormat('dd/MM/yyyy hh:mm');
|
||||
Completer<GoogleMapController> _controller = Completer();
|
||||
Set<Marker> markers = {};
|
||||
bool init = false;
|
||||
|
||||
mapBox.MapboxMap? mapboxMap;
|
||||
mapBox.PointAnnotationManager? pointAnnotationManager;
|
||||
|
||||
Set<Marker> getMarkers() {
|
||||
markers = {};
|
||||
|
||||
if (widget.eventAgenda.address!.lat != null && widget.eventAgenda.address!.lng != null) {
|
||||
markers.add(Marker(
|
||||
draggable: false,
|
||||
markerId: MarkerId(widget.eventAgenda.address!.lat.toString() +
|
||||
widget.eventAgenda.address!.lng.toString()),
|
||||
position: LatLng(
|
||||
double.parse(widget.eventAgenda.address!.lat!.toString()),
|
||||
double.parse(widget.eventAgenda.address!.lng!.toString()),
|
||||
),
|
||||
icon: BitmapDescriptor.defaultMarker,
|
||||
infoWindow: InfoWindow.noText));
|
||||
}
|
||||
return markers;
|
||||
}
|
||||
|
||||
_onMapCreated(mapBox.MapboxMap mapboxMap, Uint8List icon) {
|
||||
this.mapboxMap = mapboxMap;
|
||||
|
||||
mapboxMap.annotations.createPointAnnotationManager().then((pointAnnotationManager) async {
|
||||
this.pointAnnotationManager = pointAnnotationManager;
|
||||
pointAnnotationManager.createMulti(createPoints(LatLng(double.parse(widget.eventAgenda.address!.lat!.toString()), double.parse(widget.eventAgenda.address!.lng!.toString())), icon));
|
||||
init = true;
|
||||
});
|
||||
}
|
||||
|
||||
createPoints(LatLng position, Uint8List icon) {
|
||||
var options = <mapBox.PointAnnotationOptions>[];
|
||||
options.add(mapBox.PointAnnotationOptions(
|
||||
geometry: mapBox.Point(
|
||||
coordinates: mapBox.Position(
|
||||
position.longitude,
|
||||
position.latitude,
|
||||
)), // .toJson()
|
||||
iconSize: 1.3,
|
||||
iconOffset: [0.0, 0.0],
|
||||
symbolSortKey: 10,
|
||||
iconColor: 0,
|
||||
iconImage: null,
|
||||
image: icon, //widget.selectedMarkerIcon,
|
||||
));
|
||||
print(options.length);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
Future<void> openEmail(String email) async {
|
||||
final Uri emailLaunchUri = Uri(
|
||||
scheme: 'mailto',
|
||||
path: email,
|
||||
);
|
||||
|
||||
try {
|
||||
await launchUrl(emailLaunchUri, mode: LaunchMode.externalApplication);
|
||||
} catch (e) {
|
||||
print('Erreur lors de l\'ouverture de l\'email: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openPhone(String phone) async {
|
||||
final Uri phoneLaunchUri = Uri(
|
||||
scheme: 'tel',
|
||||
path: phone,
|
||||
);
|
||||
|
||||
try {
|
||||
await launchUrl(phoneLaunchUri, mode: LaunchMode.externalApplication);
|
||||
} catch (e) {
|
||||
print('Erreur lors de l\'ouverture de l\'email: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
|
||||
var dateToShow = widget.eventAgenda.dateFrom!.isAtSameMomentAs(widget.eventAgenda.dateTo!) ? "${formatter.format(widget.eventAgenda.dateFrom!)}": "${formatter.format(widget.eventAgenda.dateFrom!)} - ${formatter.format(widget.eventAgenda.dateTo!)}";
|
||||
Size size = MediaQuery.of(context).size;
|
||||
|
||||
if(!init) {
|
||||
print("getmarkers in build");
|
||||
getMarkers();
|
||||
init = true;
|
||||
}
|
||||
|
||||
return Dialog(
|
||||
insetPadding: const EdgeInsets.all(4.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0))
|
||||
),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
color: kBackgroundColor,
|
||||
),
|
||||
height: size.height * 0.92,
|
||||
width: size.width * 0.95,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0), topRight: Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
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: [
|
||||
kMainColor0,
|
||||
kMainColor1,
|
||||
kMainColor2,
|
||||
],
|
||||
),
|
||||
border: const Border(right: BorderSide(width: 0.05, color: kMainGrey)),
|
||||
color: Colors.grey,
|
||||
image: widget.eventAgenda.image != null ? DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
opacity: 0.4,
|
||||
image: NetworkImage(
|
||||
widget.eventAgenda.image!,
|
||||
),
|
||||
): null
|
||||
),
|
||||
width: size.width,
|
||||
height: 125,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 10, top: 4),
|
||||
child: Center(
|
||||
child: HtmlWidget(
|
||||
'<div style="text-align: center;">${widget.eventAgenda.name!}</div>',
|
||||
textStyle: const TextStyle(fontSize: 20.0, color: Colors.white),
|
||||
customStylesBuilder: (element)
|
||||
{
|
||||
return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"};
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 125),
|
||||
child: SingleChildScrollView(
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: kSecondColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Center(
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 6.0,
|
||||
children: [
|
||||
const Icon(Icons.calendar_today_rounded, color: kSecondColor, size: 18),
|
||||
Text(
|
||||
dateToShow,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: kSecondColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
constraints: BoxConstraints(minHeight: 250, maxHeight: size.height * 0.38),
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: kBackgroundLight,
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 10, bottom: 10, top: 15),
|
||||
child: Scrollbar(
|
||||
thumbVisibility: true,
|
||||
thickness: 2.0,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
HtmlWidget(
|
||||
widget.eventAgenda.description!,
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'left', 'font-family': "Roboto"};
|
||||
},
|
||||
textStyle: const TextStyle(fontSize: 15.0),
|
||||
),
|
||||
widget.eventAgenda.idVideoYoutube != null && widget.eventAgenda.idVideoYoutube!.isNotEmpty ?
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SizedBox(
|
||||
height: 250,
|
||||
width: 350,
|
||||
child: VideoViewerYoutube(videoUrl: "https://www.youtube.com/watch?v=${widget.eventAgenda.idVideoYoutube}", isAuto: false, webView: true)
|
||||
),
|
||||
) :
|
||||
SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
widget.eventAgenda.address!.lat != null && widget.eventAgenda.address!.lng != null ?
|
||||
SizedBox(
|
||||
width: size.width,
|
||||
height: size.height * 0.2,
|
||||
child: widget.mapProvider == MapProvider.Google ?
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
child: GoogleMap(
|
||||
mapToolbarEnabled: false,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: LatLng(double.parse(widget.eventAgenda.address!.lat!.toString()), double.parse(widget.eventAgenda.address!.lng!.toString())),
|
||||
zoom: 14,
|
||||
),
|
||||
onMapCreated: (GoogleMapController controller) {
|
||||
_controller.complete(controller);
|
||||
},
|
||||
markers: markers,
|
||||
),
|
||||
) :
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
child: mapBox.MapWidget(
|
||||
key: ValueKey("mapBoxWidget"),
|
||||
styleUri: mapBox.MapboxStyles.STANDARD,
|
||||
onMapCreated: (maBoxMap) {
|
||||
_onMapCreated(maBoxMap, widget.mapIcon);
|
||||
},
|
||||
cameraOptions: mapBox.CameraOptions(
|
||||
center: mapBox.Point(coordinates: mapBox.Position(double.parse(widget.eventAgenda.address!.lng!.toString()), double.parse(widget.eventAgenda.address!.lat!.toString()))), // .toJson()
|
||||
zoom: 14
|
||||
),
|
||||
),
|
||||
),
|
||||
): SizedBox(),
|
||||
widget.eventAgenda.address?.address != null &&
|
||||
widget.eventAgenda.address!.address!.isNotEmpty
|
||||
? GestureDetector(
|
||||
onTap: () async {
|
||||
final query = Uri.encodeComponent(widget.eventAgenda.address!.address!);
|
||||
final googleMapsUrl = Uri.parse("https://www.google.com/maps/search/?api=1&query=$query");
|
||||
|
||||
if (await canLaunchUrl(googleMapsUrl)) {
|
||||
await launchUrl(googleMapsUrl, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
print("Impossible d'ouvrir Google Maps");
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: size.width,
|
||||
height: 60,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.location_on, size: 13, color: kMainColor),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: SizedBox(
|
||||
width: size.width * 0.7,
|
||||
child: AutoSizeText(
|
||||
textAlign: TextAlign.center,
|
||||
widget.eventAgenda.address!.address!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: kMainColor,
|
||||
//decoration: TextDecoration.underline,
|
||||
),
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
widget.eventAgenda.phone != null && widget.eventAgenda.phone!.isNotEmpty
|
||||
? SizedBox(
|
||||
width: size.width,
|
||||
height: 35,
|
||||
child: InkWell(
|
||||
onTap: () => openPhone(widget.eventAgenda.phone!),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.phone, size: 13, color: kMainColor),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(widget.eventAgenda.phone!, style: TextStyle(fontSize: 12, color: kMainColor)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
widget.eventAgenda.email != null && widget.eventAgenda.email!.isNotEmpty
|
||||
? SizedBox(
|
||||
width: size.width,
|
||||
height: 35,
|
||||
child: InkWell(
|
||||
onTap: () => openEmail(widget.eventAgenda.email!),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.email, size: 13, color: kMainColor),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: AutoSizeText(
|
||||
widget.eventAgenda.email!,
|
||||
style: TextStyle(fontSize: 12, color: kMainColor),
|
||||
maxLines: 3
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: SizedBox(),
|
||||
widget.eventAgenda.website != null && widget.eventAgenda.website!.isNotEmpty
|
||||
? GestureDetector(
|
||||
onTap: () async {
|
||||
final url = Uri.parse(widget.eventAgenda.website!);
|
||||
if (await canLaunchUrl(url)) {
|
||||
await launchUrl(url, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
// Optionnel : afficher une erreur
|
||||
print('Impossible d\'ouvrir le lien');
|
||||
}
|
||||
},
|
||||
child: SizedBox(
|
||||
width: size.width * 0.8,
|
||||
height: 35,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.public, size: 13, color: kMainColor),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: AutoSizeText(
|
||||
textAlign: TextAlign.center,
|
||||
widget.eventAgenda.website!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: kMainColor,
|
||||
//decoration: TextDecoration.underline,
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 4.5,
|
||||
top: 4.5,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: kMainColor.withValues(alpha: 0.55),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 25,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
207
lib/Screens/Sections/Agenda/month_filter.dart
Normal file
207
lib/Screens/Sections/Agenda/month_filter.dart
Normal file
@ -0,0 +1,207 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/agenda.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';
|
||||
|
||||
class MonthFilter extends StatefulWidget {
|
||||
final List<EventAgenda> events;
|
||||
final Function(List<EventAgenda>?) onMonthSelected;
|
||||
|
||||
MonthFilter({required this.events, required this.onMonthSelected});
|
||||
|
||||
@override
|
||||
_MonthFilterState createState() => _MonthFilterState();
|
||||
}
|
||||
|
||||
class _MonthFilterState extends State<MonthFilter> with SingleTickerProviderStateMixin {
|
||||
String? _selectedMonth;
|
||||
bool _isExpanded = false;
|
||||
bool _showContent = false;
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _widthAnimation;
|
||||
|
||||
Map<String, List<String>> monthNames = {
|
||||
'fr': ['', 'Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin', 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'],
|
||||
'en': ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
||||
'de': ['', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'],
|
||||
'nl': ['', 'Januari', 'Februari', 'Maart', 'April', 'Mei', 'Juni', 'Juli', 'Augustus', 'September', 'Oktober', 'November', 'December'],
|
||||
'it': ['', 'Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'],
|
||||
'es': ['', 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
|
||||
'pl': ['', 'Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'],
|
||||
'cn': ['', '一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'],
|
||||
'uk': ['', 'Січень', 'Лютий', 'Березень', 'Квітень', 'Травень', 'Червень', 'Липень', 'Серпень', 'Вересень', 'Жовтень', 'Листопад', 'Грудень'],
|
||||
'ar': ['', 'يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو', 'يوليو', 'أغسطس', 'سبتمبر', 'أكتوبر', 'نوفمبر', 'ديسمبر'],
|
||||
};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
|
||||
_widthAnimation = Tween<double>(begin: 40, end: 265).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
void toggleExpand() {
|
||||
setState(() {
|
||||
if (_isExpanded) {
|
||||
_showContent = false;
|
||||
_isExpanded = false;
|
||||
} else {
|
||||
_isExpanded = true;
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (_isExpanded) {
|
||||
setState(() => _showContent = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
_isExpanded ? _controller.forward() : _controller.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
var primaryColor = visitAppContext.configuration?.primaryColor != null
|
||||
? Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16))
|
||||
: kSecondColor;
|
||||
|
||||
double rounded = visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0;
|
||||
|
||||
List<Map<String, dynamic>> sortedMonths = _getSortedMonths();
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _widthAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: _widthAnimation.value,
|
||||
height: _isExpanded ? 350 : 75,
|
||||
decoration: BoxDecoration(
|
||||
color: _isExpanded ? primaryColor.withValues(alpha: 0.9) : primaryColor.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(rounded),
|
||||
bottomRight: Radius.circular(rounded),
|
||||
),
|
||||
),
|
||||
child: _showContent
|
||||
? Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: toggleExpand,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: sortedMonths.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) return _buildAllItem(appContext, primaryColor);
|
||||
final monthYear = sortedMonths[index - 1]['monthYear'];
|
||||
final filteredEvents = _filterEvents(monthYear);
|
||||
final nbrEvents = filteredEvents.length;
|
||||
if (nbrEvents == 0) return const SizedBox.shrink();
|
||||
|
||||
String monthName = _getTranslatedMonthName(visitAppContext, monthYear);
|
||||
String year = RegExp(r'\d{4}').stringMatch(monthYear)!;
|
||||
|
||||
bool isSelected = _selectedMonth == monthYear;
|
||||
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'$monthName $year ($nbrEvents)',
|
||||
style: TextStyle(
|
||||
fontSize: 15.0,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? Colors.white : Colors.black,
|
||||
),
|
||||
textAlign: TextAlign.center
|
||||
),
|
||||
tileColor: isSelected ? primaryColor.withValues(alpha: 0.6) : null,
|
||||
onTap: () {
|
||||
setState(() => _selectedMonth = monthYear);
|
||||
widget.onMonthSelected(filteredEvents);
|
||||
toggleExpand(); // Auto close after tap
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: _isExpanded ? null : IconButton(
|
||||
icon: const Icon(Icons.menu, color: Colors.white),
|
||||
onPressed: toggleExpand,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAllItem(AppContext appContext, Color primaryColor) {
|
||||
final totalEvents = widget.events.length;
|
||||
final isSelected = _selectedMonth == null;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'${TranslationHelper.getFromLocale("agenda.all", appContext.getContext())} ($totalEvents)',
|
||||
style: TextStyle(
|
||||
fontSize: 15.0,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? Colors.white : Colors.black,
|
||||
),
|
||||
textAlign: TextAlign.center
|
||||
),
|
||||
tileColor: isSelected ? primaryColor.withValues(alpha: 0.6) : null,
|
||||
onTap: () {
|
||||
setState(() => _selectedMonth = null);
|
||||
widget.onMonthSelected(widget.events);
|
||||
toggleExpand(); // Auto close after tap
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getTranslatedMonthName(VisitAppContext context, String monthYear) {
|
||||
int monthIndex = int.parse(monthYear.split('-')[0]);
|
||||
return monthNames[context.language!.toLowerCase()]![monthIndex];
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _getSortedMonths() {
|
||||
Map<String, int> monthsMap = {};
|
||||
|
||||
for (var event in widget.events) {
|
||||
final key = '${event.dateFrom!.month}-${event.dateFrom!.year}';
|
||||
monthsMap[key] = (monthsMap[key] ?? 0) + 1;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> sorted = monthsMap.entries
|
||||
.map((e) => {'monthYear': e.key, 'totalEvents': e.value})
|
||||
.toList();
|
||||
|
||||
sorted.sort((a, b) {
|
||||
final aParts = a['monthYear'].split('-').map(int.parse).toList();
|
||||
final bParts = b['monthYear'].split('-').map(int.parse).toList();
|
||||
return aParts[1] != bParts[1] ? aParts[1].compareTo(bParts[1]) : aParts[0].compareTo(bParts[0]);
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
List<EventAgenda> _filterEvents(String? monthYear) {
|
||||
if (monthYear == null) return widget.events;
|
||||
|
||||
final parts = monthYear.split('-');
|
||||
final selectedMonth = int.parse(parts[0]);
|
||||
final selectedYear = int.parse(parts[1]);
|
||||
|
||||
return widget.events.where((event) {
|
||||
final date = event.dateFrom!;
|
||||
return date.month == selectedMonth && date.year == selectedYear;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/CustomAppBar.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SliderImages.dart';
|
||||
@ -13,7 +13,7 @@ import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/articleRead.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Article/audio_player.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Article/audio_player.dart';
|
||||
import 'package:mymuseum_visitapp/Services/apiService.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
@ -24,27 +24,31 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'audio_player_floating.dart';
|
||||
|
||||
class ArticlePage extends StatefulWidget {
|
||||
const ArticlePage({Key? key, required this.visitAppContextIn, required this.articleId}) : super(key: key);
|
||||
const ArticlePage({Key? key, required this.visitAppContextIn, required this.articleDTO, required this.resourcesModel, this.mainAudioId}) : super(key: key);
|
||||
|
||||
final String articleId;
|
||||
final ArticleDTO articleDTO;
|
||||
final VisitAppContext visitAppContextIn;
|
||||
final List<ResourceModel?> resourcesModel;
|
||||
final String? mainAudioId;
|
||||
|
||||
@override
|
||||
State<ArticlePage> createState() => _ArticlePageState();
|
||||
}
|
||||
|
||||
class _ArticlePageState extends State<ArticlePage> {
|
||||
SectionDTO? sectionDTO;
|
||||
ArticleDTO? articleDTO;
|
||||
List<ResourceModel?> resourcesModel = <ResourceModel?>[];
|
||||
ResourceModel? audioResourceModel;
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
File? audioFile;
|
||||
late VisitAppContext visitAppContext;
|
||||
late List<ResourceModel?> resourcesModelToShow;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.visitAppContextIn.isContentCurrentlyShown = true;
|
||||
|
||||
audioResourceModel = widget.resourcesModel.firstWhere((r) => r?.id == widget.mainAudioId, orElse: () => null);
|
||||
resourcesModelToShow = widget.resourcesModel.where((r) => r?.id != widget.mainAudioId).toList();
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@ -62,28 +66,136 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
|
||||
visitAppContext = appContext.getContext();
|
||||
|
||||
var title = TranslationHelper.get(widget.articleDTO.title, appContext.getContext());
|
||||
String cleanedTitle = title.replaceAll('\n', ' ').replaceAll('<br>', ' ');
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
appBar: CustomAppBar(
|
||||
title: sectionDTO != null ? TranslationHelper.get(sectionDTO!.title, visitAppContext) : "",
|
||||
isHomeButton: false,
|
||||
isTextSizeButton: true,
|
||||
body: 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
|
||||
),
|
||||
body: FutureBuilder(
|
||||
future: getArticle(appContext, visitAppContext.clientAPI, widget.articleId, false),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if(articleDTO != null && sectionDTO != null) {
|
||||
],
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.centerRight,
|
||||
end: Alignment.centerLeft,
|
||||
colors: [
|
||||
kMainColor0,
|
||||
kMainColor1,
|
||||
kMainColor2,
|
||||
],
|
||||
),
|
||||
image: widget.articleDTO.imageSource != null ? DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
opacity: 0.65,
|
||||
image: NetworkImage(
|
||||
widget.articleDTO.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(
|
||||
right: 10,
|
||||
top: 45,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
visitAppContext.isMaximizeTextSize = !visitAppContext.isMaximizeTextSize;
|
||||
appContext.setContext(visitAppContext);
|
||||
});
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
child: visitAppContext.isMaximizeTextSize ? const Icon(Icons.text_fields, size: 30, color: Colors.white) : const Icon(Icons.format_size, size: 30, color: Colors.white)
|
||||
),
|
||||
),
|
||||
),
|
||||
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(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
if(size.height > size.width) {
|
||||
return Column(
|
||||
children: [
|
||||
if(articleDTO!.isContentTop!)
|
||||
if(widget.articleDTO.isContentTop!)
|
||||
getContent(size, appContext),
|
||||
if(articleDTO!.isContentTop! && resourcesModel.isNotEmpty)
|
||||
getImages(size, articleDTO!.isContentTop!),
|
||||
if(widget.articleDTO.isContentTop! && resourcesModelToShow.isNotEmpty)
|
||||
getImages(size, widget.articleDTO.isContentTop!),
|
||||
|
||||
if(!articleDTO!.isContentTop! && resourcesModel.isNotEmpty)
|
||||
getImages(size, articleDTO!.isContentTop!),
|
||||
if(!articleDTO!.isContentTop!)
|
||||
if(!widget.articleDTO.isContentTop! && resourcesModelToShow.isNotEmpty)
|
||||
getImages(size, widget.articleDTO.isContentTop!),
|
||||
if(!widget.articleDTO.isContentTop!)
|
||||
getContent(size, appContext),
|
||||
|
||||
/*if(audioResourceModel != null)
|
||||
@ -99,14 +211,14 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if(articleDTO!.isContentTop!)
|
||||
if(widget.articleDTO.isContentTop!)
|
||||
getContent(size, appContext),
|
||||
if(articleDTO!.isContentTop! && resourcesModel.isNotEmpty)
|
||||
getImages(size, articleDTO!.isContentTop!),
|
||||
if(widget.articleDTO.isContentTop! && resourcesModelToShow.isNotEmpty)
|
||||
getImages(size, widget.articleDTO.isContentTop!),
|
||||
|
||||
if(!articleDTO!.isContentTop! && resourcesModel.isNotEmpty)
|
||||
getImages(size, articleDTO!.isContentTop!),
|
||||
if(!articleDTO!.isContentTop!)
|
||||
if(!widget.articleDTO.isContentTop! && resourcesModelToShow.isNotEmpty)
|
||||
getImages(size, widget.articleDTO.isContentTop!),
|
||||
if(!widget.articleDTO.isContentTop!)
|
||||
getContent(size, appContext),
|
||||
],
|
||||
),
|
||||
@ -116,19 +228,17 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const LoadingCommon();
|
||||
}
|
||||
}
|
||||
),
|
||||
floatingActionButton: FutureBuilder(
|
||||
future: getArticle(appContext, visitAppContext.clientAPI, widget.articleId, true),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 0, top: 0), //size.height*0.1
|
||||
child: audioResourceModel != null && audioResourceModel!.source != null ? AudioPlayerFloatingContainer(file: audioFile, resourceURl: audioResourceModel!.source!, isAuto: articleDTO!.isReadAudioAuto!) : null,
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(right: 0, top: 0), //size.height*0.1
|
||||
child: audioResourceModel != null && audioResourceModel!.source != null ? AudioPlayerFloatingContainer(file: audioFile, resourceURl: audioResourceModel!.source!, isAuto: widget.articleDTO.isReadAudioAuto!) : null,
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.miniEndFloat, //miniEndTop
|
||||
);
|
||||
@ -149,13 +259,13 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
),
|
||||
color: Colors.white,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
boxShadow: const [kDefaultShadow],
|
||||
),
|
||||
child: SliderImagesWidget(
|
||||
resources: resourcesModel,
|
||||
resources: resourcesModelToShow,
|
||||
height: size.height * 0.29,
|
||||
contentsDTO: articleDTO!.contents!,
|
||||
contentsDTO: widget.articleDTO.contents!,
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -174,13 +284,13 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
),
|
||||
color: Colors.white,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
boxShadow: const [kDefaultShadow],
|
||||
),
|
||||
child: SliderImagesWidget(
|
||||
resources: resourcesModel,
|
||||
resources: resourcesModelToShow,
|
||||
height: size.height * 0.29,
|
||||
contentsDTO: articleDTO!.contents!,
|
||||
contentsDTO: widget.articleDTO.contents!,
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -196,7 +306,7 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
height: size.height * 0.76,
|
||||
//color: Colors.blueAccent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0, bottom: 8.0),
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
@ -205,14 +315,14 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
),
|
||||
color: Colors.white,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
boxShadow: const [kDefaultShadow],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: HtmlWidget(
|
||||
TranslationHelper.get(articleDTO!.content, appContext.getContext()),
|
||||
TranslationHelper.get(widget.articleDTO.content, appContext.getContext()),
|
||||
textStyle: TextStyle(fontSize: (appContext.getContext() as VisitAppContext).isMaximizeTextSize ? kArticleContentBiggerSize : kArticleContentSize),
|
||||
customStylesBuilder: (element)
|
||||
{
|
||||
@ -233,7 +343,7 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
//height: size.height * 0.65,
|
||||
//color: Colors.blueAccent,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0, top: 8.0, bottom: 8.0),
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
@ -242,14 +352,14 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
),
|
||||
color: Colors.white,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
boxShadow: const [kDefaultShadow],
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(12.5),
|
||||
child: HtmlWidget(
|
||||
TranslationHelper.get(articleDTO!.content, appContext.getContext()),
|
||||
TranslationHelper.get(widget.articleDTO.content, appContext.getContext()),
|
||||
//textAlign: TextAlign.left,
|
||||
textStyle: TextStyle(fontSize: (appContext.getContext() as VisitAppContext).isMaximizeTextSize ? kArticleContentBiggerSize : kArticleContentSize, fontFamily: "Arial"),
|
||||
),
|
||||
@ -262,7 +372,7 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ArticleDTO?> getArticle(AppContext appContext, Client client, String articleId, bool isAudio) async {
|
||||
/*Future<ArticleDTO?> getArticle(AppContext appContext, Client client, String articleId, bool isAudio) async {
|
||||
try {
|
||||
if(sectionDTO == null || articleDTO == null) {
|
||||
bool isConfigOffline = (appContext.getContext() as VisitAppContext).configuration!.isOffline!;
|
||||
@ -387,7 +497,7 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
// Not needed as it's in display logic
|
||||
//ResourceModel? resourceImageOnline = await ApiService.downloadImage(client, image);
|
||||
//if(resourceImageOnline != null) {
|
||||
resourcesModel.add(ResourceModel(id: image.resourceId, source: image.resourceUrl, type: ResourceType.Image));
|
||||
resourcesModel.add(ResourceModel(id: image.resourceId, source: image.resource?.url, type: ResourceType.Image));
|
||||
/*} else {
|
||||
print("EMPTY resourcesModel online - audio");
|
||||
}*/
|
||||
@ -407,5 +517,5 @@ class _ArticlePageState extends State<ArticlePage> {
|
||||
print("IN CATCH");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
68
lib/Screens/Sections/Game/correct_overlay.dart
Normal file
68
lib/Screens/Sections/Game/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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
364
lib/Screens/Sections/Game/game_page.dart
Normal file
364
lib/Screens/Sections/Game/game_page.dart
Normal file
@ -0,0 +1,364 @@
|
||||
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/Game/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 GamePage extends StatefulWidget {
|
||||
final GameDTO section;
|
||||
GamePage({required this.section});
|
||||
|
||||
@override
|
||||
_GamePage createState() => _GamePage();
|
||||
}
|
||||
|
||||
class _GamePage extends State<GamePage> {
|
||||
GameDTO gameDTO = GameDTO();
|
||||
|
||||
int allInPlaceCount = 0;
|
||||
bool isFinished = false;
|
||||
DateTime? _gameStartTime;
|
||||
GlobalKey _widgetKey = GlobalKey();
|
||||
Size? realWidgetSize;
|
||||
List<Widget> pieces = [];
|
||||
|
||||
bool isSplittingImage = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//puzzleDTO = PuzzleDTO.fromJson(jsonDecode(widget.section!.data!))!;
|
||||
gameDTO = widget.section;
|
||||
gameDTO.rows = gameDTO.rows ?? 3;
|
||||
gameDTO.cols = gameDTO.cols ?? 3;
|
||||
_gameStartTime = DateTime.now();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
|
||||
print(gameDTO.messageDebut);
|
||||
TranslationAndResourceDTO? messageDebut = gameDTO.messageDebut != null && gameDTO.messageDebut!.isNotEmpty ? gameDTO.messageDebut!.where((message) => message.language!.toUpperCase() == visitAppContext.language!.toUpperCase()).firstOrNull : null;
|
||||
|
||||
//await Future.delayed(const Duration(milliseconds: 50));
|
||||
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
getRealWidgetSize();
|
||||
|
||||
if(gameDTO.puzzleImage != null && gameDTO.puzzleImage!.url != null) {
|
||||
//splitImage(Image.network(puzzleDTO.image!.resourceUrl!));
|
||||
splitImage(CachedNetworkImage(
|
||||
imageUrl: gameDTO.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 < gameDTO.rows!; x++) {
|
||||
for (int y = 0; y < gameDTO.cols!; y++) {
|
||||
setState(() {
|
||||
pieces.add(
|
||||
PuzzlePiece(
|
||||
key: GlobalKey(),
|
||||
image: image,
|
||||
imageSize: imageSize,
|
||||
row: x,
|
||||
col: y,
|
||||
maxRow: gameDTO.rows!,
|
||||
maxCol: gameDTO.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 == gameDTO.rows! * gameDTO.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();
|
||||
final duration = _gameStartTime != null ? DateTime.now().difference(_gameStartTime!).inSeconds : 0;
|
||||
visitAppContext.statisticsService?.track(
|
||||
VisitEventType.gameComplete,
|
||||
metadata: {'gameType': 'Puzzle', 'durationSeconds': duration},
|
||||
);
|
||||
TranslationAndResourceDTO? messageFin = gameDTO.messageFin != null && gameDTO.messageFin!.isNotEmpty ? gameDTO.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()) :
|
||||
gameDTO.puzzleImage == null || gameDTO.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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
95
lib/Screens/Sections/Game/message_dialog.dart
Normal file
95
lib/Screens/Sections/Game/message_dialog.dart
Normal file
@ -0,0 +1,95 @@
|
||||
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.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0))
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if(translationAndResourceDTO.resourceId != null)
|
||||
Container(
|
||||
constraints: BoxConstraints(maxHeight: 250),
|
||||
//color: Colors.cyan,
|
||||
height: size.height *0.45,
|
||||
width: size.width *0.5,
|
||||
child: Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.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(
|
||||
constraints: BoxConstraints(maxHeight: 350),
|
||||
//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
|
||||
);
|
||||
}
|
||||
287
lib/Screens/Sections/Game/puzzle_piece.dart
Normal file
287
lib/Screens/Sections/Game/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/Game/score_widget.dart
Normal file
14
lib/Screens/Sections/Game/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;
|
||||
}
|
||||
173
lib/Screens/Sections/Map/filter_tree.dart
Normal file
173
lib/Screens/Sections/Map/filter_tree.dart
Normal file
@ -0,0 +1,173 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/tree_node.dart';
|
||||
|
||||
class FilterTree extends StatefulWidget {
|
||||
final List<TreeNode> data;
|
||||
final bool selectOneToAll;
|
||||
final Function(TreeNode node, bool checked, int commonId) onChecked;
|
||||
final Function(TreeNode node, int id) onClicked;
|
||||
final Color? textColor;
|
||||
final Color? checkBoxColor;
|
||||
final EdgeInsets? childrenPadding;
|
||||
|
||||
FilterTree(
|
||||
{this.checkBoxColor,
|
||||
this.textColor,
|
||||
this.childrenPadding,
|
||||
required this.onChecked,
|
||||
required this.onClicked,
|
||||
required this.data,
|
||||
required this.selectOneToAll});
|
||||
|
||||
@override
|
||||
_FilterTree createState() => _FilterTree();
|
||||
}
|
||||
|
||||
class _FilterTree extends State<FilterTree> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Map<String, int> map = {};
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _buildNode(widget.data[index], const EdgeInsets.all(0));
|
||||
},
|
||||
itemCount: widget.data.length,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNode(TreeNode node, EdgeInsets childrenPadding) {
|
||||
var nonCheckedIcon = Icon(
|
||||
Icons.check_box_outline_blank,
|
||||
color: widget.checkBoxColor ?? Colors.green,
|
||||
);
|
||||
var checkedIcon = Icon(
|
||||
Icons.check_box,
|
||||
color: widget.checkBoxColor ?? Colors.green,
|
||||
);
|
||||
|
||||
// Si le nœud a des enfants, retourne l'ExpansionTile avec les enfants
|
||||
if (node.children.isNotEmpty) {
|
||||
return Padding(
|
||||
padding: childrenPadding,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
widget.onClicked(node, node.id);
|
||||
},
|
||||
child: ExpansionTile(
|
||||
iconColor: widget.checkBoxColor,
|
||||
collapsedIconColor: widget.checkBoxColor,
|
||||
tilePadding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
||||
initiallyExpanded: node.show,
|
||||
title: Container(
|
||||
margin: const EdgeInsets.only(left: 0),
|
||||
padding: const EdgeInsets.only(left: 0),
|
||||
child: Text(
|
||||
node.title,
|
||||
style: TextStyle(color: widget.textColor ?? Colors.black),
|
||||
),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: node.checked ? checkedIcon : nonCheckedIcon,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_toggleNodeSelection(node, !node.checked);
|
||||
widget.onChecked(node, node.checked, node.id);
|
||||
});
|
||||
},
|
||||
),
|
||||
children: node.children
|
||||
.map((child) => _buildNode(
|
||||
child, widget.childrenPadding ?? const EdgeInsets.all(0)))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Si le nœud n'a pas d'enfants, retourne simplement le titre sans ExpansionTile
|
||||
// avec le même padding que les éléments avec des enfants
|
||||
return Padding(
|
||||
padding: childrenPadding,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
widget.onClicked(node, node.id);
|
||||
},
|
||||
child: ListTile(
|
||||
contentPadding: childrenPadding, // Utilisez le padding des enfants
|
||||
title: Text(
|
||||
node.title,
|
||||
style: TextStyle(color: widget.textColor ?? Colors.black),
|
||||
),
|
||||
leading: IconButton(
|
||||
icon: node.checked ? checkedIcon : nonCheckedIcon,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_toggleNodeSelection(node, !node.checked);
|
||||
widget.onChecked(node, node.checked, node.id);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _toggleNodeSelection(TreeNode node, bool isChecked) {
|
||||
node.checked = isChecked;
|
||||
_updateParentNodes(node, isChecked);
|
||||
for (var child in node.children) {
|
||||
_toggleNodeSelection(child, isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
void _updateParentNodes(TreeNode node, bool checked) {
|
||||
//First Check it has parent node
|
||||
bool canContinue = false;
|
||||
if (node.pid != 0) {
|
||||
canContinue = true;
|
||||
}
|
||||
|
||||
//if it have parent node then check all childrens are checked or non checked
|
||||
bool canCheck = true;
|
||||
if (!checked) {
|
||||
for (int i = 0; i < node.children.length; i++) {
|
||||
if (node.children[i].checked != checked) {
|
||||
canCheck = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canCheck) {
|
||||
node.checked = checked;
|
||||
}
|
||||
|
||||
if (canContinue) {
|
||||
checkEachNode(widget.data, node, checked);
|
||||
}
|
||||
// if all childrens are non checked the check parent node in widget.node if it matches node id then check above parent node
|
||||
}
|
||||
|
||||
checkEachNode(List<TreeNode> checkNode, TreeNode node, bool checked) {
|
||||
bool canStop = false;
|
||||
for (int i = 0; i < checkNode.length; i++) {
|
||||
if (checkNode[i].children.isNotEmpty) {
|
||||
checkEachNode(checkNode[i].children, node, checked);
|
||||
}
|
||||
if (checkNode[i].id == node.pid && !canStop) {
|
||||
canStop = true;
|
||||
_updateParentNodes(checkNode[i], checked);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
146
lib/Screens/Sections/Map/flutter_map_view.dart
Normal file
146
lib/Screens/Sections/Map/flutter_map_view.dart
Normal file
@ -0,0 +1,146 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/map_context.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:latlong2/latlong.dart' as ll;
|
||||
|
||||
class FlutterMapView extends StatefulWidget {
|
||||
final MapDTO? mapDTO;
|
||||
final List<GeoPointDTO> geoPoints;
|
||||
final List<Map<String, dynamic>> icons;
|
||||
final String? language;
|
||||
const FlutterMapView({
|
||||
Key? key,
|
||||
this.mapDTO,
|
||||
required this.geoPoints,
|
||||
required this.icons,
|
||||
this.language,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FlutterMapViewState createState() => _FlutterMapViewState();
|
||||
}
|
||||
|
||||
class _FlutterMapViewState extends State<FlutterMapView> {
|
||||
late List<GeoPointDTO> markersList;
|
||||
late List<Marker> markers;
|
||||
bool filterZoneSelected = false;
|
||||
MapController? mapController;
|
||||
|
||||
List<Marker> createPoints(mapContext) {
|
||||
markersList = [];
|
||||
markers = [];
|
||||
|
||||
int i = 0;
|
||||
widget.geoPoints.forEach((point) {
|
||||
if (point.title!.where((translation) => translation.language == widget.language).isNotEmpty) {
|
||||
var textSansHTML = parse(point.title!.firstWhere((translation) => translation.language == widget.language).value);
|
||||
point.id = i;
|
||||
point.title = point.title!.where((t) => t.language == widget.language).toList();
|
||||
markersList.add(point);
|
||||
|
||||
//var icon = point.categorie == null ? BitmapDescriptor.fromBytes(widget.icons.where((i) => i['id'] == null).first['icon']) : widget.icons.any((i) => i['id'] == point.categorieId) ? BitmapDescriptor.fromBytes(widget.icons.where((i) => i['id'] == point.categorieId).first['icon']) : BitmapDescriptor.fromBytes(widget.icons.where((i) => i['id'] == null).first['icon']); //widget.selectedMarkerIcon,;
|
||||
|
||||
final coords = point.geometry?.type == 'Point' && point.geometry?.coordinates is List ? point.geometry!.coordinates as List : null;
|
||||
if (coords == null || coords.length < 2) return;
|
||||
final lat = (coords[1] as num).toDouble();
|
||||
final lon = (coords[0] as num).toDouble();
|
||||
|
||||
markers.add(
|
||||
Marker(
|
||||
width: 80.0,
|
||||
height: 80.0,
|
||||
point: LatLng(lat, lon),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
/*final mapContext = Provider.of<MapContext>(context, listen: false);
|
||||
mapContext.setSelectedPoint(point);
|
||||
mapContext.setSelectedPointForNavigate(point);*/
|
||||
mapContext.setSelectedPoint(point);
|
||||
//mapContext.setSelectedPointForNavigate(point);
|
||||
},
|
||||
child: widget.icons.firstWhere((i) => i['id'] == point.categorieId, orElse: () => widget.icons.first)['icon'] != null ? Image.memory(widget.icons.firstWhere((i) => i['id'] == point.categorieId, orElse: () => widget.icons.first)['icon']) : Icon(Icons.pin_drop, color: Colors.red),//widget.icons.firstWhere((i) => i['id'] == point.categorieId, orElse: () => widget.icons.first)['icon'],
|
||||
)
|
||||
),
|
||||
);
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapContext = Provider.of<MapContext>(context);
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext;
|
||||
|
||||
markers = createPoints(mapContext);
|
||||
|
||||
if (mapController == null) { // mapContext.getSelectedPointForNavigate() != null
|
||||
/*var geoPoint = mapContext.getSelectedPointForNavigate();
|
||||
var center = LatLng(double.tryParse(geoPoint.latitude!)!, double.tryParse(geoPoint.longitude!)!);*/
|
||||
mapController = MapController();
|
||||
|
||||
//mapController!.move(center, widget.mapDTO!.zoom != null ? widget.mapDTO!.zoom!.toDouble() : 12);
|
||||
mapContext.setSelectedPointForNavigate(null); // Reset after navigation
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: mapController,
|
||||
options: MapOptions(
|
||||
initialCenter: widget.mapDTO!.longitude != null && widget.mapDTO!.latitude != null ? ll.LatLng(double.tryParse(widget.mapDTO!.latitude!)!, double.tryParse(widget.mapDTO!.longitude!)!) : ll.LatLng(4.865105, 50.465503), //.toJson()
|
||||
initialZoom: widget.mapDTO!.zoom != null ? widget.mapDTO!.zoom!.toDouble() : 12,
|
||||
onTap: (Tap, lnt) => {
|
||||
mapContext.setSelectedPointForNavigate(null),
|
||||
mapContext.setSelectedPoint(null),
|
||||
}
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
|
||||
userAgentPackageName: 'be.unov.myinfomate.tablet',
|
||||
),
|
||||
MarkerLayer(
|
||||
markers: markers
|
||||
),
|
||||
],
|
||||
),
|
||||
Container(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Consumer<MapContext>(
|
||||
builder: (context, mapContext, _) {
|
||||
var geoPoint = mapContext.getSelectedPointForNavigate() as GeoPointDTO?;
|
||||
if (geoPoint != null && mapController != null) {
|
||||
final coords = geoPoint.geometry?.type == 'Point' && geoPoint.geometry?.coordinates is List ? geoPoint.geometry!.coordinates as List : null;
|
||||
if (coords != null && coords.length >= 2) {
|
||||
final lat = (coords[1] as num).toDouble();
|
||||
final lon = (coords[0] as num).toDouble();
|
||||
print("COUCOU IL FAUT NAVIGATE FLUTTER MAP");
|
||||
mapController!.move(LatLng(lat, lon), mapController!.camera.zoom);
|
||||
}
|
||||
}
|
||||
return SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
409
lib/Screens/Sections/Map/geo_point_filter.dart
Normal file
409
lib/Screens/Sections/Map/geo_point_filter.dart
Normal file
@ -0,0 +1,409 @@
|
||||
import 'dart:ui';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/filter_tree.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/tree_node.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
import 'package:diacritic/diacritic.dart';
|
||||
|
||||
import 'map_context.dart';
|
||||
|
||||
class GeoPointFilter extends StatefulWidget {
|
||||
final String language;
|
||||
final List<GeoPointDTO> geoPoints;
|
||||
final List<CategorieDTO> categories;
|
||||
final Function(List<GeoPointDTO>?) filteredPoints;
|
||||
final MapProvider provider;
|
||||
|
||||
GeoPointFilter({required this.language, required this.geoPoints, required this.categories, required this.filteredPoints, required this.provider});
|
||||
|
||||
@override
|
||||
_GeoPointFilterState createState() => _GeoPointFilterState();
|
||||
}
|
||||
|
||||
class _GeoPointFilterState extends State<GeoPointFilter> with SingleTickerProviderStateMixin {
|
||||
List<GeoPointDTO> selectedGeoPoints = [];
|
||||
late List<TreeNode> _filteredNodes;
|
||||
late ValueNotifier<String> _searchTextNotifier;
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
FocusNode focusNode = FocusNode();
|
||||
bool _isExpanded = false;
|
||||
bool _showContent = false;
|
||||
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _widthAnimation;
|
||||
late Size screenSize;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
screenSize = MediaQuery.of(context).size;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchTextNotifier = ValueNotifier<String>('');
|
||||
|
||||
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
|
||||
_widthAnimation = Tween<double>(begin: 40, end: 350).animate(
|
||||
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_filteredNodes = buildTreeNodes(widget.categories, widget.geoPoints);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _toggleExpansion() {
|
||||
setState(() {
|
||||
if (_isExpanded) {
|
||||
_showContent = false;
|
||||
_isExpanded = false;
|
||||
} else {
|
||||
_isExpanded = true;
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (_isExpanded) {
|
||||
setState(() => _showContent = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
_isExpanded ? _controller.forward() : _controller.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
List<TreeNode> buildTreeNodes(List<CategorieDTO> categories, List<GeoPointDTO> geoPoints) {
|
||||
List<TreeNode> nodes = [];
|
||||
|
||||
// Pour chaque point sans categorie, créer un noeud
|
||||
for(var pointWithoutCat in geoPoints.where((gp) => gp.categorieId == null))
|
||||
{
|
||||
if(pointWithoutCat.title!.where((l) => l.language == widget.language).firstOrNull != null) {
|
||||
// Extraire lat/lon depuis la géométrie GeoJSON (Point → [longitude, latitude])
|
||||
final coordsNocat = pointWithoutCat.geometry?.type == 'Point'
|
||||
? pointWithoutCat.geometry!.coordinates as List<dynamic>?
|
||||
: null;
|
||||
final latNocat = coordsNocat != null && coordsNocat.length >= 2 ? (coordsNocat[1] as num).toDouble().toString() : '';
|
||||
final lonNocat = coordsNocat != null && coordsNocat.length >= 2 ? (coordsNocat[0] as num).toDouble().toString() : '';
|
||||
TreeNode nodeWithoutCat = TreeNode(
|
||||
id: 000 + int.parse(
|
||||
latNocat.substring(0, min(latNocat.length, 10)).replaceAll(".", "").replaceAll("-","") + lonNocat.substring(0, min(lonNocat.length, 10)).replaceAll(".", "").replaceAll("-","")
|
||||
),
|
||||
title: parse(pointWithoutCat.title!.firstWhere((l) => l.language == widget.language).value!).documentElement!.text,
|
||||
children: [],
|
||||
checked: true, // default true
|
||||
show: false,
|
||||
pid: 0,
|
||||
commonID: 0,
|
||||
);
|
||||
nodes.add(nodeWithoutCat);
|
||||
}
|
||||
}
|
||||
|
||||
// Pour chaque catégorie, créez un nœud parent
|
||||
for (var category in categories) {
|
||||
if(category.label!.where((l) => l.language == widget.language).firstOrNull != null)
|
||||
{
|
||||
TreeNode categoryNode = TreeNode(
|
||||
id: 100 + (category.id ?? 0),
|
||||
title: parse(category.label!.firstWhere((l) => l.language == widget.language).value!).documentElement!.text,
|
||||
children: [],
|
||||
checked: true, // default true
|
||||
show: false,
|
||||
pid: 0,
|
||||
commonID: 0,
|
||||
);
|
||||
|
||||
// Ajoutez les géopoints correspondant à cette catégorie en tant qu'enfants du nœud parent
|
||||
for (var geoPoint in geoPoints.where((gp) => gp.categorieId != null)) {
|
||||
if (geoPoint.categorieId == category.id && geoPoint.title!.where((l) => l.language == widget.language).firstOrNull != null) {
|
||||
final lat = _getPointLatLon(geoPoint, true);
|
||||
final lon = _getPointLatLon(geoPoint, false);
|
||||
TreeNode geoPointNode = TreeNode(
|
||||
id: 000 + int.parse(
|
||||
lat.substring(0, min(lat.length, 10)).replaceAll(".", "").replaceAll("-", "") + lon.substring(0, min(lon.length, 10)).replaceAll(".", "").replaceAll("-", "")
|
||||
),
|
||||
title: parse(geoPoint.title!.firstWhere((l) => l.language == widget.language).value!).documentElement!.text,
|
||||
checked: true, // default true
|
||||
show: false,
|
||||
children: [],
|
||||
pid: 0,
|
||||
commonID: 0,
|
||||
);
|
||||
categoryNode.children.add(geoPointNode);
|
||||
}
|
||||
}
|
||||
|
||||
nodes.add(categoryNode);
|
||||
}
|
||||
}
|
||||
|
||||
nodes.sort((a, b) => a.title.compareTo(b.title));
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
void filterNodes() {
|
||||
String searchText = _searchController.text;
|
||||
setState(() {
|
||||
_filteredNodes = searchText.isEmpty
|
||||
? buildTreeNodes(widget.categories, widget.geoPoints)
|
||||
: _filterNodesBySearchText(searchText);
|
||||
|
||||
// if unfocus, then
|
||||
//if(searchText.isEmpty) {
|
||||
// widget.filteredPoints = //todo
|
||||
sendFilteredGeoPoint();
|
||||
//}
|
||||
});
|
||||
}
|
||||
|
||||
sendFilteredGeoPoint() {
|
||||
List<GeoPointDTO> checkedGeoPoints = [];
|
||||
// Parcourez les nœuds filtrés pour récupérer les GeoPointDTO correspondants qui sont cochés
|
||||
for (var node in _filteredNodes) {
|
||||
if (node.children.isNotEmpty) {
|
||||
for (var childNode in node.children) {
|
||||
if (childNode.checked) {
|
||||
var point = widget.geoPoints.firstWhere(
|
||||
(point) {
|
||||
String latitudePart = _getPointLatLon(point, true)
|
||||
.substring(0, min(_getPointLatLon(point, true).length, 10))
|
||||
.replaceAll(".", "")
|
||||
.replaceAll("-", "");
|
||||
String longitudePart = _getPointLatLon(point, false)
|
||||
.substring(0, min(_getPointLatLon(point, false).length, 10))
|
||||
.replaceAll(".", "")
|
||||
.replaceAll("-", "");
|
||||
int combinedValue = int.parse(latitudePart + longitudePart);
|
||||
|
||||
return combinedValue == childNode.id;
|
||||
},
|
||||
orElse: () => GeoPointDTO(id: -1),
|
||||
);
|
||||
|
||||
if (point.id != -1) {
|
||||
checkedGeoPoints.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(node.checked) {
|
||||
var point = widget.geoPoints.firstWhere(
|
||||
(point) {
|
||||
String latitudePart = _getPointLatLon(point, true)
|
||||
.substring(0, min(_getPointLatLon(point, true).length, 10))
|
||||
.replaceAll(".", "")
|
||||
.replaceAll("-", "");
|
||||
String longitudePart = _getPointLatLon(point, false)
|
||||
.substring(0, min(_getPointLatLon(point, false).length, 10))
|
||||
.replaceAll(".", "")
|
||||
.replaceAll("-", "");
|
||||
int combinedValue = int.parse(latitudePart + longitudePart);
|
||||
return combinedValue == node.id;
|
||||
},
|
||||
orElse: () => GeoPointDTO(id: -1),
|
||||
);
|
||||
|
||||
if (point.id != -1) {
|
||||
checkedGeoPoints.add(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Passez la liste des GeoPointDTO cochés à la fonction filteredPoints
|
||||
widget.filteredPoints(checkedGeoPoints);
|
||||
}
|
||||
|
||||
List<TreeNode> _filterNodesBySearchText(String searchText) {
|
||||
List<TreeNode> filteredNodes = [];
|
||||
for (var node in buildTreeNodes(widget.categories, widget.geoPoints)) {
|
||||
if (_nodeOrChildrenContainsText(node, searchText)) {
|
||||
if(node.children.isNotEmpty) {
|
||||
for (var childNode in node.children) {
|
||||
if (_nodeOrChildrenContainsText(childNode, searchText)) {
|
||||
filteredNodes.add(childNode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filteredNodes.add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
return filteredNodes;
|
||||
}
|
||||
|
||||
bool _nodeOrChildrenContainsText(TreeNode node, String searchText) {
|
||||
// Remove accent and other special characters
|
||||
String normalizedSearchText = removeDiacritics(searchText.toLowerCase());
|
||||
String normalizedTitle = removeDiacritics(node.title.toLowerCase());
|
||||
|
||||
if (normalizedTitle.contains(normalizedSearchText)) {
|
||||
return true;
|
||||
}
|
||||
for (var childNode in node.children) {
|
||||
if (_nodeOrChildrenContainsText(childNode, searchText)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
var currentLanguage = visitAppContext.language;
|
||||
final mapContext = Provider.of<MapContext>(context);
|
||||
|
||||
var primaryColor = visitAppContext.configuration != null ? visitAppContext.configuration!.primaryColor != null ? Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : kSecondColor : kSecondColor;
|
||||
double rounded = visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0;
|
||||
|
||||
return Positioned(
|
||||
left: 0,
|
||||
top: _isExpanded ? screenSize.height * 0.11 : screenSize.height / 2 - (75 / 2),
|
||||
child: AnimatedBuilder(
|
||||
animation: _widthAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: _widthAnimation.value,
|
||||
height: _isExpanded ? screenSize.height*0.78 : 75,
|
||||
decoration: BoxDecoration(
|
||||
color: _isExpanded ? kBackgroundColor.withValues(alpha: 0.9) : primaryColor.withValues(alpha: 0.8),
|
||||
borderRadius: BorderRadius.only(topRight: Radius.circular(rounded), bottomRight: Radius.circular(rounded)),
|
||||
),
|
||||
child: _showContent ? Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.close, color: primaryColor),
|
||||
onPressed: _toggleExpansion,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 60,
|
||||
width: (screenSize.width * 0.76),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0, bottom: 8.0, top: 4.0),
|
||||
child: TextField(
|
||||
focusNode: focusNode,
|
||||
controller: _searchController,
|
||||
onChanged: (value) {
|
||||
filterNodes();
|
||||
},
|
||||
cursorColor: Colors.black,
|
||||
style: const TextStyle(color: Colors.black),
|
||||
decoration: InputDecoration(
|
||||
labelText: _searchController.text.isEmpty ? TranslationHelper.getFromLocale("map.search", appContext.getContext()) : "",
|
||||
labelStyle: const TextStyle(
|
||||
color: Colors.black
|
||||
),
|
||||
focusColor: primaryColor,
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
borderSide: BorderSide(color: primaryColor)),
|
||||
//labelStyle: TextStyle(color: primaryColor),
|
||||
suffixIcon: IconButton(
|
||||
icon: _searchController.text.isNotEmpty ? Icon(Icons.close, color: primaryColor) : Icon(Icons.search, color: primaryColor),
|
||||
onPressed: () {
|
||||
if(_searchController.text.isNotEmpty) {
|
||||
_searchController.text = "";
|
||||
// TODO reset view ?
|
||||
}
|
||||
filterNodes();
|
||||
if(_searchController.text.isNotEmpty) {
|
||||
focusNode.unfocus();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: _searchTextNotifier,
|
||||
builder: (context, value, _) {
|
||||
return SizedBox(
|
||||
width: screenSize.width * 0.8,
|
||||
height: screenSize.height * 0.63,
|
||||
child: FilterTree(
|
||||
data: _filteredNodes,
|
||||
selectOneToAll: true,
|
||||
textColor: Colors.black,
|
||||
onChecked: (node, checked, commonID) {
|
||||
sendFilteredGeoPoint();
|
||||
},
|
||||
onClicked: (node, commonID) {
|
||||
var selectedNode = widget.geoPoints.firstWhere(
|
||||
(point) {
|
||||
final lat = _getPointLatLon(point, true);
|
||||
final lon = _getPointLatLon(point, false);
|
||||
String latitudePart = lat
|
||||
.substring(0, min(lat.length, 10))
|
||||
.replaceAll(".", "")
|
||||
.replaceAll("-", "");
|
||||
String longitudePart = lon
|
||||
.substring(0, min(lon.length, 10))
|
||||
.replaceAll(".", "")
|
||||
.replaceAll("-", "");
|
||||
int combinedValue =
|
||||
int.parse(latitudePart + longitudePart);
|
||||
return combinedValue == commonID;
|
||||
},
|
||||
orElse: () => GeoPointDTO(id: -1),
|
||||
);
|
||||
|
||||
if (selectedNode.id != -1) {
|
||||
mapContext.setSelectedPointForNavigate(selectedNode);
|
||||
_toggleExpansion();
|
||||
} else {
|
||||
print('Aucun point correspondant trouvé.');
|
||||
}
|
||||
},
|
||||
checkBoxColor: primaryColor,
|
||||
childrenPadding: const EdgeInsets.only(
|
||||
left: 20, top: 10, right: 0, bottom: 10),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
) : IconButton(
|
||||
icon: const Icon(Icons.search, color: Colors.white),
|
||||
onPressed: _toggleExpansion,
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getPointLatLon(GeoPointDTO point, bool isLat) {
|
||||
if (point.geometry?.type == 'Point' && point.geometry?.coordinates is List) {
|
||||
final coords = point.geometry!.coordinates as List;
|
||||
if (coords.length >= 2) {
|
||||
return (coords[isLat ? 1 : 0] as num).toDouble().toString();
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
177
lib/Screens/Sections/Map/google_map_view.dart
Normal file
177
lib/Screens/Sections/Map/google_map_view.dart
Normal file
@ -0,0 +1,177 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/map_context.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
|
||||
class GoogleMapView extends StatefulWidget {
|
||||
final MapDTO mapDTO;
|
||||
final List<GeoPointDTO> geoPoints;
|
||||
final List<Map<String, dynamic>> icons;
|
||||
final String? language;
|
||||
const GoogleMapView({
|
||||
Key? key,
|
||||
required this.mapDTO,
|
||||
required this.geoPoints,
|
||||
required this.icons,
|
||||
this.language,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_GoogleMapViewState createState() => _GoogleMapViewState();
|
||||
}
|
||||
|
||||
class _GoogleMapViewState extends State<GoogleMapView> {
|
||||
ConfigurationDTO? configurationDTO;
|
||||
Completer<GoogleMapController> _controller = Completer();
|
||||
GoogleMapController? _GoogleMapcontroller;
|
||||
Set<Marker> markers = {};
|
||||
List<GeoPointDTO>? pointsToShow = [];
|
||||
bool init = false;
|
||||
|
||||
Set<Marker> getMarkers(language, mapContext) {
|
||||
markers = {};
|
||||
|
||||
int i = 0;
|
||||
pointsToShow!.forEach((point) {
|
||||
if (point.title != null && point.title!.any((translation) => translation.language == language)) {
|
||||
var translation = point.title!.firstWhere((translation) => translation.language == language);
|
||||
var textSansHTML = parse(translation.value);
|
||||
point.id = i;
|
||||
var coords = _getPointLatLon(point);
|
||||
if (coords != null) {
|
||||
var icon = point.categorieId == null
|
||||
? BitmapDescriptor.bytes(widget.icons.where((i) => i['id'] == null).first['icon'])
|
||||
: widget.icons.any((i) => i['id'] == point.categorieId)
|
||||
? BitmapDescriptor.bytes(widget.icons.where((i) => i['id'] == point.categorieId).first['icon'])
|
||||
: BitmapDescriptor.bytes(widget.icons.where((i) => i['id'] == null).first['icon']);
|
||||
|
||||
markers.add(Marker(
|
||||
draggable: false,
|
||||
markerId: MarkerId(parse(textSansHTML.body!.text).documentElement!.text + coords.latitude.toString() + coords.longitude.toString()),
|
||||
position: coords,
|
||||
icon: icon,
|
||||
onTap: () {
|
||||
mapContext.setSelectedPoint(point);
|
||||
(Provider.of<AppContext>(context, listen: false).getContext() as VisitAppContext)
|
||||
.statisticsService?.track(
|
||||
VisitEventType.mapPoiTap,
|
||||
metadata: {
|
||||
'geoPointId': point.id,
|
||||
'geoPointTitle': parse(textSansHTML.body!.text).documentElement!.text,
|
||||
},
|
||||
);
|
||||
},
|
||||
infoWindow: InfoWindow.noText));
|
||||
}
|
||||
}
|
||||
i++;
|
||||
});
|
||||
return markers;
|
||||
}
|
||||
|
||||
LatLng? _getPointLatLon(GeoPointDTO point) {
|
||||
if (point.geometry?.type == 'Point' && point.geometry?.coordinates != null && point.geometry!.coordinates! is List) {
|
||||
final coords = point.geometry!.coordinates! as List;
|
||||
if (coords.length >= 2) {
|
||||
return LatLng(
|
||||
(coords[1] as num).toDouble(),
|
||||
(coords[0] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapContext = Provider.of<MapContext>(context);
|
||||
//final appContext = Provider.of<AppContext>(context);
|
||||
|
||||
pointsToShow = widget.geoPoints;
|
||||
getMarkers(widget.language, mapContext);
|
||||
|
||||
MapType type = MapType.hybrid;
|
||||
if(widget.mapDTO.mapType != null) {
|
||||
switch(widget.mapDTO.mapType!.value) {
|
||||
case 0: type = MapType.none; break;
|
||||
case 1: type = MapType.normal; break;
|
||||
case 2: type = MapType.satellite; break;
|
||||
case 3: type = MapType.terrain; break;
|
||||
case 4: type = MapType.hybrid; break;
|
||||
}
|
||||
}
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: GoogleMap(
|
||||
mapType: type,
|
||||
mapToolbarEnabled: false,
|
||||
indoorViewEnabled: false,
|
||||
initialCameraPosition: CameraPosition(
|
||||
target: widget.mapDTO.longitude != null && widget.mapDTO.latitude != null
|
||||
? LatLng(double.tryParse(widget.mapDTO.latitude!)!, double.tryParse(widget.mapDTO.longitude!)!)
|
||||
: LatLng(50.465503, 4.865105),
|
||||
zoom: widget.mapDTO.zoom != null ? widget.mapDTO.zoom!.toDouble() : 18,
|
||||
),
|
||||
onMapCreated: (GoogleMapController controller) {
|
||||
if (!kIsWeb) {
|
||||
_controller.complete(controller);
|
||||
_GoogleMapcontroller = controller;
|
||||
}
|
||||
},
|
||||
markers: markers,
|
||||
onTap: (LatLng location) {
|
||||
mapContext.setSelectedPoint(null);
|
||||
mapContext.setSelectedPointForNavigate(null);
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Consumer<MapContext>(
|
||||
builder: (context, mapContext, _) {
|
||||
var geopoint = mapContext.getSelectedPointForNavigate() as GeoPointDTO?;
|
||||
if (geopoint != null && _GoogleMapcontroller != null) {
|
||||
_GoogleMapcontroller!.getZoomLevel().then((actualZoom) {
|
||||
var zoomToNavigate = actualZoom <= 12.0 ? 15.0 : actualZoom;
|
||||
var coords = _getPointLatLon(geopoint);
|
||||
if (coords != null) {
|
||||
_GoogleMapcontroller!.animateCamera(CameraUpdate.newCameraPosition(
|
||||
CameraPosition(
|
||||
target: coords,
|
||||
tilt: 0.0,
|
||||
bearing: 0.0,
|
||||
zoom: zoomToNavigate
|
||||
)
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
return SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
264
lib/Screens/Sections/Map/map_box_view.dart
Normal file
264
lib/Screens/Sections/Map/map_box_view.dart
Normal file
@ -0,0 +1,264 @@
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' as mapBox;
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/map_context.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/map_page.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
|
||||
class MapBoxView extends StatefulWidget {
|
||||
final MapDTO? mapDTO;
|
||||
final List<GeoPointDTO> geoPoints;
|
||||
final List<Map<String, dynamic>> icons;
|
||||
final String? language;
|
||||
const MapBoxView({
|
||||
Key? key,
|
||||
this.mapDTO,
|
||||
required this.geoPoints,
|
||||
required this.icons,
|
||||
this.language,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MapBoxViewState createState() => _MapBoxViewState();
|
||||
}
|
||||
|
||||
class AnnotationClickListener extends mapBox.OnPointAnnotationClickListener {
|
||||
late List<GeoPointDTO> markersList;
|
||||
late MapContext mapContext;
|
||||
|
||||
AnnotationClickListener({
|
||||
required this.markersList,
|
||||
required this.mapContext,
|
||||
});
|
||||
|
||||
@override
|
||||
void onPointAnnotationClick(mapBox.PointAnnotation annotation) {
|
||||
try{
|
||||
var markerToShow = markersList.firstWhere((ml) {
|
||||
if (ml.geometry?.type == 'Point' && ml.geometry?.coordinates != null && ml.geometry!.coordinates! is List) {
|
||||
final coords = ml.geometry!.coordinates! as List;
|
||||
if (coords.length >= 2) {
|
||||
final lat = (coords[1] as num).toDouble();
|
||||
final lon = (coords[0] as num).toDouble();
|
||||
return "${parse(ml.title!.first.value).documentElement!.text}$lat$lon" == annotation.textField;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
mapContext.setSelectedPoint(markerToShow);
|
||||
//mapContext.setSelectedPointForNavigate(markerToShow);
|
||||
} catch(e) {
|
||||
print("ISSSUE setSelectedMarker");
|
||||
print(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _MapBoxViewState extends State<MapBoxView> {
|
||||
late mapBox.MapboxMap? mapboxMap;
|
||||
mapBox.PointAnnotationManager? pointAnnotationManager;
|
||||
bool filterZoneSelected = false;
|
||||
|
||||
createPoints() {
|
||||
var options = <mapBox.PointAnnotationOptions>[];
|
||||
int i = 0;
|
||||
markersList = [];
|
||||
pointsToShow!.forEach((point) {
|
||||
if(point.title!.where((translation) => translation.language == widget.language).firstOrNull != null) {
|
||||
var textSansHTML = parse(point.title!.firstWhere((translation) => translation.language == widget.language).value);
|
||||
point.id = i;
|
||||
point.title = point.title!.where((t) => t.language == widget.language).toList();
|
||||
/*var mapMarker = new MapMarker(
|
||||
id: i,
|
||||
title: parse(textSansHTML.body!.text).documentElement!.text,
|
||||
description: point.description!.firstWhere((translation) => translation.language == widget.language).value,
|
||||
longitude: point.longitude,
|
||||
latitude: point.latitude,
|
||||
contents: point.contents
|
||||
);*/
|
||||
markersList.add(point);
|
||||
var coords = _getPointLatLon(point);
|
||||
if (coords != null) {
|
||||
options.add(mapBox.PointAnnotationOptions(
|
||||
geometry: mapBox.Point(
|
||||
coordinates: mapBox.Position(
|
||||
coords.longitude,
|
||||
coords.latitude,
|
||||
)), // .toJson()
|
||||
iconSize: 1.3,
|
||||
textField: "${parse(textSansHTML.body!.text).documentElement!.text}${coords.latitude}${coords.longitude}",
|
||||
textOpacity: 0.0,
|
||||
iconOffset: [0.0, 0.0],
|
||||
symbolSortKey: 10,
|
||||
iconColor: 0,
|
||||
iconImage: null,
|
||||
image: point.categorieId == null ? widget.icons.where((i) => i['id'] == null).first['icon'] : widget.icons.any((i) => i['id'] == point.categorieId) ? widget.icons.where((i) => i['id'] == point.categorieId).first['icon'] : widget.icons.where((i) => i['id'] == null).first['icon'], //widget.selectedMarkerIcon,
|
||||
)); // ,
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
print(options.length);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
_onMapCreated(mapBox.MapboxMap mapboxMap, MapContext mapContext) {
|
||||
this.mapboxMap = mapboxMap;
|
||||
|
||||
mapboxMap.annotations.createPointAnnotationManager().then((pointAnnotationManager) async {
|
||||
this.pointAnnotationManager = pointAnnotationManager;
|
||||
pointAnnotationManager.createMulti(createPoints());
|
||||
pointAnnotationManager.addOnPointAnnotationClickListener(AnnotationClickListener(mapContext: mapContext, markersList: markersList));
|
||||
init = true;
|
||||
});
|
||||
}
|
||||
|
||||
ConfigurationDTO? configurationDTO;
|
||||
//Completer<GoogleMapController> _controller = Completer();
|
||||
//Set<Marker> markers = {};
|
||||
List<GeoPointDTO>? pointsToShow = [];
|
||||
List<String>? selectedCategories = [];
|
||||
bool init = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
pointsToShow = widget.geoPoints;//widget.mapDTO!.points;
|
||||
var nonNullCat = widget.mapDTO!.categories!.where((c) => c.label!.where((element) => element.language == widget.language).firstOrNull != null);
|
||||
selectedCategories = nonNullCat.map((categorie) => categorie.label!.firstWhere((element) => element.language == widget.language).value!).toList();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mapContext = Provider.of<MapContext>(context);
|
||||
|
||||
pointsToShow = widget.geoPoints;//widget.mapDTO!.points;
|
||||
|
||||
if(pointAnnotationManager != null) {
|
||||
pointAnnotationManager!.deleteAll();
|
||||
pointAnnotationManager!.createMulti(createPoints());
|
||||
//mapContext.notifyListeners();
|
||||
}
|
||||
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext;
|
||||
Size size = MediaQuery.of(context).size;
|
||||
|
||||
var type = mapBox.MapboxStyles.STANDARD;
|
||||
if(widget.mapDTO!.mapTypeMapbox != null) {
|
||||
switch(widget.mapDTO!.mapTypeMapbox!) {
|
||||
case MapTypeMapBox.standard:
|
||||
type = mapBox.MapboxStyles.STANDARD;
|
||||
break;
|
||||
case MapTypeMapBox.streets:
|
||||
type = mapBox.MapboxStyles.MAPBOX_STREETS;
|
||||
break;
|
||||
case MapTypeMapBox.outdoors:
|
||||
type = mapBox.MapboxStyles.OUTDOORS;
|
||||
break;
|
||||
case MapTypeMapBox.light:
|
||||
type = mapBox.MapboxStyles.LIGHT;
|
||||
break;
|
||||
case MapTypeMapBox.dark:
|
||||
type = mapBox.MapboxStyles.DARK;
|
||||
break;
|
||||
case MapTypeMapBox.satellite:
|
||||
type = mapBox.MapboxStyles.SATELLITE;
|
||||
break;
|
||||
case MapTypeMapBox.satellite_streets:
|
||||
type = mapBox.MapboxStyles.SATELLITE_STREETS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: mapBox.MapWidget(
|
||||
key: ValueKey("mapBoxWidget"),
|
||||
styleUri: type,
|
||||
onMapCreated: (maBoxMap) {
|
||||
_onMapCreated(maBoxMap, mapContext);
|
||||
},
|
||||
onTapListener: (listener) {
|
||||
// close on tap
|
||||
//mapContext.setSelectedPoint(null);
|
||||
mapContext.setSelectedPointForNavigate(null);
|
||||
},
|
||||
cameraOptions: mapBox.CameraOptions(
|
||||
center: mapBox.Point(coordinates: widget.mapDTO!.longitude != null && widget.mapDTO!.latitude != null ? mapBox.Position(double.tryParse(widget.mapDTO!.longitude!)!, double.tryParse(widget.mapDTO!.latitude!)!) : mapBox.Position(4.865105, 50.465503)), //.toJson()
|
||||
zoom: widget.mapDTO!.zoom != null ? widget.mapDTO!.zoom!.toDouble() : 12),
|
||||
)
|
||||
),
|
||||
Container(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return Consumer<MapContext>(
|
||||
builder: (context, mapContext, _) {
|
||||
var geoPoint = mapContext.getSelectedPointForNavigate() as GeoPointDTO?;
|
||||
if (geoPoint != null && mapboxMap != null) {
|
||||
print("COUCOU IL FAUT NAVUGATE MAPBOX");
|
||||
// TODO Handle zoomDetail
|
||||
var coords = _getPointLatLon(geoPoint);
|
||||
if (coords != null) {
|
||||
mapboxMap?.easeTo(
|
||||
mapBox.CameraOptions(
|
||||
center: mapBox.Point(coordinates: mapBox.Position(coords.longitude, coords.latitude)), //.toJson()
|
||||
zoom: 16,
|
||||
bearing: 0,
|
||||
pitch: 3),
|
||||
mapBox.MapAnimationOptions(duration: 2000, startDelay: 0));
|
||||
}
|
||||
}
|
||||
return SizedBox();
|
||||
},
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
/*Positioned(
|
||||
left: 5,
|
||||
top: 35,
|
||||
child: SizedBox(
|
||||
width: size.width * 0.3,
|
||||
height: size.height * 0.76,
|
||||
child: GeoPointFilter(
|
||||
language: tabletAppContext.language!,
|
||||
geoPoints: widget.mapDTO!.points!,
|
||||
categories: widget.mapDTO!.categories!,
|
||||
filteredPoints: (filteredPoints) {
|
||||
print("COUCOU FILTERED POINTS");
|
||||
print(filteredPoints);
|
||||
}),
|
||||
),
|
||||
),*/
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
LatLng? _getPointLatLon(GeoPointDTO point) {
|
||||
if (point.geometry?.type == 'Point' && point.geometry?.coordinates != null && point.geometry!.coordinates! is List) {
|
||||
final coords = point.geometry!.coordinates! as List;
|
||||
if (coords.length >= 2) {
|
||||
return LatLng(
|
||||
(coords[1] as num).toDouble(),
|
||||
(coords[0] as num).toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
22
lib/Screens/Sections/Map/map_context.dart
Normal file
22
lib/Screens/Sections/Map/map_context.dart
Normal file
@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
|
||||
class MapContext with ChangeNotifier {
|
||||
GeoPointDTO? _selectedPoint;
|
||||
GeoPointDTO? _selectedPointNavigate;
|
||||
|
||||
MapContext(this._selectedPoint);
|
||||
|
||||
getSelectedPoint() => _selectedPoint;
|
||||
setSelectedPoint(GeoPointDTO? selectedPoint) {
|
||||
_selectedPoint = selectedPoint;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
getSelectedPointForNavigate() => _selectedPointNavigate;
|
||||
setSelectedPointForNavigate(GeoPointDTO? selectedPointNavigate) {
|
||||
_selectedPointNavigate = selectedPointNavigate;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
}
|
||||
188
lib/Screens/Sections/Map/map_page.dart
Normal file
188
lib/Screens/Sections/Map/map_page.dart
Normal file
@ -0,0 +1,188 @@
|
||||
//import 'dart:async';
|
||||
import 'dart:convert';
|
||||
// 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
// 'package:flutter/services.dart';
|
||||
//import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/flutter_map_view.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/geo_point_filter.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/map_box_view.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/marker_view.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
/*import 'package:tablet_app/Components/loading.dart';
|
||||
import 'package:tablet_app/Components/loading_common.dart';*/
|
||||
//import 'dart:ui' as ui;
|
||||
import 'package:flutter/widgets.dart';
|
||||
//import 'package:http/http.dart' as http;
|
||||
|
||||
import 'google_map_view.dart';
|
||||
//import 'package:image/image.dart' as IMG;
|
||||
|
||||
//Set<Marker> markers = {};
|
||||
List<GeoPointDTO> markersList = [];
|
||||
|
||||
class MapPage extends StatefulWidget {
|
||||
final MapDTO section;
|
||||
final List<Map<String, dynamic>> icons;
|
||||
MapPage({Key? key, required this.section, required this.icons}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MapPage createState() => _MapPage();
|
||||
}
|
||||
|
||||
class _MapPage extends State<MapPage> {
|
||||
MapDTO? mapDTO;
|
||||
//Completer<GoogleMapController> _controller = Completer();
|
||||
//Uint8List? selectedMarkerIcon;
|
||||
late ValueNotifier<List<GeoPointDTO>> _geoPoints = ValueNotifier<List<GeoPointDTO>>([]);
|
||||
|
||||
/*Future<Uint8List> getBytesFromAsset(ByteData data, int width) async {
|
||||
//ByteData data = await rootBundle.load(path);
|
||||
ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(),
|
||||
targetWidth: width);
|
||||
ui.FrameInfo fi = await codec.getNextFrame();
|
||||
return (await fi.image.toByteData(format: ui.ImageByteFormat.png))
|
||||
!.buffer
|
||||
.asUint8List();
|
||||
}*/
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//mapDTO = MapDTO.fromJson(jsonDecode(widget.section.data!));
|
||||
mapDTO = widget.section;
|
||||
_geoPoints.value = mapDTO!.points!;
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// TODO: implement dispose
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/*static final CameraPosition _kLake = CameraPosition(
|
||||
bearing: 192.8334901395799,
|
||||
target: LatLng(37.43296265331129, -122.08832357078792),
|
||||
tilt: 59.440717697143555,
|
||||
zoom: 59.151926040649414);*/
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//final mapContext = Provider.of<MapContext>(context);
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext;
|
||||
|
||||
var primaryColor = visitAppContext.configuration != null ? visitAppContext.configuration!.primaryColor != null ? Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : kSecondColor : kSecondColor;
|
||||
|
||||
/*return FutureBuilder(
|
||||
future: getByteIcon(mapDTO!.iconSource),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
} else if (snapshot.connectionState == ConnectionState.none) {
|
||||
return Text("No data");
|
||||
} else {
|
||||
return Center(
|
||||
child: Container(
|
||||
child: LoadingCommon()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);*/
|
||||
return MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
removeBottom: true,
|
||||
child: Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: <Widget>[
|
||||
ValueListenableBuilder<List<GeoPointDTO>>(
|
||||
valueListenable: _geoPoints,
|
||||
builder: (context, value, _) {
|
||||
switch(mapDTO!.mapProvider) {
|
||||
case MapProvider.Google:
|
||||
return GoogleMapView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO!, icons: widget.icons);
|
||||
case MapProvider.MapBox:
|
||||
return MapBoxView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO, icons: widget.icons);
|
||||
// If mapbox bug as 3.24 flutter, we can test via this new widget
|
||||
return FlutterMapView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO, icons: widget.icons);
|
||||
default:
|
||||
// By default google
|
||||
return GoogleMapView(language: appContext.getContext().language, geoPoints: value, mapDTO: mapDTO!, icons: widget.icons);
|
||||
}
|
||||
}
|
||||
),
|
||||
GeoPointFilter(
|
||||
language: visitAppContext.language!,
|
||||
geoPoints: mapDTO!.points!,
|
||||
categories: mapDTO!.categories!,
|
||||
provider: mapDTO!.mapProvider == null ? MapProvider.Google : mapDTO!.mapProvider!,
|
||||
filteredPoints: (value) {
|
||||
_geoPoints.value = value!;
|
||||
}),
|
||||
MarkerViewWidget(),
|
||||
Positioned(
|
||||
top: 35,
|
||||
left: 10,
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.arrow_back, size: 23, color: Colors.white)
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
]
|
||||
/*floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _goToTheLake,
|
||||
label: Text('To the lake!'),
|
||||
icon: Icon(Icons.directions_boat),
|
||||
),*/
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/*getByteIcon(String? source) async {
|
||||
if(source != null) {
|
||||
if(kIsWeb) {
|
||||
Uint8List fileData = await http.readBytes(Uri.parse(source));
|
||||
selectedMarkerIcon = resizeImage(fileData, 40);
|
||||
} else {
|
||||
final ByteData imageData = await NetworkAssetBundle(Uri.parse(source)).load("");
|
||||
selectedMarkerIcon = await getBytesFromAsset(imageData, 50);
|
||||
}
|
||||
} else {
|
||||
// default icon
|
||||
final ByteData bytes = await rootBundle.load('assets/icons/marker.png');
|
||||
selectedMarkerIcon = await getBytesFromAsset(bytes, 25);
|
||||
}
|
||||
}*/
|
||||
|
||||
/*Uint8List resizeImage(Uint8List data, int width) {
|
||||
Uint8List resizedData = data;
|
||||
IMG.Image img = IMG.decodeImage(data)!;
|
||||
IMG.Image resized = IMG.copyResize(img, width: width);
|
||||
resizedData = Uint8List.fromList(IMG.encodeJpg(resized));
|
||||
return resizedData;
|
||||
}*/
|
||||
|
||||
/*Future<void> _goToTheLake() async {
|
||||
final GoogleMapController controller = await _controller.future;
|
||||
controller.animateCamera(CameraUpdate.newCameraPosition(_kLake));
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
619
lib/Screens/Sections/Map/marker_view.dart
Normal file
619
lib/Screens/Sections/Map/marker_view.dart
Normal file
@ -0,0 +1,619 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.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/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:photo_view/photo_view.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:html/parser.dart' show parse;
|
||||
|
||||
|
||||
import '../../../constants.dart';
|
||||
import 'map_context.dart';
|
||||
|
||||
class MarkerViewWidget extends StatefulWidget {
|
||||
const MarkerViewWidget({super.key});
|
||||
|
||||
@override
|
||||
_MarkerInfoWidget createState() => _MarkerInfoWidget();
|
||||
}
|
||||
|
||||
class _MarkerInfoWidget extends State<MarkerViewWidget> {
|
||||
CarouselSliderController? sliderController;
|
||||
ValueNotifier<int> currentIndex = ValueNotifier<int>(1);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
sliderController = CarouselSliderController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
sliderController = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
int breakPointPrice = 50;
|
||||
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final mapContext = Provider.of<MapContext>(context);
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext;
|
||||
var language = visitAppContext.language;
|
||||
GeoPointDTO? selectedPoint = mapContext.getSelectedPoint() as GeoPointDTO?;
|
||||
|
||||
ScrollController scrollDescription = ScrollController();
|
||||
ScrollController scrollPrice = ScrollController();
|
||||
|
||||
var isPointPrice = selectedPoint != null && selectedPoint.prices != null && selectedPoint.prices!.isNotEmpty && selectedPoint.prices!.any((d) => d.language == language) && selectedPoint.prices!.firstWhere((d) => d.language == language).value != null && selectedPoint.prices!.firstWhere((d) => d.language == language).value!.trim().isNotEmpty;
|
||||
|
||||
Color primaryColor = Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16));
|
||||
|
||||
Size sizeMarker = Size(size.width * 0.93, size.height * 0.8);
|
||||
return Center(
|
||||
child: Visibility(
|
||||
visible: selectedPoint != null,
|
||||
child: Container(
|
||||
width: sizeMarker.width,
|
||||
height: sizeMarker.height,
|
||||
margin: const EdgeInsets.symmetric(vertical: 3, horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: kBackgroundColor, // Colors.amberAccent //kBackgroundLight,
|
||||
//shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 10.0),
|
||||
/*boxShadow: [
|
||||
BoxShadow(
|
||||
color: kBackgroundSecondGrey,
|
||||
spreadRadius: 0.5,
|
||||
blurRadius: 1.1,
|
||||
offset: Offset(0, 1.1), // changes position of shadow
|
||||
),
|
||||
],*/
|
||||
),
|
||||
child: Stack(
|
||||
children: <Widget> [
|
||||
if(selectedPoint != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0), topRight: Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
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: [
|
||||
kMainColor0,
|
||||
kMainColor1,
|
||||
kMainColor2,
|
||||
],
|
||||
),
|
||||
border: const Border(right: BorderSide(width: 0.05, color: kMainGrey)),
|
||||
color: Colors.grey,
|
||||
image: selectedPoint.imageUrl != null ? DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
opacity: 0.4,
|
||||
image: NetworkImage(
|
||||
selectedPoint.imageUrl!,
|
||||
),
|
||||
): null
|
||||
),
|
||||
width: size.width,
|
||||
height: 75,
|
||||
child: Center(
|
||||
child: HtmlWidget(
|
||||
selectedPoint.title!.firstWhere((t) => t.language == language).value!,
|
||||
textStyle: const TextStyle(fontSize: 20.0),
|
||||
customStylesBuilder: (element)
|
||||
{
|
||||
return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"};
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if(selectedPoint != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 75),
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
//mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Container(
|
||||
height: size.height * 0.4,
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: kBackgroundLight,
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 10, bottom: 10, top: 15),
|
||||
child: Scrollbar(
|
||||
controller: scrollDescription,
|
||||
thumbVisibility: true,
|
||||
thickness: 2.0,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollDescription,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
if(selectedPoint.description!.any((d) => d.language == language) && selectedPoint.description!.firstWhere((d) => d.language == language).value != null && selectedPoint.description!.firstWhere((d) => d.language == language).value!.trim().isNotEmpty)
|
||||
HtmlWidget(
|
||||
selectedPoint.description!.firstWhere((d) => d.language == language).value!,
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'left', 'font-family': "Roboto"};
|
||||
},
|
||||
textStyle: const TextStyle(fontSize: kMenuDescriptionDetailSize),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: size.height * 0.2,
|
||||
child: CarouselSlider(
|
||||
carouselController: sliderController,
|
||||
options: CarouselOptions(
|
||||
onPageChanged: (int index, CarouselPageChangedReason reason) {
|
||||
currentIndex.value = index + 1;
|
||||
},
|
||||
height: size.height *0.33,
|
||||
enlargeCenterPage: true,
|
||||
pageSnapping: true,
|
||||
reverse: false,
|
||||
),
|
||||
items: selectedPoint.contents!.map<Widget>((ContentDTO i) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
AppContext appContext = Provider.of<AppContext>(context);
|
||||
var resourcetoShow = getElementForResource(context, appContext, i, true);
|
||||
return resourcetoShow;
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 10.0),
|
||||
child: Column(
|
||||
children: [
|
||||
if(isPointPrice && selectedPoint.prices!.firstWhere((d) => d.language == language).value!.length > breakPointPrice)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
|
||||
child: Container(
|
||||
height: size.height * 0.18,
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
color: kBackgroundLight,
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0))
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 20, right: 10, bottom: 10, top: 15),
|
||||
child: Scrollbar(
|
||||
controller: scrollPrice,
|
||||
thumbVisibility: true,
|
||||
thickness: 2.0,
|
||||
child: SingleChildScrollView(
|
||||
controller: scrollPrice,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
//crossAxisAlignment: CrossAxisAlignment.center,
|
||||
//mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Center(child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: Icon(Icons.price_change_outlined, color: primaryColor, size: 25),
|
||||
)),
|
||||
HtmlWidget(
|
||||
selectedPoint.prices!.firstWhere((d) => d.language == language).value!,
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'left', 'font-family': "Roboto"};
|
||||
},
|
||||
textStyle: const TextStyle(fontSize: kMenuDescriptionDetailSize),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if(isPointPrice && selectedPoint.prices!.firstWhere((d) => d.language == language).value!.length <= breakPointPrice)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.price_change_outlined, color: primaryColor, size: 13),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: HtmlWidget(
|
||||
selectedPoint.prices!.firstWhere((d) => d.language == language).value!,
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'left', 'font-family': "Roboto"};
|
||||
},
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
selectedPoint.phone != null && selectedPoint.phone!.isNotEmpty && selectedPoint.phone!.any((d) => d.language == language) && selectedPoint.phone!.firstWhere((d) => d.language == language).value != null && selectedPoint.phone!.firstWhere((d) => d.language == language).value!.trim().isNotEmpty ? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.phone, color: primaryColor, size: 13),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Text(parse(selectedPoint.phone!.firstWhere((p) => p.language == language).value!).documentElement!.text, style: const TextStyle(fontSize: 18)),
|
||||
)
|
||||
],
|
||||
): const SizedBox(),
|
||||
selectedPoint.email != null && selectedPoint.email!.isNotEmpty && selectedPoint.email!.any((d) => d.language == language) && selectedPoint.email!.firstWhere((d) => d.language == language).value != null && selectedPoint.email!.firstWhere((d) => d.language == language).value!.trim().isNotEmpty ? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.email, color: primaryColor, size: 13),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: AutoSizeText(parse(selectedPoint.email!.firstWhere((p) => p.language == language).value!).documentElement!.text, style: const TextStyle(fontSize: 18), maxLines: 3),
|
||||
)
|
||||
],
|
||||
): const SizedBox(),
|
||||
selectedPoint.site != null && selectedPoint.site!.isNotEmpty && selectedPoint.site!.any((d) => d.language == language) && selectedPoint.site!.firstWhere((d) => d.language == language).value != null && selectedPoint.site!.firstWhere((d) => d.language == language).value!.trim().isNotEmpty ? Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.public, color: primaryColor, size: 13),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: AutoSizeText(parse(selectedPoint.site!.firstWhere((p) => p.language == language).value!).documentElement!.text, style: const TextStyle(fontSize: 18), maxLines: 3),
|
||||
)
|
||||
],
|
||||
): const SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
/*Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: HtmlWidget(
|
||||
(mapContext.getSelectedMarker() as MapMarker).title!,
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'center'};
|
||||
},
|
||||
textStyle: TextStyle(fontWeight: FontWeight.w500, fontSize: kIsWeb ? kWebTitleSize : kTitleSize)
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 75),
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if((mapContext.getSelectedMarker() as MapMarker).contents != null && (mapContext.getSelectedMarker() as MapMarker).contents!.length > 0)
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
//color: Colors.green,
|
||||
child: CarouselSlider(
|
||||
carouselController: sliderController,
|
||||
options: CarouselOptions(
|
||||
onPageChanged: (int index, CarouselPageChangedReason reason) {
|
||||
currentIndex.value = index + 1;
|
||||
},
|
||||
height: size.height *0.35,
|
||||
enlargeCenterPage: true,
|
||||
pageSnapping: true,
|
||||
reverse: false,
|
||||
),
|
||||
items: (mapContext.getSelectedMarker() as MapMarker).contents!.map<Widget>((ContentGeoPoint i) {
|
||||
return Builder(
|
||||
builder: (BuildContext context) {
|
||||
AppContext appContext = Provider.of<AppContext>(context);
|
||||
var resourcetoShow = getElementForResource(context, appContext, i);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: resourcetoShow,
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
//color: Colors.red,
|
||||
height: 25,
|
||||
width: sizeMarker.width,
|
||||
//color: Colors.amber,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 0),
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: currentIndex,
|
||||
builder: (context, value, _) {
|
||||
return Text(
|
||||
value.toString()+'/'+(mapContext.getSelectedMarker() as MapMarker).contents!.length.toString(),
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
);
|
||||
}
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Description
|
||||
Container(
|
||||
height: (mapContext.getSelectedMarker() as MapMarker).contents != null && (mapContext.getSelectedMarker() as MapMarker).contents!.length > 0 ? size.height *0.3 : size.height *0.6,
|
||||
width: MediaQuery.of(context).size.width *0.4,
|
||||
decoration: BoxDecoration(
|
||||
color: kBackgroundColor,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(tabletAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 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: HtmlWidget(
|
||||
(mapContext.getSelectedMarker() as MapMarker).description!,
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'center'};
|
||||
},
|
||||
textStyle: TextStyle(fontSize: kIsWeb ? kWebDescriptionSize : kDescriptionSize)
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
),
|
||||
if((mapContext.getSelectedMarker() as MapMarker).contents != null && (mapContext.getSelectedMarker() as MapMarker).contents!.length > 1)
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).size.height * 0.125,
|
||||
right: -10,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if ((mapContext.getSelectedMarker() as MapMarker).contents!.length > 0)
|
||||
sliderController!.nextPage(duration: new Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.chevron_right,
|
||||
size: kIsWeb ? 100 : 85,
|
||||
color: kTestSecondColor,
|
||||
),
|
||||
)
|
||||
),
|
||||
if((mapContext.getSelectedMarker() as MapMarker).contents != null && (mapContext.getSelectedMarker() as MapMarker).contents!.length > 1)
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).size.height * 0.125,
|
||||
left: -10,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if ((mapContext.getSelectedMarker() as MapMarker).contents!.length > 0)
|
||||
sliderController!.previousPage(duration: new Duration(milliseconds: 500), curve: Curves.fastOutSlowIn);
|
||||
},
|
||||
child: Icon(
|
||||
Icons.chevron_left,
|
||||
size: kIsWeb ? 100 : 85,
|
||||
color: kTestSecondColor,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),*/
|
||||
Positioned(
|
||||
right: 6.5,
|
||||
top: 12.5,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
mapContext.setSelectedPoint(null);
|
||||
mapContext.setSelectedPointForNavigate(null);
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 50,
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
size: 25,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
])
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getElementForResource(BuildContext context, AppContext appContext, ContentDTO i, bool addFullScreen) {
|
||||
var widgetToInclude;
|
||||
Size size = MediaQuery.of(context).size;
|
||||
VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext;
|
||||
|
||||
switch(i.resource?.type) {
|
||||
case ResourceType.Image:
|
||||
case ResourceType.ImageUrl:
|
||||
widgetToInclude = GestureDetector(
|
||||
onTap: () {
|
||||
if(addFullScreen) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0))
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
// title: Text(eventAgenda.name!),
|
||||
content: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
color: kBackgroundColor,
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 250, minHeight: 250, maxHeight: 350),
|
||||
height: size.height * 0.6,
|
||||
width: size.width * 0.85,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
//color: Colors.yellow,
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 15.0)),
|
||||
),
|
||||
child: PhotoView(
|
||||
imageProvider: ImageCustomProvider.getImageProvider(appContext, i.resourceId!, i.resource!.url!),
|
||||
minScale: PhotoViewComputedScale.contained * 0.8,
|
||||
maxScale: PhotoViewComputedScale.contained * 3.0,
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: kBackgroundGrey,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 15.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
//color: kBackgroundLight,
|
||||
image: DecorationImage(
|
||||
image: ImageCustomProvider.getImageProvider(appContext, i.resourceId!, i.resource!.url!),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 15.0)),
|
||||
/*border: Border.all(
|
||||
color: kBackgroundGrey,
|
||||
width: 1.0,
|
||||
),*/
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case ResourceType.Video:
|
||||
case ResourceType.VideoUrl:
|
||||
case ResourceType.Audio:
|
||||
widgetToInclude = GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
if(addFullScreen && i.resource!.type != ResourceType.Audio) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0))
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
// title: Text(eventAgenda.name!),
|
||||
content: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
color: kBackgroundColor,
|
||||
),
|
||||
constraints: const BoxConstraints(minWidth: 250, minHeight: 250, maxHeight: 350),
|
||||
height: size.height * 0.6,
|
||||
width: size.width * 0.85,
|
||||
child: Center(child: showElementForResource(ResourceDTO(id: i.resourceId, url: i.resource?.url, type: i.resource?.type), appContext, false, true)),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
child: IgnorePointer(
|
||||
ignoring: i.resource!.type != ResourceType.Audio,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.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,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: ClipRect(
|
||||
child: widgetToInclude,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
42
lib/Screens/Sections/Map/tree_node.dart
Normal file
42
lib/Screens/Sections/Map/tree_node.dart
Normal file
@ -0,0 +1,42 @@
|
||||
class TreeNode {
|
||||
bool checked;
|
||||
bool show;
|
||||
int id;
|
||||
int pid;
|
||||
int commonID;
|
||||
String title;
|
||||
List<TreeNode> children;
|
||||
|
||||
TreeNode({
|
||||
required this.checked,
|
||||
required this.show,
|
||||
required this.id,
|
||||
required this.pid,
|
||||
required this.commonID,
|
||||
required this.title,
|
||||
required this.children,
|
||||
});
|
||||
|
||||
factory TreeNode.fromJson(Map<String, dynamic> json) {
|
||||
return TreeNode(
|
||||
checked: json['checked'] ?? false,
|
||||
show: json['show'] ?? false,
|
||||
id: json['id'] ?? 0,
|
||||
pid: json['pid'] ?? 0,
|
||||
commonID: json['commonID'] ?? 0,
|
||||
title: json['title'] ?? '',
|
||||
children: (json['children'] as List<dynamic>)
|
||||
.map((childJson) => TreeNode.fromJson(childJson))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
List<int> getAllChildrenTitles() {
|
||||
List<int> id = [];
|
||||
for (var child in children) {
|
||||
id.add(child.id);
|
||||
id.addAll(child.getAllChildrenTitles());
|
||||
}
|
||||
return id;
|
||||
}
|
||||
}
|
||||
379
lib/Screens/Sections/Menu/menu_page.dart
Normal file
379
lib/Screens/Sections/Menu/menu_page.dart
Normal file
@ -0,0 +1,379 @@
|
||||
import 'dart:convert';
|
||||
import 'package:diacritic/diacritic.dart';
|
||||
import 'package:flutter/foundation.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/ScannerBouton.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SearchBox.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SearchNumberBox.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SlideFromRouteRight.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/ImageCustomProvider.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/section_page.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MenuPage extends StatefulWidget {
|
||||
final MenuDTO section;
|
||||
final bool isImageBackground;
|
||||
MenuPage({required this.section, required this.isImageBackground});
|
||||
|
||||
@override
|
||||
_MenuPageState createState() => _MenuPageState();
|
||||
}
|
||||
|
||||
class _MenuPageState extends State<MenuPage> {
|
||||
//MenuDTO menuDTO = MenuDTO();
|
||||
SectionDTO? selectedSection;
|
||||
bool isImageBackground = false;
|
||||
late List<dynamic> rawSubSectionsData;
|
||||
late List<SectionDTO> subSections;
|
||||
String? searchValue;
|
||||
int? searchNumberValue;
|
||||
|
||||
late List<SectionDTO> _allSections;
|
||||
final ValueNotifier<List<SectionDTO>> filteredSections = ValueNotifier([]);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
/*print(widget.section.data);
|
||||
menuDTO = MenuDTO.fromJson(jsonDecode(widget.section.data!))!;
|
||||
print(menuDTO);*/
|
||||
//menuDTO = widget.section;
|
||||
rawSubSectionsData = jsonDecode(jsonEncode(widget.section.sections));
|
||||
//menuDTO.sections!.sort((a, b) => a.order!.compareTo(b.order!)); // useless, we get these after that
|
||||
subSections = jsonDecode(jsonEncode(rawSubSectionsData)).map((json) => SectionDTO.fromJson(json)).whereType<SectionDTO>().toList();
|
||||
|
||||
isImageBackground = widget.isImageBackground;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final appContext = Provider.of<AppContext>(context, listen: false);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
_allSections = subSections;
|
||||
applyFilters(visitAppContext);
|
||||
});
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
Size size = MediaQuery.of(context).size;
|
||||
VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext;
|
||||
ConfigurationDTO configurationDTO = appContext.getContext().configuration;
|
||||
Color backgroundColor = appContext.getContext().configuration != null ? Color(int.parse(appContext.getContext().configuration.secondaryColor.split('(0x')[1].split(')')[0], radix: 16)) : Colors.white;
|
||||
Color textColor = backgroundColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
|
||||
Color? primaryColor = configurationDTO.primaryColor != null ? Color(int.parse(configurationDTO.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : null;
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
top: false,
|
||||
child: 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: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 15),
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
//setState(() {
|
||||
/**/
|
||||
Navigator.of(context).pop();
|
||||
/*visitAppContext.configuration = null;
|
||||
visitAppContext.isScanningBeacons = false;*/
|
||||
/*Navigator.of(context).pushAndRemoveUntil(MaterialPageRoute(
|
||||
builder: (context) => const HomePage3(),
|
||||
),(route) => false);*/
|
||||
//});
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.arrow_back, size: 23, color: Colors.white)
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
// TODO add a check if search used.
|
||||
SearchBox(
|
||||
width: size.width *0.55,
|
||||
onChanged: (value) {
|
||||
searchValue = value?.trim();
|
||||
applyFilters(visitAppContext);
|
||||
}
|
||||
),
|
||||
// TODO add a check if number used.
|
||||
Expanded(
|
||||
child: SearchNumberBox(onChanged: (value) {
|
||||
if (value != null && value.isNotEmpty) {
|
||||
searchNumberValue = int.tryParse(value);
|
||||
} else {
|
||||
searchNumberValue = null;
|
||||
}
|
||||
FocusScope.of(context).unfocus();
|
||||
applyFilters(visitAppContext);
|
||||
}
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
// Our background
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder<List<SectionDTO>>(
|
||||
valueListenable: filteredSections,
|
||||
builder: (context, value, child) {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 1, childAspectRatio: kIsWeb ? 1.7 : 1.95),
|
||||
itemCount: value.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
//SectionDTO? section = await (appContext.getContext() as TabletAppContext).clientAPI!.sectionApi!.sectionGetDetail(menuDTO.sections![index].id!);
|
||||
SectionDTO section = value[index];
|
||||
var rawSectionData = rawSubSectionsData[index];
|
||||
(appContext.getContext() as VisitAppContext).statisticsService?.track(
|
||||
VisitEventType.menuItemTap,
|
||||
metadata: {'targetSectionId': section.id, 'menuItemTitle': section.title?.firstOrNull?.value},
|
||||
);
|
||||
Navigator.push(
|
||||
context,
|
||||
SlideFromRightRoute(page: SectionPage(
|
||||
configuration: configurationDTO,
|
||||
rawSection: rawSectionData,
|
||||
visitAppContextIn: appContext.getContext(),
|
||||
sectionId: section.id!,
|
||||
)),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
decoration: isImageBackground ? boxDecoration(appContext, value[index], false, rawSubSectionsData[index]) : null,
|
||||
padding: const EdgeInsets.all(20),
|
||||
margin: const EdgeInsets.symmetric(vertical: 15, horizontal: 15),
|
||||
child: isImageBackground ? Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: FractionallySizedBox(
|
||||
heightFactor: 0.5,
|
||||
child: Column(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: HtmlWidget(
|
||||
value[index].title!.where((translation) => translation.language == appContext.getContext().language).firstOrNull?.value ?? "",
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'right', 'font-family': "Roboto"};
|
||||
},
|
||||
textStyle: new TextStyle(fontSize: kMenuTitleDetailSize),
|
||||
),
|
||||
),
|
||||
/*Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: HtmlWidget(
|
||||
menuDTO.sections![index].description!.firstWhere((translation) => translation.language == appContext.getContext().language).value!,
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'right'};
|
||||
},
|
||||
textStyle: new TextStyle(fontSize: kIsWeb? kWebSectionDescriptionDetailSize: kSectionDescriptionDetailSize, fontFamily: ""),
|
||||
),
|
||||
),*/
|
||||
],
|
||||
)
|
||||
),
|
||||
) : Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: value[index].imageSource == null && value[index].type != SectionType.Video ? kBackgroundColor : null, // default color if no image
|
||||
shape: BoxShape.rectangle,
|
||||
image: value[index].imageSource != null || value[index].type == SectionType.Video ? new DecorationImage(
|
||||
fit: BoxFit.contain, // contain or cover ?
|
||||
image: ImageCustomProvider.getImageProvider(appContext, value[index].imageId, value[index].type == SectionType.Video ? getYoutubeThumbnailUrl(rawSubSectionsData[index]) : value[index].imageSource!),
|
||||
): null,
|
||||
),
|
||||
)
|
||||
),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
//color: Colors.yellow,
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: size.width * 0.3,
|
||||
),
|
||||
child: Center(
|
||||
child: HtmlWidget(
|
||||
value[index].title!.where((translation) => translation.language == appContext.getContext().language).firstOrNull?.value ?? "",
|
||||
customStylesBuilder: (element) {
|
||||
return {'text-align': 'center', 'font-family': "Roboto"};
|
||||
},
|
||||
textStyle: const TextStyle(fontSize: 20),//calculateFontSize(constraints.maxWidth, constraints.maxHeight, kIsWeb ? kWebMenuTitleDetailSize : kMenuTitleDetailSize)),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: ScannerBouton(appContext: appContext),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void applyFilters(VisitAppContext visitAppContext) {
|
||||
List<SectionDTO> result = _allSections;
|
||||
|
||||
if (searchValue != null && searchValue!.isNotEmpty) {
|
||||
result = result.where((s) {
|
||||
final rawTitle = TranslationHelper.get(s.title, visitAppContext);
|
||||
final plainText = stripHtmlTags(rawTitle);
|
||||
final normalizedTitle = removeDiacritics(plainText.toLowerCase());
|
||||
final normalizedSearch = removeDiacritics(searchValue!.toLowerCase());
|
||||
return normalizedTitle.contains(normalizedSearch);
|
||||
}).toList();
|
||||
} else if (searchNumberValue != null) {
|
||||
result = result.where((s) => s.order! + 1 == searchNumberValue).toList();
|
||||
}
|
||||
|
||||
filteredSections.value = result;
|
||||
}
|
||||
|
||||
String stripHtmlTags(String htmlText) {
|
||||
final exp = RegExp(r'<[^>]*>', multiLine: true, caseSensitive: false);
|
||||
return htmlText.replaceAll(exp, '');
|
||||
}
|
||||
}
|
||||
|
||||
boxDecoration(AppContext appContext, SectionDTO section, bool isSelected, Object rawSubSectionData) {
|
||||
VisitAppContext visitAppContext = appContext.getContext() as VisitAppContext;
|
||||
|
||||
return BoxDecoration(
|
||||
color: kBackgroundLight,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0),
|
||||
image: section.imageSource != null || section.type == SectionType.Video ? DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: !isSelected? ColorFilter.mode(kBackgroundLight.withValues(alpha: 0.35), BlendMode.dstATop) : null,
|
||||
image: ImageCustomProvider.getImageProvider(appContext, section.imageId, section.type == SectionType.Video ? getYoutubeThumbnailUrl(rawSubSectionData) : section.imageSource!),
|
||||
): null,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: kBackgroundSecondGrey,
|
||||
spreadRadius: 0.3,
|
||||
blurRadius: 5,
|
||||
offset: Offset(0, 1.5), // changes position of shadow
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String getYoutubeThumbnailUrl(Object rawSectionData) {
|
||||
try{
|
||||
VideoDTO videoDTO = VideoDTO.fromJson(rawSectionData)!;
|
||||
|
||||
String thumbnailUrl = "";
|
||||
if(videoDTO.source_ != null) {
|
||||
//VideoDTO? videoDTO = VideoDTO.fromJson(jsonDecode(sectionDTO.data!));
|
||||
Uri uri = Uri.parse(videoDTO.source_!);
|
||||
String videoId = uri.queryParameters['v']!;
|
||||
// Construire l'URL du thumbnail en utilisant l'identifiant de la vidéo YouTube
|
||||
thumbnailUrl = 'https://img.youtube.com/vi/$videoId/0.jpg';
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
|
||||
} catch(e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
135
lib/Screens/Sections/PDF/pdf_filter.dart
Normal file
135
lib/Screens/Sections/PDF/pdf_filter.dart
Normal file
@ -0,0 +1,135 @@
|
||||
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/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PdfFilter extends StatefulWidget {
|
||||
final List<OrderedTranslationAndResourceDTO> pdfsList;
|
||||
final Function(int?) onPDFSelected;
|
||||
|
||||
PdfFilter({required this.pdfsList, required this.onPDFSelected});
|
||||
|
||||
@override
|
||||
_PdfFilterState createState() => _PdfFilterState();
|
||||
}
|
||||
|
||||
class _PdfFilterState extends State<PdfFilter> with SingleTickerProviderStateMixin {
|
||||
bool _isExpanded = false;
|
||||
bool _showContent = false;
|
||||
int _selectedOrderPdf = 0;
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _widthAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
|
||||
_widthAnimation = Tween<double>(begin: 40, end: 250).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
void toggleExpand() {
|
||||
setState(() {
|
||||
if (_isExpanded) {
|
||||
_showContent = false;
|
||||
_isExpanded = false;
|
||||
} else {
|
||||
_isExpanded = true;
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (_isExpanded) {
|
||||
setState(() => _showContent = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
_isExpanded ? _controller.forward() : _controller.reverse();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
var currentLanguage = visitAppContext.language;
|
||||
|
||||
var primaryColor = visitAppContext.configuration?.primaryColor != null
|
||||
? Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16))
|
||||
: kSecondColor;
|
||||
|
||||
double rounded = visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0;
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _widthAnimation,
|
||||
builder: (context, child) {
|
||||
return Container(
|
||||
width: _widthAnimation.value,
|
||||
height: _isExpanded ? 300 : 75,
|
||||
decoration: BoxDecoration(
|
||||
color: _isExpanded ? primaryColor.withValues(alpha: 0.9) : primaryColor.withValues(alpha: 0.5) ,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(rounded),
|
||||
bottomRight: Radius.circular(rounded),
|
||||
),
|
||||
),
|
||||
child: _showContent
|
||||
? Column(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
onPressed: toggleExpand,
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: widget.pdfsList.length,
|
||||
itemBuilder: (context, index) {
|
||||
var pdfItem = widget.pdfsList[index];
|
||||
var title = pdfItem.translationAndResourceDTOs!
|
||||
.firstWhere((pfat) => pfat.language == currentLanguage)
|
||||
.value;
|
||||
|
||||
bool isSelected = _selectedOrderPdf == pdfItem.order;
|
||||
|
||||
return ListTile(
|
||||
title: HtmlWidget(
|
||||
title!,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 15.0,
|
||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
color: isSelected ? Colors.white : Colors.black,
|
||||
fontFamily: 'Roboto',
|
||||
),
|
||||
customStylesBuilder: (element)
|
||||
{
|
||||
return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"};
|
||||
},
|
||||
),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_selectedOrderPdf = pdfItem.order!;
|
||||
});
|
||||
widget.onPDFSelected(_selectedOrderPdf);
|
||||
toggleExpand(); // collapse after selection
|
||||
},
|
||||
tileColor: isSelected ? primaryColor.withValues(alpha: 0.6) : null,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: _isExpanded ? null : IconButton(
|
||||
icon: const Icon(Icons.menu, color: Colors.white),
|
||||
onPressed: toggleExpand,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
349
lib/Screens/Sections/PDF/pdf_page.dart
Normal file
349
lib/Screens/Sections/PDF/pdf_page.dart
Normal file
@ -0,0 +1,349 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
//import 'dart:html';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_pdfview/flutter_pdfview.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/PDF/pdf_filter.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
||||
class PDFPage extends StatefulWidget {
|
||||
final PdfDTO section;
|
||||
const PDFPage({super.key, required this.section});
|
||||
|
||||
@override
|
||||
_PDFPage createState() => _PDFPage();
|
||||
}
|
||||
|
||||
class _PDFPage extends State<PDFPage> {
|
||||
PdfDTO pdfDTO = PdfDTO();
|
||||
String remotePDFpath = "";
|
||||
final Completer<PDFViewController> _controller = Completer<PDFViewController>();
|
||||
int? pages = 0;
|
||||
int? currentPage = 0;
|
||||
bool isReady = false;
|
||||
String errorMessage = '';
|
||||
late ValueNotifier<OrderedTranslationAndResourceDTO?> selectedPdf = ValueNotifier<OrderedTranslationAndResourceDTO?>(pdfDTO.pdfs!.first);
|
||||
ValueNotifier<Map<String, int>> currentState = ValueNotifier<Map<String, int>>({'page': 0, 'total': 1});
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
/*print(widget.section!.data);
|
||||
pdfDTO = PdfDTO.fromJson(jsonDecode(widget.section!.data!))!;
|
||||
print(pdfDTO);*/
|
||||
pdfDTO = widget.section;
|
||||
pdfDTO.pdfs!.sort((a, b) => a.order!.compareTo(b.order!));
|
||||
|
||||
super.initState();
|
||||
|
||||
/*createFileOfPdfUrl(pdfDTO.source_!).then((f) {
|
||||
setState(() {
|
||||
remotePDFpath = f.path;
|
||||
print("paaath");
|
||||
print(remotePDFpath);
|
||||
});
|
||||
});*/
|
||||
}
|
||||
|
||||
Future<File?> createFileOfPdfUrl(VisitAppContext visitAppContext, OrderedTranslationAndResourceDTO pdfFileDTO) async {
|
||||
Completer<File?> completer = Completer();
|
||||
|
||||
if(pdfFileDTO.translationAndResourceDTOs!.firstWhere((pfat) => pfat.language == visitAppContext.language).resourceId == null) {
|
||||
completer.complete(null);
|
||||
} else {
|
||||
var file = await _checkIfLocalResourceExists(visitAppContext, pdfFileDTO.translationAndResourceDTOs!.firstWhere((pfat) => pfat.language == visitAppContext.language).resourceId!);
|
||||
|
||||
if(file == null) {
|
||||
print("Start download file from internet!");
|
||||
try {
|
||||
// "https://berlin2017.droidcon.cod.newthinking.net/sites/global.droidcon.cod.newthinking.net/files/media/documents/Flutter%20-%2060FPS%20UI%20of%20the%20future%20%20-%20DroidconDE%2017.pdf";
|
||||
// final url = "https://pdfkit.org/docs/guide.pdf";
|
||||
final url = pdfFileDTO.translationAndResourceDTOs!.firstWhere((pfat) => pfat.language == visitAppContext.language).resource!.url!;
|
||||
final filename = url.substring(url.lastIndexOf("/") + 1);
|
||||
var request = await HttpClient().getUrl(Uri.parse(url));
|
||||
var response = await request.close();
|
||||
var bytes = await consolidateHttpClientResponseBytes(response);
|
||||
var dir = await getApplicationDocumentsDirectory();
|
||||
print("Download files");
|
||||
print("${dir.path}/$filename");
|
||||
File file = File("${dir.path}/$filename");
|
||||
|
||||
await file.writeAsBytes(bytes, flush: true);
|
||||
completer.complete(file);
|
||||
} catch (e) {
|
||||
throw Exception('Error parsing asset file!');
|
||||
}
|
||||
} else {
|
||||
print("FOUND FILE PDF");
|
||||
completer.complete(file);
|
||||
}
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
//_webView = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
|
||||
var primaryColor = visitAppContext.configuration != null ? visitAppContext.configuration!.primaryColor != null ? new Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : kSecondColor : kSecondColor;
|
||||
|
||||
pdfDTO.pdfs!.sort((a, b) => a.order!.compareTo(b.order!));
|
||||
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: pdfDTO.pdfs != null && pdfDTO.pdfs!.isNotEmpty ?
|
||||
Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ValueListenableBuilder<OrderedTranslationAndResourceDTO?>(
|
||||
valueListenable: selectedPdf,
|
||||
builder: (context, value, _) {
|
||||
return FutureBuilder(
|
||||
future: createFileOfPdfUrl(visitAppContext, value!),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
print("snapshot.data");
|
||||
print(snapshot.data);
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if(snapshot.data == null) {
|
||||
return Center(child: Text("Aucun fichier à afficher"));
|
||||
} else {
|
||||
return Stack(
|
||||
children: [
|
||||
PDFView(
|
||||
filePath: snapshot.data.path,
|
||||
enableSwipe: true,
|
||||
fitEachPage: true,
|
||||
swipeHorizontal: true,
|
||||
autoSpacing: false,
|
||||
pageFling: true,
|
||||
fitPolicy: FitPolicy.HEIGHT,
|
||||
onRender: (_pages) {
|
||||
//setState(() {
|
||||
pages = _pages;
|
||||
isReady = true;
|
||||
//});
|
||||
},
|
||||
onError: (error) {
|
||||
print(error.toString());
|
||||
},
|
||||
onPageError: (page, error) {
|
||||
print('$page: ${error.toString()}');
|
||||
},
|
||||
onViewCreated: (PDFViewController pdfViewController) {
|
||||
//_controller.complete(pdfViewController);
|
||||
},
|
||||
onPageChanged: (int? page, int? total) {
|
||||
currentPage = page;
|
||||
pages = total;
|
||||
currentState.value = {'page': page!, 'total': total!};
|
||||
|
||||
print('page change: $page/$total');
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
bottom: 20,
|
||||
left: 20,
|
||||
child: ValueListenableBuilder<Map<String, int>>(
|
||||
valueListenable: currentState,
|
||||
builder: (context, value, _) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
color: primaryColor
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text("${value["page"]!+1}/${value["total"]!}",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 20)),
|
||||
)
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Center(
|
||||
child: Container(
|
||||
child: LoadingCommon()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
pdfDTO.pdfs!.length > 1 ? Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: PdfFilter(
|
||||
pdfsList: pdfDTO.pdfs!,
|
||||
onPDFSelected: (selectedOrder) {
|
||||
selectedPdf.value = pdfDTO.pdfs!.firstWhere((pdf) => pdf.order == selectedOrder);
|
||||
}),
|
||||
): const SizedBox(),
|
||||
],
|
||||
),
|
||||
) :
|
||||
Center(child: Text("Aucun pdf à afficher", style: TextStyle(fontSize: kNoneInfoOrIncorrect))))
|
||||
)
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
/**/
|
||||
}
|
||||
} //_webView
|
||||
|
||||
Future<File?> _checkIfLocalResourceExists(VisitAppContext visitAppContext, String resourceId) 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(resourceId))) {
|
||||
File file = File(fileList.firstWhere((fileL) => fileL.uri.pathSegments.last.contains(resourceId)).path);
|
||||
return file;
|
||||
}
|
||||
} catch (e) {
|
||||
print("ERROR _checkIfLocalResourceExists PDF");
|
||||
print(e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:mymuseum_visitapp/Components/Carousel/carousel_slider.dart' as cs;
|
||||
import 'package:carousel_slider/carousel_controller.dart';
|
||||
import 'package:carousel_slider/carousel_slider.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
|
||||
@ -23,7 +24,7 @@ class QuestionsListWidget extends StatefulWidget {
|
||||
|
||||
class _QuestionsListWidget extends State<QuestionsListWidget> {
|
||||
List<QuestionSubDTO> _questionsSubDTO = <QuestionSubDTO>[];
|
||||
cs.CarouselController? sliderController;
|
||||
CarouselSliderController? sliderController;
|
||||
int currentIndex = 1;
|
||||
|
||||
bool kIsWeb = false;
|
||||
@ -31,7 +32,7 @@ class _QuestionsListWidget extends State<QuestionsListWidget> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
sliderController = cs.CarouselController();
|
||||
sliderController = CarouselSliderController();
|
||||
_questionsSubDTO = widget.questionsSubDTO!;
|
||||
}
|
||||
|
||||
@ -57,8 +58,8 @@ class _QuestionsListWidget extends State<QuestionsListWidget> {
|
||||
MediaQuery.of(context).size.height * 0.85 :
|
||||
MediaQuery.of(context).size.height * 0.75 :
|
||||
widget.orientation == Orientation.portrait ?
|
||||
MediaQuery.of(context).size.height :
|
||||
MediaQuery.of(context).size.height,
|
||||
MediaQuery.of(context).size.height * 0.88:
|
||||
MediaQuery.of(context).size.height * 0.88,
|
||||
width: double.infinity,
|
||||
//color: Colors.orange,
|
||||
child: Stack(
|
||||
@ -69,10 +70,10 @@ class _QuestionsListWidget extends State<QuestionsListWidget> {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
if(_questionsSubDTO.isNotEmpty)
|
||||
cs.CarouselSlider(
|
||||
CarouselSlider(
|
||||
carouselController: sliderController,
|
||||
options: cs.CarouselOptions(
|
||||
onPageChanged: (int index, cs.CarouselPageChangedReason reason) {
|
||||
options: CarouselOptions(
|
||||
onPageChanged: (int index, CarouselPageChangedReason reason) {
|
||||
setState(() {
|
||||
currentIndex = index + 1;
|
||||
});
|
||||
512
lib/Screens/Sections/Quiz/quizz_page.dart
Normal file
512
lib/Screens/Sections/Quiz/quizz_page.dart
Normal file
@ -0,0 +1,512 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
import 'dart:typed_data';
|
||||
|
||||
//import 'package:confetti/confetti.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/CustomAppBar.dart';
|
||||
import 'package:mymuseum_visitapp/Components/rounded_button.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/ResponseSubDTO.dart';
|
||||
import 'package:mymuseum_visitapp/Models/articleRead.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
//import 'package:mymuseum_visitapp/Screens/Quizz/drawPath.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Quiz/questions_list.dart';
|
||||
//import 'package:mymuseum_visitapp/Screens/Quizz/showResponses.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class QuizPage extends StatefulWidget {
|
||||
const QuizPage({Key? key, required this.visitAppContextIn, required this.quizDTO, required this.resourcesModel, this.sectionId}) : super(key: key);
|
||||
|
||||
final QuizDTO quizDTO;
|
||||
final VisitAppContext visitAppContextIn;
|
||||
final List<ResourceModel?> resourcesModel;
|
||||
final String? sectionId;
|
||||
|
||||
@override
|
||||
State<QuizPage> createState() => _QuizPageState();
|
||||
}
|
||||
|
||||
class _QuizPageState extends State<QuizPage> {
|
||||
List<ResourceModel?> resourcesModel = <ResourceModel?>[];
|
||||
ResourceModel? audioResourceModel;
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
late Uint8List audiobytes;
|
||||
late VisitAppContext visitAppContext;
|
||||
|
||||
List<QuestionSubDTO> _questionsSubDTO = <QuestionSubDTO>[];
|
||||
//ConfettiController? _controllerCenter;
|
||||
int currentIndex = 1;
|
||||
bool showResult = false;
|
||||
bool showResponses = false;
|
||||
|
||||
bool kIsWeb = false;
|
||||
bool isResultPage = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.visitAppContextIn.isContentCurrentlyShown = true;
|
||||
|
||||
//_controllerCenter = ConfettiController(duration: const Duration(seconds: 10));
|
||||
//_controllerCenter!.play();
|
||||
|
||||
if(widget.quizDTO.questions != null) {
|
||||
_questionsSubDTO = QuestionSubDTO().fromJSON(widget.quizDTO.questions!);
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
visitAppContext.isContentCurrentlyShown = false;
|
||||
currentIndex = 1;
|
||||
//_controllerCenter!.dispose();
|
||||
|
||||
if(widget.quizDTO.questions != null) {
|
||||
_questionsSubDTO = QuestionSubDTO().fromJSON(widget.quizDTO.questions!);
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
Size size = MediaQuery.of(context).size;
|
||||
visitAppContext = appContext.getContext();
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
/*appBar: CustomAppBar(
|
||||
title: TranslationHelper.get(widget.quizDTO.title, visitAppContext),
|
||||
isHomeButton: false,
|
||||
),*/
|
||||
body: Column(
|
||||
children: [
|
||||
Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
Container(
|
||||
height: size.height * 0.12,
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: kMainGrey,
|
||||
spreadRadius: 0.5,
|
||||
blurRadius: 2,
|
||||
offset: Offset(0, 1), // changes position of shadow
|
||||
),
|
||||
],
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.centerRight,
|
||||
end: Alignment.centerLeft,
|
||||
colors: [
|
||||
kMainColor0,
|
||||
kMainColor1,
|
||||
kMainColor2,
|
||||
],
|
||||
),
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(25),
|
||||
bottomRight: Radius.circular(25),
|
||||
),
|
||||
image: widget.quizDTO.imageSource != null ? DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
opacity: 0.65,
|
||||
image: NetworkImage(
|
||||
widget.quizDTO.imageSource!,
|
||||
),
|
||||
): null,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 35,
|
||||
left: 10,
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
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)
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
if(showResult) {
|
||||
var goodResponses = 0;
|
||||
for (var question in _questionsSubDTO) {
|
||||
if(question.chosen == question.responsesSubDTO!.indexWhere((response) => response.isGood!)) {
|
||||
goodResponses +=1;
|
||||
}
|
||||
}
|
||||
log("goodResponses =" + goodResponses.toString());
|
||||
widget.visitAppContextIn.statisticsService?.track(
|
||||
VisitEventType.quizComplete,
|
||||
sectionId: widget.sectionId,
|
||||
metadata: {'score': goodResponses, 'totalQuestions': widget.quizDTO.questions!.length},
|
||||
);
|
||||
List<TranslationAndResourceDTO> levelToShow = [];
|
||||
var test = goodResponses/widget.quizDTO.questions!.length;
|
||||
|
||||
if((0 == test || test < 0.25) && widget.quizDTO.badLevel != null) {
|
||||
levelToShow = widget.quizDTO.badLevel!;
|
||||
}
|
||||
if((test>=0.25 && test < 0.5) && widget.quizDTO.mediumLevel != null) {
|
||||
levelToShow = widget.quizDTO.mediumLevel!;
|
||||
}
|
||||
if((test>=0.5 && test < 0.75) && widget.quizDTO.goodLevel != null) {
|
||||
levelToShow = widget.quizDTO.goodLevel!;
|
||||
}
|
||||
if((test>=0.75 && test <= 1) && widget.quizDTO.greatLevel != null) {
|
||||
levelToShow = widget.quizDTO.greatLevel!;
|
||||
}
|
||||
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
/*Center(
|
||||
child: SizedBox(
|
||||
width: 5,
|
||||
height: 5,
|
||||
child: ConfettiWidget(
|
||||
confettiController: _controllerCenter!,
|
||||
blastDirectionality: BlastDirectionality.explosive,
|
||||
shouldLoop: false, // start again as soon as the animation is finished
|
||||
colors: const [
|
||||
kMainColor,
|
||||
kSecondColor,
|
||||
kConfigurationColor,
|
||||
kMainColor1
|
||||
//Colors.pink,
|
||||
//Colors.orange,
|
||||
//Colors.purple
|
||||
], // manually specify the colors to be used
|
||||
createParticlePath: drawPath, // define a custom shape/path.
|
||||
),
|
||||
),
|
||||
),*/
|
||||
if (orientation == Orientation.portrait)
|
||||
Column(
|
||||
children: [
|
||||
if (!showResponses && levelToShow.firstWhere((label) => label.language == visitAppContext.language).resource?.url != null) // TODO SUPPORT OTHER THAN IMAGES
|
||||
resultImage(visitAppContext, size, levelToShow, orientation),
|
||||
if(!showResponses)
|
||||
// TEXT BOX WITH MAIN SCORE
|
||||
Text('$goodResponses/${widget.quizDTO.questions!.length}', textAlign: TextAlign.center, style: TextStyle(fontSize: kIsWeb ? (showResponses ? 60 : 100) : 75, color: kBackgroundSecondGrey)),
|
||||
],
|
||||
),
|
||||
|
||||
if (orientation == Orientation.landscape)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if(!showResponses)
|
||||
// TEXT BOX WITH MAIN SCORE
|
||||
Text('$goodResponses/${widget.quizDTO.questions!.length}', textAlign: TextAlign.center, style: TextStyle(fontSize: kIsWeb ? (showResponses ? 60 : 100) : 75, color: kBackgroundSecondGrey)),
|
||||
if (!showResponses && levelToShow.firstWhere((label) => label.language == visitAppContext.language).resource?.url != null)
|
||||
resultImage(visitAppContext, size, levelToShow, orientation),
|
||||
],
|
||||
),
|
||||
|
||||
if(!showResponses)
|
||||
// TEXT BOX WITH LEVEL TEXT RESULT
|
||||
resultText(size, levelToShow, appContext),
|
||||
if(showResponses)
|
||||
QuestionsListWidget(
|
||||
questionsSubDTO: _questionsSubDTO,
|
||||
isShowResponse: true,
|
||||
onShowResponse: () {},
|
||||
orientation: orientation,
|
||||
),
|
||||
// RESPONSE BOX
|
||||
//ShowReponsesWidget(questionsSubDTO: _questionsSubDTO),
|
||||
|
||||
if(orientation == Orientation.portrait && !showResponses)
|
||||
// Buttons
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: resultButtons(size, orientation, visitAppContext),
|
||||
),
|
||||
if(orientation == Orientation.landscape && !showResponses)
|
||||
// Buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: resultButtons(size, orientation, visitAppContext),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
} else {
|
||||
return QuestionsListWidget(
|
||||
isShowResponse: false,
|
||||
questionsSubDTO: _questionsSubDTO,
|
||||
onShowResponse: () {
|
||||
setState(() {
|
||||
showResult = true;
|
||||
});
|
||||
},
|
||||
orientation: orientation,
|
||||
);
|
||||
}
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: showResponses ? FloatingActionButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
showResult = false;
|
||||
showResponses = false;
|
||||
currentIndex = 1;
|
||||
_questionsSubDTO = QuestionSubDTO().fromJSON(widget.quizDTO.questions!);
|
||||
});
|
||||
},
|
||||
backgroundColor: kBackgroundSecondGrey,
|
||||
child: const Icon(Icons.undo),
|
||||
) : null,
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.miniEndFloat,
|
||||
);
|
||||
}
|
||||
|
||||
/*Future<QuizDTO?> getQuizz(AppContext appContext, Client client, String sectionId) async {
|
||||
try {
|
||||
if(sectionDTO == null || quizDTO == null) {
|
||||
bool isConfigOffline = (appContext.getContext() as VisitAppContext).configuration!.isOffline!;
|
||||
if(isConfigOffline)
|
||||
{
|
||||
// OFFLINE
|
||||
List<Map<String, dynamic>> sectionTest = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.sections, sectionId);
|
||||
if(sectionTest.isNotEmpty) {
|
||||
sectionDTO = DatabaseHelper.instance.getSectionFromDB(sectionTest.first);
|
||||
try {
|
||||
SectionRead sectionRead = SectionRead(id: sectionDTO!.id!, readTime: DateTime.now().millisecondsSinceEpoch);
|
||||
await DatabaseHelper.instance.insert(DatabaseTableType.articleRead, sectionRead.toMap());
|
||||
visitAppContext.readSections.add(sectionRead);
|
||||
|
||||
appContext.setContext(visitAppContext);
|
||||
} catch (e) {
|
||||
print("DATABASE ERROR SECTIONREAD");
|
||||
print(e);
|
||||
}
|
||||
} else {
|
||||
print("EMPTY SECTION");
|
||||
}
|
||||
} else
|
||||
{
|
||||
// ONLINE
|
||||
SectionDTO? sectionOnline = await client.sectionApi!.sectionGetDetail(sectionId);
|
||||
if(sectionOnline != null) {
|
||||
sectionDTO = sectionOnline;
|
||||
} else {
|
||||
print("EMPTY SECTION");
|
||||
}
|
||||
}
|
||||
|
||||
if(sectionDTO!.type == SectionType.Quiz) {
|
||||
quizDTO = QuizDTO.fromJson(jsonDecode(sectionDTO!.data!));
|
||||
}
|
||||
if(quizDTO != null) {
|
||||
quizDTO!.questions!.sort((a, b) => a.order!.compareTo(b.order!));
|
||||
_questionsSubDTO = QuestionSubDTO().fromJSON(quizDTO!.questions!);
|
||||
if(quizDTO!.questions != null && quizDTO!.questions!.isNotEmpty) {
|
||||
quizDTO!.questions!.sort((a, b) => a.order!.compareTo(b.order!));
|
||||
for (var question in quizDTO!.questions!) {
|
||||
if(isConfigOffline)
|
||||
{
|
||||
// OFFLINE
|
||||
if(question.imageBackgroundResourceId != null) {
|
||||
List<Map<String, dynamic>> ressourceQuizz = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.resources, question.imageBackgroundResourceId!);
|
||||
if(ressourceQuizz.isNotEmpty) {
|
||||
resourcesModel.add(DatabaseHelper.instance.getResourceFromDB(ressourceQuizz.first));
|
||||
} else {
|
||||
print("EMPTY resourcesModel - second");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ONLINE
|
||||
if(question.imageBackgroundResourceId != null) {
|
||||
resourcesModel.add(ResourceModel(id: question.imageBackgroundResourceId, source: question.imageBackgroundResourceUrl, type: ResourceType.Image));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
//print(sectionDTO!.title);
|
||||
});
|
||||
} else {
|
||||
return null; // TODO return local list..
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
print("IN CATCH");
|
||||
return null;
|
||||
}
|
||||
}*/
|
||||
|
||||
resultImage(VisitAppContext visitAppContext, Size size, List<TranslationAndResourceDTO> levelToShow, Orientation orientation) {
|
||||
return Container(
|
||||
//height: size.height * 0.2,
|
||||
//width: size.width * 0.25,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: size.height * 0.25,
|
||||
maxWidth: kIsWeb ? size.width * 0.20 : orientation == Orientation.portrait ? size.width * 0.85 : size.width * 0.4,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
image: levelToShow.where((label) => label.language == visitAppContext.language).isNotEmpty ? DecorationImage(
|
||||
fit: BoxFit.contain,
|
||||
opacity: 0.85,
|
||||
image: NetworkImage(
|
||||
levelToShow.firstWhere((label) => label.language == visitAppContext.language).resource!.url!,
|
||||
),
|
||||
): null,
|
||||
borderRadius: const BorderRadius.all( Radius.circular(50.0)),
|
||||
border: Border.all(
|
||||
color: kBackgroundGrey,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
//borderRadius: BorderRadius.all(Radius.circular(25.0)),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff7c94b6),
|
||||
image: DecorationImage(
|
||||
image: NetworkImage(
|
||||
levelToShow.firstWhere((label) => label.language == visitAppContext.language).resource!.url!, // TODO REDUNDANCY here??
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
borderRadius: const BorderRadius.all( Radius.circular(50.0)),
|
||||
border: Border.all(
|
||||
color: kBackgroundGrey,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
resultText(Size size, List<TranslationAndResourceDTO> levelToShow, AppContext appContext) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: Container(
|
||||
width: size.width *0.75,
|
||||
height: kIsWeb ? (showResponses ? size.height *0.10 : size.height *0.20) : size.height *0.25,
|
||||
decoration: BoxDecoration(
|
||||
color: kBackgroundLight, //kBackgroundLight
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(10.0),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: kBackgroundSecondGrey,
|
||||
spreadRadius: 0.3,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(15.0),
|
||||
child: HtmlWidget(
|
||||
TranslationHelper.getWithResource(levelToShow, appContext.getContext() as VisitAppContext),textStyle: const TextStyle(fontFamily: 'Roboto', fontSize: 20),
|
||||
customStylesBuilder: (element)
|
||||
{
|
||||
return {'text-align': 'center', 'font-family': "Roboto", '-webkit-line-clamp': "2"};
|
||||
}
|
||||
)
|
||||
//Text(, textAlign: TextAlign.center, style: TextStyle(fontSize: kIsWeb ? kDescriptionSize : kDescriptionSize)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
resultButtons(Size size, Orientation orientation, VisitAppContext visitAppContext) {
|
||||
return [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: SizedBox(
|
||||
height: kIsWeb ? 50 : 40,
|
||||
width: orientation == Orientation.portrait ? size.width * 0.6 : size.width * 0.35,
|
||||
child: RoundedButton(
|
||||
text: TranslationHelper.getFromLocale("restart", visitAppContext),
|
||||
color: kBackgroundSecondGrey,
|
||||
textColor: kBackgroundLight,
|
||||
icon: Icons.undo,
|
||||
press: () {
|
||||
setState(() {
|
||||
showResult = false;
|
||||
showResponses = false;
|
||||
currentIndex = 1;
|
||||
_questionsSubDTO = QuestionSubDTO().fromJSON(widget.quizDTO.questions!);
|
||||
});
|
||||
},
|
||||
fontSize: 18,
|
||||
horizontal: 20,
|
||||
vertical: 5
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: SizedBox(
|
||||
height: kIsWeb ? 50 : 40,
|
||||
width: orientation == Orientation.portrait ? size.width * 0.6 : size.width * 0.35,
|
||||
child: RoundedButton(
|
||||
text: TranslationHelper.getFromLocale("responses", visitAppContext),
|
||||
color: kBackgroundSecondGrey,
|
||||
textColor: kBackgroundLight,
|
||||
icon: Icons.assignment_turned_in,
|
||||
press: () {
|
||||
setState(() {
|
||||
showResponses = true;
|
||||
});
|
||||
},
|
||||
fontSize: 18,
|
||||
horizontal: 20,
|
||||
vertical: 5
|
||||
),
|
||||
),
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
||||
359
lib/Screens/Sections/Slider/slider_page.dart
Normal file
359
lib/Screens/Sections/Slider/slider_page.dart
Normal file
@ -0,0 +1,359 @@
|
||||
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 visitAppContext = appContext.getContext() as VisitAppContext;
|
||||
Color? primaryColor = visitAppContext.configuration!.primaryColor != null ? Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : null;
|
||||
|
||||
configurationDTO = appContext.getContext().configuration;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
color: kBackgroundLight,
|
||||
),
|
||||
if(sliderDTO.contents != null && sliderDTO.contents!.isNotEmpty)
|
||||
Center(
|
||||
child: CarouselSlider(
|
||||
carouselController: sliderController,
|
||||
options: CarouselOptions(
|
||||
onPageChanged: (int index, CarouselPageChangedReason reason) {
|
||||
currentIndex.value = index + 1;
|
||||
},
|
||||
enableInfiniteScroll: false,
|
||||
height: MediaQuery.of(context).size.height * 0.92,
|
||||
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: kBackgroundGrey,
|
||||
//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(visitAppContext.currentAppConfigurationLink?.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: kBackgroundLight,// kBackgroundLight,
|
||||
shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.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: ExpandingDotsEffect(activeDotColor: primaryColor!),
|
||||
);
|
||||
|
||||
/*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: kBackgroundLight,
|
||||
shape: BoxShape.rectangle,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: kBackgroundGrey,
|
||||
spreadRadius: 0.3,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2), // changes position of shadow
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.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: kBackgroundLight,
|
||||
shape: BoxShape.rectangle,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: kBackgroundGrey,
|
||||
spreadRadius: 0.3,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2), // changes position of shadow
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 15.0),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case ResourceType.Video:
|
||||
case ResourceType.VideoUrl:
|
||||
case ResourceType.Audio:
|
||||
widgetToInclude = Container(
|
||||
decoration: BoxDecoration(
|
||||
color: kBackgroundLight,
|
||||
//shape: BoxShape.rectangle,
|
||||
borderRadius: BorderRadius.circular(visitAppContext.currentAppConfigurationLink?.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,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: ClipRect(
|
||||
child: widgetToInclude,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
263
lib/Screens/Sections/Video/video_page.dart
Normal file
263
lib/Screens/Sections/Video/video_page.dart
Normal file
@ -0,0 +1,263 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.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/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
//import 'package:youtube_player_iframe/youtube_player_iframe.dart' as iframe;
|
||||
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
||||
|
||||
class VideoPage extends StatefulWidget {
|
||||
final VideoDTO section;
|
||||
VideoPage({required this.section});
|
||||
|
||||
@override
|
||||
_VideoPage createState() => _VideoPage();
|
||||
}
|
||||
|
||||
class _VideoPage extends State<VideoPage> {
|
||||
//iframe.YoutubePlayer? _videoViewWeb;
|
||||
YoutubePlayer? _videoView;
|
||||
VideoDTO? videoDTO;
|
||||
late YoutubePlayerController _controller;
|
||||
bool isFullScreen = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//print(widget.section!.data);
|
||||
//videoDTO = VideoDTO.fromJson(jsonDecode(widget.section!.data!));
|
||||
//print(videoDTO);
|
||||
videoDTO= widget.section;
|
||||
|
||||
String? videoId;
|
||||
if (videoDTO!.source_ != null && videoDTO!.source_!.isNotEmpty) {
|
||||
videoId = YoutubePlayer.convertUrlToId(videoDTO!.source_!);
|
||||
|
||||
/*if (false) {
|
||||
final _controllerWeb = iframe.YoutubePlayerController(
|
||||
params: iframe.YoutubePlayerParams(
|
||||
mute: false,
|
||||
showControls: true,
|
||||
showFullscreenButton: false,
|
||||
loop: true,
|
||||
showVideoAnnotations: false,
|
||||
strictRelatedVideos: false,
|
||||
enableKeyboard: false,
|
||||
enableCaption: false,
|
||||
pointerEvents: iframe.PointerEvents.auto
|
||||
),
|
||||
);
|
||||
|
||||
_controllerWeb.loadVideo(videoDTO!.source_!);
|
||||
|
||||
_videoViewWeb = iframe.YoutubePlayer(
|
||||
controller: _controllerWeb,
|
||||
//showVideoProgressIndicator: false,
|
||||
/*progressIndicatorColor: Colors.amber,
|
||||
progressColors: ProgressBarColors(
|
||||
playedColor: Colors.amber,
|
||||
handleColor: Colors.amberAccent,
|
||||
),*/
|
||||
);
|
||||
} else {*/
|
||||
videoId = YoutubePlayer.convertUrlToId(videoDTO!.source_!);
|
||||
_controller = YoutubePlayerController(
|
||||
initialVideoId: videoId!,
|
||||
flags: const YoutubePlayerFlags(
|
||||
autoPlay: true,
|
||||
controlsVisibleAtStart: false,
|
||||
loop: true,
|
||||
hideControls: false,
|
||||
hideThumbnail: true,
|
||||
),
|
||||
)..addListener(_onYoutubePlayerChanged);
|
||||
|
||||
_videoView = YoutubePlayer(
|
||||
controller: _controller,
|
||||
//showVideoProgressIndicator: false,
|
||||
progressIndicatorColor: kMainColor,
|
||||
progressColors: const ProgressBarColors(
|
||||
playedColor: kMainColor,
|
||||
handleColor: kSecondColor,
|
||||
),
|
||||
);
|
||||
//}
|
||||
_controller.toggleFullScreenMode();
|
||||
super.initState();
|
||||
}
|
||||
}
|
||||
|
||||
void _onYoutubePlayerChanged() {
|
||||
if (_controller.value.isFullScreen) {
|
||||
setState(() {
|
||||
isFullScreen = true;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
isFullScreen = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_videoView = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
|
||||
var title = TranslationHelper.get(widget.section.title, appContext.getContext());
|
||||
String cleanedTitle = title.replaceAll('\n', ' ').replaceAll('<br>', ' ');
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
!isFullScreen ? 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,
|
||||
),
|
||||
) : const SizedBox(),
|
||||
Column(
|
||||
children: <Widget>[
|
||||
!isFullScreen ? 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: () {
|
||||
_controller.dispose();
|
||||
_videoView = null;
|
||||
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)
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
): const SizedBox(),
|
||||
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: !isFullScreen ? const BorderRadius.only(
|
||||
topLeft: Radius.circular(30),
|
||||
topRight: Radius.circular(30),
|
||||
): BorderRadius.zero,
|
||||
child: videoDTO!.source_ != null && videoDTO!.source_!.isNotEmpty ?
|
||||
_videoView :
|
||||
const Center(child: Text("La vidéo ne peut pas être affichée, l'url est incorrecte", style: TextStyle(fontSize: kNoneInfoOrIncorrect))))
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
isFullScreen ? Positioned(
|
||||
top: 35,
|
||||
left: 10,
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
_controller.toggleFullScreenMode();
|
||||
_controller.dispose();
|
||||
_videoView = null;
|
||||
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)
|
||||
),
|
||||
)
|
||||
),
|
||||
): const SizedBox(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
382
lib/Screens/Sections/Weather/weather_page.dart
Normal file
382
lib/Screens/Sections/Weather/weather_page.dart
Normal file
@ -0,0 +1,382 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Models/weatherData.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class WeatherPage extends StatefulWidget {
|
||||
final WeatherDTO section;
|
||||
WeatherPage({required this.section});
|
||||
|
||||
@override
|
||||
State<WeatherPage> createState() => _WeatherPageState();
|
||||
}
|
||||
|
||||
class _WeatherPageState extends State<WeatherPage> {
|
||||
WeatherDTO weatherDTO = WeatherDTO();
|
||||
WeatherData? weatherData = null;
|
||||
int nbrNextHours = 5;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
/*print(widget.section!.data);
|
||||
weatherDTO = WeatherDTO.fromJson(jsonDecode(widget.section!.data!))!;
|
||||
print(weatherDTO);*/
|
||||
weatherDTO = widget.section;
|
||||
if(weatherDTO.result != null) {
|
||||
Map<String, dynamic> weatherResultInJson = jsonDecode(weatherDTO.result!);
|
||||
weatherData = WeatherData.fromJson(weatherResultInJson);
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
String formatTimestamp(int timestamp, AppContext appContext, bool isHourOnly, bool isDateOnly) {
|
||||
|
||||
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
||||
|
||||
// Determine the date format based on the application language
|
||||
String dateFormat = appContext.getContext().language.toString().toUpperCase() == "EN" ?
|
||||
'MM/dd/yyyy HH:mm'
|
||||
: 'dd/MM/yyyy HH:mm';
|
||||
|
||||
if(isHourOnly) {
|
||||
dateFormat = 'HH:mm';
|
||||
}
|
||||
|
||||
if(isDateOnly) {
|
||||
dateFormat = dateFormat.replaceAll('/yyyy HH:mm', '');
|
||||
}
|
||||
|
||||
String formattedDate = DateFormat(dateFormat).format(dateTime);
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
String getTranslatedDayOfWeek(int timestamp, AppContext appContext, bool isDate) {
|
||||
DateTime dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
|
||||
|
||||
String dayToPrint = "";
|
||||
|
||||
print("dateTime.weekday");
|
||||
print(dateTime.weekday);
|
||||
|
||||
switch(dateTime.weekday) {
|
||||
case 1:
|
||||
dayToPrint = TranslationHelper.getFromLocale("monday", appContext.getContext());
|
||||
break;
|
||||
case 2:
|
||||
dayToPrint = TranslationHelper.getFromLocale("tuesday", appContext.getContext());
|
||||
break;
|
||||
case 3:
|
||||
dayToPrint = TranslationHelper.getFromLocale("wednesday", appContext.getContext());
|
||||
break;
|
||||
case 4:
|
||||
dayToPrint = TranslationHelper.getFromLocale("thursday", appContext.getContext());
|
||||
break;
|
||||
case 5:
|
||||
dayToPrint = TranslationHelper.getFromLocale("friday", appContext.getContext());
|
||||
break;
|
||||
case 6:
|
||||
dayToPrint = TranslationHelper.getFromLocale("saturday", appContext.getContext());
|
||||
break;
|
||||
case 7:
|
||||
dayToPrint = TranslationHelper.getFromLocale("sunday", appContext.getContext());
|
||||
break;
|
||||
}
|
||||
|
||||
return isDate ? "${dayToPrint} ${formatTimestamp(timestamp, appContext, false, true)}" : dayToPrint;
|
||||
}
|
||||
|
||||
List<WeatherForecast> getNextFiveDaysForecast(List<WeatherForecast> allForecasts) {
|
||||
List<WeatherForecast> nextFiveDaysForecast = [];
|
||||
DateTime today = DateTime.now();
|
||||
|
||||
List<WeatherForecast> nextDay1All = allForecasts.where((af) => (DateTime.fromMillisecondsSinceEpoch(af.dt! * 1000)).day == (today.add(Duration(days: 1))).day).toList();
|
||||
List<WeatherForecast> nextDay2All = allForecasts.where((af) => (DateTime.fromMillisecondsSinceEpoch(af.dt! * 1000)).day == (today.add(Duration(days: 2))).day).toList();
|
||||
List<WeatherForecast> nextDay3All = allForecasts.where((af) => (DateTime.fromMillisecondsSinceEpoch(af.dt! * 1000)).day == (today.add(Duration(days: 3))).day).toList();
|
||||
List<WeatherForecast> nextDay4All = allForecasts.where((af) => (DateTime.fromMillisecondsSinceEpoch(af.dt! * 1000)).day == (today.add(Duration(days: 4))).day).toList();
|
||||
List<WeatherForecast> nextDay5All = allForecasts.where((af) => (DateTime.fromMillisecondsSinceEpoch(af.dt! * 1000)).day == (today.add(Duration(days: 5))).day).toList();
|
||||
|
||||
var nextDay1MiddayTest = nextDay1All.where((nd) => (DateTime.fromMillisecondsSinceEpoch(nd.dt! * 1000)).hour == 12).firstOrNull;
|
||||
if(nextDay1All.isNotEmpty) {
|
||||
WeatherForecast nextDay1AllSummary = nextDay1MiddayTest ?? nextDay1All.last;
|
||||
nextFiveDaysForecast.add(nextDay1AllSummary);
|
||||
}
|
||||
|
||||
var nextDay2MiddayTest = nextDay2All.where((nd) => (DateTime.fromMillisecondsSinceEpoch(nd.dt! * 1000)).hour == 12).firstOrNull;
|
||||
if(nextDay2All.isNotEmpty) {
|
||||
WeatherForecast nextDay2Midday = nextDay2MiddayTest ?? nextDay2All.last;
|
||||
nextFiveDaysForecast.add(nextDay2Midday);
|
||||
}
|
||||
|
||||
var nextDay3MiddayTest = nextDay3All.where((nd) => (DateTime.fromMillisecondsSinceEpoch(nd.dt! * 1000)).hour == 12).firstOrNull;
|
||||
if(nextDay3All.isNotEmpty) {
|
||||
WeatherForecast nextDay3Midday = nextDay3MiddayTest ?? nextDay3All.last;
|
||||
nextFiveDaysForecast.add(nextDay3Midday);
|
||||
}
|
||||
|
||||
var nextDay4MiddayTest = nextDay4All.where((nd) => (DateTime.fromMillisecondsSinceEpoch(nd.dt! * 1000)).hour == 12).firstOrNull;
|
||||
if(nextDay4All.isNotEmpty) {
|
||||
WeatherForecast nextDay4Midday = nextDay4MiddayTest ?? nextDay4All.last;
|
||||
nextFiveDaysForecast.add(nextDay4Midday);
|
||||
}
|
||||
|
||||
var nextDay5MiddayTest = nextDay5All.where((nd) => (DateTime.fromMillisecondsSinceEpoch(nd.dt! * 1000)).hour == 12).firstOrNull;
|
||||
if(nextDay5All.isNotEmpty) {
|
||||
WeatherForecast nextDay5Midday = nextDay5MiddayTest ?? nextDay5All.last;
|
||||
nextFiveDaysForecast.add(nextDay5Midday);
|
||||
}
|
||||
|
||||
return nextFiveDaysForecast;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
VisitAppContext visitAppContext = appContext.getContext();
|
||||
|
||||
var primaryColor = visitAppContext.configuration != null ? visitAppContext.configuration!.primaryColor != null ? Color(int.parse(visitAppContext.configuration!.primaryColor!.split('(0x')[1].split(')')[0], radix: 16)) : kSecondColor : kSecondColor;
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
weatherData == null ? const Center(child: Text("Aucune donnée à afficher")) : Container( // TODO translate ?
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topRight,
|
||||
end: Alignment.bottomLeft,
|
||||
stops: const [
|
||||
0.2,
|
||||
0.5,
|
||||
0.9,
|
||||
0.95
|
||||
],
|
||||
colors: [
|
||||
Colors.blue[50]!,
|
||||
Colors.blue[100]!,
|
||||
Colors.blue[200]!,
|
||||
Colors.blue[300]!
|
||||
]
|
||||
)
|
||||
),
|
||||
//color: Colors.yellow,
|
||||
//height: 300,
|
||||
//width: 300,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(child: Text(weatherDTO.city!, style: const TextStyle(fontSize: kSectionTitleDetailSize, fontWeight: FontWeight.w500, color: Colors.black54, fontFamily: "Roboto"))),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text("${weatherData!.list!.first.main!.temp!.round().toString()}°", style: const TextStyle(fontSize: 55.0, fontWeight: FontWeight.w400, color: Colors.black54, fontFamily: "Roboto")),
|
||||
),
|
||||
Container(
|
||||
//color: Colors.green,
|
||||
height: size.height * 0.2,
|
||||
width: size.width * 0.45,
|
||||
constraints: BoxConstraints(minWidth: 80),
|
||||
child: Center(
|
||||
child: CachedNetworkImage(imageUrl: "https://openweathermap.org/img/wn/${weatherData!.list!.first.weather!.first.icon!}@4x.png")
|
||||
)
|
||||
),
|
||||
Container(
|
||||
// color: Colors.green,
|
||||
width: size.width * 0.2,
|
||||
//color: Colors.red,
|
||||
constraints: BoxConstraints(minWidth: 100),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.water_drop_outlined, color: kSecondColor),
|
||||
Text("${weatherData!.list!.first.pop!.round().toString()}%", style: const TextStyle(fontSize: 15.0, fontWeight: FontWeight.w400, color: Colors.black54, fontFamily: "Roboto")),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.air, color: kSecondColor),
|
||||
Text("${(weatherData!.list!.first.wind!.speed! * 3.6).toStringAsFixed(1)}km/h", style: const TextStyle(fontSize: 15.0, fontWeight: FontWeight.w400, color: Colors.black54, fontFamily: "Roboto")),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]),
|
||||
Container(
|
||||
height: size.height * 0.25,
|
||||
width: size.width,
|
||||
/*decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
//color: Colors.grey,
|
||||
),*/
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 15, bottom: 10),
|
||||
child: Align(alignment: Alignment.centerLeft, child: Text(TranslationHelper.getFromLocale("weather.hourly", appContext.getContext()), style: const TextStyle(fontSize: 22, fontWeight: FontWeight.w400, color: Colors.black54, fontFamily: "Roboto"))),
|
||||
),
|
||||
Container(
|
||||
height: size.height * 0.18,
|
||||
width: size.width,
|
||||
//color: Colors.lightGreen,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: List.generate(
|
||||
nbrNextHours,
|
||||
(index) {
|
||||
final weatherForecast = weatherData!.list!.sublist(1)[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
height: size.height * 0.15,
|
||||
width: size.width * 0.25,
|
||||
constraints: const BoxConstraints(minWidth: 125, maxWidth: 250),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
color: Colors.lightBlueAccent,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: kBackgroundGrey.withValues(alpha: 0.6),
|
||||
spreadRadius: 0.75,
|
||||
blurRadius: 3.1,
|
||||
offset: Offset(0, 2.5), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(formatTimestamp(weatherForecast.dt!, appContext, true, false), style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.w400, color: Colors.white, fontFamily: "Roboto")),
|
||||
Center(child: CachedNetworkImage(imageUrl: "https://openweathermap.org/img/wn/${weatherForecast.weather!.first.icon!}.png")),
|
||||
Text('${weatherForecast.main!.temp!.round().toString()}°', style: const TextStyle(fontSize: 20.0, fontWeight: FontWeight.w600, color: Colors.white, fontFamily: "Roboto")),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(0.0),
|
||||
child: Container(
|
||||
height: size.height * 0.3,
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
//color: Colors.amber,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Align(alignment: Alignment.centerLeft, child: Text(TranslationHelper.getFromLocale("weather.nextdays", appContext.getContext()), style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w400, color: Colors.black54, fontFamily: "Roboto"))),
|
||||
),
|
||||
Container(
|
||||
height: size.height * 0.23,
|
||||
width: size.width,
|
||||
//color: Colors.lightGreen,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children:List.generate(
|
||||
getNextFiveDaysForecast(weatherData!.list!).length, // nbrNextHours
|
||||
(index) {
|
||||
final weatherForecastNextDay = getNextFiveDaysForecast(weatherData!.list!)[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
height: size.height * 0.22,
|
||||
width: size.width * 0.125,
|
||||
constraints: const BoxConstraints(minWidth: 150, maxWidth: 250),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(visitAppContext.currentAppConfigurationLink?.roundedValue?.toDouble() ?? 20.0)),
|
||||
color: Colors.lightBlue,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: kBackgroundGrey.withValues(alpha: 0.5),
|
||||
spreadRadius: 0.75,
|
||||
blurRadius: 3.1,
|
||||
offset: const Offset(0, 2.5), // changes position of shadow
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(child: CachedNetworkImage(imageUrl: "https://openweathermap.org/img/wn/${weatherForecastNextDay.weather!.first.icon!}@2x.png")),
|
||||
Text('${weatherForecastNextDay.main!.temp!.round().toString()}°', style: const TextStyle(fontSize: 25.0, fontWeight: FontWeight.w600, color: Colors.white, fontFamily: "Roboto")),
|
||||
Text(getTranslatedDayOfWeek(weatherForecastNextDay.dt!, appContext, true), style: const TextStyle(fontSize: 16.0, fontWeight: FontWeight.w400, color: Colors.white, fontFamily: "Roboto")),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 35,
|
||||
left: 10,
|
||||
child: SizedBox(
|
||||
width: 50,
|
||||
height: 50,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: primaryColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.arrow_back, size: 23, color: Colors.white)
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//_webView
|
||||
242
lib/Screens/Sections/Web/web_page.dart
Normal file
242
lib/Screens/Sections/Web/web_page.dart
Normal file
@ -0,0 +1,242 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.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/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
|
||||
|
||||
class WebPage extends StatefulWidget {
|
||||
final WebDTO section;
|
||||
WebPage({required this.section});
|
||||
|
||||
@override
|
||||
_WebPage createState() => _WebPage();
|
||||
}
|
||||
|
||||
class _WebPage extends State<WebPage> {
|
||||
//final IFrameElement _iframeElement = IFrameElement();
|
||||
//WebView _webView;
|
||||
WebDTO webDTO = WebDTO();
|
||||
WebViewController? controller;
|
||||
PlatformWebViewController? controllerWeb;
|
||||
bool isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//print(widget.section!.data);
|
||||
webDTO = widget.section;
|
||||
//webDTO = WebDTO.fromJson(jsonDecode(widget.section!.data!))!;
|
||||
print(webDTO);
|
||||
|
||||
|
||||
if(kIsWeb) {
|
||||
/*_iframeElement.src = webDTO.source_!;
|
||||
_iframeElement.style.border = 'none';
|
||||
|
||||
//ignore: undefined_prefixed_name
|
||||
ui.platformViewRegistry.registerViewFactory(
|
||||
webDTO.source_!, //use source as registered key to ensure uniqueness
|
||||
(int viewId) => _iframeElement,
|
||||
);*/
|
||||
} else {
|
||||
|
||||
if(webDTO.source_ != null && webDTO.source_!.length > 0) {
|
||||
try {
|
||||
controller = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setBackgroundColor(const Color(0x00000000))
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onProgress: (int progress) {
|
||||
// Update loading bar.
|
||||
},
|
||||
onPageStarted: (String url) {},
|
||||
onPageFinished: (String url) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
},
|
||||
onWebResourceError: (WebResourceError error) {},
|
||||
onNavigationRequest: (NavigationRequest request) {
|
||||
Uri sourceUri = Uri.parse(webDTO.source_!);
|
||||
Uri requestUri = Uri.parse(request.url);
|
||||
|
||||
if (requestUri.host != sourceUri.host) { // handle navigation to site
|
||||
print('blocking navigation to $request}');
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
),
|
||||
)
|
||||
..loadRequest(Uri.parse(webDTO.source_!));
|
||||
} catch (e) {
|
||||
print("Invalid source ${webDTO.source_}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.initState();
|
||||
/*_webView = WebView(
|
||||
initialUrl: webDTO.source_, //"https://my.matterport.com/show/?m=k8bvdezfHbT"
|
||||
javascriptMode: JavascriptMode.unrestricted,
|
||||
navigationDelegate: (NavigationRequest request) {
|
||||
print(request.url);
|
||||
print(webDTO.source_);
|
||||
if (request.url != webDTO.source_) {
|
||||
print('blocking navigation to $request}');
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
print('allowing navigation to $request');
|
||||
return NavigationDecision.navigate;
|
||||
}
|
||||
);*/
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
//_webView = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Size size = MediaQuery.of(context).size;
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
|
||||
var title = TranslationHelper.get(widget.section.title, appContext.getContext());
|
||||
String cleanedTitle = title.replaceAll('\n', ' ').replaceAll('<br>', ' ');
|
||||
|
||||
return webDTO.source_ != null && webDTO.source_!.isNotEmpty ?
|
||||
kIsWeb ?
|
||||
HtmlElementView(
|
||||
key: UniqueKey(),
|
||||
viewType: webDTO.source_!,
|
||||
) :
|
||||
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: isLoading ? Center(child: SizedBox(height: size.height * 0.15, child: const LoadingCommon())) : WebViewWidget(controller: controller!))
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
) :
|
||||
const Center(child: Text("La page internet ne peut pas être affichée, l'url est incorrecte ou vide", style: TextStyle(fontSize: kNoneInfoOrIncorrect)));
|
||||
}
|
||||
} //_webView
|
||||
@ -1,190 +0,0 @@
|
||||
import 'package:diacritic/diacritic.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SearchBox.dart';
|
||||
import 'package:mymuseum_visitapp/Components/SearchNumberBox.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Article/article_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Quizz/quizz_page.dart';
|
||||
import 'package:mymuseum_visitapp/Services/apiService.dart';
|
||||
import 'package:mymuseum_visitapp/app_context.dart';
|
||||
import 'package:mymuseum_visitapp/constants.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'section_card.dart';
|
||||
|
||||
class Body extends StatefulWidget {
|
||||
const Body({Key? key, required this.configurationId}) : super(key: key);
|
||||
|
||||
final String? configurationId;
|
||||
|
||||
@override
|
||||
State<Body> createState() => _BodyState();
|
||||
}
|
||||
|
||||
class _BodyState extends State<Body> {
|
||||
List<SectionDTO> sections = [];
|
||||
List<SectionDTO> sectionsToDisplay = [];
|
||||
String? searchValue;
|
||||
int? searchNumberValue;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
Size size = MediaQuery.of(context).size;
|
||||
|
||||
return SafeArea(
|
||||
bottom: false,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
SearchBox(onChanged: (value) {
|
||||
setState(() {
|
||||
if(value != null && value != "") {
|
||||
searchValue = value;
|
||||
} else {
|
||||
searchValue = null;
|
||||
}
|
||||
});
|
||||
}),
|
||||
Expanded(
|
||||
child: SearchNumberBox(onChanged: (value) {
|
||||
setState(() {
|
||||
if(value != null && value != "") {
|
||||
searchNumberValue = int.parse(value);
|
||||
} else {
|
||||
searchNumberValue = null;
|
||||
FocusScope.of(context).unfocus();
|
||||
}
|
||||
});
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
//const SizedBox(height: kDefaultPadding / 2),
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
// Our background
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 0),
|
||||
decoration: const BoxDecoration(
|
||||
color: kBackgroundColor,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(40),
|
||||
topRight: Radius.circular(40),
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: getSections(appContext),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
/*print("SECTIONTODISPA");
|
||||
print(sectionsToDisplay);*/
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 0),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
if(!(appContext.getContext() as VisitAppContext).configuration!.isOffline!) {
|
||||
// Force refresh if online
|
||||
setState(() {});
|
||||
} },
|
||||
child: ListView.builder(
|
||||
itemCount: sectionsToDisplay.length,
|
||||
itemBuilder: (context, index) => SectionCard(
|
||||
itemCount: sectionsToDisplay.length,
|
||||
itemIndex: index,
|
||||
sectionDTO: sectionsToDisplay[index],
|
||||
press: () {
|
||||
switch(sectionsToDisplay[index].type) {
|
||||
case SectionType.Article:
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ArticlePage(
|
||||
visitAppContextIn: appContext.getContext(),
|
||||
articleId: sectionsToDisplay[index].id!,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
case SectionType.Quizz:
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => QuizzPage(
|
||||
visitAppContextIn: appContext.getContext(),
|
||||
sectionId: sectionsToDisplay[index].id!,
|
||||
),
|
||||
),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// TODO HANDLE, SHOW NOT SUPPORTED
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else if (snapshot.connectionState == ConnectionState.none) {
|
||||
return Text(TranslationHelper.getFromLocale("noData", appContext.getContext()));
|
||||
} else {
|
||||
return Center(
|
||||
child: Container(
|
||||
height: size.height * 0.15,
|
||||
child: LoadingCommon()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getSections(AppContext appContext) async {
|
||||
VisitAppContext visitAppContext = (appContext.getContext() as VisitAppContext);
|
||||
if(visitAppContext.configuration!.isOffline!)
|
||||
{
|
||||
// OFFLINE
|
||||
sections = List<SectionDTO>.from(await DatabaseHelper.instance.getData(DatabaseTableType.sections));
|
||||
}
|
||||
else
|
||||
{
|
||||
// ONLINE
|
||||
List<SectionDTO>? sectionsDownloaded = await ApiService.getAllSections(visitAppContext.clientAPI, visitAppContext.configuration!.id!);
|
||||
//print(sectionsDownloaded);
|
||||
if(sectionsDownloaded!.isNotEmpty) {
|
||||
sections = sectionsDownloaded.where((s) => s.type == SectionType.Article || s.type == SectionType.Quizz).toList(); // TODO Support more than Article and Quizz section type
|
||||
//print(sections);
|
||||
}
|
||||
}
|
||||
|
||||
sections = sections.where((s) => s.configurationId == widget.configurationId).toList();
|
||||
sections.sort((a,b) => a.order!.compareTo(b.order!));
|
||||
sectionsToDisplay = sections;
|
||||
|
||||
visitAppContext.currentSections = sectionsToDisplay;
|
||||
|
||||
if(searchValue != '' && searchValue != null) {
|
||||
sectionsToDisplay = sections.where((s) => removeDiacritics(TranslationHelper.get(s.title, appContext.getContext()).toLowerCase()).contains(removeDiacritics(searchValue.toString().toLowerCase()))).toList();
|
||||
} else {
|
||||
if(searchNumberValue != null) {
|
||||
sectionsToDisplay = sections.where((s) => s.order!+1 == searchNumberValue).toList();
|
||||
} else {
|
||||
sectionsToDisplay = sections;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
384
lib/Screens/section_page.dart
Normal file
384
lib/Screens/section_page.dart
Normal file
@ -0,0 +1,384 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
//import 'package:confetti/confetti.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Components/loading_common.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Models/articleRead.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Agenda/agenda_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Article/article_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/map_context.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Map/map_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Menu/menu_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/PDF/pdf_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Game/game_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Quiz/quizz_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Slider/slider_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Video/video_page.dart';
|
||||
import 'package:mymuseum_visitapp/Screens/Sections/Weather/weather_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';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:image/image.dart' as IMG;
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class SectionPage extends StatefulWidget {
|
||||
const SectionPage({Key? key, required this.rawSection, required this.visitAppContextIn, required this.configuration, required this.sectionId}) : super(key: key);
|
||||
|
||||
final Object? rawSection;
|
||||
final String sectionId;
|
||||
final ConfigurationDTO configuration;
|
||||
final VisitAppContext visitAppContextIn;
|
||||
|
||||
@override
|
||||
State<SectionPage> createState() => _SectionPageState();
|
||||
}
|
||||
|
||||
class _SectionPageState extends State<SectionPage> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
SectionDTO? sectionDTO;
|
||||
late VisitAppContext visitAppContext;
|
||||
late dynamic rawSectionData;
|
||||
List<ResourceModel?> resourcesModel = <ResourceModel?>[];
|
||||
|
||||
late final MapContext mapContext = MapContext(null);
|
||||
List<Map<String, dynamic>>? icons;
|
||||
|
||||
String? mainAudioId;
|
||||
DateTime? _sectionOpenTime;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
widget.visitAppContextIn.isContentCurrentlyShown = true;
|
||||
_sectionOpenTime = DateTime.now();
|
||||
super.initState();
|
||||
// Track SectionView (fire-and-forget)
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.visitAppContextIn.statisticsService?.track(
|
||||
VisitEventType.sectionView,
|
||||
sectionId: widget.sectionId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
visitAppContext.isContentCurrentlyShown = false;
|
||||
// Track SectionLeave with duration
|
||||
final duration = _sectionOpenTime != null
|
||||
? DateTime.now().difference(_sectionOpenTime!).inSeconds
|
||||
: null;
|
||||
widget.visitAppContextIn.statisticsService?.track(
|
||||
VisitEventType.sectionLeave,
|
||||
sectionId: widget.sectionId,
|
||||
durationSeconds: duration,
|
||||
);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appContext = Provider.of<AppContext>(context);
|
||||
Size size = MediaQuery.of(context).size;
|
||||
visitAppContext = appContext.getContext();
|
||||
|
||||
var test = SectionDTO.fromJson(jsonDecode(jsonEncode(widget.rawSection)));
|
||||
|
||||
return Scaffold(
|
||||
key: _scaffoldKey,
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: MediaQuery.removeViewInsets(
|
||||
context: context,
|
||||
removeBottom: true,
|
||||
child: OrientationBuilder(
|
||||
builder: (context, orientation) {
|
||||
return FutureBuilder(
|
||||
future: getSectionDetail(appContext, visitAppContext.clientAPI, widget.sectionId),
|
||||
builder: (context, AsyncSnapshot<dynamic> snapshot) {
|
||||
var sectionResult = snapshot.data;
|
||||
if(sectionDTO != null && sectionResult != null) {
|
||||
switch(sectionDTO!.type) {
|
||||
case SectionType.Agenda:
|
||||
AgendaDTO agendaDTO = AgendaDTO.fromJson(sectionResult)!;
|
||||
return AgendaPage(section: agendaDTO);
|
||||
case SectionType.Article:
|
||||
ArticleDTO articleDTO = ArticleDTO.fromJson(sectionResult)!;
|
||||
return ArticlePage(
|
||||
visitAppContextIn: widget.visitAppContextIn,
|
||||
articleDTO: articleDTO,
|
||||
resourcesModel: resourcesModel,
|
||||
mainAudioId: mainAudioId,
|
||||
);
|
||||
case SectionType.Map:
|
||||
MapDTO mapDTO = MapDTO.fromJson(sectionResult)!;
|
||||
return ChangeNotifierProvider<MapContext>.value(
|
||||
value: mapContext,
|
||||
child: MapPage(section: mapDTO, icons: icons ?? []),
|
||||
);
|
||||
case SectionType.Menu:
|
||||
MenuDTO menuDTO = MenuDTO.fromJson(sectionResult)!;
|
||||
return MenuPage(section: menuDTO, isImageBackground: visitAppContext.currentAppConfigurationLink?.isSectionImageBackground ?? false);
|
||||
case SectionType.Pdf:
|
||||
PdfDTO pdfDTO = PdfDTO.fromJson(sectionResult)!;
|
||||
return PDFPage(section: pdfDTO);
|
||||
case SectionType.Game:
|
||||
GameDTO gameDTO = GameDTO.fromJson(sectionResult)!;
|
||||
return GamePage(section: gameDTO);
|
||||
case SectionType.Quiz:
|
||||
QuizDTO quizDTO = QuizDTO.fromJson(sectionResult)!;
|
||||
return QuizPage(
|
||||
visitAppContextIn: widget.visitAppContextIn,
|
||||
quizDTO: quizDTO,
|
||||
resourcesModel: resourcesModel,
|
||||
sectionId: widget.sectionId,
|
||||
);
|
||||
case SectionType.Slider:
|
||||
SliderDTO sliderDTO = SliderDTO.fromJson(sectionResult)!;
|
||||
return SliderPage(section: sliderDTO);
|
||||
case SectionType.Video:
|
||||
VideoDTO videoDTO = VideoDTO.fromJson(sectionResult)!;
|
||||
return VideoPage(section: videoDTO);
|
||||
case SectionType.Weather:
|
||||
WeatherDTO weatherDTO = WeatherDTO.fromJson(sectionResult)!;
|
||||
return WeatherPage(section: weatherDTO);
|
||||
case SectionType.Web:
|
||||
WebDTO webDTO = WebDTO.fromJson(sectionResult)!;
|
||||
return WebPage(section: webDTO);
|
||||
default:
|
||||
return const Center(child: Text("Unsupported type"));
|
||||
}
|
||||
} else {
|
||||
return const LoadingCommon();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Future<dynamic> getSectionDetail(AppContext appContext, Client client, String sectionId) async {
|
||||
try {
|
||||
bool isConfigOffline = widget.configuration.isOffline!;
|
||||
if(widget.rawSection == null) {
|
||||
if(isConfigOffline)
|
||||
{
|
||||
// OFFLINE
|
||||
List<Map<String, dynamic>> sectionTest = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.sections, sectionId);
|
||||
if(sectionTest.isNotEmpty) {
|
||||
sectionDTO = DatabaseHelper.instance.getSectionFromDB(sectionTest.first);
|
||||
try {
|
||||
SectionRead sectionRead = SectionRead(id: sectionDTO!.id!, readTime: DateTime.now().millisecondsSinceEpoch);
|
||||
await DatabaseHelper.instance.insert(DatabaseTableType.articleRead, sectionRead.toMap());
|
||||
visitAppContext.readSections.add(sectionRead);
|
||||
|
||||
appContext.setContext(visitAppContext);
|
||||
} catch (e) {
|
||||
print("DATABASE ERROR SECTIONREAD");
|
||||
print(e);
|
||||
}
|
||||
} else {
|
||||
print("EMPTY SECTION");
|
||||
}
|
||||
} else
|
||||
{
|
||||
// ONLINE
|
||||
rawSectionData = await client.sectionApi!.sectionGetDetail(sectionId);
|
||||
SectionDTO sectionOnline = jsonDecode(jsonEncode(rawSectionData)).map((json) => SectionDTO.fromJson(json)).whereType<SectionDTO>().toList();
|
||||
sectionDTO = sectionOnline;
|
||||
}
|
||||
|
||||
/*setState(() {
|
||||
//print(sectionDTO!.title);
|
||||
});*/
|
||||
} else {
|
||||
rawSectionData = widget.rawSection;
|
||||
sectionDTO = SectionDTO.fromJson(jsonDecode(jsonEncode(rawSectionData)));
|
||||
}
|
||||
|
||||
switch(sectionDTO!.type)
|
||||
{
|
||||
case SectionType.Quiz:
|
||||
QuizDTO? quizDTO = QuizDTO.fromJson(rawSectionData);
|
||||
if(quizDTO != null) {
|
||||
quizDTO.questions!.sort((a, b) => a.order!.compareTo(b.order!));
|
||||
if(quizDTO.questions != null && quizDTO.questions!.isNotEmpty) {
|
||||
quizDTO.questions!.sort((a, b) => a.order!.compareTo(b.order!));
|
||||
for (var question in quizDTO.questions!) {
|
||||
if(isConfigOffline)
|
||||
{
|
||||
// OFFLINE
|
||||
if(question.imageBackgroundResourceId != null) {
|
||||
List<Map<String, dynamic>> ressourceQuizz = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.resources, question.imageBackgroundResourceId!);
|
||||
if(ressourceQuizz.isNotEmpty) {
|
||||
resourcesModel.add(DatabaseHelper.instance.getResourceFromDB(ressourceQuizz.first));
|
||||
} else {
|
||||
print("EMPTY resourcesModel - second");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ONLINE
|
||||
if(question.imageBackgroundResourceId != null) {
|
||||
resourcesModel.add(ResourceModel(id: question.imageBackgroundResourceId, source: question.imageBackgroundResourceUrl, type: ResourceType.Image));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case SectionType.Article:
|
||||
ArticleDTO articleDTO = ArticleDTO.fromJson(rawSectionData)!;
|
||||
var audioToDownload = articleDTO.audioIds!.firstWhere((a) => a.language == visitAppContext.language!).value;
|
||||
var othersToDownload = articleDTO.contents!.map((c) => c.resourceId);
|
||||
mainAudioId = audioToDownload;
|
||||
var resourcesToDownload = [];
|
||||
resourcesToDownload.add(audioToDownload);
|
||||
resourcesToDownload.addAll(othersToDownload);
|
||||
|
||||
for (var resourceToDownload in resourcesToDownload) {
|
||||
if(isConfigOffline)
|
||||
{
|
||||
// OFFLINE
|
||||
List<Map<String, dynamic>> ressourceArticle = await DatabaseHelper.instance.queryWithColumnId(DatabaseTableType.resources, resourceToDownload);
|
||||
if(ressourceArticle.isNotEmpty) {
|
||||
resourcesModel.add(DatabaseHelper.instance.getResourceFromDB(ressourceArticle.first));
|
||||
} else {
|
||||
print("EMPTY resourcesModel - second");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ONLINE
|
||||
ResourceDTO? resourceDTO = await client.resourceApi!.resourceGetDetail(resourceToDownload);
|
||||
if(resourceDTO != null && resourceDTO.url != null) {
|
||||
// ONLINE
|
||||
//ResourceModel? resourceAudioOnline = await ApiService.downloadAudio(client, resourceDTO.url!, resourceDTO.id!);
|
||||
ResourceModel resourceAudioOnline = ResourceModel();
|
||||
resourceAudioOnline.id = resourceDTO.id;
|
||||
resourceAudioOnline.source = resourceDTO.url;
|
||||
resourceAudioOnline.type = resourceDTO.type;
|
||||
|
||||
resourcesModel.add(resourceAudioOnline);
|
||||
|
||||
/*Uint8List base64String = base64Decode(resourceAudioOnline.path!); // GET FROM FILE
|
||||
audiobytes = base64String;*/
|
||||
} else {
|
||||
print("EMPTY resourcesModel online - audio");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case SectionType.Map:
|
||||
MapDTO mapDTO = MapDTO.fromJson(rawSectionData)!;
|
||||
icons = await getByteIcons(visitAppContext, mapDTO);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return rawSectionData;
|
||||
|
||||
} catch (e) {
|
||||
print(e);
|
||||
print("IN CATCH");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> getBytesFromAsset(ByteData data, int width) async {
|
||||
//ByteData data = await rootBundle.load(path);
|
||||
ui.Codec codec = await ui.instantiateImageCodec(data.buffer.asUint8List(), targetWidth: width);
|
||||
ui.FrameInfo fi = await codec.getNextFrame();
|
||||
return (await fi.image.toByteData(format: ui.ImageByteFormat.png))!.buffer.asUint8List();
|
||||
}
|
||||
|
||||
Uint8List resizeImage(Uint8List data, int width) {
|
||||
Uint8List resizedData = data;
|
||||
IMG.Image img = IMG.decodeImage(data)!;
|
||||
IMG.Image resized = IMG.copyResize(img, width: width);
|
||||
resizedData = Uint8List.fromList(IMG.encodeJpg(resized));
|
||||
return resizedData;
|
||||
}
|
||||
|
||||
Future<File?> _checkIfLocalResourceExists(VisitAppContext visitAppContext, String iconId) 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(iconId))) {
|
||||
File file = File(fileList.firstWhere((fileL) => fileL.uri.pathSegments.last.contains(iconId)).path);
|
||||
return file;
|
||||
}
|
||||
} catch(e) {
|
||||
print("ERROR _checkIfLocalResourceExists CachedCustomResource");
|
||||
print(e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getByteIcons(VisitAppContext visitAppContext, MapDTO mapDTO) async {
|
||||
//var mapDTO = MapDTO.fromJson(jsonDecode(section.data!));
|
||||
Uint8List selectedMarkerIcon;
|
||||
if (mapDTO.iconSource != null) {
|
||||
if (kIsWeb) {
|
||||
Uint8List fileData = await http.readBytes(Uri.parse(mapDTO.iconSource!));
|
||||
selectedMarkerIcon = resizeImage(fileData, 40);
|
||||
} else {
|
||||
File? localIcon = await _checkIfLocalResourceExists(visitAppContext, mapDTO.iconResourceId!);
|
||||
if(localIcon == null) {
|
||||
final ByteData imageData = await NetworkAssetBundle(Uri.parse(mapDTO.iconSource!)).load("");
|
||||
selectedMarkerIcon = await getBytesFromAsset(imageData, 50);
|
||||
} else {
|
||||
Uint8List bytes = await localIcon.readAsBytes();
|
||||
selectedMarkerIcon = await getBytesFromAsset(ByteData.view(bytes.buffer), 50);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Icône par défaut
|
||||
final ByteData bytes = await rootBundle.load('assets/icons/marker.png');
|
||||
selectedMarkerIcon = await getBytesFromAsset(bytes, 25);
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> icons = [];
|
||||
|
||||
icons.add({'id': null, 'icon': selectedMarkerIcon});
|
||||
|
||||
// Utiliser Future.forEach() pour itérer de manière asynchrone sur la liste des catégories
|
||||
await Future.forEach(mapDTO.categories!, (cat) async {
|
||||
if (cat.resourceDTO != null && cat.resourceDTO!.url != null && cat.resourceDTO!.id != null) {
|
||||
Uint8List categoryIcon;
|
||||
if (kIsWeb) {
|
||||
categoryIcon = await http.readBytes(Uri.parse(cat.resourceDTO!.url!));
|
||||
} else {
|
||||
File? localIcon = await _checkIfLocalResourceExists(visitAppContext, cat.resourceDTO!.id!);
|
||||
if(localIcon == null) {
|
||||
final ByteData imageData = await NetworkAssetBundle(Uri.parse(cat.resourceDTO!.url!)).load("");
|
||||
categoryIcon = await getBytesFromAsset(imageData, 50);
|
||||
} else {
|
||||
Uint8List bytes = await localIcon.readAsBytes();
|
||||
categoryIcon = await getBytesFromAsset(ByteData.view(bytes.buffer), 50);
|
||||
}
|
||||
}
|
||||
icons.add({'id': cat.id, 'icon': categoryIcon});
|
||||
}
|
||||
});
|
||||
|
||||
return icons;
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/networkCheck.dart';
|
||||
import 'package:mymuseum_visitapp/Models/resourceModel.dart';
|
||||
@ -15,14 +15,14 @@ class ApiService {
|
||||
try {
|
||||
List<ConfigurationDTO>? configurations;
|
||||
bool isOnline = await hasNetwork();
|
||||
if(isOnline) {
|
||||
if(true) { // TODO isOnline
|
||||
configurations = await client.configurationApi!.configurationGet(instanceId: visitAppContext?.instanceId);
|
||||
|
||||
if(configurations != null) {
|
||||
if(configurations.isNotEmpty) {
|
||||
for(var configuration in configurations) {
|
||||
if(configuration.imageId != null) {
|
||||
await downloadAndPushLocalImage(client, ContentDTO(resourceUrl: configuration.imageSource, resourceId: configuration.imageId));
|
||||
await downloadAndPushLocalImage(client, ContentDTO(resourceId: configuration.imageId, resource: ResourceDTO(url: configuration.imageSource, id: configuration.imageId)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -40,12 +40,13 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<SectionDTO>?> getAllSections(Client client, String configurationId) async {
|
||||
static Future<List<dynamic>?> getAllSections(Client client, String configurationId) async {
|
||||
try {
|
||||
bool isOnline = await hasNetwork();
|
||||
if(isOnline) {
|
||||
List<SectionDTO>? sections = await client.sectionApi!.sectionGetFromConfiguration(configurationId);
|
||||
return sections;
|
||||
final rawList = await client.sectionApi!.sectionGetFromConfigurationDetail(configurationId);
|
||||
var sections = rawList?.map((json) => SectionDTO.fromJson(json)).toList();
|
||||
return rawList ?? [];
|
||||
} else {
|
||||
return []; // TODO return local list..
|
||||
}
|
||||
@ -67,7 +68,7 @@ class ApiService {
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
print("getAllSections IN CATCH");
|
||||
print("getAllBeacons IN CATCH");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -84,7 +85,7 @@ class ApiService {
|
||||
if(isOnline) {
|
||||
ResourceModel? resourceModel = await downloadImage(client, contentDTO);
|
||||
if(resourceModel != null) {
|
||||
await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap());
|
||||
//await DatabaseHelper.instance.insert(DatabaseTableType.resources, resourceModel.toMap());
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
@ -98,13 +99,13 @@ class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ResourceModel?> downloadImage(Client client, ContentDTO contentDTO) async {
|
||||
var source = contentDTO.resourceUrl;
|
||||
static Future<ResourceModel?> downloadImage(Client client, ContentDTO contentDTO) async { // TODO CHECK wtf mymuseum
|
||||
var source = contentDTO.resource!.url;
|
||||
//print("SOURCE getAndDownloadImage");
|
||||
if(contentDTO.resourceUrl != null) {
|
||||
if(contentDTO.resourceUrl!.contains("localhost:5000")){
|
||||
if(contentDTO.resource!.url != null) {
|
||||
if(contentDTO.resource!.url!.contains("localhost:5000")){
|
||||
print("Contains localhost:5000");
|
||||
source = contentDTO.resourceUrl!.replaceAll("http://localhost:5000", client.apiApi!.basePath);
|
||||
source = contentDTO.resource!.url!.replaceAll("http://localhost:5000", client.apiApi!.basePath);
|
||||
}
|
||||
} else {
|
||||
source = "https://api.mymuseum.be/api/Resource/"+contentDTO.resourceId!; // TODO UPDATE ROUTE
|
||||
@ -118,7 +119,7 @@ class ApiService {
|
||||
await for(dynamic d in response) { _downloadData.addAll(d); }
|
||||
//print("AFTER");
|
||||
final base64Str = base64.encode(_downloadData);
|
||||
ResourceModel resourceModel = ResourceModel(id: contentDTO.resourceId, source: contentDTO.resourceUrl, path: base64Str, type: ResourceType.Image);
|
||||
ResourceModel resourceModel = ResourceModel(id: contentDTO.resourceId, source: contentDTO.resource!.url, path: base64Str, type: ResourceType.Image);
|
||||
return resourceModel;
|
||||
}
|
||||
|
||||
@ -162,7 +163,7 @@ class ApiService {
|
||||
}
|
||||
|
||||
static Future<File?> getResource(AppContext appContext, ConfigurationDTO configurationDTO, String imageId) async {
|
||||
if((appContext.getContext() as VisitAppContext).configuration == null || (appContext.getContext() as VisitAppContext).configuration!.isOffline!)
|
||||
if(configurationDTO.isOffline!)
|
||||
{
|
||||
Directory? appDocumentsDirectory = Platform.isIOS ? await getApplicationDocumentsDirectory() : await getDownloadsDirectory();
|
||||
String localPath = appDocumentsDirectory!.path;
|
||||
@ -192,8 +193,7 @@ class ApiService {
|
||||
try {
|
||||
bool isOnline = await hasNetwork();
|
||||
if(isOnline) {
|
||||
ExportConfigurationDTO? exportConfiguration = await client.configurationApi!.configurationExport(configurationId, language);
|
||||
|
||||
ExportConfigurationDTO? exportConfiguration = await client.configurationApi!.configurationExport(configurationId, language: language);
|
||||
return exportConfiguration;
|
||||
} else {
|
||||
return null; // TODO return local list..
|
||||
|
||||
53
lib/Services/assistantService.dart
Normal file
53
lib/Services/assistantService.dart
Normal file
@ -0,0 +1,53 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Models/visitContext.dart';
|
||||
import 'package:mymuseum_visitapp/Models/AssistantResponse.dart';
|
||||
|
||||
class AiChatMessage {
|
||||
final String role;
|
||||
final String content;
|
||||
AiChatMessage({required this.role, required this.content});
|
||||
Map<String, dynamic> toJson() => {'role': role, 'content': content};
|
||||
}
|
||||
|
||||
class AssistantService {
|
||||
final VisitAppContext visitAppContext;
|
||||
final List<AiChatMessage> history = [];
|
||||
|
||||
AssistantService({required this.visitAppContext});
|
||||
|
||||
Future<AssistantResponse> chat({
|
||||
required String message,
|
||||
String? configurationId,
|
||||
}) async {
|
||||
final baseUrl = visitAppContext.clientAPI.apiApi?.basePath ?? 'https://api.mymuseum.be';
|
||||
|
||||
final body = {
|
||||
'message': message,
|
||||
'instanceId': visitAppContext.instanceId,
|
||||
'appType': 'Mobile',
|
||||
'configurationId': configurationId,
|
||||
'language': visitAppContext.language?.toUpperCase() ?? 'FR',
|
||||
'history': history.map((m) => m.toJson()).toList(),
|
||||
};
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl/api/ai/chat'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode(body),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body) as Map<String, dynamic>;
|
||||
final result = AssistantResponse.fromJson(data);
|
||||
history.add(AiChatMessage(role: 'user', content: message));
|
||||
history.add(AiChatMessage(role: 'assistant', content: result.reply));
|
||||
return result;
|
||||
} else {
|
||||
throw Exception('Erreur assistant: ${response.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
void clearHistory() => history.clear();
|
||||
}
|
||||
@ -2,7 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/DatabaseHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/modelsHelper.dart';
|
||||
import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
|
||||
@ -61,7 +61,7 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
||||
ExportConfigurationDTO? exportConfigurationDTO;
|
||||
try{
|
||||
// Retrieve all url from resource to download (get all resource from configuration en somme)
|
||||
exportConfigurationDTO = await visitAppContext.clientAPI.configurationApi!.configurationExport(widget.configuration.id!, isAllLanguages ? null : visitAppContext.language); // tabletAppContext.configuration!.id! // 65c5f0ee4c030e63ce16bff5 TODO Remove
|
||||
exportConfigurationDTO = await visitAppContext.clientAPI.configurationApi!.configurationExport(widget.configuration.id!, language: isAllLanguages ? null : visitAppContext.language); // tabletAppContext.configuration!.id! // 65c5f0ee4c030e63ce16bff5 TODO Remove
|
||||
} catch(e) {
|
||||
print("Erreur lors du téléchargement de la configuration et de ses ressources !");
|
||||
print(e);
|
||||
@ -148,7 +148,7 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
||||
if(sections!.isNotEmpty) {
|
||||
|
||||
List<SectionDTO> sectionsInDB = await DatabaseHelper.instance.queryWithConfigurationId(DatabaseTableType.sections, widget.configuration.id!);
|
||||
List<SectionDTO> sectionsToKeep = sections.where((s) => s.type == SectionType.Article || s.type == SectionType.Quizz).toList(); // TODO handle other type of section (for now, Article and Quizz)
|
||||
List<SectionDTO> sectionsToKeep = sections.where((s) => s.type == SectionType.Article || s.type == SectionType.Quiz).toList(); // TODO handle other type of section (for now, Article and Quizz)
|
||||
|
||||
sectionsToKeep.sort((a,b) => a.order!.compareTo(b.order!));
|
||||
int newOrder = 0;
|
||||
@ -176,8 +176,10 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Endpoint back with all resources ? Just all get all resourceDTO from a configuration..
|
||||
|
||||
// Download all images..
|
||||
ArticleDTO? articleDTO = ArticleDTO.fromJson(jsonDecode(section.data!));
|
||||
/*ArticleDTO? articleDTO = ArticleDTO.fromJson(jsonDecode(section.data!)); // TODO section data
|
||||
|
||||
if(articleDTO != null) {
|
||||
for(var image in articleDTO.contents!) {
|
||||
@ -200,7 +202,7 @@ class _DownloadConfigurationWidgetState extends State<DownloadConfigurationWidge
|
||||
//audiosNotWorking = await importAudio(visitAppContext, exportConfigurationDTO, audioId.value!, audiosNotWorking);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
newOrder = newOrder + 1;
|
||||
}
|
||||
|
||||
|
||||
52
lib/Services/statisticsService.dart
Normal file
52
lib/Services/statisticsService.dart
Normal file
@ -0,0 +1,52 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:manager_api_new/api.dart';
|
||||
import 'package:mymuseum_visitapp/client.dart';
|
||||
|
||||
class StatisticsService {
|
||||
final Client clientAPI;
|
||||
final String? instanceId;
|
||||
final String? configurationId;
|
||||
final String? appType; // "Mobile" or "Tablet"
|
||||
final String? language;
|
||||
final String sessionId;
|
||||
|
||||
StatisticsService({
|
||||
required this.clientAPI,
|
||||
required this.instanceId,
|
||||
required this.configurationId,
|
||||
this.appType = 'Mobile',
|
||||
this.language,
|
||||
}) : sessionId = _generateSessionId();
|
||||
|
||||
static String _generateSessionId() {
|
||||
final rand = Random();
|
||||
final bytes = List<int>.generate(16, (_) => rand.nextInt(256));
|
||||
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
}
|
||||
|
||||
Future<void> track(
|
||||
String eventType, {
|
||||
String? sectionId,
|
||||
int? durationSeconds,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) async {
|
||||
try {
|
||||
await clientAPI.statsApi!.statsTrackEvent(VisitEventDTO(
|
||||
instanceId: instanceId ?? '',
|
||||
configurationId: configurationId,
|
||||
sectionId: sectionId,
|
||||
sessionId: sessionId,
|
||||
eventType: eventType,
|
||||
appType: appType,
|
||||
language: language,
|
||||
durationSeconds: durationSeconds,
|
||||
metadata: metadata != null ? jsonEncode(metadata) : null,
|
||||
timestamp: DateTime.now(),
|
||||
));
|
||||
} catch (_) {
|
||||
// fire-and-forget — never block the UI on stats errors
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,12 @@
|
||||
// Openapi Generator last run: : 2025-05-27T14:44:29.936214
|
||||
import 'package:openapi_generator_annotations/openapi_generator_annotations.dart';
|
||||
|
||||
@Openapi(
|
||||
additionalProperties:
|
||||
AdditionalProperties(pubName: 'manager_api', pubAuthor: 'Fransolet Thomas', useEnumExtension: true),
|
||||
inputSpecFile: 'lib/api/swagger.yaml',
|
||||
AdditionalProperties(pubName: 'manager_api_new', pubAuthor: 'Fransolet Thomas', useEnumExtension: true),
|
||||
inputSpec: InputSpec(path: 'lib/api/swagger.yaml'),
|
||||
generatorName: Generator.dart,
|
||||
alwaysRun: true,
|
||||
outputDirectory: 'manager_api')
|
||||
outputDirectory: 'manager_api_new')
|
||||
class Example extends OpenapiGeneratorConfig {}
|
||||
|
||||
/*
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//import 'package:managerapi/api.dart';
|
||||
//import 'package:openapi_dart_common/openapi.dart';
|
||||
|
||||
import 'package:manager_api/api.dart';
|
||||
import 'package:manager_api_new/api.dart';
|
||||
|
||||
class Client {
|
||||
ApiClient? _apiClient;
|
||||
@ -25,15 +25,26 @@ class Client {
|
||||
DeviceApi? _deviceApi;
|
||||
DeviceApi? get deviceApi => _deviceApi;
|
||||
|
||||
Client(String path) {
|
||||
_apiClient = ApiClient(
|
||||
basePath: path); // "http://192.168.31.96"
|
||||
//basePath: "https://localhost:44339");
|
||||
InstanceApi? _instanceApi;
|
||||
InstanceApi? get instanceApi => _instanceApi;
|
||||
|
||||
ApplicationInstanceApi? _applicationInstanceApi;
|
||||
ApplicationInstanceApi? get applicationInstanceApi => _applicationInstanceApi;
|
||||
|
||||
StatsApi? _statsApi;
|
||||
StatsApi? get statsApi => _statsApi;
|
||||
|
||||
Client(String path, {String? apiKey}) {
|
||||
_apiClient = ApiClient(basePath: path);
|
||||
if (apiKey != null) _apiClient!.addDefaultHeader('X-Api-Key', apiKey);
|
||||
_authenticationApi = AuthenticationApi(_apiClient);
|
||||
_userApi = UserApi(_apiClient);
|
||||
_configurationApi = ConfigurationApi(_apiClient);
|
||||
_sectionApi = SectionApi(_apiClient);
|
||||
_resourceApi = ResourceApi(_apiClient);
|
||||
_deviceApi = DeviceApi(_apiClient);
|
||||
_instanceApi = InstanceApi(_apiClient);
|
||||
_applicationInstanceApi = ApplicationInstanceApi(_apiClient);
|
||||
_statsApi = StatsApi(_apiClient);
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// API configuration
|
||||
const kApiBaseUrl = 'http://192.168.31.228:5000'; // Replace with production URL
|
||||
const kInstancePinCode = ''; // TODO: fill in the instance PIN code
|
||||
const kInstanceId = '63514fd67ed8c735aaa4b8f2'; // TODO: fill in the instance ID
|
||||
|
||||
// Colors - TO FILL WITH CORRECT COLOR
|
||||
const kBackgroundColor = Color(0xFFFFFFFF);
|
||||
const kMainColor = Color(0xFF306bac);
|
||||
|
||||
145
lib/l10n/app_localizations.dart
Normal file
145
lib/l10n/app_localizations.dart
Normal file
@ -0,0 +1,145 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_fr.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations? of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('fr')
|
||||
];
|
||||
|
||||
/// No description provided for @visitTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'List of tours'**
|
||||
String get visitTitle;
|
||||
|
||||
/// No description provided for @visitDownloadWarning.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'To follow this tour, you must first download it'**
|
||||
String get visitDownloadWarning;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['en', 'fr'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
case 'fr':
|
||||
return AppLocalizationsFr();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.');
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user