Compare commits

..

2 Commits

Author SHA1 Message Date
Thomas Fransolet
e851929dbc misc 2026-07-14 17:20:30 +02:00
Thomas Fransolet
0f2fa9bf3e refacto misc + bento layout 2026-07-14 17:20:20 +02:00
67 changed files with 2941 additions and 1577 deletions

View File

@ -34,11 +34,15 @@ lib/
## Client API généré (manager_api_new/) ## Client API généré (manager_api_new/)
**C'est ici que le client est généré.** Il est consommé via dépendance locale par `tablet-app` et `mymuseum-visitapp`. **C'est ici que le client est généré.** Il est consommé via dépendance locale par `tablet-app` et `mymuseum-visitapp`.
- **Ne pas modifier manuellement** les fichiers dans `manager_api_new/` - **Éditer manuellement** les fichiers de `manager_api_new/` en miroir des changements backend — **ne pas relancer le générateur OpenAPI**, il écraserait des patches locaux déjà accumulés dans ce client.
- Régénérer via OpenAPI generator quand le backend change : `openapi_generator_config.json` à la racine
- 16 classes API : AuthenticationApi, ConfigurationApi, SectionApi, SectionMapApi, SectionEventApi, ResourceApi, DeviceApi, UserApi, StatsApi, AIApi, etc. - 16 classes API : AuthenticationApi, ConfigurationApi, SectionApi, SectionMapApi, SectionEventApi, ResourceApi, DeviceApi, UserApi, StatsApi, AIApi, etc.
- 130+ DTOs générés dans `manager_api_new/lib/model/` - 130+ DTOs générés dans `manager_api_new/lib/model/`
## Package layout partagé (myinfomate_layout/)
Package Dart pur (pas de dépendance Flutter) contenant `bentoLayout()` — le placement "bento" dense (chaque card occupe colSpan × lignes rowSpan sur une grille à N colonnes, même principe que `grid-auto-flow: dense` en CSS). Utilisé pour l'aperçu live de `app_configuration_link_screen.dart` ET par `mymuseum-visitapp` (même placement, pour que l'aperçu soit fidèle au rendu réel). Côté web (`visitapp-web`), le même rendu est obtenu nativement via CSS Grid (pas de portage JS nécessaire). Tests dans `myinfomate_layout/test/` (fixtures `bento_layout_cases.json`).
La taille d'une card = deux entiers `gridColSpan` / `gridRowSpan` (1 ou 2), portés par `AppConfigurationLinkDTO` (donc réglables indépendamment par plateforme). Dans l'aperçu, ils se règlent via un sélecteur de forme par ligne (`card_shape_selector.dart`) OU par glisser du coin d'une card (drag 2D) — les deux écrivent le même champ.
## État central (ManagerAppContext) ## État central (ManagerAppContext)
Contient : credentials, accessToken, instanceId, instanceDTO, référence au client API, configuration/section sélectionnée. Contient : credentials, accessToken, instanceId, instanceDTO, référence au client API, configuration/section sélectionnée.
Accès via `context.read<AppContext>()` ou `context.watch<AppContext>()`. Accès via `context.read<AppContext>()` ou `context.watch<AppContext>()`.

View File

@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:manager_app/constants.dart';
class CardShape {
final int col;
final int row;
const CardShape(this.col, this.row);
}
/// Les 4 formes proposées pour une card dans la grille bento.
const List<CardShape> kCardShapes = [
CardShape(1, 1), // standard
CardShape(2, 1), // large
CardShape(1, 2), // haute
CardShape(2, 2), // grande vedette
];
/// Sélecteur de forme compact raccourci équivalent au drag de coin dans
/// l'aperçu (les deux écrivent gridColSpan/gridRowSpan sur le même lien).
class CardShapeSelector extends StatelessWidget {
final int colSpan;
final int rowSpan;
final void Function(int col, int row) onChanged;
const CardShapeSelector({
super.key,
required this.colSpan,
required this.rowSpan,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: kSecond.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: kCardShapes.map((shape) {
final isSelected = shape.col == colSpan && shape.row == rowSpan;
return GestureDetector(
onTap: () => onChanged(shape.col, shape.row),
child: Container(
width: 26,
height: 26,
margin: const EdgeInsets.symmetric(horizontal: 2),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isSelected ? kPrimaryColor : Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Container(
width: shape.col == 2 ? 16 : 8,
height: shape.row == 2 ? 16 : 8,
decoration: BoxDecoration(
color: isSelected ? kWhite : kBodyTextColor,
borderRadius: BorderRadius.circular(2),
),
),
),
);
}).toList(),
),
);
}
}

View File

@ -7,6 +7,7 @@ class CheckInputContainer extends StatefulWidget {
final bool? isChecked; final bool? isChecked;
final IconData? icon; final IconData? icon;
final String label; final String label;
final String? subtitle;
final ValueChanged<bool> onChanged; final ValueChanged<bool> onChanged;
final double fontSize; final double fontSize;
const CheckInputContainer({ const CheckInputContainer({
@ -14,6 +15,7 @@ class CheckInputContainer extends StatefulWidget {
this.isChecked, this.isChecked,
this.icon, this.icon,
required this.label, required this.label,
this.subtitle,
required this.onChanged, required this.onChanged,
this.fontSize = 18 this.fontSize = 18
}) : super(key: key); }) : super(key: key);
@ -51,9 +53,23 @@ class _CheckInputContainerState extends State<CheckInputContainer> {
size: 25.0, size: 25.0,
), ),
), ),
Text( Column(
widget.label, crossAxisAlignment: CrossAxisAlignment.start,
style: new TextStyle(fontSize: widget.fontSize, fontWeight: FontWeight.w300) mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.label,
style: new TextStyle(fontSize: widget.fontSize, fontWeight: FontWeight.w300)
),
if (widget.subtitle != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
widget.subtitle!,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
),
],
), ),
], ],
) )

View File

