From c50083b19f70fb84fe84ec89e3fb9b2e37bd3409 Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Thu, 3 Jul 2025 17:37:24 +0200 Subject: [PATCH] Agenda + misc for sub menu (pdf, agenda) --- lib/Models/agenda.dart | 128 +++++ lib/Screens/Sections/Agenda/agenda_page.dart | 198 ++++++++ .../Sections/Agenda/event_list_item.dart | 163 ++++++ lib/Screens/Sections/Agenda/event_popup.dart | 475 ++++++++++++++++++ lib/Screens/Sections/Agenda/month_filter.dart | 207 ++++++++ .../Sections/Map/geo_point_filter.dart | 1 - lib/Screens/Sections/PDF/pdf_filter.dart | 27 +- lib/Screens/section_page.dart | 6 +- pubspec.lock | 2 +- pubspec.yaml | 1 + 10 files changed, 1197 insertions(+), 11 deletions(-) create mode 100644 lib/Models/agenda.dart create mode 100644 lib/Screens/Sections/Agenda/agenda_page.dart create mode 100644 lib/Screens/Sections/Agenda/event_list_item.dart create mode 100644 lib/Screens/Sections/Agenda/event_popup.dart create mode 100644 lib/Screens/Sections/Agenda/month_filter.dart diff --git a/lib/Models/agenda.dart b/lib/Models/agenda.dart new file mode 100644 index 0000000..0c2439e --- /dev/null +++ b/lib/Models/agenda.dart @@ -0,0 +1,128 @@ +import 'dart:convert'; + +class Agenda { + List events; + + Agenda({required this.events}); + + factory Agenda.fromJson(String jsonString) { + final List jsonList = json.decode(jsonString); + List 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 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 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'], + ); + } +} diff --git a/lib/Screens/Sections/Agenda/agenda_page.dart b/lib/Screens/Sections/Agenda/agenda_page.dart new file mode 100644 index 0000000..63021b9 --- /dev/null +++ b/lib/Screens/Sections/Agenda/agenda_page.dart @@ -0,0 +1,198 @@ +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 { + AgendaDTO agendaDTO = AgendaDTO(); + late Agenda agenda; + late ValueNotifier> filteredAgenda = ValueNotifier>([]); + 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 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 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(context).getContext(); + + return FutureBuilder(future: getAndParseJsonInfo(visitAppContext), + builder: (BuildContext context, AsyncSnapshot 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>( + 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}"); + 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 : []; + 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 \ No newline at end of file diff --git a/lib/Screens/Sections/Agenda/event_list_item.dart b/lib/Screens/Sections/Agenda/event_list_item.dart new file mode 100644 index 0000000..10593c2 --- /dev/null +++ b/lib/Screens/Sections/Agenda/event_list_item.dart @@ -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(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.configuration!.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.configuration!.roundedValue?.toDouble() ?? 20.0), topRight: Radius.circular(visitAppContext.configuration!.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.configuration!.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: [ + Row( + children: [ + 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.configuration!.roundedValue?.toDouble() ?? 20.0), bottomRight: Radius.circular(visitAppContext.configuration!.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: [ + HtmlWidget( + '
${eventAgenda.name!.length > 75 ? eventAgenda.name!.substring(0, 75) + " ..." : eventAgenda.name}
', + 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, + ), + ),*/ + ], + ), + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/Screens/Sections/Agenda/event_popup.dart b/lib/Screens/Sections/Agenda/event_popup.dart new file mode 100644 index 0000000..7223f13 --- /dev/null +++ b/lib/Screens/Sections/Agenda/event_popup.dart @@ -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 createState() => _EventPopupState(); +} + +class _EventPopupState extends State { + final DateFormat formatter = DateFormat('dd/MM/yyyy hh:mm'); + Completer _controller = Completer(); + Set markers = {}; + bool init = false; + + mapBox.MapboxMap? mapboxMap; + mapBox.PointAnnotationManager? pointAnnotationManager; + + Set 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 = []; + 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 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 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(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.configuration!.roundedValue?.toDouble() ?? 20.0)) + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(visitAppContext.configuration!.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.configuration!.roundedValue?.toDouble() ?? 20.0), topRight: Radius.circular(visitAppContext.configuration!.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( + '
${widget.eventAgenda.name!}
', + 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.configuration!.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.configuration!.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.configuration!.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, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/Screens/Sections/Agenda/month_filter.dart b/lib/Screens/Sections/Agenda/month_filter.dart new file mode 100644 index 0000000..2af8579 --- /dev/null +++ b/lib/Screens/Sections/Agenda/month_filter.dart @@ -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 events; + final Function(List?) onMonthSelected; + + MonthFilter({required this.events, required this.onMonthSelected}); + + @override + _MonthFilterState createState() => _MonthFilterState(); +} + +class _MonthFilterState extends State with SingleTickerProviderStateMixin { + String? _selectedMonth; + bool _isExpanded = false; + bool _showContent = false; + late AnimationController _controller; + late Animation _widthAnimation; + + Map> 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(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(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.configuration?.roundedValue?.toDouble() ?? 20.0; + + List> 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> _getSortedMonths() { + Map monthsMap = {}; + + for (var event in widget.events) { + final key = '${event.dateFrom!.month}-${event.dateFrom!.year}'; + monthsMap[key] = (monthsMap[key] ?? 0) + 1; + } + + List> 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 _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(); + } +} diff --git a/lib/Screens/Sections/Map/geo_point_filter.dart b/lib/Screens/Sections/Map/geo_point_filter.dart index 1775550..541b754 100644 --- a/lib/Screens/Sections/Map/geo_point_filter.dart +++ b/lib/Screens/Sections/Map/geo_point_filter.dart @@ -80,7 +80,6 @@ class _GeoPointFilterState extends State with SingleTickerProvid }); } _isExpanded ? _controller.forward() : _controller.reverse(); - }); } diff --git a/lib/Screens/Sections/PDF/pdf_filter.dart b/lib/Screens/Sections/PDF/pdf_filter.dart index ec6d444..242706b 100644 --- a/lib/Screens/Sections/PDF/pdf_filter.dart +++ b/lib/Screens/Sections/PDF/pdf_filter.dart @@ -17,7 +17,8 @@ class PdfFilter extends StatefulWidget { } class _PdfFilterState extends State with SingleTickerProviderStateMixin { - bool isExpanded = false; + bool _isExpanded = false; + bool _showContent = false; int _selectedOrderPdf = 0; late AnimationController _controller; late Animation _widthAnimation; @@ -25,14 +26,24 @@ class _PdfFilterState extends State with SingleTickerProviderStateMix @override void initState() { super.initState(); - _controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300)); + _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300)); _widthAnimation = Tween(begin: 40, end: 250).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); } void toggleExpand() { setState(() { - isExpanded = !isExpanded; - isExpanded ? _controller.forward() : _controller.reverse(); + 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(); }); } @@ -59,15 +70,15 @@ class _PdfFilterState extends State with SingleTickerProviderStateMix builder: (context, child) { return Container( width: _widthAnimation.value, - height: isExpanded ? 300 : 75, + height: _isExpanded ? 300 : 75, decoration: BoxDecoration( - color: isExpanded ? primaryColor.withValues(alpha: 0.9) : primaryColor.withValues(alpha: 0.5) , + color: _isExpanded ? primaryColor.withValues(alpha: 0.9) : primaryColor.withValues(alpha: 0.5) , borderRadius: BorderRadius.only( topRight: Radius.circular(rounded), bottomRight: Radius.circular(rounded), ), ), - child: isExpanded + child: _showContent ? Column( children: [ IconButton( @@ -113,7 +124,7 @@ class _PdfFilterState extends State with SingleTickerProviderStateMix ), ], ) - : IconButton( + : _isExpanded ? null : IconButton( icon: const Icon(Icons.menu, color: Colors.white), onPressed: toggleExpand, ), diff --git a/lib/Screens/section_page.dart b/lib/Screens/section_page.dart index 05eb390..4ed841b 100644 --- a/lib/Screens/section_page.dart +++ b/lib/Screens/section_page.dart @@ -14,6 +14,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/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'; @@ -77,7 +78,7 @@ class _SectionPageState extends State { return Scaffold( key: _scaffoldKey, resizeToAvoidBottomInset: false, - appBar: test!.type == SectionType.Menu || test.type == SectionType.Agenda ? CustomAppBar( + appBar: test!.type == SectionType.Menu ? CustomAppBar( title: sectionDTO != null ? TranslationHelper.get(sectionDTO!.title, visitAppContext) : "", isHomeButton: false, ) : null, @@ -92,6 +93,9 @@ class _SectionPageState extends State { 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); diff --git a/pubspec.lock b/pubspec.lock index a0aa5a4..b34dca3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1394,7 +1394,7 @@ packages: source: hosted version: "0.3.1" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" diff --git a/pubspec.yaml b/pubspec.yaml index b1940b0..db26dec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: qr_flutter: ^4.1.0 # multi flutter_map: ^7.0.2 #all image: ^4.1.7 + url_launcher: ^6.3.1 manager_api_new: path: manager_api_new