Agenda + misc for sub menu (pdf, agenda)

This commit is contained in:
Thomas Fransolet 2025-07-03 17:37:24 +02:00
parent f07570d8ee
commit c50083b19f
10 changed files with 1197 additions and 11 deletions

128
lib/Models/agenda.dart Normal file
View 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'],
);
}
}

View File

@ -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<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}");
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

View 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.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: <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.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: <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,
),
),*/
],
),
),
),
),
],
),
);
}
}

View 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.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(
'<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.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,
),
),
),
),
],
),
),
);
}
}

View 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.configuration?.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();
}
}

View File

@ -80,7 +80,6 @@ class _GeoPointFilterState extends State<GeoPointFilter> with SingleTickerProvid
}); });
} }
_isExpanded ? _controller.forward() : _controller.reverse(); _isExpanded ? _controller.forward() : _controller.reverse();
}); });
} }

View File

@ -17,7 +17,8 @@ class PdfFilter extends StatefulWidget {
} }
class _PdfFilterState extends State<PdfFilter> with SingleTickerProviderStateMixin { class _PdfFilterState extends State<PdfFilter> with SingleTickerProviderStateMixin {
bool isExpanded = false; bool _isExpanded = false;
bool _showContent = false;
int _selectedOrderPdf = 0; int _selectedOrderPdf = 0;
late AnimationController _controller; late AnimationController _controller;
late Animation<double> _widthAnimation; late Animation<double> _widthAnimation;
@ -25,14 +26,24 @@ class _PdfFilterState extends State<PdfFilter> with SingleTickerProviderStateMix
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = AnimationController(vsync: this, duration: Duration(milliseconds: 300)); _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 300));
_widthAnimation = Tween<double>(begin: 40, end: 250).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); _widthAnimation = Tween<double>(begin: 40, end: 250).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
} }
void toggleExpand() { void toggleExpand() {
setState(() { setState(() {
isExpanded = !isExpanded; if (_isExpanded) {
isExpanded ? _controller.forward() : _controller.reverse(); _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<PdfFilter> with SingleTickerProviderStateMix
builder: (context, child) { builder: (context, child) {
return Container( return Container(
width: _widthAnimation.value, width: _widthAnimation.value,
height: isExpanded ? 300 : 75, height: _isExpanded ? 300 : 75,
decoration: BoxDecoration( 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( borderRadius: BorderRadius.only(
topRight: Radius.circular(rounded), topRight: Radius.circular(rounded),
bottomRight: Radius.circular(rounded), bottomRight: Radius.circular(rounded),
), ),
), ),
child: isExpanded child: _showContent
? Column( ? Column(
children: [ children: [
IconButton( IconButton(
@ -113,7 +124,7 @@ class _PdfFilterState extends State<PdfFilter> with SingleTickerProviderStateMix
), ),
], ],
) )
: IconButton( : _isExpanded ? null : IconButton(
icon: const Icon(Icons.menu, color: Colors.white), icon: const Icon(Icons.menu, color: Colors.white),
onPressed: toggleExpand, onPressed: toggleExpand,
), ),

View File

@ -14,6 +14,7 @@ import 'package:mymuseum_visitapp/Helpers/translationHelper.dart';
import 'package:mymuseum_visitapp/Models/articleRead.dart'; import 'package:mymuseum_visitapp/Models/articleRead.dart';
import 'package:mymuseum_visitapp/Models/resourceModel.dart'; import 'package:mymuseum_visitapp/Models/resourceModel.dart';
import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart';
import 'package:mymuseum_visitapp/Screens/Sections/Agenda/agenda_page.dart';
import 'package:mymuseum_visitapp/Screens/Sections/Article/article_page.dart'; import 'package:mymuseum_visitapp/Screens/Sections/Article/article_page.dart';
import 'package:mymuseum_visitapp/Screens/Sections/Map/map_context.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/Map/map_page.dart';
@ -77,7 +78,7 @@ class _SectionPageState extends State<SectionPage> {
return Scaffold( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
resizeToAvoidBottomInset: false, 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) : "", title: sectionDTO != null ? TranslationHelper.get(sectionDTO!.title, visitAppContext) : "",
isHomeButton: false, isHomeButton: false,
) : null, ) : null,
@ -92,6 +93,9 @@ class _SectionPageState extends State<SectionPage> {
var sectionResult = snapshot.data; var sectionResult = snapshot.data;
if(sectionDTO != null && sectionResult != null) { if(sectionDTO != null && sectionResult != null) {
switch(sectionDTO!.type) { switch(sectionDTO!.type) {
case SectionType.Agenda:
AgendaDTO agendaDTO = AgendaDTO.fromJson(sectionResult)!;
return AgendaPage(section: agendaDTO);
case SectionType.Article: case SectionType.Article:
ArticleDTO articleDTO = ArticleDTO.fromJson(sectionResult)!; ArticleDTO articleDTO = ArticleDTO.fromJson(sectionResult)!;
return ArticlePage(visitAppContextIn: widget.visitAppContextIn, articleDTO: articleDTO, resourcesModel: resourcesModel); return ArticlePage(visitAppContextIn: widget.visitAppContextIn, articleDTO: articleDTO, resourcesModel: resourcesModel);

View File

@ -1394,7 +1394,7 @@ packages:
source: hosted source: hosted
version: "0.3.1" version: "0.3.1"
url_launcher: url_launcher:
dependency: transitive dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603"

View File

@ -70,6 +70,7 @@ dependencies:
qr_flutter: ^4.1.0 # multi qr_flutter: ^4.1.0 # multi
flutter_map: ^7.0.2 #all flutter_map: ^7.0.2 #all
image: ^4.1.7 image: ^4.1.7
url_launcher: ^6.3.1
manager_api_new: manager_api_new:
path: manager_api_new path: manager_api_new