@ -65,7 +65,7 @@ class _MultiSelectDropdownLanguageContainerState
), ),
const SizedBox(height: 8, width: 10), const SizedBox(height: 8, width: 10),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 35), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
/*decoration: BoxDecoration( /*decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade400, width: 1.2), border: Border.all(color: Colors.grey.shade400, width: 1.2),

View File

@ -36,24 +36,26 @@ class MultiStringInputAndResourceContainer extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context); final appContext = Provider.of<AppContext>(context);
ManagerAppContext managerAppContext = appContext.getContext(); ManagerAppContext managerAppContext = appContext.getContext();
Size size = MediaQuery.of(context).size;
return Container( return Container(
child: Row( child: Row(
children: [ children: [
Align( ConstrainedBox(
alignment: AlignmentDirectional.centerStart, constraints: BoxConstraints(maxWidth: 150),
child: AutoSizeText( child: Align(
label, alignment: AlignmentDirectional.centerStart,
style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.w300), child: AutoSizeText(
maxLines: 2, label,
maxFontSize: fontSize, style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.w300),
textAlign: TextAlign.center, maxLines: 2,
) maxFontSize: fontSize,
textAlign: TextAlign.center,
)
),
), ),
Padding( Padding(
padding: const EdgeInsets.all(10.0), padding: const EdgeInsets.all(10.0),
child: Container( child: ConstrainedBox(
width: size.width *0.15, constraints: BoxConstraints(maxWidth: 140),
child: InkWell( child: InkWell(
onTap: () { onTap: () {
List<TranslationAndResourceDTO> newValues = <TranslationAndResourceDTO>[]; List<TranslationAndResourceDTO> newValues = <TranslationAndResourceDTO>[];

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:html/parser.dart' as html_parser;
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
import 'package:manager_app/Components/multi_input_modal.dart'; import 'package:manager_app/Components/multi_input_modal.dart';
import 'package:manager_app/Components/multi_string_input_html_modal.dart'; import 'package:manager_app/Components/multi_string_input_html_modal.dart';
@ -21,6 +22,7 @@ class MultiStringInputContainer extends StatelessWidget {
final bool isHTML; final bool isHTML;
final double fontSize; final double fontSize;
final bool isMandatory; final bool isMandatory;
final bool showPreview;
const MultiStringInputContainer({ const MultiStringInputContainer({
Key? key, Key? key,
@ -35,8 +37,20 @@ class MultiStringInputContainer extends StatelessWidget {
this.isHTML = false, this.isHTML = false,
this.fontSize = 18, this.fontSize = 18,
this.isMandatory = true, this.isMandatory = true,
this.showPreview = false,
}) : super(key: key); }) : super(key: key);
String _plainTextPreview(String htmlOrText) {
if (htmlOrText.trim().isEmpty) return "";
try {
final doc = html_parser.parse(htmlOrText);
final text = (doc.body?.text ?? htmlOrText).trim();
return text.isEmpty ? htmlOrText.trim() : text;
} catch (_) {
return htmlOrText;
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appContext = Provider.of<AppContext>(context); final appContext = Provider.of<AppContext>(context);
@ -106,23 +120,74 @@ class MultiStringInputContainer extends StatelessWidget {
); );
} }
}, },
child: Container( child: showPreview
height: 50, ? Builder(builder: (context) {
width: 180, final languages = managerAppContext.selectedConfiguration!.languages!;
decoration: BoxDecoration( final completed = initialValue
color: color, .where((t) => (t.value ?? "").trim().isNotEmpty)
borderRadius: BorderRadius.circular(50), .length;
), final currentTranslation = initialValue.isNotEmpty
alignment: Alignment.center, ? (initialValue.firstWhere(
child: Text( (t) => t.language == 'FR',
resourceTypes == null ? "Changer traductions" : "Changer ressources", orElse: () => initialValue.first)
style: const TextStyle( .value ??
color: kWhite, "")
fontWeight: FontWeight.w400, : "";
fontSize: 16, final preview = _plainTextPreview(currentTranslation);
), return Container(
), height: 54,
), width: 200,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(50),
),
alignment: Alignment.centerLeft,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
preview.isEmpty ? "Aucun texte —" : preview,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: kWhite,
fontWeight: FontWeight.w600,
fontSize: 13,
fontStyle: preview.isEmpty ? FontStyle.italic : FontStyle.normal,
),
),
const SizedBox(height: 2),
Text(
"$completed / ${languages.length} langues traduites",
style: const TextStyle(
color: kWhite,
fontWeight: FontWeight.w400,
fontSize: 11,
),
),
],
),
);
})
: Container(
height: 50,
width: 180,
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(50),
),
alignment: Alignment.center,
child: Text(
resourceTypes == null ? "Changer traductions" : "Changer ressources",
style: const TextStyle(
color: kWhite,
fontWeight: FontWeight.w400,
fontSize: 16,
),
),
),
) )
), ),
], ],

View File

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import '../constants.dart';
class NumberStepperField extends StatelessWidget {
final String label;
final num? value;
final num min;
final num max;
final num step;
final String? unit;
final ValueChanged<num> onChanged;
const NumberStepperField({
Key? key,
required this.label,
required this.value,
this.min = 0,
this.max = 999999,
this.step = 1,
this.unit,
required this.onChanged,
}) : super(key: key);
num get _current => (value ?? min).clamp(min, max);
String _format(num n) => n.truncateToDouble() == n ? n.toInt().toString() : n.toString();
@override
Widget build(BuildContext context) {
final current = _current;
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(label, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
const SizedBox(width: 12),
Container(
decoration: BoxDecoration(
border: Border.all(color: kSecond),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.remove_circle_outline),
color: current <= min ? Colors.grey[400] : kPrimaryColor,
onPressed: current <= min
? null
: () => onChanged((current - step).clamp(min, max)),
),
SizedBox(
width: 56,
child: Text(
unit != null ? "${_format(current)} $unit" : _format(current),
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
),
IconButton(
icon: const Icon(Icons.add_circle_outline),
color: current >= max ? Colors.grey[400] : kPrimaryColor,
onPressed: current >= max
? null
: () => onChanged((current + step).clamp(min, max)),
),
],
),
),
],
);
}
}

View File

@ -34,19 +34,18 @@ class _PDFFileInputContainerState extends State<PDFFileInputContainer> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container( return Container(
child: Row( child: Row(
children: [ children: [
Align( Expanded(
alignment: AlignmentDirectional.centerStart, child: Align(
child: Text(widget.label, style: TextStyle(fontSize: widget.fontSize, fontWeight: FontWeight.w300)) alignment: AlignmentDirectional.centerStart,
child: Text(widget.label, style: TextStyle(fontSize: widget.fontSize, fontWeight: FontWeight.w300), overflow: TextOverflow.ellipsis)
),
), ),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Container( child: Container(
width: size.width *0.16,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
List<OrderedTranslationAndResourceDTO> newValues = <OrderedTranslationAndResourceDTO>[]; List<OrderedTranslationAndResourceDTO> newValues = <OrderedTranslationAndResourceDTO>[];

View File

@ -60,7 +60,7 @@ class _ReorderableCustomListState<T> extends State<ReorderableCustomList<T>> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = itemsMiddle[index]; final item = itemsMiddle[index];
return Container( return Container(
key: ValueKey(item), key: ValueKey(identityHashCode(item)),
margin: const EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,

View File

@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import '../constants.dart';
class SectionCard extends StatelessWidget {
final IconData icon;
final String title;
final String? subtitle;
final Widget child;
final EdgeInsetsGeometry padding;
const SectionCard({
Key? key,
required this.icon,
required this.title,
this.subtitle,
required this.child,
this.padding = const EdgeInsets.all(16),
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: kSecond),
borderRadius: BorderRadius.circular(12),
),
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(icon, color: kPrimaryColor, size: 22),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
if (subtitle != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
subtitle!,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
),
],
),
),
],
),
const SizedBox(height: 12),
child,
],
),
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,243 @@
import 'package:flutter/material.dart';
import 'package:manager_api_new/api.dart';
import 'package:manager_app/constants.dart';
import 'package:myinfomate_layout/myinfomate_layout.dart';
const double _kGap = 8;
/// Aperçu live de la grille bento (même placement dense que web/mobile en prod).
/// Glisser le coin d'une card change ses spans (colonnes × lignes) ; le résultat
/// snappe sur des cellules entières. [onSpanChanged] doit rester synchronisé avec
/// le même état que le sélecteur de forme de la liste.
/// Glisser la poignée (coin haut-gauche) d'une card sur une autre échange leur
/// position dans la liste le dense packing recalcule le reste automatiquement.
class BentoPreviewGrid extends StatelessWidget {
final List<AppConfigurationLinkDTO> links;
final int columns;
final void Function(AppConfigurationLinkDTO link, int colSpan, int rowSpan) onSpanChanged;
final void Function(String fromId, String toId) onSwap;
const BentoPreviewGrid({
super.key,
required this.links,
required this.columns,
required this.onSpanChanged,
required this.onSwap,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
final width = constraints.maxWidth;
// Seuls les blocs actifs apparaissent réellement dans l'app — sinon le
// switch "actif/inactif" de la liste n'aurait aucun effet sur l'aperçu.
final activeLinks = links.where((l) => l.isActive == true).toList();
if (width <= 0 || activeLinks.isEmpty) return const SizedBox();
final cols = columns < 1 ? 1 : columns;
final maxSpan = cols < 2 ? 1 : 2;
final cellWidth = (width - (cols - 1) * _kGap) / cols;
final cellHeight = cellWidth * 0.78;
// Trié par `order` plutôt que par l'ordre du tableau reçu : la liste de
// gauche (ReorderableCustomList) travaille sur sa propre copie interne et
// ne réordonne que les DTO (leur champ `order`), jamais le tableau `links`
// lui-même sans ce tri, l'aperçu ignorerait le reorder de la liste.
final orderedLinks = [...activeLinks]..sort((a, b) => (a.order ?? 0).compareTo(b.order ?? 0));
final result = bentoLayout(
orderedLinks
.map((l) => BentoItem(
id: l.id!,
colSpan: (l.gridColSpan ?? 1).clamp(1, maxSpan),
rowSpan: (l.gridRowSpan ?? 1).clamp(1, 2),
))
.toList(),
cols,
);
final totalHeight =
result.rowCount * cellHeight + (result.rowCount - 1) * _kGap;
final byId = {for (final l in activeLinks) l.id!: l};
return SizedBox(
width: width,
height: totalHeight < 0 ? 0 : totalHeight,
child: Stack(
children: result.placements.map((p) {
final link = byId[p.id]!;
return Positioned(
left: p.col * (cellWidth + _kGap),
top: p.row * (cellHeight + _kGap),
width: p.colSpan * cellWidth + (p.colSpan - 1) * _kGap,
height: p.rowSpan * cellHeight + (p.rowSpan - 1) * _kGap,
child: DragTarget<String>(
onWillAcceptWithDetails: (details) => details.data != link.id,
onAcceptWithDetails: (details) => onSwap(details.data, link.id!),
builder: (context, candidateData, rejectedData) {
return _BentoCard(
link: link,
cellWidth: cellWidth,
cellHeight: cellHeight,
maxSpan: maxSpan,
isDropTarget: candidateData.isNotEmpty,
onSpanChanged: (c, r) => onSpanChanged(link, c, r),
);
},
),
);
}).toList(),
),
);
});
}
}
class _BentoCard extends StatefulWidget {
final AppConfigurationLinkDTO link;
final double cellWidth;
final double cellHeight;
final int maxSpan;
final bool isDropTarget;
final void Function(int colSpan, int rowSpan) onSpanChanged;
const _BentoCard({
required this.link,
required this.cellWidth,
required this.cellHeight,
required this.maxSpan,
required this.isDropTarget,
required this.onSpanChanged,
});
@override
State<_BentoCard> createState() => _BentoCardState();
}
class _BentoCardState extends State<_BentoCard> {
double _accDx = 0;
double _accDy = 0;
int _startCol = 1;
int _startRow = 1;
@override
Widget build(BuildContext context) {
final link = widget.link;
final hasImage = link.configuration?.imageSource != null;
return Container(
decoration: BoxDecoration(
color: hasImage ? null : kPrimaryColor.withValues(alpha: 0.55),
borderRadius: BorderRadius.circular(10),
image: hasImage
? DecorationImage(image: NetworkImage(link.configuration!.imageSource!), fit: BoxFit.cover)
: null,
border: widget.isDropTarget ? Border.all(color: kPrimaryColor, width: 3) : null,
),
child: Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.transparent, Colors.black.withValues(alpha: 0.7)],
stops: const [0.4, 1.0],
),
),
),
),
Positioned(
bottom: 6,
left: 8,
right: 8,
child: Text(
link.configuration?.label ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600),
),
),
// Poignée de déplacement glisser sur une autre card échange leur position.
// Coin opposé à la poignée de resize pour éviter tout conflit de geste.
Positioned(
left: 0,
top: 0,
child: Draggable<String>(
data: link.id,
feedback: Material(
color: Colors.transparent,
child: Opacity(
opacity: 0.85,
child: SizedBox(
width: widget.cellWidth,
height: widget.cellHeight,
child: _dragHandle(),
),
),
),
child: _dragHandle(),
),
),
// Poignée de coin drag 2D : largeur = colonnes, hauteur = lignes.
Positioned(
right: 0,
bottom: 0,
child: MouseRegion(
cursor: SystemMouseCursors.resizeDownRight,
child: GestureDetector(
onPanStart: (_) {
_accDx = 0;
_accDy = 0;
_startCol = (link.gridColSpan ?? 1).clamp(1, widget.maxSpan);
_startRow = (link.gridRowSpan ?? 1).clamp(1, 2);
},
onPanUpdate: (details) {
_accDx += details.delta.dx;
_accDy += details.delta.dy;
final newCol = (_startCol + (_accDx / (widget.cellWidth + _kGap)).round())
.clamp(1, widget.maxSpan);
final newRow = (_startRow + (_accDy / (widget.cellHeight + _kGap)).round())
.clamp(1, 2);
if (newCol != (link.gridColSpan ?? 1) || newRow != (link.gridRowSpan ?? 1)) {
widget.onSpanChanged(newCol, newRow);
}
},
child: Container(
width: 20,
height: 20,
margin: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: kWhite,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: kPrimaryColor, width: 2),
),
child: Icon(Icons.open_in_full, size: 11, color: kPrimaryColor),
),
),
),
),
],
),
);
}
Widget _dragHandle() {
return MouseRegion(
cursor: SystemMouseCursors.grab,
child: Container(
width: 20,
height: 20,
margin: const EdgeInsets.all(3),
decoration: BoxDecoration(
color: kWhite,
borderRadius: BorderRadius.circular(5),
border: Border.all(color: kPrimaryColor, width: 2),
),
child: Icon(Icons.drag_indicator, size: 11, color: kPrimaryColor),
),
);
}
}

View File

@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
/// Cadre navigateur pendant de [PhoneMockup] pour l'aperçu de l'onglet Web.
/// [width]/[height] sont imposés par l'appelant (espace réellement disponible
/// dans le panneau d'aperçu), [width] permettant en plus de simuler une largeur
/// de viewport donnée (desktop vs mobile web) visitapp-web est un site
/// responsive, pas une app figée à une taille.
class BrowserFrame extends StatelessWidget {
final Widget child;
final double width;
final double height;
const BrowserFrame({super.key, required this.child, required this.width, required this.height});
@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black, width: 6),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Column(
children: [
Container(
height: 28,
color: const Color(0xFFE8EAED),
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Row(
children: const [
_Dot(color: Color(0xFFFF5F57)),
SizedBox(width: 6),
_Dot(color: Color(0xFFFEBC2E)),
SizedBox(width: 6),
_Dot(color: Color(0xFF28C840)),
],
),
),
Expanded(child: Container(color: Colors.white, child: child)),
],
),
),
);
}
}
class _Dot extends StatelessWidget {
final Color color;
const _Dot({required this.color});
@override
Widget build(BuildContext context) {
return Container(
width: 10,
height: 10,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
}
}

View File

@ -1,17 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// [width]/[height] sont imposés par l'appelant (espace réellement disponible
/// dans le panneau d'aperçu) plutôt que dérivés de tout l'écran, pour que le
/// cadre entier reste visible sans avoir à scroller pour le voir en entier.
class PhoneMockup extends StatelessWidget { class PhoneMockup extends StatelessWidget {
final Widget child; final Widget child;
final double width;
final double height;
const PhoneMockup({super.key, required this.child}); const PhoneMockup({super.key, required this.child, required this.width, required this.height});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return Container( return Container(
width: size.width * 0.4, width: width,
height: size.height * 0.6, height: height,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.black, color: Colors.black,
borderRadius: BorderRadius.circular(40), borderRadius: BorderRadius.circular(40),
@ -32,8 +35,8 @@ class PhoneMockup extends StatelessWidget {
// Notch // Notch
Positioned( Positioned(
top: 8, top: 8,
left:(size.width * 0.35) / 2, left:(width * 0.35) / 2,
right: (size.width * 0.35) / 2, right: (width * 0.35) / 2,
child: Container( child: Container(
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -52,7 +52,6 @@ class _WebInstanceLoaderState extends State<WebInstanceLoader> {
instanceId: instanceDTO.id, instanceId: instanceDTO.id,
appType: AppType.Web, appType: AppType.Web,
languages: ['fr'], languages: ['fr'],
layoutMainPage: LayoutMainPageType.MasonryGrid,
isAssistant: false, isAssistant: false,
); );
@ -119,7 +118,7 @@ class WebAppScreen extends StatelessWidget {
children: [ children: [
if (instanceDTO?.webSlug != null || instanceDTO?.publicApiKey != null) if (instanceDTO?.webSlug != null || instanceDTO?.publicApiKey != null)
Card( Card(
margin: const EdgeInsets.fromLTRB(0, 0, 0, 8), margin: const EdgeInsets.fromLTRB(16, 8, 16, 8),
color: kWhite, color: kWhite,
elevation: 0, elevation: 0,
child: Padding( child: Padding(
@ -129,14 +128,23 @@ class WebAppScreen extends StatelessWidget {
children: [ children: [
Text('Accès web', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 21)), Text('Accès web', style: TextStyle(fontWeight: FontWeight.w500, fontSize: 21)),
const SizedBox(height: 12), const SizedBox(height: 12),
_InfoRow( Row(
label: 'URL visiteurs', crossAxisAlignment: CrossAxisAlignment.start,
value: 'app.myinfomate.be/${instanceDTO?.webSlug ?? ''}', children: [
), Expanded(
const SizedBox(height: 8), child: _InfoRow(
_InfoRow( label: 'URL visiteurs',
label: 'Clé publique API', value: 'app.myinfomate.be/${instanceDTO?.webSlug ?? ''}',
value: instanceDTO?.publicApiKey ?? '', ),
),
const SizedBox(width: 24),
Expanded(
child: _InfoRow(
label: 'Clé publique API',
value: instanceDTO?.publicApiKey ?? '',
),
),
],
), ),
], ],
), ),

View File

@ -1,10 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
import 'package:manager_app/constants.dart'; import 'package:manager_app/constants.dart';
import 'package:manager_app/Components/check_input_container.dart'; import 'package:manager_app/Components/check_input_container.dart';
import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart';
import 'package:manager_app/Components/single_select_container.dart'; import 'package:manager_app/Components/single_select_container.dart';
import 'package:manager_app/Components/string_input_container.dart';
import 'package:manager_app/Components/message_notification.dart'; import 'package:manager_app/Components/message_notification.dart';
import 'package:manager_app/Components/confirmation_dialog.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:manager_app/app_context.dart'; import 'package:manager_app/app_context.dart';
import 'package:manager_app/l10n/app_localizations.dart'; import 'package:manager_app/l10n/app_localizations.dart';
@ -12,6 +15,8 @@ import 'package:manager_app/Models/managerContext.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'showNewOrUpdateEventAgenda.dart'; import 'showNewOrUpdateEventAgenda.dart';
enum _AgendaDateFilter { all, thisMonth, thisYear, past }
class AgendaConfig extends StatefulWidget { class AgendaConfig extends StatefulWidget {
final String? color; final String? color;
final String? label; final String? label;
@ -33,6 +38,94 @@ class AgendaConfig extends StatefulWidget {
class _AgendaConfigState extends State<AgendaConfig> { class _AgendaConfigState extends State<AgendaConfig> {
late AgendaDTO agendaDTO; late AgendaDTO agendaDTO;
List<EventAgendaDTO> events = []; List<EventAgendaDTO> events = [];
String _searchFilter = '';
_AgendaDateFilter _dateFilter = _AgendaDateFilter.all;
List<EventAgendaDTO> get _filteredEvents {
final now = DateTime.now();
var filtered = events.where((event) {
switch (_dateFilter) {
case _AgendaDateFilter.thisMonth:
return event.dateFrom != null &&
event.dateFrom!.year == now.year &&
event.dateFrom!.month == now.month;
case _AgendaDateFilter.thisYear:
return event.dateFrom != null && event.dateFrom!.year == now.year;
case _AgendaDateFilter.past:
return event.dateFrom != null && event.dateFrom!.isBefore(now);
case _AgendaDateFilter.all:
return true;
}
}).toList();
if (_searchFilter.isNotEmpty) {
filtered = filtered.where((event) {
final label = _labelFor(event);
return label.toLowerCase().contains(_searchFilter.toLowerCase());
}).toList();
}
filtered.sort((a, b) {
if (a.dateFrom == null && b.dateFrom == null) return 0;
if (a.dateFrom == null) return 1;
if (b.dateFrom == null) return -1;
return a.dateFrom!.compareTo(b.dateFrom!);
});
return filtered;
}
String _labelFor(EventAgendaDTO event) {
if (event.label != null && event.label!.isNotEmpty) {
return (event.label!.firstWhere((t) => t.language == 'FR',
orElse: () => event.label![0]))
.value ??
'';
}
return '';
}
/// Groups events synced from the same external agenda that share the same
/// address and dates: the sync creates one EventAgendaDTO per language
/// instead of one multilingual event, so this recombines them for display.
String? _syncGroupKey(EventAgendaDTO event) {
if (event.isSynced != true) return null;
final dates =
"${event.dateFrom?.toIso8601String()}|${event.dateTo?.toIso8601String()}";
final address = event.address?.address?.trim();
if (address != null && address.isNotEmpty) {
return "addr:$address|$dates";
}
// No address to match on (common for these synced events): fall back to
// an identical title, which only happens when the source repeats the
// same untranslated name per language pass.
final label = _labelFor(event).trim();
if (label.isNotEmpty) {
return "label:$label|$dates";
}
return null;
}
List<List<EventAgendaDTO>> get _displayGroups {
final groups = <List<EventAgendaDTO>>[];
final groupByKey = <String, List<EventAgendaDTO>>{};
for (final event in _filteredEvents) {
final key = _syncGroupKey(event);
if (key == null) {
groups.add([event]);
continue;
}
final existing = groupByKey[key];
if (existing != null) {
existing.add(event);
} else {
final group = [event];
groupByKey[key] = group;
groups.add(group);
}
}
return groups;
}
@override @override
void initState() { void initState() {
@ -142,6 +235,39 @@ class _AgendaConfigState extends State<AgendaConfig> {
], ],
), ),
), ),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 280,
child: StringInputContainer(
label: AppLocalizations.of(context)!.searchLabel,
onChanged: (v) => setState(() => _searchFilter = v),
),
),
const SizedBox(width: 16),
Wrap(
spacing: 8,
children: [
_buildDateFilterChip(
_AgendaDateFilter.all,
AppLocalizations.of(context)!.agendaFilterAll),
_buildDateFilterChip(
_AgendaDateFilter.thisMonth,
AppLocalizations.of(context)!.agendaFilterThisMonth),
_buildDateFilterChip(
_AgendaDateFilter.thisYear,
AppLocalizations.of(context)!.agendaFilterThisYear),
_buildDateFilterChip(
_AgendaDateFilter.past,
AppLocalizations.of(context)!.agendaFilterPast),
],
),
],
),
),
Container( Container(
height: 600, height: 600,
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
@ -149,116 +275,164 @@ class _AgendaConfigState extends State<AgendaConfig> {
? Center( ? Center(
child: Text(AppLocalizations.of(context)!.noEvents, child: Text(AppLocalizations.of(context)!.noEvents,
style: TextStyle(fontStyle: FontStyle.italic))) style: TextStyle(fontStyle: FontStyle.italic)))
: ListView.builder( : Builder(builder: (context) {
itemCount: events.length, final groups = _displayGroups;
itemBuilder: (context, index) { return ListView.builder(
final event = events[index]; itemCount: groups.length,
return Card( itemBuilder: (context, index) {
elevation: 2, final group = groups[index];
margin: const EdgeInsets.symmetric( final primary = group.firstWhere(
horizontal: 16, vertical: 6), (e) => e.label?.any((t) => t.language == 'FR') ??
shape: RoundedRectangleBorder( false,
borderRadius: BorderRadius.circular(12)), orElse: () => group.first);
child: ListTile( return Card(
contentPadding: const EdgeInsets.symmetric( elevation: 2,
horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(
leading: CircleAvatar( horizontal: 16, vertical: 6),
backgroundColor: kPrimaryColor.withOpacity(0.1), shape: RoundedRectangleBorder(
child: borderRadius: BorderRadius.circular(12)),
const Icon(Icons.event, color: kPrimaryColor), child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 8),
leading: CircleAvatar(
backgroundColor: kPrimaryColor.withOpacity(0.1),
child: const Icon(Icons.event,
color: kPrimaryColor),
),
title: HtmlWidget(
(primary.label != null &&
primary.label!.isNotEmpty)
? (primary.label!.firstWhere(
(t) => t.language == 'FR',
orElse: () => primary.label![0]))
.value ??
"${AppLocalizations.of(context)!.agendaEventFallback} $index"
: "${AppLocalizations.of(context)!.agendaEventFallback} $index",
textStyle:
const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4),
child: Text([
_formatEventDate(primary),
primary.address?.address ??
AppLocalizations.of(context)!.noAddress,
].where((s) => s.isNotEmpty).join('')),
),
trailing: group.length > 1
? _buildLanguageChips(group)
: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit,
color: kPrimaryColor),
onPressed: () =>
_editEvent(context, primary),
),
IconButton(
icon: const Icon(Icons.delete,
color: kError),
onPressed: () =>
_deleteEvent(context, primary),
),
],
),
), ),
title: HtmlWidget( );
(event.label != null && event.label!.isNotEmpty) },
? (event.label!.firstWhere( );
(t) => t.language == 'FR', }),
orElse: () => event.label![0])).value ?? "${AppLocalizations.of(context)!.agendaEventFallback} $index"
: "${AppLocalizations.of(context)!.agendaEventFallback} $index",
textStyle: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 4),
child:
Text(event.address?.address ?? AppLocalizations.of(context)!.noAddress),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.edit,
color: kPrimaryColor),
onPressed: () {
showNewOrUpdateEventAgenda(
context,
event,
agendaDTO.id ?? "",
(updatedEvent) async {
try {
final result = await _api(context)
.sectionAgendaUpdateEventAgenda(
updatedEvent);
if (result != null && mounted) {
setState(
() => events[index] = result);
showNotification(
kSuccess,
kWhite,
AppLocalizations.of(context)!.agendaEventUpdatedSuccess,
context,
null);
}
} catch (e) {
showNotification(
kError,
kWhite,
AppLocalizations.of(context)!.agendaEventUpdateError,
context,
null);
rethrow;
}
},
);
},
),
IconButton(
icon: const Icon(Icons.delete, color: kError),
onPressed: () async {
try {
if (event.id != null) {
await _api(context)
.sectionAgendaDeleteEventAgenda(
event.id!);
}
if (mounted) {
setState(() => events.removeAt(index));
showNotification(
kSuccess,
kWhite,
AppLocalizations.of(context)!.agendaEventDeletedSuccess,
context,
null);
}
} catch (e) {
showNotification(
kError,
kWhite,
AppLocalizations.of(context)!.agendaEventDeleteError,
context,
null);
}
},
),
],
),
),
);
},
),
), ),
], ],
], ],
); );
} }
void _editEvent(BuildContext context, EventAgendaDTO event) {
showNewOrUpdateEventAgenda(
context,
event,
agendaDTO.id ?? "",
(updatedEvent) async {
try {
final result =
await _api(context).sectionAgendaUpdateEventAgenda(updatedEvent);
if (result != null && mounted) {
setState(() =>
events[events.indexWhere((e) => e.id == event.id)] = result);
showNotification(kSuccess, kWhite,
AppLocalizations.of(context)!.agendaEventUpdatedSuccess, context, null);
}
} catch (e) {
showNotification(kError, kWhite,
AppLocalizations.of(context)!.agendaEventUpdateError, context, null);
rethrow;
}
},
);
}
void _deleteEvent(BuildContext context, EventAgendaDTO event) {
showConfirmationDialog(
AppLocalizations.of(context)!.agendaEventDeleteConfirm,
() {},
() async {
try {
if (event.id != null) {
await _api(context).sectionAgendaDeleteEventAgenda(event.id!);
}
if (mounted) {
setState(() => events.removeWhere((e) => e.id == event.id));
showNotification(kSuccess, kWhite,
AppLocalizations.of(context)!.agendaEventDeletedSuccess, context, null);
}
} catch (e) {
showNotification(kError, kWhite,
AppLocalizations.of(context)!.agendaEventDeleteError, context, null);
}
},
context,
);
}
Widget _buildLanguageChips(List<EventAgendaDTO> group) {
return Wrap(
spacing: 4,
children: group.map((event) {
final language = (event.label != null && event.label!.isNotEmpty)
? event.label![0].language ?? '?'
: '?';
return InputChip(
label: Text(language.toUpperCase(),
style: const TextStyle(fontSize: 12)),
onPressed: () => _editEvent(context, event),
onDeleted: () => _deleteEvent(context, event),
deleteIconColor: kError,
);
}).toList(),
);
}
Widget _buildDateFilterChip(_AgendaDateFilter filter, String label) {
return ChoiceChip(
label: Text(label),
selected: _dateFilter == filter,
selectedColor: kPrimaryColor.withOpacity(0.2),
onSelected: (_) => setState(() => _dateFilter = filter),
);
}
String _formatEventDate(EventAgendaDTO event) {
if (event.dateFrom == null) return '';
final dateFormat = DateFormat('dd/MM/yyyy');
final from = dateFormat.format(event.dateFrom!.toLocal());
if (event.dateTo == null || event.dateTo!.isAtSameMomentAs(event.dateFrom!)) {
return from;
}
return "$from - ${dateFormat.format(event.dateTo!.toLocal())}";
}
Widget _buildAgendaHeader(Size size, String mapProviderIn) { Widget _buildAgendaHeader(Size size, String mapProviderIn) {
return Container( return Container(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),

View File

@ -78,6 +78,7 @@ void showNewOrUpdateEventAgenda(
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
isHTML: true, isHTML: true,
showPreview: true,
), ),
), ),
SizedBox(width: 20), SizedBox(width: 20),
@ -92,6 +93,7 @@ void showNewOrUpdateEventAgenda(
maxLines: 5, maxLines: 5,
isTitle: false, isTitle: false,
isHTML: true, isHTML: true,
showPreview: true,
), ),
), ),
], ],

View File

@ -72,9 +72,10 @@ class _ArticleConfigState extends State<ArticleConfig> {
child: Column( child: Column(
children: [ children: [
Container( Container(
height: size.height * 0.15, constraints: BoxConstraints(minHeight: size.height * 0.15),
//width: size.width * 0.5,//, //width: size.width * 0.5,//,
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [ children: [
Column( Column(
@ -96,6 +97,7 @@ class _ArticleConfigState extends State<ArticleConfig> {
}); });
}, },
maxLines: 1, maxLines: 1,
showPreview: true,
), ),
CheckInputContainer( CheckInputContainer(
label: "Contenu au-dessus :", label: "Contenu au-dessus :",

View File

@ -97,6 +97,7 @@ void showNewOrUpdateMapAnnotation(
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
isHTML: false, isHTML: false,
showPreview: true,
), ),
), ),
const SizedBox(width: 20), const SizedBox(width: 20),

View File

@ -86,6 +86,7 @@ void showNewOrUpdateProgrammeBlock(
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
isHTML: true, isHTML: true,
showPreview: true,
), ),
), ),
SizedBox(width: 20), SizedBox(width: 20),
@ -100,6 +101,7 @@ void showNewOrUpdateProgrammeBlock(
maxLines: 1, maxLines: 1,
isTitle: false, isTitle: false,
isHTML: true, isHTML: true,
showPreview: true,
), ),
), ),
], ],

View File

@ -74,6 +74,7 @@ Future<CategorieDTO?> showNewOrUpdateCategory(CategorieDTO? inputCategorieDTO, A
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
isHTML: true, isHTML: true,
showPreview: true,
) )
), ),
Container( Container(

View File

@ -111,6 +111,7 @@ void showNewOrUpdateGeoPoint(MapDTO mapDTO, GeoPointDTO? inputGeoPointDTO,
}, },
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
showPreview: true,
), ),
), ),
SizedBox(width: 20), SizedBox(width: 20),
@ -129,6 +130,7 @@ void showNewOrUpdateGeoPoint(MapDTO mapDTO, GeoPointDTO? inputGeoPointDTO,
}, },
maxLines: 1, maxLines: 1,
isTitle: false, isTitle: false,
showPreview: true,
), ),
), ),
SizedBox(width: 20), SizedBox(width: 20),
@ -147,6 +149,7 @@ void showNewOrUpdateGeoPoint(MapDTO mapDTO, GeoPointDTO? inputGeoPointDTO,
}, },
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
showPreview: true,
), ),
), ),
], ],
@ -170,6 +173,7 @@ void showNewOrUpdateGeoPoint(MapDTO mapDTO, GeoPointDTO? inputGeoPointDTO,
}, },
maxLines: 1, maxLines: 1,
isTitle: false, isTitle: false,
showPreview: true,
), ),
), ),
SizedBox(width: 20), SizedBox(width: 20),
@ -188,6 +192,7 @@ void showNewOrUpdateGeoPoint(MapDTO mapDTO, GeoPointDTO? inputGeoPointDTO,
}, },
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
showPreview: true,
), ),
), ),
SizedBox(width: 20), SizedBox(width: 20),
@ -206,6 +211,7 @@ void showNewOrUpdateGeoPoint(MapDTO mapDTO, GeoPointDTO? inputGeoPointDTO,
}, },
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
showPreview: true,
), ),
), ),
], ],
@ -230,6 +236,7 @@ void showNewOrUpdateGeoPoint(MapDTO mapDTO, GeoPointDTO? inputGeoPointDTO,
}, },
maxLines: 1, maxLines: 1,
isTitle: false, isTitle: false,
showPreview: true,
), ),
), ),
SizedBox(width: 20), SizedBox(width: 20),

View File

@ -210,6 +210,7 @@ class _SubSectionEditScreenState extends State<SubSectionEditScreen> {
maxLines: 1, maxLines: 1,
isHTML: true, isHTML: true,
isTitle: true, isTitle: true,
showPreview: true,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
MultiStringInputContainer( MultiStringInputContainer(
@ -226,6 +227,7 @@ class _SubSectionEditScreenState extends State<SubSectionEditScreen> {
maxLines: 1, maxLines: 1,
isHTML: true, isHTML: true,
isTitle: false, isTitle: false,
showPreview: true,
), ),
], ],
), ),

View File

@ -8,9 +8,10 @@ import 'package:manager_app/l10n/app_localizations.dart';
import 'package:manager_app/Components/rounded_button.dart'; import 'package:manager_app/Components/rounded_button.dart';
import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart';
import 'package:manager_app/Components/check_input_container.dart'; import 'package:manager_app/Components/check_input_container.dart';
import 'package:manager_app/Components/string_input_container.dart'; import 'package:manager_app/Components/number_stepper_field.dart';
import 'package:manager_app/Components/resource_input_container.dart'; import 'package:manager_app/Components/resource_input_container.dart';
import 'package:manager_app/Components/reorderable_custom_list.dart'; import 'package:manager_app/Components/reorderable_custom_list.dart';
import 'package:manager_app/Components/section_card.dart';
import 'showNewOrUpdateGuidedStep.dart'; import 'showNewOrUpdateGuidedStep.dart';
void showNewOrUpdateGuidedPath( void showNewOrUpdateGuidedPath(
@ -44,7 +45,6 @@ void showNewOrUpdateGuidedPath(
// contentWidth = dialogWidth minus the 24px padding on each side // contentWidth = dialogWidth minus the 24px padding on each side
final double contentWidth = dialogWidth - 48; final double contentWidth = dialogWidth - 48;
final double halfWidth = (contentWidth - 20) / 2; final double halfWidth = (contentWidth - 20) / 2;
final double thirdWidth = (contentWidth - 40) / 3;
return Dialog( return Dialog(
shape: shape:
@ -95,6 +95,7 @@ void showNewOrUpdateGuidedPath(
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
isHTML: true, isHTML: true,
showPreview: true,
), ),
), ),
SizedBox(width: 20), SizedBox(width: 20),
@ -109,243 +110,257 @@ void showNewOrUpdateGuidedPath(
maxLines: 1, maxLines: 1,
isTitle: false, isTitle: false,
isHTML: true, isHTML: true,
showPreview: true,
), ),
), ),
], ],
), ),
Divider(height: 24), SizedBox(height: 16),
// Options // Options
Row( SectionCard(
children: [ icon: Icons.settings,
SizedBox( title: "Options du parcours",
width: thirdWidth, child: Column(
child: CheckInputContainer( crossAxisAlignment: CrossAxisAlignment.start,
children: [
CheckInputContainer(
label: AppLocalizations.of(context)!.linearLabel, label: AppLocalizations.of(context)!.linearLabel,
subtitle: "Les étapes doivent être suivies dans l'ordre.",
isChecked: workingPath.isLinear ?? false, isChecked: workingPath.isLinear ?? false,
onChanged: (val) => setState( onChanged: (val) => setState(
() => workingPath.isLinear = val), () => workingPath.isLinear = val),
), ),
), CheckInputContainer(
SizedBox(width: 20),
SizedBox(
width: thirdWidth,
child: CheckInputContainer(
label: AppLocalizations.of(context)!.requiredSuccessLabel, label: AppLocalizations.of(context)!.requiredSuccessLabel,
subtitle: "Le quiz de chaque étape doit être validé pour débloquer la suivante.",
isChecked: isChecked:
workingPath.requireSuccessToAdvance ?? workingPath.requireSuccessToAdvance ??
false, false,
onChanged: (val) => setState(() => workingPath onChanged: (val) => setState(() => workingPath
.requireSuccessToAdvance = val), .requireSuccessToAdvance = val),
), ),
), CheckInputContainer(
SizedBox(width: 20),
SizedBox(
width: thirdWidth,
child: CheckInputContainer(
label: "Cacher les suivantes :", label: "Cacher les suivantes :",
subtitle: "Seule l'étape en cours est visible sur la carte.",
isChecked: isChecked:
workingPath.hideNextStepsUntilComplete ?? workingPath.hideNextStepsUntilComplete ??
false, false,
onChanged: (val) => setState(() => workingPath onChanged: (val) => setState(() => workingPath
.hideNextStepsUntilComplete = val), .hideNextStepsUntilComplete = val),
), ),
), SizedBox(height: 8),
], NumberStepperField(
), label: "Durée estimée",
SizedBox(height: 8), value: workingPath.estimatedDurationMinutes,
SizedBox( min: 0,
width: thirdWidth, max: 600,
child: StringInputContainer( unit: "min",
label: "Durée estimée (min) :", onChanged: (val) => setState(() =>
initialValue: workingPath.estimatedDurationMinutes?.toString() ?? "", workingPath.estimatedDurationMinutes = val.toInt()),
onChanged: (val) => setState(() =>
workingPath.estimatedDurationMinutes = int.tryParse(val)),
),
),
Divider(height: 24),
// Mode jeu
CheckInputContainer(
label: "Mode jeu (escape game / chasse au trésor)",
isChecked: workingPath.isGameMode ?? false,
onChanged: (val) =>
setState(() => workingPath.isGameMode = val),
),
if (workingPath.isGameMode == true) ...[
SizedBox(height: 8),
Row(
children: [
Expanded(
child: MultiStringInputContainer(
label: "Message de début :",
modalLabel: "Message d'introduction du jeu",
color: kPrimaryColor,
initialValue: (workingPath.gameMessageDebut ?? [])
.map((t) => TranslationDTO(language: t.language, value: t.value))
.toList(),
onGetResult: (val) {
setState(() {
workingPath.gameMessageDebut = val.map((t) {
final prev = (workingPath.gameMessageDebut ?? []).firstWhere(
(e) => e.language == t.language,
orElse: () => TranslationAndResourceDTO(),
);
return TranslationAndResourceDTO(
language: t.language,
value: t.value,
resourceId: prev.resourceId,
resource: prev.resource,
);
}).toList();
});
},
maxLines: 2,
isTitle: false,
isHTML: true,
),
),
SizedBox(width: 20),
Expanded(
child: MultiStringInputContainer(
label: "Message de fin :",
modalLabel: "Message de félicitations du jeu",
color: kPrimaryColor,
initialValue: (workingPath.gameMessageFin ?? [])
.map((t) => TranslationDTO(language: t.language, value: t.value))
.toList(),
onGetResult: (val) {
setState(() {
workingPath.gameMessageFin = val.map((t) {
final prev = (workingPath.gameMessageFin ?? []).firstWhere(
(e) => e.language == t.language,
orElse: () => TranslationAndResourceDTO(),
);
return TranslationAndResourceDTO(
language: t.language,
value: t.value,
resourceId: prev.resourceId,
resource: prev.resource,
);
}).toList();
});
},
maxLines: 2,
isTitle: false,
isHTML: true,
),
), ),
], ],
), ),
],
Divider(height: 24),
// Étapes
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(AppLocalizations.of(context)!.pathStepsLabel,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15)),
IconButton(
icon: Icon(Icons.add_circle_outline,
color: kSuccess),
onPressed: () {
showNewOrUpdateGuidedStep(
context,
null,
workingPath.id ?? "temp",
workingPath.isGameMode ?? false,
(newStep) async {
setState(() {
newStep.order =
workingPath.steps?.length ?? 0;
workingPath.steps = [
...(workingPath.steps ?? []),
newStep
];
stepsRevision++;
});
},
);
},
),
],
), ),
if (workingPath.steps?.isEmpty ?? true) SizedBox(height: 16),
Padding( // Mode jeu
padding: const EdgeInsets.symmetric(vertical: 8), SectionCard(
child: Text( icon: Icons.sports_esports,
AppLocalizations.of(context)!.noStepConfigured, title: "Mode jeu",
style: TextStyle( subtitle: "Escape game ou chasse au trésor",
fontStyle: FontStyle.italic, child: Column(
color: Colors.grey[600]), crossAxisAlignment: CrossAxisAlignment.start,
), children: [
) CheckInputContainer(
else label: "Mode jeu (escape game / chasse au trésor)",
ReorderableCustomList<GuidedStepDTO>( subtitle: "Active les messages de jeu ci-dessous et le vocabulaire escape game sur les étapes.",
key: ValueKey(stepsRevision), isChecked: workingPath.isGameMode ?? false,
items: workingPath.steps!, onChanged: (val) =>
shrinkWrap: true, setState(() => workingPath.isGameMode = val),
onChanged: (updatedList) { ),
setState(() { if (workingPath.isGameMode == true) ...[
for (var i = 0; i < updatedList.length; i++) { SizedBox(height: 8),
updatedList[i].order = i; Row(
} children: [
workingPath.steps = List.from(updatedList); Expanded(
}); child: MultiStringInputContainer(
}, label: "Message de début :",
itemBuilder: (context, index, step) { modalLabel: "Message d'introduction du jeu",
return ListTile( color: kPrimaryColor,
leading: initialValue: (workingPath.gameMessageDebut ?? [])
CircleAvatar(child: Text("${index + 1}")), .map((t) => TranslationDTO(language: t.language, value: t.value))
title: HtmlWidget( .toList(),
step.title != null && step.title!.isNotEmpty onGetResult: (val) {
? step.title!
.firstWhere(
(t) => t.language == 'FR',
orElse: () =>
step.title![0])
.value ??
"${AppLocalizations.of(context)!.stepFallback} $index"
: "${AppLocalizations.of(context)!.stepFallback} $index",
),
subtitle: workingPath.isGameMode ?? false
? Text(
"${step.quizQuestions?.length ?? 0} question(s)")
: null,
);
},
actions: [
(context, index, step) => IconButton(
icon: Icon(Icons.edit,
color: kPrimaryColor),
onPressed: () {
showNewOrUpdateGuidedStep(
context,
step,
workingPath.id ?? "temp",
workingPath.isGameMode ?? false,
(updatedStep) async {
setState(() { setState(() {
updatedStep.order = step.order; workingPath.gameMessageDebut = val.map((t) {
workingPath.steps![index] = final prev = (workingPath.gameMessageDebut ?? []).firstWhere(
updatedStep; (e) => e.language == t.language,
stepsRevision++; orElse: () => TranslationAndResourceDTO(),
);
return TranslationAndResourceDTO(
language: t.language,
value: t.value,
resourceId: prev.resourceId,
resource: prev.resource,
);
}).toList();
}); });
}, },
); maxLines: 2,
}, isTitle: false,
), isHTML: true,
(context, index, step) => IconButton( showPreview: true,
icon: Icon(Icons.delete, color: kError), ),
onPressed: () { ),
setState(() { SizedBox(width: 20),
workingPath.steps!.removeAt(index); Expanded(
stepsRevision++; child: MultiStringInputContainer(
}); label: "Message de fin :",
}, modalLabel: "Message de félicitations du jeu",
), color: kPrimaryColor,
initialValue: (workingPath.gameMessageFin ?? [])
.map((t) => TranslationDTO(language: t.language, value: t.value))
.toList(),
onGetResult: (val) {
setState(() {
workingPath.gameMessageFin = val.map((t) {
final prev = (workingPath.gameMessageFin ?? []).firstWhere(
(e) => e.language == t.language,
orElse: () => TranslationAndResourceDTO(),
);
return TranslationAndResourceDTO(
language: t.language,
value: t.value,
resourceId: prev.resourceId,
resource: prev.resource,
);
}).toList();
});
},
maxLines: 2,
isTitle: false,
isHTML: true,
showPreview: true,
),
),
],
),
],
], ],
), ),
),
SizedBox(height: 16),
// Étapes
SectionCard(
icon: Icons.list_alt,
title: AppLocalizations.of(context)!.pathStepsLabel,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: Icon(Icons.add_circle_outline,
color: kSuccess),
onPressed: () {
showNewOrUpdateGuidedStep(
context,
null,
workingPath.id ?? "temp",
workingPath.isGameMode ?? false,
(newStep) async {
setState(() {
newStep.order =
workingPath.steps?.length ?? 0;
workingPath.steps = [
...(workingPath.steps ?? []),
newStep
];
stepsRevision++;
});
},
);
},
),
),
if (workingPath.steps?.isEmpty ?? true)
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
AppLocalizations.of(context)!.noStepConfigured,
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey[600]),
),
)
else
ReorderableCustomList<GuidedStepDTO>(
key: ValueKey(stepsRevision),
items: workingPath.steps!,
shrinkWrap: true,
onChanged: (updatedList) {
setState(() {
for (var i = 0; i < updatedList.length; i++) {
updatedList[i].order = i;
}
workingPath.steps = List.from(updatedList);
});
},
itemBuilder: (context, index, step) {
return ListTile(
leading:
CircleAvatar(child: Text("${index + 1}")),
title: HtmlWidget(
step.title != null && step.title!.isNotEmpty
? step.title!
.firstWhere(
(t) => t.language == 'FR',
orElse: () =>
step.title![0])
.value ??
"${AppLocalizations.of(context)!.stepFallback} $index"
: "${AppLocalizations.of(context)!.stepFallback} $index",
),
subtitle: workingPath.isGameMode ?? false
? Text(
"${step.quizQuestions?.length ?? 0} question(s)")
: null,
);
},
actions: [
(context, index, step) => IconButton(
icon: Icon(Icons.edit,
color: kPrimaryColor),
onPressed: () {
showNewOrUpdateGuidedStep(
context,
step,
workingPath.id ?? "temp",
workingPath.isGameMode ?? false,
(updatedStep) async {
setState(() {
updatedStep.order = step.order;
workingPath.steps![index] =
updatedStep;
stepsRevision++;
});
},
);
},
),
(context, index, step) => IconButton(
icon: Icon(Icons.delete, color: kError),
onPressed: () {
setState(() {
workingPath.steps!.removeAt(index);
stepsRevision++;
});
},
),
],
),
],
),
),
], ],
), ),
), ),

View File

@ -5,10 +5,11 @@ import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:manager_api_new/api.dart'; import 'package:manager_api_new/api.dart';
import 'package:manager_app/Components/confirmation_dialog.dart'; import 'package:manager_app/Components/confirmation_dialog.dart';
import 'package:manager_app/Components/geometry_input_container.dart'; import 'package:manager_app/Components/geometry_input_container.dart';
import 'package:manager_app/Components/number_stepper_field.dart';
import 'package:manager_app/Components/reorderable_custom_list.dart'; import 'package:manager_app/Components/reorderable_custom_list.dart';
import 'package:manager_app/Components/multi_string_input_container.dart'; import 'package:manager_app/Components/multi_string_input_container.dart';
import 'package:manager_app/Components/rounded_button.dart'; import 'package:manager_app/Components/rounded_button.dart';
import 'package:manager_app/Components/string_input_container.dart'; import 'package:manager_app/Components/section_card.dart';
import 'package:manager_app/Screens/Configurations/Section/SubSection/Slider/listView_card_image.dart'; import 'package:manager_app/Screens/Configurations/Section/SubSection/Slider/listView_card_image.dart';
import 'package:manager_app/Screens/Configurations/Section/SubSection/Slider/new_update_image_slider.dart'; import 'package:manager_app/Screens/Configurations/Section/SubSection/Slider/new_update_image_slider.dart';
import 'package:manager_app/app_context.dart'; import 'package:manager_app/app_context.dart';
@ -93,6 +94,7 @@ void showNewOrUpdateGuidedStep(
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
isHTML: true, isHTML: true,
showPreview: true,
), ),
), ),
SizedBox(width: 20), SizedBox(width: 20),
@ -107,300 +109,378 @@ void showNewOrUpdateGuidedStep(
maxLines: 1, maxLines: 1,
isTitle: false, isTitle: false,
isHTML: true, isHTML: true,
showPreview: true,
), ),
), ),
], ],
), ),
Divider(height: 24), SizedBox(height: 16),
// Géométrie Directement avec GeometryDTO // Emplacement Directement avec GeometryDTO
GeometryInputContainer( SectionCard(
label: AppLocalizations.of(context)!.stepLocationLabel, icon: Icons.location_on,
initialGeometry: workingStep.geometry, title: "Emplacement",
initialColor: null, subtitle: "Position et déclenchement géolocalisé de l'étape",
onSave: (geometry, color) { child: Column(
setState(() { crossAxisAlignment: CrossAxisAlignment.start,
workingStep.geometry = geometry;
});
},
),
SizedBox(height: 8),
SwitchListTile(
dense: true,
contentPadding: EdgeInsets.zero,
title: Text("Déclenchement géolocalisé"),
value: workingStep.isGeoTriggered ?? false,
onChanged: (val) => setState(() => workingStep.isGeoTriggered = val),
activeThumbColor: kPrimaryColor,
),
if (workingStep.isGeoTriggered == true) ...[
SizedBox(height: 8),
SizedBox(
width: halfWidth,
child: StringInputContainer(
label: "Rayon (mètres) :",
initialValue: workingStep.zoneRadiusMeters?.toString() ?? "",
onChanged: (val) => setState(() =>
workingStep.zoneRadiusMeters = double.tryParse(val)),
),
),
],
Divider(height: 24),
// Comportement de l'étape
Text("Comportement", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
SizedBox(height: 8),
Row(
children: [
Expanded(
child: SwitchListTile(
dense: true,
title: Text(AppLocalizations.of(context)!.initiallyHiddenLabel),
value: workingStep.isHiddenInitially ?? false,
onChanged: (val) => setState(() => workingStep.isHiddenInitially = val),
activeThumbColor: kPrimaryColor,
),
),
Expanded(
child: SwitchListTile(
dense: true,
title: Text(AppLocalizations.of(context)!.lockedLabel),
value: workingStep.isStepLocked ?? false,
onChanged: (val) => setState(() => workingStep.isStepLocked = val),
activeThumbColor: kPrimaryColor,
),
),
],
),
Divider(height: 24),
Text("Contenu riche", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
SizedBox(height: 8),
// Audio
MultiStringInputContainer(
label: "Audio :",
resourceTypes: [ResourceType.Audio],
modalLabel: "Audio du guide",
initialValue: workingStep.audioIds!,
onGetResult: (val) => setState(() => workingStep.audioIds = val.isEmpty ? null : val),
maxLines: 1,
isTitle: false,
isHTML: false,
),
SizedBox(height: 12),
// Images
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Images :", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
IconButton(
icon: Icon(Icons.add_circle_outline, color: kSuccess),
onPressed: () async {
final result = await showNewOrUpdateContentSlider(null, appCtx, context, true, false);
if (result != null) {
setState(() {
result.order = workingStep.contents!.length;
workingStep.contents = [...workingStep.contents!, result];
});
}
},
),
],
),
if (workingStep.contents!.isEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
"Aucune image — cliquez + pour en ajouter",
style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey[600], fontSize: 13),
),
)
else
SizedBox(
height: 250,
child: ReorderableListView(
scrollDirection: Axis.horizontal,
onReorderItem: (oldIndex, newIndex) {
setState(() {
final item = workingStep.contents!.removeAt(oldIndex);
workingStep.contents!.insert(newIndex, item);
for (var i = 0; i < workingStep.contents!.length; i++) {
workingStep.contents![i].order = i;
}
});
},
children: List.generate(workingStep.contents!.length, (i) => ListViewCardContent(
workingStep.contents!,
i,
Key('content_$i'),
appCtx,
(updated) => setState(() => workingStep.contents = List.from(updated)),
true,
false,
)),
),
),
// Questions uniquement en mode Escape Game
if (isEscapeMode) ...[
Divider(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text(AppLocalizations.of(context)!.questionsChallengesLabel, GeometryInputContainer(
style: TextStyle( label: AppLocalizations.of(context)!.stepLocationLabel,
fontWeight: FontWeight.bold, initialGeometry: workingStep.geometry,
fontSize: 15)), initialColor: null,
IconButton( onSave: (geometry, color) {
icon: Icon(Icons.add_circle_outline, setState(() {
color: kSuccess), workingStep.geometry = geometry;
onPressed: () { });
showNewOrUpdateQuizQuestion(
context,
null,
workingStep.id ?? "temp",
isEscapeMode,
(newQuestion) {
setState(() {
newQuestion.order =
workingStep.quizQuestions?.length ?? 0;
workingStep.quizQuestions = [
...(workingStep.quizQuestions ??
[]),
newQuestion
];
questionsRevision++;
});
},
);
}, },
), ),
SizedBox(height: 8),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Row(
children: [
Switch(
value: workingStep.isGeoTriggered ?? false,
onChanged: (val) => setState(() => workingStep.isGeoTriggered = val),
activeThumbColor: kPrimaryColor,
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("Déclenchement géolocalisé", style: TextStyle(fontSize: 14)),
Text(
"L'étape se débloque automatiquement quand le visiteur entre dans la zone.",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
],
),
),
if (workingStep.isGeoTriggered == true)
Padding(
padding: const EdgeInsets.only(left: 12),
child: NumberStepperField(
label: "Rayon",
value: workingStep.zoneRadiusMeters,
min: 1,
max: 500,
unit: "m",
onChanged: (val) => setState(() =>
workingStep.zoneRadiusMeters = val.toDouble()),
),
),
],
),
], ],
), ),
SizedBox( ),
width: halfWidth, SizedBox(height: 16),
child: SwitchListTile( // Comportement de l'étape
dense: true, SectionCard(
contentPadding: EdgeInsets.zero, icon: Icons.tune,
title: Text("Timer"), title: "Comportement",
value: workingStep.isStepTimer ?? false, subtitle: "Visibilité et verrouillage de l'étape",
onChanged: (val) => setState(() => workingStep.isStepTimer = val), child: Row(
activeThumbColor: kPrimaryColor, children: [
), Expanded(
), child: SwitchListTile(
if (workingStep.isStepTimer == true) ...[
SizedBox(height: 8),
Row(
children: [
SizedBox(
width: halfWidth,
child: StringInputContainer(
label: AppLocalizations.of(context)!.durationSecondsLabel,
initialValue: workingStep.timerSeconds?.toString() ?? "",
onChanged: (val) => setState(() =>
workingStep.timerSeconds = int.tryParse(val)),
),
),
SizedBox(width: 20),
SizedBox(
width: halfWidth,
child: MultiStringInputContainer(
label: "Message d'expiration :",
modalLabel: "Message d'expiration du timer",
initialValue: workingStep.timerExpiredMessage ?? [],
onGetResult: (val) => setState(() =>
workingStep.timerExpiredMessage = val.isEmpty ? null : val),
maxLines: 2,
isTitle: false,
isHTML: true,
),
),
],
),
SizedBox(height: 8),
],
if (workingStep.quizQuestions == null ||
workingStep.quizQuestions!.isEmpty)
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: Text(
AppLocalizations.of(context)!.noQuestionsConfigured,
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey[600]),
),
)
else
ReorderableCustomList<QuizQuestion>(
key: ValueKey(questionsRevision),
items: workingStep.quizQuestions!,
shrinkWrap: true,
onChanged: (updatedList) {
setState(() {
for (var i = 0; i < updatedList.length; i++) {
updatedList[i].order = i;
}
workingStep.quizQuestions =
List.from(updatedList);
});
},
itemBuilder: (context, qIndex, question) {
return ListTile(
dense: true, dense: true,
title: HtmlWidget( title: Text(AppLocalizations.of(context)!.initiallyHiddenLabel),
question.label.isNotEmpty
? question.label
.firstWhere(
(t) => t.language == 'FR',
orElse: () =>
question.label[0])
.value ??
"Question $qIndex"
: "Question $qIndex",
textStyle: TextStyle(fontSize: 14),
),
subtitle: Text( subtitle: Text(
"Type: ${question.validationQuestionType?.value == 2 ? 'Puzzle' : question.validationQuestionType?.value == 1 ? 'QCM' : 'Texte'}"), "N'apparaît pas sur la carte tant qu'elle n'a pas été débloquée.",
); style: TextStyle(fontSize: 12),
}, ),
actions: [ value: workingStep.isHiddenInitially ?? false,
(context, qIndex, question) => IconButton( onChanged: (val) => setState(() => workingStep.isHiddenInitially = val),
icon: Icon(Icons.edit, activeThumbColor: kPrimaryColor,
size: 18, color: kPrimaryColor), ),
onPressed: () { ),
showNewOrUpdateQuizQuestion( Expanded(
context, child: SwitchListTile(
question, dense: true,
workingStep.id ?? "temp", title: Text(AppLocalizations.of(context)!.lockedLabel),
isEscapeMode, subtitle: Text(
(updatedQuestion) { "Visible mais non accessible tant que les étapes précédentes ne sont pas terminées.",
setState(() { style: TextStyle(fontSize: 12),
updatedQuestion.order = ),
question.order; value: workingStep.isStepLocked ?? false,
workingStep.quizQuestions![ onChanged: (val) => setState(() => workingStep.isStepLocked = val),
qIndex] = updatedQuestion; activeThumbColor: kPrimaryColor,
questionsRevision++; ),
}); ),
}, ],
); ),
),
SizedBox(height: 16),
SectionCard(
icon: Icons.perm_media,
title: "Contenu riche",
subtitle: "Audio guide et images affichés sur la fiche de l'étape",
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Audio
MultiStringInputContainer(
label: "Audio :",
resourceTypes: [ResourceType.Audio],
modalLabel: "Audio du guide",
initialValue: workingStep.audioIds!,
onGetResult: (val) => setState(() => workingStep.audioIds = val.isEmpty ? null : val),
maxLines: 1,
isTitle: false,
isHTML: false,
),
SizedBox(height: 12),
// Images
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Images :", style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
IconButton(
icon: Icon(Icons.add_circle_outline, color: kSuccess),
onPressed: () async {
final result = await showNewOrUpdateContentSlider(null, appCtx, context, true, false);
if (result != null) {
setState(() {
result.order = workingStep.contents!.length;
workingStep.contents = [...workingStep.contents!, result];
});
}
},
),
],
),
if (workingStep.contents!.isEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
"Aucune image — cliquez + pour en ajouter",
style: TextStyle(fontStyle: FontStyle.italic, color: Colors.grey[600], fontSize: 13),
),
)
else
SizedBox(
height: 250,
child: ReorderableListView(
scrollDirection: Axis.horizontal,
onReorderItem: (oldIndex, newIndex) {
setState(() {
final item = workingStep.contents!.removeAt(oldIndex);
workingStep.contents!.insert(newIndex, item);
for (var i = 0; i < workingStep.contents!.length; i++) {
workingStep.contents![i].order = i;
}
});
},
children: List.generate(workingStep.contents!.length, (i) => ListViewCardContent(
workingStep.contents!,
i,
Key('content_$i'),
appCtx,
(updated) => setState(() => workingStep.contents = List.from(updated)),
true,
false,
)),
),
),
],
),
),
SizedBox(height: 16),
// Questions disponibles pour tous les parcours (guidés et escape game)
SectionCard(
icon: Icons.quiz,
title: AppLocalizations.of(context)!.questionsChallengesLabel,
subtitle: isEscapeMode
? "Énigme à résoudre pour progresser"
: "Quiz optionnel pour cette étape",
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: Icon(Icons.add_circle_outline,
color: kSuccess),
onPressed: () {
showNewOrUpdateQuizQuestion(
context,
null,
workingStep.id ?? "temp",
isEscapeMode,
(newQuestion) {
setState(() {
newQuestion.order =
workingStep.quizQuestions?.length ?? 0;
workingStep.quizQuestions = [
...(workingStep.quizQuestions ??
[]),
newQuestion
];
questionsRevision++;
});
}, },
);
},
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Row(
children: [
Switch(
value: workingStep.isStepTimer ?? false,
onChanged: (val) => setState(() => workingStep.isStepTimer = val),
activeThumbColor: kPrimaryColor,
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text("Timer", style: TextStyle(fontSize: 14)),
Text(
"Chronomètre la réponse ; passé le délai, le message d'expiration s'affiche.",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
],
), ),
(context, qIndex, question) => IconButton( ),
icon: Icon(Icons.delete, if (workingStep.isStepTimer == true)
size: 18, color: kError), Padding(
onPressed: () { padding: const EdgeInsets.only(left: 12),
showConfirmationDialog( child: NumberStepperField(
"Supprimer cette question ?", label: AppLocalizations.of(context)!.durationSecondsLabel,
() {}, value: workingStep.timerSeconds,
() => setState(() { min: 0,
workingStep.quizQuestions! max: 3600,
.removeAt(qIndex); unit: "sec",
questionsRevision++; onChanged: (val) => setState(() => workingStep.timerSeconds = val.toInt()),
}), ),
context,
);
},
), ),
],
),
if (workingStep.isStepTimer == true) ...[
SizedBox(height: 8),
Row(
children: [
Expanded(
child: MultiStringInputContainer(
label: "Message d'expiration :",
modalLabel: "Message d'expiration du timer",
initialValue: workingStep.timerExpiredMessage ?? [],
onGetResult: (val) => setState(() =>
workingStep.timerExpiredMessage = val.isEmpty ? null : val),
maxLines: 2,
isTitle: false,
isHTML: true,
showPreview: true,
),
),
],
),
SizedBox(height: 8),
], ],
), if (workingStep.quizQuestions == null ||
], workingStep.quizQuestions!.isEmpty)
Padding(
padding:
const EdgeInsets.symmetric(vertical: 8),
child: Text(
AppLocalizations.of(context)!.noQuestionsConfigured,
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey[600]),
),
)
else
ReorderableCustomList<QuizQuestion>(
key: ValueKey(questionsRevision),
items: workingStep.quizQuestions!,
shrinkWrap: true,
onChanged: (updatedList) {
setState(() {
for (var i = 0; i < updatedList.length; i++) {
updatedList[i].order = i;
}
workingStep.quizQuestions =
List.from(updatedList);
});
},
itemBuilder: (context, qIndex, question) {
return ListTile(
dense: true,
title: HtmlWidget(
question.label.isNotEmpty
? question.label
.firstWhere(
(t) => t.language == 'FR',
orElse: () =>
question.label[0])
.value ??
"Question $qIndex"
: "Question $qIndex",
textStyle: TextStyle(fontSize: 14),
),
subtitle: Text(
"Type: ${question.validationQuestionType?.value == 2 ? 'Puzzle' : question.validationQuestionType?.value == 1 ? 'QCM' : 'Texte'}"),
);
},
actions: [
(context, qIndex, question) => IconButton(
icon: Icon(Icons.edit,
size: 18, color: kPrimaryColor),
onPressed: () {
showNewOrUpdateQuizQuestion(
context,
question,
workingStep.id ?? "temp",
isEscapeMode,
(updatedQuestion) {
setState(() {
updatedQuestion.order =
question.order;
workingStep.quizQuestions![
qIndex] = updatedQuestion;
questionsRevision++;
});
},
);
},
),
(context, qIndex, question) => IconButton(
icon: Icon(Icons.delete,
size: 18, color: kError),
onPressed: () {
showConfirmationDialog(
"Supprimer cette question ?",
() {},
() => setState(() {
workingStep.quizQuestions!
.removeAt(qIndex);
questionsRevision++;
}),
context,
);
},
),
],
),
],
),
),
], ],
), ),
), ),
@ -436,11 +516,6 @@ void showNewOrUpdateGuidedStep(
if (workingStep.isGeoTriggered != true) { if (workingStep.isGeoTriggered != true) {
workingStep.zoneRadiusMeters = null; workingStep.zoneRadiusMeters = null;
} }
if (!isEscapeMode) {
workingStep.isStepTimer = false;
workingStep.timerSeconds = null;
workingStep.timerExpiredMessage = null;
}
try { try {
await onSave(workingStep); await onSave(workingStep);
if (context.mounted) Navigator.pop(context); if (context.mounted) Navigator.pop(context);

View File

@ -115,6 +115,7 @@ void showNewOrUpdateQuizQuestion(
maxLines: 3, maxLines: 3,
isTitle: false, isTitle: false,
isHTML: true, isHTML: true,
showPreview: true,
), ),
SizedBox(height: 16), SizedBox(height: 16),
@ -167,6 +168,7 @@ void showNewOrUpdateQuizQuestion(
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
isHTML: true, isHTML: true,
showPreview: true,
); );
}), }),
SizedBox(height: 4), SizedBox(height: 4),
@ -256,6 +258,7 @@ void showNewOrUpdateQuizQuestion(
maxLines: 1, maxLines: 1,
isTitle: true, isTitle: true,
isHTML: true, isHTML: true,
showPreview: true,
), ),
), ),
// Supprimer // Supprimer

View File

@ -97,7 +97,8 @@ Future<ContentDTO?> showNewOrUpdateContentSlider(ContentDTO? inputContentDTO, Ap
} }
}, },
maxLines: 1, maxLines: 1,
isTitle: true isTitle: true,
showPreview: true
), ),
MultiStringInputContainer( MultiStringInputContainer(
label: AppLocalizations.of(context)!.displayedDescriptionLabel, label: AppLocalizations.of(context)!.displayedDescriptionLabel,
@ -113,7 +114,8 @@ Future<ContentDTO?> showNewOrUpdateContentSlider(ContentDTO? inputContentDTO, Ap
} }
}, },
maxLines: 1, maxLines: 1,
isTitle: false isTitle: false,
showPreview: true
), ),
], ],
) )

View File

@ -34,19 +34,43 @@ class _VideoConfigState extends State<VideoConfig> {
late VideoDTO resourceSource; late VideoDTO resourceSource;
late _VideoSourceMode _mode; late _VideoSourceMode _mode;
String? _selectedResourceLabel; String? _selectedResourceLabel;
String? _youtubeUrl;
String? _vimeoUrl;
String? _resourceUrl;
@override @override
void initState() { void initState() {
resourceSource = widget.initialValue; resourceSource = widget.initialValue;
_mode = _detectMode(resourceSource.source_); _mode = _detectMode(resourceSource.source_);
switch (_mode) {
case _VideoSourceMode.youtube:
_youtubeUrl = resourceSource.source_;
break;
case _VideoSourceMode.vimeo:
_vimeoUrl = resourceSource.source_;
break;
case _VideoSourceMode.resource:
_resourceUrl = resourceSource.source_;
break;
}
super.initState(); super.initState();
} }
void _onModeChanged(_VideoSourceMode mode) { void _onModeChanged(_VideoSourceMode mode) {
setState(() { setState(() {
_mode = mode; _mode = mode;
resourceSource.source_ = null;
_selectedResourceLabel = null; _selectedResourceLabel = null;
switch (mode) {
case _VideoSourceMode.youtube:
resourceSource.source_ = _youtubeUrl;
break;
case _VideoSourceMode.vimeo:
resourceSource.source_ = _vimeoUrl;
break;
case _VideoSourceMode.resource:
resourceSource.source_ = _resourceUrl;
break;
}
}); });
widget.onChanged(resourceSource); widget.onChanged(resourceSource);
} }
@ -68,9 +92,11 @@ class _VideoConfigState extends State<VideoConfig> {
const SizedBox(height: 12), const SizedBox(height: 12),
if (_mode == _VideoSourceMode.youtube) if (_mode == _VideoSourceMode.youtube)
StringInputContainer( StringInputContainer(
key: const ValueKey('youtube'),
label: 'URL YouTube :', label: 'URL YouTube :',
initialValue: _mode == _VideoSourceMode.youtube ? resourceSource.source_ : null, initialValue: _youtubeUrl,
onChanged: (url) { onChanged: (url) {
_youtubeUrl = url;
resourceSource.source_ = url; resourceSource.source_ = url;
widget.onChanged(resourceSource); widget.onChanged(resourceSource);
}, },
@ -79,9 +105,11 @@ class _VideoConfigState extends State<VideoConfig> {
), ),
if (_mode == _VideoSourceMode.vimeo) if (_mode == _VideoSourceMode.vimeo)
StringInputContainer( StringInputContainer(
key: const ValueKey('vimeo'),
label: 'URL Vimeo :', label: 'URL Vimeo :',
initialValue: _mode == _VideoSourceMode.vimeo ? resourceSource.source_ : null, initialValue: _vimeoUrl,
onChanged: (url) { onChanged: (url) {
_vimeoUrl = url;
resourceSource.source_ = url; resourceSource.source_ = url;
widget.onChanged(resourceSource); widget.onChanged(resourceSource);
}, },
@ -91,9 +119,10 @@ class _VideoConfigState extends State<VideoConfig> {
if (_mode == _VideoSourceMode.resource) if (_mode == _VideoSourceMode.resource)
_ResourceVideoPicker( _ResourceVideoPicker(
selectedLabel: _selectedResourceLabel, selectedLabel: _selectedResourceLabel,
selectedUrl: _mode == _VideoSourceMode.resource ? resourceSource.source_ : null, selectedUrl: _resourceUrl,
onChanged: (resource) { onChanged: (resource) {
setState(() { setState(() {
_resourceUrl = resource.url;
resourceSource.source_ = resource.url; resourceSource.source_ = resource.url;
_selectedResourceLabel = resource.label; _selectedResourceLabel = resource.label;
}); });

View File

@ -273,6 +273,7 @@ class _SectionDetailScreenState extends State<SectionDetailScreen> {
maxLines: 1, maxLines: 1,
isHTML: true, isHTML: true,
isTitle: true, isTitle: true,
showPreview: true,
), ),
], ],
); );

View File

@ -200,7 +200,8 @@ class _ConfigurationDetailScreenState extends State<ConfigurationDetailScreen> {
isHTML: true, isHTML: true,
isMandatory: false, isMandatory: false,
maxLines: 1, maxLines: 1,
onGetResult: (value) => config.title = value, onGetResult: (value) => setState(() => config.title = value),
showPreview: true,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Wrap( Wrap(

View File

@ -117,7 +117,6 @@ class _DeviceElementState extends State<DeviceElement> {
DeviceDTO? device = await managerAppContext.clientAPI!.deviceApi!.deviceUpdateMainInfos(deviceToUpdate); DeviceDTO? device = await managerAppContext.clientAPI!.deviceApi!.deviceUpdateMainInfos(deviceToUpdate);
appConfigurationToUpdate.configurationId = deviceToUpdate.configurationId; appConfigurationToUpdate.configurationId = deviceToUpdate.configurationId;
appConfigurationToUpdate.layoutMainPage = LayoutMainPageType.SimpleGrid; // Hardcoded for now as not supported
AppConfigurationLinkDTO? result = await managerAppContext.clientAPI!.applicationInstanceApi!.applicationInstanceUpdateApplicationLink(appConfigurationToUpdate); AppConfigurationLinkDTO? result = await managerAppContext.clientAPI!.applicationInstanceApi!.applicationInstanceUpdateApplicationLink(appConfigurationToUpdate);
//print(device); //print(device);

View File

@ -289,6 +289,11 @@
"addEvent": "Add an event", "addEvent": "Add an event",
"noEvents": "No events", "noEvents": "No events",
"noAddress": "No address", "noAddress": "No address",
"agendaFilterAll": "All",
"agendaFilterThisMonth": "This month",
"agendaFilterThisYear": "This year",
"agendaFilterPast": "Past",
"agendaEventDeleteConfirm": "Are you sure you want to delete this event?",
"onlineLabel": "Online:", "onlineLabel": "Online:",
"mapViewLabel": "Map view:", "mapViewLabel": "Map view:",
"mapServiceLabel": "Map service:", "mapServiceLabel": "Map service:",
@ -362,6 +367,10 @@
"languagesLabel": "Languages:", "languagesLabel": "Languages:",
"featuredEventLabel": "Featured event:", "featuredEventLabel": "Featured event:",
"chooseEvent": "Choose an event", "chooseEvent": "Choose an event",
"previewWebTitle": "Preview — Web",
"previewMobileTitle": "Preview — Mobile",
"previewDesktop": "Desktop",
"previewMobile": "Mobile",
"noneOption": "None", "noneOption": "None",
"aiAssistantLabel": "AI assistant:", "aiAssistantLabel": "AI assistant:",
"appUpdatedSuccess": "Mobile application updated", "appUpdatedSuccess": "Mobile application updated",

View File

@ -289,6 +289,11 @@
"addEvent": "Ajouter un évènement", "addEvent": "Ajouter un évènement",
"noEvents": "Aucun évènement", "noEvents": "Aucun évènement",
"noAddress": "Pas d'adresse", "noAddress": "Pas d'adresse",
"agendaFilterAll": "Tous",
"agendaFilterThisMonth": "Ce mois-ci",
"agendaFilterThisYear": "Cette année",
"agendaFilterPast": "Passés",
"agendaEventDeleteConfirm": "Êtes-vous sûr de vouloir supprimer cet évènement ?",
"onlineLabel": "En ligne :", "onlineLabel": "En ligne :",
"mapViewLabel": "Vue carte :", "mapViewLabel": "Vue carte :",
"mapServiceLabel": "Service carte :", "mapServiceLabel": "Service carte :",
@ -362,6 +367,10 @@
"languagesLabel": "Langues :", "languagesLabel": "Langues :",
"featuredEventLabel": "Evènement à l'affiche :", "featuredEventLabel": "Evènement à l'affiche :",
"chooseEvent": "Choisir un évènement", "chooseEvent": "Choisir un évènement",
"previewWebTitle": "Aperçu — Web",
"previewMobileTitle": "Aperçu — Mobile",
"previewDesktop": "Desktop",
"previewMobile": "Mobile",
"noneOption": "Aucun", "noneOption": "Aucun",
"aiAssistantLabel": "Assistant IA :", "aiAssistantLabel": "Assistant IA :",
"appUpdatedSuccess": "Application mobile mise à jour", "appUpdatedSuccess": "Application mobile mise à jour",

View File

@ -1462,6 +1462,36 @@ abstract class AppLocalizations {
/// **'Pas d\'adresse'** /// **'Pas d\'adresse'**
String get noAddress; String get noAddress;
/// No description provided for @agendaFilterAll.
///
/// In fr, this message translates to:
/// **'Tous'**
String get agendaFilterAll;
/// No description provided for @agendaFilterThisMonth.
///
/// In fr, this message translates to:
/// **'Ce mois-ci'**
String get agendaFilterThisMonth;
/// No description provided for @agendaFilterThisYear.
///
/// In fr, this message translates to:
/// **'Cette année'**
String get agendaFilterThisYear;
/// No description provided for @agendaFilterPast.
///
/// In fr, this message translates to:
/// **'Passés'**
String get agendaFilterPast;
/// No description provided for @agendaEventDeleteConfirm.
///
/// In fr, this message translates to:
/// **'Êtes-vous sûr de vouloir supprimer cet évènement ?'**
String get agendaEventDeleteConfirm;
/// No description provided for @onlineLabel. /// No description provided for @onlineLabel.
/// ///
/// In fr, this message translates to: /// In fr, this message translates to:
@ -1864,6 +1894,30 @@ abstract class AppLocalizations {
/// **'Choisir un évènement'** /// **'Choisir un évènement'**
String get chooseEvent; String get chooseEvent;
/// No description provided for @previewWebTitle.
///
/// In fr, this message translates to:
/// **'Aperçu — Web'**
String get previewWebTitle;
/// No description provided for @previewMobileTitle.
///
/// In fr, this message translates to:
/// **'Aperçu — Mobile'**
String get previewMobileTitle;
/// No description provided for @previewDesktop.
///
/// In fr, this message translates to:
/// **'Desktop'**
String get previewDesktop;
/// No description provided for @previewMobile.
///
/// In fr, this message translates to:
/// **'Mobile'**
String get previewMobile;
/// No description provided for @noneOption. /// No description provided for @noneOption.
/// ///
/// In fr, this message translates to: /// In fr, this message translates to:

View File

@ -737,6 +737,22 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get noAddress => 'No address'; String get noAddress => 'No address';
@override
String get agendaFilterAll => 'All';
@override
String get agendaFilterThisMonth => 'This month';
@override
String get agendaFilterThisYear => 'This year';
@override
String get agendaFilterPast => 'Past';
@override
String get agendaEventDeleteConfirm =>
'Are you sure you want to delete this event?';
@override @override
String get onlineLabel => 'Online:'; String get onlineLabel => 'Online:';
@ -943,6 +959,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get chooseEvent => 'Choose an event'; String get chooseEvent => 'Choose an event';
@override
String get previewWebTitle => 'Preview — Web';
@override
String get previewMobileTitle => 'Preview — Mobile';
@override
String get previewDesktop => 'Desktop';
@override
String get previewMobile => 'Mobile';
@override @override
String get noneOption => 'None'; String get noneOption => 'None';

View File

@ -760,6 +760,22 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get noAddress => 'Pas d\'adresse'; String get noAddress => 'Pas d\'adresse';
@override
String get agendaFilterAll => 'Tous';
@override
String get agendaFilterThisMonth => 'Ce mois-ci';
@override
String get agendaFilterThisYear => 'Cette année';
@override
String get agendaFilterPast => 'Passés';
@override
String get agendaEventDeleteConfirm =>
'Êtes-vous sûr de vouloir supprimer cet évènement ?';
@override @override
String get onlineLabel => 'En ligne :'; String get onlineLabel => 'En ligne :';
@ -973,6 +989,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get chooseEvent => 'Choisir un évènement'; String get chooseEvent => 'Choisir un évènement';
@override
String get previewWebTitle => 'Aperçu — Web';
@override
String get previewMobileTitle => 'Aperçu — Mobile';
@override
String get previewDesktop => 'Desktop';
@override
String get previewMobile => 'Mobile';
@override @override
String get noneOption => 'Aucun'; String get noneOption => 'Aucun';

View File

@ -746,6 +746,22 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get noAddress => 'Geen adres'; String get noAddress => 'Geen adres';
@override
String get agendaFilterAll => 'Alle';
@override
String get agendaFilterThisMonth => 'Deze maand';
@override
String get agendaFilterThisYear => 'Dit jaar';
@override
String get agendaFilterPast => 'Verleden';
@override
String get agendaEventDeleteConfirm =>
'Weet u zeker dat u dit evenement wilt verwijderen?';
@override @override
String get onlineLabel => 'Online:'; String get onlineLabel => 'Online:';
@ -954,6 +970,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get chooseEvent => 'Kies een evenement'; String get chooseEvent => 'Kies een evenement';
@override
String get previewWebTitle => 'Voorbeeld — Web';
@override
String get previewMobileTitle => 'Voorbeeld — Mobiel';
@override
String get previewDesktop => 'Desktop';
@override
String get previewMobile => 'Mobiel';
@override @override
String get noneOption => 'Geen'; String get noneOption => 'Geen';

View File

@ -289,6 +289,11 @@
"addEvent": "Evenement toevoegen", "addEvent": "Evenement toevoegen",
"noEvents": "Geen evenementen", "noEvents": "Geen evenementen",
"noAddress": "Geen adres", "noAddress": "Geen adres",
"agendaFilterAll": "Alle",
"agendaFilterThisMonth": "Deze maand",
"agendaFilterThisYear": "Dit jaar",
"agendaFilterPast": "Verleden",
"agendaEventDeleteConfirm": "Weet u zeker dat u dit evenement wilt verwijderen?",
"onlineLabel": "Online:", "onlineLabel": "Online:",
"mapViewLabel": "Kaartweergave:", "mapViewLabel": "Kaartweergave:",
"mapServiceLabel": "Kaartservice:", "mapServiceLabel": "Kaartservice:",
@ -362,6 +367,10 @@
"languagesLabel": "Talen:", "languagesLabel": "Talen:",
"featuredEventLabel": "Uitgelicht evenement:", "featuredEventLabel": "Uitgelicht evenement:",
"chooseEvent": "Kies een evenement", "chooseEvent": "Kies een evenement",
"previewWebTitle": "Voorbeeld — Web",
"previewMobileTitle": "Voorbeeld — Mobiel",
"previewDesktop": "Desktop",
"previewMobile": "Mobiel",
"noneOption": "Geen", "noneOption": "Geen",
"aiAssistantLabel": "AI-assistent:", "aiAssistantLabel": "AI-assistent:",
"appUpdatedSuccess": "Mobiele applicatie bijgewerkt", "appUpdatedSuccess": "Mobiele applicatie bijgewerkt",

View File

@ -238,7 +238,6 @@ Class | Method | HTTP request | Description
- [GuidedStepTriggerGeoPoint](doc//GuidedStepTriggerGeoPoint.md) - [GuidedStepTriggerGeoPoint](doc//GuidedStepTriggerGeoPoint.md)
- [Instance](doc//Instance.md) - [Instance](doc//Instance.md)
- [InstanceDTO](doc//InstanceDTO.md) - [InstanceDTO](doc//InstanceDTO.md)
- [LayoutMainPageType](doc//LayoutMainPageType.md)
- [LoginDTO](doc//LoginDTO.md) - [LoginDTO](doc//LoginDTO.md)
- [MapAnnotation](doc//MapAnnotation.md) - [MapAnnotation](doc//MapAnnotation.md)
- [MapAnnotationDTO](doc//MapAnnotationDTO.md) - [MapAnnotationDTO](doc//MapAnnotationDTO.md)

View File

@ -13,13 +13,13 @@ Name | Type | Description | Notes
**id** | **String** | | [optional] **id** | **String** | | [optional]
**order** | **int** | | [optional] **order** | **int** | | [optional]
**isActive** | **bool** | | [optional] **isActive** | **bool** | | [optional]
**weightMasonryGrid** | **int** | | [optional] **gridColSpan** | **int** | | [optional]
**gridRowSpan** | **int** | | [optional]
**isDate** | **bool** | | [optional] **isDate** | **bool** | | [optional]
**isHour** | **bool** | | [optional] **isHour** | **bool** | | [optional]
**roundedValue** | **int** | | [optional] **roundedValue** | **int** | | [optional]
**screenPercentageSectionsMainPage** | **int** | | [optional] **screenPercentageSectionsMainPage** | **int** | | [optional]
**isSectionImageBackground** | **bool** | | [optional] **isSectionImageBackground** | **bool** | | [optional]
**layoutMainPage** | [**LayoutMainPageType**](LayoutMainPageType.md) | | [optional]
**loaderImageId** | **String** | | [optional] **loaderImageId** | **String** | | [optional]
**loaderImageUrl** | **String** | | [optional] **loaderImageUrl** | **String** | | [optional]
**primaryColor** | **String** | | [optional] **primaryColor** | **String** | | [optional]

View File

@ -18,7 +18,6 @@ Name | Type | Description | Notes
**loaderImageUrl** | **String** | | [optional] **loaderImageUrl** | **String** | | [optional]
**primaryColor** | **String** | | [optional] **primaryColor** | **String** | | [optional]
**secondaryColor** | **String** | | [optional] **secondaryColor** | **String** | | [optional]
**layoutMainPage** | [**LayoutMainPageType**](LayoutMainPageType.md) | | [optional]
**languages** | **List<String>** | | [optional] [default to const []] **languages** | **List<String>** | | [optional] [default to const []]
**sectionEventId** | **String** | | [optional] **sectionEventId** | **String** | | [optional]
**sectionEvent** | [**ApplicationInstanceSectionEvent**](ApplicationInstanceSectionEvent.md) | | [optional] **sectionEvent** | [**ApplicationInstanceSectionEvent**](ApplicationInstanceSectionEvent.md) | | [optional]

View File

@ -14,7 +14,8 @@ Name | Type | Description | Notes
**applicationInstanceId** | **String** | | [optional] **applicationInstanceId** | **String** | | [optional]
**order** | **int** | | [optional] **order** | **int** | | [optional]
**isActive** | **bool** | | [optional] **isActive** | **bool** | | [optional]
**weightMasonryGrid** | **int** | | [optional] **gridColSpan** | **int** | | [optional]
**gridRowSpan** | **int** | | [optional]
**isDate** | **bool** | | [optional] **isDate** | **bool** | | [optional]
**isHour** | **bool** | | [optional] **isHour** | **bool** | | [optional]
**roundedValue** | **int** | | [optional] **roundedValue** | **int** | | [optional]
@ -22,7 +23,6 @@ Name | Type | Description | Notes
**isSectionImageBackground** | **bool** | | [optional] **isSectionImageBackground** | **bool** | | [optional]
**deviceId** | **String** | | [optional] **deviceId** | **String** | | [optional]
**device** | [**AppConfigurationLinkDTODevice**](AppConfigurationLinkDTODevice.md) | | [optional] **device** | [**AppConfigurationLinkDTODevice**](AppConfigurationLinkDTODevice.md) | | [optional]
**layoutMainPage** | [**LayoutMainPageType**](LayoutMainPageType.md) | | [optional]
**loaderImageId** | **String** | | [optional] **loaderImageId** | **String** | | [optional]
**loaderImageUrl** | **String** | | [optional] **loaderImageUrl** | **String** | | [optional]
**primaryColor** | **String** | | [optional] **primaryColor** | **String** | | [optional]

View File

@ -18,7 +18,6 @@ Name | Type | Description | Notes
**loaderImageUrl** | **String** | | [optional] **loaderImageUrl** | **String** | | [optional]
**primaryColor** | **String** | | [optional] **primaryColor** | **String** | | [optional]
**secondaryColor** | **String** | | [optional] **secondaryColor** | **String** | | [optional]
**layoutMainPage** | [**LayoutMainPageType**](LayoutMainPageType.md) | | [optional]
**languages** | **List<String>** | | [optional] [default to const []] **languages** | **List<String>** | | [optional] [default to const []]
**sectionEventId** | **String** | | [optional] **sectionEventId** | **String** | | [optional]
**sectionEvent** | [**ApplicationInstanceSectionEvent**](ApplicationInstanceSectionEvent.md) | | [optional] **sectionEvent** | [**ApplicationInstanceSectionEvent**](ApplicationInstanceSectionEvent.md) | | [optional]

View File

@ -18,7 +18,6 @@ Name | Type | Description | Notes
**loaderImageUrl** | **String** | | [optional] **loaderImageUrl** | **String** | | [optional]
**primaryColor** | **String** | | [optional] **primaryColor** | **String** | | [optional]
**secondaryColor** | **String** | | [optional] **secondaryColor** | **String** | | [optional]
**layoutMainPage** | [**LayoutMainPageType**](LayoutMainPageType.md) | | [optional]
**languages** | **List<String>** | | [optional] [default to const []] **languages** | **List<String>** | | [optional] [default to const []]
**sectionEventId** | **String** | | [optional] **sectionEventId** | **String** | | [optional]
**sectionEventDTO** | [**ApplicationInstanceDTOSectionEventDTO**](ApplicationInstanceDTOSectionEventDTO.md) | | [optional] **sectionEventDTO** | [**ApplicationInstanceDTOSectionEventDTO**](ApplicationInstanceDTOSectionEventDTO.md) | | [optional]

View File

@ -1,14 +0,0 @@
# manager_api_new.model.LayoutMainPageType
## Load the model package
```dart
import 'package:manager_api_new/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -118,7 +118,6 @@ part 'model/instance.dart';
part 'model/instance_dto.dart'; part 'model/instance_dto.dart';
part 'model/instance_quota_dto.dart'; part 'model/instance_quota_dto.dart';
part 'model/subscription_plan_dto.dart'; part 'model/subscription_plan_dto.dart';
part 'model/layout_main_page_type.dart';
part 'model/login_dto.dart'; part 'model/login_dto.dart';
part 'model/map_annotation.dart'; part 'model/map_annotation.dart';
part 'model/map_annotation_dto.dart'; part 'model/map_annotation_dto.dart';

View File

@ -361,8 +361,6 @@ class ApiClient {
return InstanceDTO.fromJson(value); return InstanceDTO.fromJson(value);
case 'InstanceQuotaDTO': case 'InstanceQuotaDTO':
return InstanceQuotaDTO.fromJson(value); return InstanceQuotaDTO.fromJson(value);
case 'LayoutMainPageType':
return LayoutMainPageTypeTypeTransformer().decode(value);
case 'LoginDTO': case 'LoginDTO':
return LoginDTO.fromJson(value); return LoginDTO.fromJson(value);
case 'MapAnnotation': case 'MapAnnotation':

View File

@ -80,9 +80,6 @@ String parameterToString(dynamic value) {
if (value is GeometryType) { if (value is GeometryType) {
return GeometryTypeTypeTransformer().encode(value).toString(); return GeometryTypeTypeTransformer().encode(value).toString();
} }
if (value is LayoutMainPageType) {
return LayoutMainPageTypeTypeTransformer().encode(value).toString();
}
if (value is MapProvider) { if (value is MapProvider) {
return MapProviderTypeTransformer().encode(value).toString(); return MapProviderTypeTransformer().encode(value).toString();
} }

View File

@ -18,13 +18,13 @@ class AppConfigurationLink {
this.id, this.id,
this.order, this.order,
this.isActive, this.isActive,
this.weightMasonryGrid, this.gridColSpan,
this.gridRowSpan,
this.isDate, this.isDate,
this.isHour, this.isHour,
this.roundedValue, this.roundedValue,
this.screenPercentageSectionsMainPage, this.screenPercentageSectionsMainPage,
this.isSectionImageBackground, this.isSectionImageBackground,
this.layoutMainPage,
this.loaderImageId, this.loaderImageId,
this.loaderImageUrl, this.loaderImageUrl,
this.primaryColor, this.primaryColor,
@ -51,7 +51,9 @@ class AppConfigurationLink {
/// ///
bool? isActive; bool? isActive;
int? weightMasonryGrid; int? gridColSpan;
int? gridRowSpan;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@ -81,14 +83,6 @@ class AppConfigurationLink {
/// ///
bool? isSectionImageBackground; bool? isSectionImageBackground;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
LayoutMainPageType? layoutMainPage;
String? loaderImageId; String? loaderImageId;
String? loaderImageUrl; String? loaderImageUrl;
@ -114,14 +108,14 @@ class AppConfigurationLink {
other.id == id && other.id == id &&
other.order == order && other.order == order &&
other.isActive == isActive && other.isActive == isActive &&
other.weightMasonryGrid == weightMasonryGrid && other.gridColSpan == gridColSpan &&
other.gridRowSpan == gridRowSpan &&
other.isDate == isDate && other.isDate == isDate &&
other.isHour == isHour && other.isHour == isHour &&
other.roundedValue == roundedValue && other.roundedValue == roundedValue &&
other.screenPercentageSectionsMainPage == other.screenPercentageSectionsMainPage ==
screenPercentageSectionsMainPage && screenPercentageSectionsMainPage &&
other.isSectionImageBackground == isSectionImageBackground && other.isSectionImageBackground == isSectionImageBackground &&
other.layoutMainPage == layoutMainPage &&
other.loaderImageId == loaderImageId && other.loaderImageId == loaderImageId &&
other.loaderImageUrl == loaderImageUrl && other.loaderImageUrl == loaderImageUrl &&
other.primaryColor == primaryColor && other.primaryColor == primaryColor &&
@ -139,7 +133,8 @@ class AppConfigurationLink {
(id == null ? 0 : id!.hashCode) + (id == null ? 0 : id!.hashCode) +
(order == null ? 0 : order!.hashCode) + (order == null ? 0 : order!.hashCode) +
(isActive == null ? 0 : isActive!.hashCode) + (isActive == null ? 0 : isActive!.hashCode) +
(weightMasonryGrid == null ? 0 : weightMasonryGrid!.hashCode) + (gridColSpan == null ? 0 : gridColSpan!.hashCode) +
(gridRowSpan == null ? 0 : gridRowSpan!.hashCode) +
(isDate == null ? 0 : isDate!.hashCode) + (isDate == null ? 0 : isDate!.hashCode) +
(isHour == null ? 0 : isHour!.hashCode) + (isHour == null ? 0 : isHour!.hashCode) +
(roundedValue == null ? 0 : roundedValue!.hashCode) + (roundedValue == null ? 0 : roundedValue!.hashCode) +
@ -149,7 +144,6 @@ class AppConfigurationLink {
(isSectionImageBackground == null (isSectionImageBackground == null
? 0 ? 0
: isSectionImageBackground!.hashCode) + : isSectionImageBackground!.hashCode) +
(layoutMainPage == null ? 0 : layoutMainPage!.hashCode) +
(loaderImageId == null ? 0 : loaderImageId!.hashCode) + (loaderImageId == null ? 0 : loaderImageId!.hashCode) +
(loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) + (loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) +
(primaryColor == null ? 0 : primaryColor!.hashCode) + (primaryColor == null ? 0 : primaryColor!.hashCode) +
@ -161,7 +155,7 @@ class AppConfigurationLink {
@override @override
String toString() => String toString() =>
'AppConfigurationLink[configurationId=$configurationId, applicationInstanceId=$applicationInstanceId, id=$id, order=$order, isActive=$isActive, weightMasonryGrid=$weightMasonryGrid, isDate=$isDate, isHour=$isHour, roundedValue=$roundedValue, screenPercentageSectionsMainPage=$screenPercentageSectionsMainPage, isSectionImageBackground=$isSectionImageBackground, layoutMainPage=$layoutMainPage, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, configuration=$configuration, applicationInstance=$applicationInstance, deviceId=$deviceId, device=$device]'; 'AppConfigurationLink[configurationId=$configurationId, applicationInstanceId=$applicationInstanceId, id=$id, order=$order, isActive=$isActive, gridColSpan=$gridColSpan, gridRowSpan=$gridRowSpan, isDate=$isDate, isHour=$isHour, roundedValue=$roundedValue, screenPercentageSectionsMainPage=$screenPercentageSectionsMainPage, isSectionImageBackground=$isSectionImageBackground, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, configuration=$configuration, applicationInstance=$applicationInstance, deviceId=$deviceId, device=$device]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -182,10 +176,15 @@ class AppConfigurationLink {
} else { } else {
json[r'isActive'] = null; json[r'isActive'] = null;
} }
if (this.weightMasonryGrid != null) { if (this.gridColSpan != null) {
json[r'weightMasonryGrid'] = this.weightMasonryGrid; json[r'gridColSpan'] = this.gridColSpan;
} else { } else {
json[r'weightMasonryGrid'] = null; json[r'gridColSpan'] = null;
}
if (this.gridRowSpan != null) {
json[r'gridRowSpan'] = this.gridRowSpan;
} else {
json[r'gridRowSpan'] = null;
} }
if (this.isDate != null) { if (this.isDate != null) {
json[r'isDate'] = this.isDate; json[r'isDate'] = this.isDate;
@ -213,11 +212,6 @@ class AppConfigurationLink {
} else { } else {
json[r'isSectionImageBackground'] = null; json[r'isSectionImageBackground'] = null;
} }
if (this.layoutMainPage != null) {
json[r'layoutMainPage'] = this.layoutMainPage;
} else {
json[r'layoutMainPage'] = null;
}
if (this.loaderImageId != null) { if (this.loaderImageId != null) {
json[r'loaderImageId'] = this.loaderImageId; json[r'loaderImageId'] = this.loaderImageId;
} else { } else {
@ -288,7 +282,8 @@ class AppConfigurationLink {
id: mapValueOfType<String>(json, r'id'), id: mapValueOfType<String>(json, r'id'),
order: mapValueOfType<int>(json, r'order'), order: mapValueOfType<int>(json, r'order'),
isActive: mapValueOfType<bool>(json, r'isActive'), isActive: mapValueOfType<bool>(json, r'isActive'),
weightMasonryGrid: mapValueOfType<int>(json, r'weightMasonryGrid'), gridColSpan: mapValueOfType<int>(json, r'gridColSpan'),
gridRowSpan: mapValueOfType<int>(json, r'gridRowSpan'),
isDate: mapValueOfType<bool>(json, r'isDate'), isDate: mapValueOfType<bool>(json, r'isDate'),
isHour: mapValueOfType<bool>(json, r'isHour'), isHour: mapValueOfType<bool>(json, r'isHour'),
roundedValue: mapValueOfType<int>(json, r'roundedValue'), roundedValue: mapValueOfType<int>(json, r'roundedValue'),
@ -296,7 +291,6 @@ class AppConfigurationLink {
mapValueOfType<int>(json, r'screenPercentageSectionsMainPage'), mapValueOfType<int>(json, r'screenPercentageSectionsMainPage'),
isSectionImageBackground: isSectionImageBackground:
mapValueOfType<bool>(json, r'isSectionImageBackground'), mapValueOfType<bool>(json, r'isSectionImageBackground'),
layoutMainPage: LayoutMainPageType.fromJson(json[r'layoutMainPage']),
loaderImageId: mapValueOfType<String>(json, r'loaderImageId'), loaderImageId: mapValueOfType<String>(json, r'loaderImageId'),
loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'), loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'),
primaryColor: mapValueOfType<String>(json, r'primaryColor'), primaryColor: mapValueOfType<String>(json, r'primaryColor'),

View File

@ -23,7 +23,6 @@ class AppConfigurationLinkApplicationInstance {
this.loaderImageUrl, this.loaderImageUrl,
this.primaryColor, this.primaryColor,
this.secondaryColor, this.secondaryColor,
this.layoutMainPage,
this.languages = const [], this.languages = const [],
this.sectionEventId, this.sectionEventId,
this.sectionEvent, this.sectionEvent,
@ -50,14 +49,6 @@ class AppConfigurationLinkApplicationInstance {
String? secondaryColor; String? secondaryColor;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
LayoutMainPageType? layoutMainPage;
List<String>? languages; List<String>? languages;
String? sectionEventId; String? sectionEventId;
@ -86,7 +77,6 @@ class AppConfigurationLinkApplicationInstance {
other.loaderImageUrl == loaderImageUrl && other.loaderImageUrl == loaderImageUrl &&
other.primaryColor == primaryColor && other.primaryColor == primaryColor &&
other.secondaryColor == secondaryColor && other.secondaryColor == secondaryColor &&
other.layoutMainPage == layoutMainPage &&
_deepEquality.equals(other.languages, languages) && _deepEquality.equals(other.languages, languages) &&
other.sectionEventId == sectionEventId && other.sectionEventId == sectionEventId &&
other.sectionEvent == sectionEvent && other.sectionEvent == sectionEvent &&
@ -105,7 +95,6 @@ class AppConfigurationLinkApplicationInstance {
(loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) + (loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) +
(primaryColor == null ? 0 : primaryColor!.hashCode) + (primaryColor == null ? 0 : primaryColor!.hashCode) +
(secondaryColor == null ? 0 : secondaryColor!.hashCode) + (secondaryColor == null ? 0 : secondaryColor!.hashCode) +
(layoutMainPage == null ? 0 : layoutMainPage!.hashCode) +
(languages == null ? 0 : languages!.hashCode) + (languages == null ? 0 : languages!.hashCode) +
(sectionEventId == null ? 0 : sectionEventId!.hashCode) + (sectionEventId == null ? 0 : sectionEventId!.hashCode) +
(sectionEvent == null ? 0 : sectionEvent!.hashCode) + (sectionEvent == null ? 0 : sectionEvent!.hashCode) +
@ -113,7 +102,7 @@ class AppConfigurationLinkApplicationInstance {
@override @override
String toString() => String toString() =>
'AppConfigurationLinkApplicationInstance[instanceId=$instanceId, appType=$appType, id=$id, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, layoutMainPage=$layoutMainPage, languages=$languages, sectionEventId=$sectionEventId, sectionEvent=$sectionEvent, isAssistant=$isAssistant]'; 'AppConfigurationLinkApplicationInstance[instanceId=$instanceId, appType=$appType, id=$id, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, languages=$languages, sectionEventId=$sectionEventId, sectionEvent=$sectionEvent, isAssistant=$isAssistant]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -159,11 +148,6 @@ class AppConfigurationLinkApplicationInstance {
} else { } else {
json[r'secondaryColor'] = null; json[r'secondaryColor'] = null;
} }
if (this.layoutMainPage != null) {
json[r'layoutMainPage'] = this.layoutMainPage;
} else {
json[r'layoutMainPage'] = null;
}
if (this.languages != null) { if (this.languages != null) {
json[r'languages'] = this.languages; json[r'languages'] = this.languages;
} else { } else {
@ -219,7 +203,6 @@ class AppConfigurationLinkApplicationInstance {
loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'), loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'),
primaryColor: mapValueOfType<String>(json, r'primaryColor'), primaryColor: mapValueOfType<String>(json, r'primaryColor'),
secondaryColor: mapValueOfType<String>(json, r'secondaryColor'), secondaryColor: mapValueOfType<String>(json, r'secondaryColor'),
layoutMainPage: LayoutMainPageType.fromJson(json[r'layoutMainPage']),
languages: json[r'languages'] is Iterable languages: json[r'languages'] is Iterable
? (json[r'languages'] as Iterable) ? (json[r'languages'] as Iterable)
.cast<String>() .cast<String>()

View File

@ -19,7 +19,8 @@ class AppConfigurationLinkDTO {
this.applicationInstanceId, this.applicationInstanceId,
this.order, this.order,
this.isActive, this.isActive,
this.weightMasonryGrid, this.gridColSpan,
this.gridRowSpan,
this.isDate, this.isDate,
this.isHour, this.isHour,
this.roundedValue, this.roundedValue,
@ -27,7 +28,6 @@ class AppConfigurationLinkDTO {
this.isSectionImageBackground, this.isSectionImageBackground,
this.deviceId, this.deviceId,
this.device, this.device,
this.layoutMainPage,
this.loaderImageId, this.loaderImageId,
this.loaderImageUrl, this.loaderImageUrl,
this.primaryColor, this.primaryColor,
@ -52,7 +52,9 @@ class AppConfigurationLinkDTO {
/// ///
bool? isActive; bool? isActive;
int? weightMasonryGrid; int? gridColSpan;
int? gridRowSpan;
/// ///
/// Please note: This property should have been non-nullable! Since the specification file /// Please note: This property should have been non-nullable! Since the specification file
@ -86,14 +88,6 @@ class AppConfigurationLinkDTO {
DeviceDTO? device; DeviceDTO? device;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
LayoutMainPageType? layoutMainPage;
String? loaderImageId; String? loaderImageId;
String? loaderImageUrl; String? loaderImageUrl;
@ -112,7 +106,8 @@ class AppConfigurationLinkDTO {
other.applicationInstanceId == applicationInstanceId && other.applicationInstanceId == applicationInstanceId &&
other.order == order && other.order == order &&
other.isActive == isActive && other.isActive == isActive &&
other.weightMasonryGrid == weightMasonryGrid && other.gridColSpan == gridColSpan &&
other.gridRowSpan == gridRowSpan &&
other.isDate == isDate && other.isDate == isDate &&
other.isHour == isHour && other.isHour == isHour &&
other.roundedValue == roundedValue && other.roundedValue == roundedValue &&
@ -121,7 +116,6 @@ class AppConfigurationLinkDTO {
other.isSectionImageBackground == isSectionImageBackground && other.isSectionImageBackground == isSectionImageBackground &&
other.deviceId == deviceId && other.deviceId == deviceId &&
other.device == device && other.device == device &&
other.layoutMainPage == layoutMainPage &&
other.loaderImageId == loaderImageId && other.loaderImageId == loaderImageId &&
other.loaderImageUrl == loaderImageUrl && other.loaderImageUrl == loaderImageUrl &&
other.primaryColor == primaryColor && other.primaryColor == primaryColor &&
@ -136,7 +130,8 @@ class AppConfigurationLinkDTO {
(applicationInstanceId == null ? 0 : applicationInstanceId!.hashCode) + (applicationInstanceId == null ? 0 : applicationInstanceId!.hashCode) +
(order == null ? 0 : order!.hashCode) + (order == null ? 0 : order!.hashCode) +
(isActive == null ? 0 : isActive!.hashCode) + (isActive == null ? 0 : isActive!.hashCode) +
(weightMasonryGrid == null ? 0 : weightMasonryGrid!.hashCode) + (gridColSpan == null ? 0 : gridColSpan!.hashCode) +
(gridRowSpan == null ? 0 : gridRowSpan!.hashCode) +
(isDate == null ? 0 : isDate!.hashCode) + (isDate == null ? 0 : isDate!.hashCode) +
(isHour == null ? 0 : isHour!.hashCode) + (isHour == null ? 0 : isHour!.hashCode) +
(roundedValue == null ? 0 : roundedValue!.hashCode) + (roundedValue == null ? 0 : roundedValue!.hashCode) +
@ -148,7 +143,6 @@ class AppConfigurationLinkDTO {
: isSectionImageBackground!.hashCode) + : isSectionImageBackground!.hashCode) +
(deviceId == null ? 0 : deviceId!.hashCode) + (deviceId == null ? 0 : deviceId!.hashCode) +
(device == null ? 0 : device!.hashCode) + (device == null ? 0 : device!.hashCode) +
(layoutMainPage == null ? 0 : layoutMainPage!.hashCode) +
(loaderImageId == null ? 0 : loaderImageId!.hashCode) + (loaderImageId == null ? 0 : loaderImageId!.hashCode) +
(loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) + (loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) +
(primaryColor == null ? 0 : primaryColor!.hashCode) + (primaryColor == null ? 0 : primaryColor!.hashCode) +
@ -156,7 +150,7 @@ class AppConfigurationLinkDTO {
@override @override
String toString() => String toString() =>
'AppConfigurationLinkDTO[id=$id, configurationId=$configurationId, configuration=$configuration, applicationInstanceId=$applicationInstanceId, order=$order, isActive=$isActive, weightMasonryGrid=$weightMasonryGrid, isDate=$isDate, isHour=$isHour, roundedValue=$roundedValue, screenPercentageSectionsMainPage=$screenPercentageSectionsMainPage, isSectionImageBackground=$isSectionImageBackground, deviceId=$deviceId, device=$device, layoutMainPage=$layoutMainPage, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor]'; 'AppConfigurationLinkDTO[id=$id, configurationId=$configurationId, configuration=$configuration, applicationInstanceId=$applicationInstanceId, order=$order, isActive=$isActive, gridColSpan=$gridColSpan, gridRowSpan=$gridRowSpan, isDate=$isDate, isHour=$isHour, roundedValue=$roundedValue, screenPercentageSectionsMainPage=$screenPercentageSectionsMainPage, isSectionImageBackground=$isSectionImageBackground, deviceId=$deviceId, device=$device, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -190,10 +184,15 @@ class AppConfigurationLinkDTO {
} else { } else {
json[r'isActive'] = null; json[r'isActive'] = null;
} }
if (this.weightMasonryGrid != null) { if (this.gridColSpan != null) {
json[r'weightMasonryGrid'] = this.weightMasonryGrid; json[r'gridColSpan'] = this.gridColSpan;
} else { } else {
json[r'weightMasonryGrid'] = null; json[r'gridColSpan'] = null;
}
if (this.gridRowSpan != null) {
json[r'gridRowSpan'] = this.gridRowSpan;
} else {
json[r'gridRowSpan'] = null;
} }
if (this.isDate != null) { if (this.isDate != null) {
json[r'isDate'] = this.isDate; json[r'isDate'] = this.isDate;
@ -231,11 +230,6 @@ class AppConfigurationLinkDTO {
} else { } else {
json[r'device'] = null; json[r'device'] = null;
} }
if (this.layoutMainPage != null) {
json[r'layoutMainPage'] = this.layoutMainPage;
} else {
json[r'layoutMainPage'] = null;
}
if (this.loaderImageId != null) { if (this.loaderImageId != null) {
json[r'loaderImageId'] = this.loaderImageId; json[r'loaderImageId'] = this.loaderImageId;
} else { } else {
@ -288,7 +282,8 @@ class AppConfigurationLinkDTO {
mapValueOfType<String>(json, r'applicationInstanceId'), mapValueOfType<String>(json, r'applicationInstanceId'),
order: mapValueOfType<int>(json, r'order'), order: mapValueOfType<int>(json, r'order'),
isActive: mapValueOfType<bool>(json, r'isActive'), isActive: mapValueOfType<bool>(json, r'isActive'),
weightMasonryGrid: mapValueOfType<int>(json, r'weightMasonryGrid'), gridColSpan: mapValueOfType<int>(json, r'gridColSpan'),
gridRowSpan: mapValueOfType<int>(json, r'gridRowSpan'),
isDate: mapValueOfType<bool>(json, r'isDate'), isDate: mapValueOfType<bool>(json, r'isDate'),
isHour: mapValueOfType<bool>(json, r'isHour'), isHour: mapValueOfType<bool>(json, r'isHour'),
roundedValue: mapValueOfType<int>(json, r'roundedValue'), roundedValue: mapValueOfType<int>(json, r'roundedValue'),
@ -298,7 +293,6 @@ class AppConfigurationLinkDTO {
mapValueOfType<bool>(json, r'isSectionImageBackground'), mapValueOfType<bool>(json, r'isSectionImageBackground'),
deviceId: mapValueOfType<String>(json, r'deviceId'), deviceId: mapValueOfType<String>(json, r'deviceId'),
device: DeviceDTO.fromJson(json[r'device']), device: DeviceDTO.fromJson(json[r'device']),
layoutMainPage: LayoutMainPageType.fromJson(json[r'layoutMainPage']),
loaderImageId: mapValueOfType<String>(json, r'loaderImageId'), loaderImageId: mapValueOfType<String>(json, r'loaderImageId'),
loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'), loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'),
primaryColor: mapValueOfType<String>(json, r'primaryColor'), primaryColor: mapValueOfType<String>(json, r'primaryColor'),

View File

@ -23,7 +23,6 @@ class ApplicationInstance {
this.loaderImageUrl, this.loaderImageUrl,
this.primaryColor, this.primaryColor,
this.secondaryColor, this.secondaryColor,
this.layoutMainPage,
this.languages = const [], this.languages = const [],
this.sectionEventId, this.sectionEventId,
this.sectionEvent, this.sectionEvent,
@ -50,14 +49,6 @@ class ApplicationInstance {
String? secondaryColor; String? secondaryColor;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
LayoutMainPageType? layoutMainPage;
List<String>? languages; List<String>? languages;
String? sectionEventId; String? sectionEventId;
@ -86,7 +77,6 @@ class ApplicationInstance {
other.loaderImageUrl == loaderImageUrl && other.loaderImageUrl == loaderImageUrl &&
other.primaryColor == primaryColor && other.primaryColor == primaryColor &&
other.secondaryColor == secondaryColor && other.secondaryColor == secondaryColor &&
other.layoutMainPage == layoutMainPage &&
_deepEquality.equals(other.languages, languages) && _deepEquality.equals(other.languages, languages) &&
other.sectionEventId == sectionEventId && other.sectionEventId == sectionEventId &&
other.sectionEvent == sectionEvent && other.sectionEvent == sectionEvent &&
@ -105,7 +95,6 @@ class ApplicationInstance {
(loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) + (loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) +
(primaryColor == null ? 0 : primaryColor!.hashCode) + (primaryColor == null ? 0 : primaryColor!.hashCode) +
(secondaryColor == null ? 0 : secondaryColor!.hashCode) + (secondaryColor == null ? 0 : secondaryColor!.hashCode) +
(layoutMainPage == null ? 0 : layoutMainPage!.hashCode) +
(languages == null ? 0 : languages!.hashCode) + (languages == null ? 0 : languages!.hashCode) +
(sectionEventId == null ? 0 : sectionEventId!.hashCode) + (sectionEventId == null ? 0 : sectionEventId!.hashCode) +
(sectionEvent == null ? 0 : sectionEvent!.hashCode) + (sectionEvent == null ? 0 : sectionEvent!.hashCode) +
@ -113,7 +102,7 @@ class ApplicationInstance {
@override @override
String toString() => String toString() =>
'ApplicationInstance[instanceId=$instanceId, appType=$appType, id=$id, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, layoutMainPage=$layoutMainPage, languages=$languages, sectionEventId=$sectionEventId, sectionEvent=$sectionEvent, isAssistant=$isAssistant]'; 'ApplicationInstance[instanceId=$instanceId, appType=$appType, id=$id, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, languages=$languages, sectionEventId=$sectionEventId, sectionEvent=$sectionEvent, isAssistant=$isAssistant]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -159,11 +148,6 @@ class ApplicationInstance {
} else { } else {
json[r'secondaryColor'] = null; json[r'secondaryColor'] = null;
} }
if (this.layoutMainPage != null) {
json[r'layoutMainPage'] = this.layoutMainPage;
} else {
json[r'layoutMainPage'] = null;
}
if (this.languages != null) { if (this.languages != null) {
json[r'languages'] = this.languages; json[r'languages'] = this.languages;
} else { } else {
@ -219,7 +203,6 @@ class ApplicationInstance {
loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'), loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'),
primaryColor: mapValueOfType<String>(json, r'primaryColor'), primaryColor: mapValueOfType<String>(json, r'primaryColor'),
secondaryColor: mapValueOfType<String>(json, r'secondaryColor'), secondaryColor: mapValueOfType<String>(json, r'secondaryColor'),
layoutMainPage: LayoutMainPageType.fromJson(json[r'layoutMainPage']),
languages: json[r'languages'] is Iterable languages: json[r'languages'] is Iterable
? (json[r'languages'] as Iterable) ? (json[r'languages'] as Iterable)
.cast<String>() .cast<String>()

View File

@ -23,7 +23,6 @@ class ApplicationInstanceDTO {
this.loaderImageUrl, this.loaderImageUrl,
this.primaryColor, this.primaryColor,
this.secondaryColor, this.secondaryColor,
this.layoutMainPage,
this.languages = const [], this.languages = const [],
this.sectionEventId, this.sectionEventId,
this.sectionEventDTO, this.sectionEventDTO,
@ -57,14 +56,6 @@ class ApplicationInstanceDTO {
String? secondaryColor; String? secondaryColor;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
LayoutMainPageType? layoutMainPage;
List<String>? languages; List<String>? languages;
String? sectionEventId; String? sectionEventId;
@ -89,7 +80,6 @@ class ApplicationInstanceDTO {
other.loaderImageUrl == loaderImageUrl && other.loaderImageUrl == loaderImageUrl &&
other.primaryColor == primaryColor && other.primaryColor == primaryColor &&
other.secondaryColor == secondaryColor && other.secondaryColor == secondaryColor &&
other.layoutMainPage == layoutMainPage &&
_deepEquality.equals(other.languages, languages) && _deepEquality.equals(other.languages, languages) &&
other.sectionEventId == sectionEventId && other.sectionEventId == sectionEventId &&
other.sectionEventDTO == sectionEventDTO && other.sectionEventDTO == sectionEventDTO &&
@ -109,7 +99,6 @@ class ApplicationInstanceDTO {
(loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) + (loaderImageUrl == null ? 0 : loaderImageUrl!.hashCode) +
(primaryColor == null ? 0 : primaryColor!.hashCode) + (primaryColor == null ? 0 : primaryColor!.hashCode) +
(secondaryColor == null ? 0 : secondaryColor!.hashCode) + (secondaryColor == null ? 0 : secondaryColor!.hashCode) +
(layoutMainPage == null ? 0 : layoutMainPage!.hashCode) +
(languages == null ? 0 : languages!.hashCode) + (languages == null ? 0 : languages!.hashCode) +
(sectionEventId == null ? 0 : sectionEventId!.hashCode) + (sectionEventId == null ? 0 : sectionEventId!.hashCode) +
(sectionEventDTO == null ? 0 : sectionEventDTO!.hashCode) + (sectionEventDTO == null ? 0 : sectionEventDTO!.hashCode) +
@ -118,7 +107,7 @@ class ApplicationInstanceDTO {
@override @override
String toString() => String toString() =>
'ApplicationInstanceDTO[id=$id, instanceId=$instanceId, appType=$appType, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, layoutMainPage=$layoutMainPage, languages=$languages, sectionEventId=$sectionEventId, sectionEventDTO=$sectionEventDTO, isAssistant=$isAssistant, hasStats=$hasStats]'; 'ApplicationInstanceDTO[id=$id, instanceId=$instanceId, appType=$appType, configurations=$configurations, mainImageId=$mainImageId, mainImageUrl=$mainImageUrl, loaderImageId=$loaderImageId, loaderImageUrl=$loaderImageUrl, primaryColor=$primaryColor, secondaryColor=$secondaryColor, languages=$languages, sectionEventId=$sectionEventId, sectionEventDTO=$sectionEventDTO, isAssistant=$isAssistant, hasStats=$hasStats]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -172,11 +161,6 @@ class ApplicationInstanceDTO {
} else { } else {
json[r'secondaryColor'] = null; json[r'secondaryColor'] = null;
} }
if (this.layoutMainPage != null) {
json[r'layoutMainPage'] = this.layoutMainPage;
} else {
json[r'layoutMainPage'] = null;
}
if (this.languages != null) { if (this.languages != null) {
json[r'languages'] = this.languages; json[r'languages'] = this.languages;
} else { } else {
@ -237,7 +221,6 @@ class ApplicationInstanceDTO {
loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'), loaderImageUrl: mapValueOfType<String>(json, r'loaderImageUrl'),
primaryColor: mapValueOfType<String>(json, r'primaryColor'), primaryColor: mapValueOfType<String>(json, r'primaryColor'),
secondaryColor: mapValueOfType<String>(json, r'secondaryColor'), secondaryColor: mapValueOfType<String>(json, r'secondaryColor'),
layoutMainPage: LayoutMainPageType.fromJson(json[r'layoutMainPage']),
languages: json[r'languages'] is Iterable languages: json[r'languages'] is Iterable
? (json[r'languages'] as Iterable) ? (json[r'languages'] as Iterable)
.cast<String>() .cast<String>()

View File

@ -1,102 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
/// 0 = SimpleGrid 1 = MasonryGrid
class LayoutMainPageType {
/// Instantiate a new enum with the provided [value].
const LayoutMainPageType._(this.value);
/// The underlying value of this enum member.
final int value;
@override
String toString() => value.toString();
int toJson() => value;
static const SimpleGrid = LayoutMainPageType._(0);
static const MasonryGrid = LayoutMainPageType._(1);
/// List of all possible values in this [enum][LayoutMainPageType].
static const values = <LayoutMainPageType>[
SimpleGrid,
MasonryGrid,
];
static LayoutMainPageType? fromJson(dynamic value) =>
LayoutMainPageTypeTypeTransformer().decode(value);
static List<LayoutMainPageType> listFromJson(
dynamic json, {
bool growable = false,
}) {
final result = <LayoutMainPageType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = LayoutMainPageType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [LayoutMainPageType] to int,
/// and [decode] dynamic data back to [LayoutMainPageType].
class LayoutMainPageTypeTypeTransformer {
factory LayoutMainPageTypeTypeTransformer() =>
_instance ??= const LayoutMainPageTypeTypeTransformer._();
const LayoutMainPageTypeTypeTransformer._();
int encode(LayoutMainPageType data) => data.value;
/// Decodes a [dynamic value][data] to a LayoutMainPageType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
LayoutMainPageType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
if(data.runtimeType == String) {
switch (data.toString()) {
case r'SimpleGrid': return LayoutMainPageType.SimpleGrid;
case r'MasonryGrid': return LayoutMainPageType.MasonryGrid;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
} else {
if(data.runtimeType == int) {
switch (data) {
case 0: return LayoutMainPageType.SimpleGrid;
case 1: return LayoutMainPageType.MasonryGrid;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
}
}
return null;
}
/// Singleton [LayoutMainPageTypeTypeTransformer] instance.
static LayoutMainPageTypeTypeTransformer? _instance;
}

View File

@ -91,11 +91,6 @@ void main() {
// TODO // TODO
}); });
// LayoutMainPageType layoutMainPage
test('to test the property `layoutMainPage`', () async {
// TODO
});
// List<String> languages (default value: const []) // List<String> languages (default value: const [])
test('to test the property `languages`', () async { test('to test the property `languages`', () async {
// TODO // TODO

View File

@ -41,8 +41,13 @@ void main() {
// TODO // TODO
}); });
// int weightMasonryGrid // int gridColSpan
test('to test the property `weightMasonryGrid`', () async { test('to test the property `gridColSpan`', () async {
// TODO
});
// int gridRowSpan
test('to test the property `gridRowSpan`', () async {
// TODO // TODO
}); });
}); });

View File

@ -41,8 +41,13 @@ void main() {
// TODO // TODO
}); });
// int weightMasonryGrid // int gridColSpan
test('to test the property `weightMasonryGrid`', () async { test('to test the property `gridColSpan`', () async {
// TODO
});
// int gridRowSpan
test('to test the property `gridRowSpan`', () async {
// TODO // TODO
}); });

View File

@ -91,11 +91,6 @@ void main() {
// TODO // TODO
}); });
// LayoutMainPageType layoutMainPage
test('to test the property `layoutMainPage`', () async {
// TODO
});
// List<String> languages (default value: const []) // List<String> languages (default value: const [])
test('to test the property `languages`', () async { test('to test the property `languages`', () async {
// TODO // TODO

View File

@ -1,17 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:manager_api_new/api.dart';
import 'package:test/test.dart';
// tests for LayoutMainPageType
void main() {
group('test LayoutMainPageType', () {});
}

View File

@ -0,0 +1 @@
export 'src/bento_layout.dart';

View File

@ -0,0 +1,94 @@
class BentoItem {
final String id;
final int colSpan;
final int rowSpan;
const BentoItem({required this.id, this.colSpan = 1, this.rowSpan = 1});
}
class BentoPlacement {
final String id;
final int col;
final int row;
final int colSpan;
final int rowSpan;
const BentoPlacement({
required this.id,
required this.col,
required this.row,
required this.colSpan,
required this.rowSpan,
});
}
class BentoResult {
final List<BentoPlacement> placements;
final int rowCount;
const BentoResult({required this.placements, required this.rowCount});
}
/// Placement "bento" dense sur une grille de [columns] colonnes.
/// Chaque item occupe colSpan × rowSpan cellules ; on place chaque item à la
/// première cellule libre (parcours ligne par ligne) qui l'accueille — même
/// principe que `grid-auto-flow: dense` en CSS, pour que l'aperçu Flutter et le
/// rendu web restent cohérents.
BentoResult bentoLayout(List<BentoItem> items, int columns) {
final cols = columns < 1 ? 1 : columns;
final occupied = <List<bool>>[];
void ensureRow(int r) {
while (occupied.length <= r) {
occupied.add(List<bool>.filled(cols, false));
}
}
bool fits(int r, int c, int cs, int rs) {
if (c + cs > cols) return false;
for (var rr = r; rr < r + rs; rr++) {
ensureRow(rr);
for (var cc = c; cc < c + cs; cc++) {
if (occupied[rr][cc]) return false;
}
}
return true;
}
void fill(int r, int c, int cs, int rs) {
for (var rr = r; rr < r + rs; rr++) {
ensureRow(rr);
for (var cc = c; cc < c + cs; cc++) {
occupied[rr][cc] = true;
}
}
}
final placements = <BentoPlacement>[];
for (final item in items) {
final cs = item.colSpan.clamp(1, cols);
final rs = item.rowSpan < 1 ? 1 : item.rowSpan;
var placed = false;
var r = 0;
while (!placed) {
for (var c = 0; c + cs <= cols; c++) {
if (fits(r, c, cs, rs)) {
fill(r, c, cs, rs);
placements.add(BentoPlacement(id: item.id, col: c, row: r, colSpan: cs, rowSpan: rs));
placed = true;
break;
}
}
if (!placed) r++;
}
}
var rowCount = occupied.length;
while (rowCount > 0 && occupied[rowCount - 1].every((cell) => !cell)) {
rowCount--;
}
return BentoResult(placements: placements, rowCount: rowCount);
}

View File

@ -0,0 +1,381 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: f6526c100095fd63a916824e3da344bbbd50c25c8f56bcd52d13c59d35bbe422
url: "https://pub.dev"
source: hosted
version: "104.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "6c6d751533496152e78f71c46ad001260c5e74e0f9ec1f1cbc165a0467c086d6"
url: "https://pub.dev"
source: hosted
version: "14.0.0"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "956a3de0725ca232ad353565a8290d3357592bf4250f6f298a185e2d949c5d3d"
url: "https://pub.dev"
source: hosted
version: "1.15.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.20"
meta:
dependency: transitive
description:
name: meta
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d
url: "https://pub.dev"
source: hosted
version: "3.0.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: "0d5ba5602ec3baa28c8ce365e1efc5575969c765f45c554a3e167dc7945b9c30"
url: "https://pub.dev"
source: hosted
version: "1.31.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "475610b2aa23c19687cce2961e44b0cc57cafe220f67c2b80201231b2a07fbe7"
url: "https://pub.dev"
source: hosted
version: "0.7.13"
test_core:
dependency: transitive
description:
name: test_core
sha256: a39c204a4fc7a7ccb04a2b985e359fda3cc37e45e0b8ac61c3fb1a05aa832132
url: "https://pub.dev"
source: hosted
version: "0.6.19"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.0 <4.0.0"

View File

@ -0,0 +1,7 @@
name: myinfomate_layout
version: 1.0.0
description: Placement "bento" partagé (grille à spans colSpan x rowSpan, dense) pour les apps MyInfoMate.
environment:
sdk: '>=2.17.0 <4.0.0'
dev_dependencies:
test: ^1.24.0

View File

@ -0,0 +1,44 @@
import 'dart:convert';
import 'dart:io';
import 'package:myinfomate_layout/myinfomate_layout.dart';
import 'package:test/test.dart';
void main() {
final fixturePath =
'${Directory.current.path}/test/fixtures/bento_layout_cases.json';
final fixture =
jsonDecode(File(fixturePath).readAsStringSync()) as Map<String, dynamic>;
final cases = fixture['cases'] as List<dynamic>;
for (final rawCase in cases) {
final testCase = rawCase as Map<String, dynamic>;
test(testCase['name'], () {
final items = (testCase['items'] as List<dynamic>)
.map((raw) => BentoItem(
id: raw['id'] as String,
colSpan: raw['colSpan'] as int,
rowSpan: raw['rowSpan'] as int,
))
.toList();
final result = bentoLayout(items, testCase['columns'] as int);
final expected = testCase['expected'] as Map<String, dynamic>;
expect(result.rowCount, expected['rowCount']);
final expectedPlacements = expected['placements'] as List<dynamic>;
expect(result.placements.length, expectedPlacements.length);
for (var i = 0; i < result.placements.length; i++) {
final p = result.placements[i];
final e = expectedPlacements[i] as Map<String, dynamic>;
expect(p.id, e['id']);
expect(p.col, e['col']);
expect(p.row, e['row']);
expect(p.colSpan, e['colSpan']);
expect(p.rowSpan, e['rowSpan']);
}
});
}
}

View File

@ -0,0 +1,72 @@
{
"cases": [
{
"name": "two_columns_uniform",
"columns": 2,
"items": [
{ "id": "a", "colSpan": 1, "rowSpan": 1 },
{ "id": "b", "colSpan": 1, "rowSpan": 1 },
{ "id": "c", "colSpan": 1, "rowSpan": 1 }
],
"expected": {
"rowCount": 2,
"placements": [
{ "id": "a", "col": 0, "row": 0, "colSpan": 1, "rowSpan": 1 },
{ "id": "b", "col": 1, "row": 0, "colSpan": 1, "rowSpan": 1 },
{ "id": "c", "col": 0, "row": 1, "colSpan": 1, "rowSpan": 1 }
]
}
},
{
"name": "wide_then_dense_backfill",
"columns": 4,
"items": [
{ "id": "big", "colSpan": 2, "rowSpan": 2 },
{ "id": "a", "colSpan": 1, "rowSpan": 1 },
{ "id": "b", "colSpan": 1, "rowSpan": 1 },
{ "id": "c", "colSpan": 1, "rowSpan": 1 },
{ "id": "d", "colSpan": 1, "rowSpan": 1 }
],
"expected": {
"rowCount": 2,
"placements": [
{ "id": "big", "col": 0, "row": 0, "colSpan": 2, "rowSpan": 2 },
{ "id": "a", "col": 2, "row": 0, "colSpan": 1, "rowSpan": 1 },
{ "id": "b", "col": 3, "row": 0, "colSpan": 1, "rowSpan": 1 },
{ "id": "c", "col": 2, "row": 1, "colSpan": 1, "rowSpan": 1 },
{ "id": "d", "col": 3, "row": 1, "colSpan": 1, "rowSpan": 1 }
]
}
},
{
"name": "tall_card",
"columns": 2,
"items": [
{ "id": "tall", "colSpan": 1, "rowSpan": 2 },
{ "id": "a", "colSpan": 1, "rowSpan": 1 },
{ "id": "b", "colSpan": 1, "rowSpan": 1 }
],
"expected": {
"rowCount": 2,
"placements": [
{ "id": "tall", "col": 0, "row": 0, "colSpan": 1, "rowSpan": 2 },
{ "id": "a", "col": 1, "row": 0, "colSpan": 1, "rowSpan": 1 },
{ "id": "b", "col": 1, "row": 1, "colSpan": 1, "rowSpan": 1 }
]
}
},
{
"name": "colspan_clamped_to_columns",
"columns": 2,
"items": [
{ "id": "wide", "colSpan": 3, "rowSpan": 1 }
],
"expected": {
"rowCount": 1,
"placements": [
{ "id": "wide", "col": 0, "row": 0, "colSpan": 2, "rowSpan": 1 }
]
}
}
]
}

View File

@ -1028,6 +1028,13 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.7" version: "0.1.7"
myinfomate_layout:
dependency: "direct main"
description:
path: myinfomate_layout
relative: true
source: path
version: "1.0.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:

View File

@ -67,6 +67,8 @@ dependencies:
package_info_plus: ^4.2.0 package_info_plus: ^4.2.0
manager_api_new: manager_api_new:
path: manager_api_new path: manager_api_new
myinfomate_layout:
path: myinfomate_layout
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.