Compare commits

..

1 Commits

Author SHA1 Message Date
ElPoyo
bc93f3fa9a feat: Mise à jour à la version 1.1.14 et refonte du support Audio/TTS pour le Web
- Mise à jour de la version de l'application à `1.1.14` dans `app_version.dart` et `version.json`.
- Migration de `AudioFeedbackService` vers l'API Web native (`dart:js_interop`, `package:web`) pour corriger les problèmes d'autoplay et supprimer la dépendance `audioplayers`.
- Réécriture de `TextToSpeechService` utilisant `window.speechSynthesis` en remplacement de `flutter_tts` pour une meilleure compatibilité Web (notamment sous Linux).
- Suppression des dépendances obsolètes `audioplayers` et `flutter_tts` du `pubspec.yaml`.
- Ajout d'une gestion de file d'attente (`_scanQueue`) dans `EventPreparationPage` pour traiter les scans de codes-barres de manière séquentielle.
- Intégration d'un bouton de diagnostic (`AudioDiagnosticButton`) pour tester manuellement l'audio et la synthèse vocale.
- Ajout d'un script de test JavaScript `test_audio_tts.js` pour faciliter le débogage dans la console du navigateur.
- Ajout de directives de style et d'architecture Dart/Flutter dans `.github/agents/`.
2026-03-08 19:51:13 +01:00
10 changed files with 1027 additions and 108 deletions

View File

@@ -22,6 +22,8 @@ assets/packages/flutter_dropzone_web/assets/flutter_dropzone.js,1748366257688,d6
assets/assets/Google__G__logo.svg,1741027482182,b365d560438f8f04caf08ffaa5d8995eff6c09b36b4483f44d6f5f82d8559d4f assets/assets/Google__G__logo.svg,1741027482182,b365d560438f8f04caf08ffaa5d8995eff6c09b36b4483f44d6f5f82d8559d4f
assets/assets/google.png,1741029771653,537ca60ffa74c28eca4e62d249237403a7d47d2bc90bb90431b8d5aa923a92ee assets/assets/google.png,1741029771653,537ca60ffa74c28eca4e62d249237403a7d47d2bc90bb90431b8d5aa923a92ee
assets/assets/EM2_NsurB.jpg,1741031103452,687267bbd4e1a663ffc1d2256c34969dd424cbaaf503b530db529a345570ddcd assets/assets/EM2_NsurB.jpg,1741031103452,687267bbd4e1a663ffc1d2256c34969dd424cbaaf503b530db529a345570ddcd
assets/assets/sounds/ok.mp3,1771938119844,cb452794752fa5e7622b2bd9413e9245464788be3f88cc838a7c9716f87f82a3
assets/assets/sounds/error.mp3,1771938125144,5e1974fa40050421304357c75e834ab5f7c8ba7a61acfbb5885ed913afc0fc0b
assets/assets/logos/SquareLogoWhite.png,1760462340000,786ce2571303bb96dfae1fba5faaab57a9142468fa29ad73ab6b3c1f75be3703 assets/assets/logos/SquareLogoWhite.png,1760462340000,786ce2571303bb96dfae1fba5faaab57a9142468fa29ad73ab6b3c1f75be3703
assets/assets/logos/SquareLogoBlack.png,1760462340000,b4425fae1dbd25ce7c218c602d530f75d85e0eb444746b48b09b5028ed88bbd1 assets/assets/logos/SquareLogoBlack.png,1760462340000,b4425fae1dbd25ce7c218c602d530f75d85e0eb444746b48b09b5028ed88bbd1
assets/assets/logos/RectangleLogoWhite.png,1760462340000,1f6df22df6560a2dae2d42cf6e29f01e6df4002f1a9c20a8499923d74b02115c assets/assets/logos/RectangleLogoWhite.png,1760462340000,1f6df22df6560a2dae2d42cf6e29f01e6df4002f1a9c20a8499923d74b02115c
@@ -31,16 +33,17 @@ assets/assets/images/tshirt-incrust.webp,1737393735487,af7cb34adfca19c0b41c8eb63
assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc assets/assets/icons/truss.svg,1761734811263,8ddfbbb4f96de5614348eb23fa55f61b2eb1edb064719a8bbd791c35883ec4cc
assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde assets/assets/icons/tape.svg,1761734809221,631183f0ff972aa4dc3f9f51dc7abd41a607df749d1f9a44fa7e77202d95ccde
assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d assets/assets/icons/flight-case.svg,1761734822495,0cef47fdf5d7efdd110763c32f792ef9735df35c4f42ae7d02d5fbda40e6148d
version.json,1771936797689,d107c3101fad9b2d43d0444e55cd784da08fa919db377f837d8b87a14d13d0be version.json,1772532792027,2b3f91e827bc27a1901342a048b1bd81d0aabc50935717f9851e1a3ad6cb7411
index.html,1771936803931,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10 test_audio_tts.js,1772532705302,d7b70556456d3b5e7832506b2dafe31480d94db8d0027b89c1633cc9b5c5bdae
flutter_service_worker.js,1771936884025,fcbf7871d99f057b5b0d89f4d158bf26151769d47c42a538bba4fcd29c8503b9 index.html,1772532797157,4e8c00552c71ef134bead8bc03706952e7a415d70fca602a3839dc02a3f7ae10
flutter_bootstrap.js,1771936803915,2e42b2a1c547488d5f71f985eecb263baf89d65f364a6522ab2bd3ec39b0a1b8 flutter_bootstrap.js,1772532797146,ca3df8691f4db5962ed165489bd051dfd31307628ab4f1ee68842dc747d39fd9
assets/FontManifest.json,1771936879989,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5 flutter_service_worker.js,1772532894886,9ce6b8d9f09c957b763a8d3db3baf03c96d4f84e805f6d629294749d9966cfad
assets/AssetManifest.json,1771936879989,4bf36ad943b43d5ad6d8d214e15d84a5633d80d8e08158af25f55baf8980a4e9 assets/FontManifest.json,1772532889954,e38b95988f5d060cf9b7ce97cb5ac9236d6f4cc04a11d69567df97b2b4cbc5e5
assets/AssetManifest.bin.json,1771936879989,c3fa09840d3272b5c70eb6303934d535526be377a9b64fd62ab4f7c48351bd9e assets/AssetManifest.json,1772532889954,0e35e7214421c832bf41b0af7c03037e66fee508b857d3143f40f6862e454dd6
assets/AssetManifest.bin,1771936879987,c9151561940e124b5fc93ff810ec49ddb3472e8ae438063c6c5c448444fa2b9e assets/AssetManifest.bin.json,1772532889954,3a244f5f866d93c17f420cc01b1ba318584b4da92af9512d9ba4acd099b49d53
assets/shaders/ink_sparkle.frag,1771936880185,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406 assets/AssetManifest.bin,1772532889954,205908d2fcf1ca9708b7d1f91ec7ea80c5f07eaf6cfc1458cb9364a4d9106907
assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1771936882976,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb assets/shaders/ink_sparkle.frag,1772532890224,591c7517d5cb43eb91ea451e0d3f9f585cbf8298cf6c46a9144b77cb0775a406
assets/fonts/MaterialIcons-Regular.otf,1771936882976,8d5c5d5749998c2d29a44f452643a016f3461a3d8cd40af1ef52c9be9c6fe021 assets/packages/cupertino_icons/assets/CupertinoIcons.ttf,1772532893514,d41473de1f7708a0702d7f19327693486512db442f6ab0cf7774e6d6576f9fcb
assets/NOTICES,1771936879989,538f39b8622001ef1c5520fab893e310b1f6f10bf65c6413c9412c9c14945bfa assets/fonts/MaterialIcons-Regular.otf,1772532893530,71c7128cf890cf3e18fffca405a98480f174bb3fa79d20c575b473d36c8c3093
main.dart.js,1771936874652,2f1686906db6e8313b40447f700f72100c53797a775179e5193fe803e3446125 assets/NOTICES,1772532889955,8479783d331c9ff6d2b2e2e0a4b1705eda46ab0000b7753779fb98526ae54d74
main.dart.js,1772532888607,df89975075062e0983691b8997b9e4a1ae4b4d5dfe6c06ca5b42ffa5407fdd3f

View File

@@ -0,0 +1,450 @@
---
description: 'Instructions for writing Dart and Flutter code following the official recommendations.'
applyTo: '**/*.dart'
---
# Dart and Flutter
Best practices recommended by the Dart and Flutter teams. These instructions were taken from [Effective Dart](https://dart.dev/effective-dart) and [Architecture Recommendations](https://docs.flutter.dev/app-architecture/recommendations).
## Effective Dart
Over the past several years, we've written a ton of Dart code and learned a lot about what works well and what doesn't. We're sharing this with you so you can write consistent, robust, fast code too. There are two overarching themes:
1. **Be consistent.** When it comes to things like formatting, and casing, arguments about which is better are subjective and impossible to resolve. What we do know is that being *consistent* is objectively helpful.
If two pieces of code look different it should be because they *are* different in some meaningful way. When a bit of code stands out and catches your eye, it should do so for a useful reason.
2. **Be brief.** Dart was designed to be familiar, so it inherits many of the same statements and expressions as C, Java, JavaScript and other languages. But we created Dart because there is a lot of room to improve on what those languages offer. We added a bunch of features, from string interpolation to initializing formals, to help you express your intent more simply and easily.
If there are multiple ways to say something, you should generally pick the most concise one. This is not to say you should `code golf` yourself into cramming a whole program into a single line. The goal is code that is *economical*, not *dense*.
### The topics
We split the guidelines into a few separate topics for easy digestion:
* **Style** This defines the rules for laying out and organizing code, or at least the parts that `dart format` doesn't handle for you. The style topic also specifies how identifiers are formatted: `camelCase`, `using_underscores`, etc.
* **Documentation** This tells you everything you need to know about what goes inside comments. Both doc comments and regular, run-of-the-mill code comments.
* **Usage** This teaches you how to make the best use of language features to implement behavior. If it's in a statement or expression, it's covered here.
* **Design** This is the softest topic, but the one with the widest scope. It covers what we've learned about designing consistent, usable APIs for libraries. If it's in a type signature or declaration, this goes over it.
### How to read the topics
Each topic is broken into a few sections. Sections contain a list of guidelines. Each guideline starts with one of these words:
* **DO** guidelines describe practices that should always be followed. There will almost never be a valid reason to stray from them.
* **DON'T** guidelines are the converse: things that are almost never a good idea. Hopefully, we don't have as many of these as other languages do because we have less historical baggage.
* **PREFER** guidelines are practices that you *should* follow. However, there may be circumstances where it makes sense to do otherwise. Just make sure you understand the full implications of ignoring the guideline when you do.
* **AVOID** guidelines are the dual to "prefer": stuff you shouldn't do but where there may be good reasons to on rare occasions.
* **CONSIDER** guidelines are practices that you might or might not want to follow, depending on circumstances, precedents, and your own preference.
Some guidelines describe an **exception** where the rule does *not* apply. When listed, the exceptions may not be exhaustive—you might still need to use your judgement on other cases.
This sounds like the police are going to beat down your door if you don't have your laces tied correctly. Things aren't that bad. Most of the guidelines here are common sense and we're all reasonable people. The goal, as always, is nice, readable and maintainable code.
### Rules
#### Style
##### Identifiers
* DO name types using `UpperCamelCase`.
* DO name extensions using `UpperCamelCase`.
* DO name packages, directories, and source files using `lowercase_with_underscores`.
* DO name import prefixes using `lowercase_with_underscores`.
* DO name other identifiers using `lowerCamelCase`.
* PREFER using `lowerCamelCase` for constant names.
* DO capitalize acronyms and abbreviations longer than two letters like words.
* PREFER using wildcards for unused callback parameters.
* DON'T use a leading underscore for identifiers that aren't private.
* DON'T use prefix letters.
* DON'T explicitly name libraries.
##### Ordering
* DO place `dart:` imports before other imports.
* DO place `package:` imports before relative imports.
* DO specify exports in a separate section after all imports.
* DO sort sections alphabetically.
##### Formatting
* DO format your code using `dart format`.
* CONSIDER changing your code to make it more formatter-friendly.
* PREFER lines 80 characters or fewer.
* DO use curly braces for all flow control statements.
#### Documentation
##### Comments
* DO format comments like sentences.
* DON'T use block comments for documentation.
##### Doc comments
* DO use `///` doc comments to document members and types.
* PREFER writing doc comments for public APIs.
* CONSIDER writing a library-level doc comment.
* CONSIDER writing doc comments for private APIs.
* DO start doc comments with a single-sentence summary.
* DO separate the first sentence of a doc comment into its own paragraph.
* AVOID redundancy with the surrounding context.
* PREFER starting comments of a function or method with third-person verbs if its main purpose is a side effect.
* PREFER starting a non-boolean variable or property comment with a noun phrase.
* PREFER starting a boolean variable or property comment with "Whether" followed by a noun or gerund phrase.
* PREFER a noun phrase or non-imperative verb phrase for a function or method if returning a value is its primary purpose.
* DON'T write documentation for both the getter and setter of a property.
* PREFER starting library or type comments with noun phrases.
* CONSIDER including code samples in doc comments.
* DO use square brackets in doc comments to refer to in-scope identifiers.
* DO use prose to explain parameters, return values, and exceptions.
* DO put doc comments before metadata annotations.
##### Markdown
* AVOID using markdown excessively.
* AVOID using HTML for formatting.
* PREFER backtick fences for code blocks.
##### Writing
* PREFER brevity.
* AVOID abbreviations and acronyms unless they are obvious.
* PREFER using "this" instead of "the" to refer to a member's instance.
#### Usage
##### Libraries
* DO use strings in `part of` directives.
* DON'T import libraries that are inside the `src` directory of another package.
* DON'T allow an import path to reach into or out of `lib`.
* PREFER relative import paths.
##### Null
* DON'T explicitly initialize variables to `null`.
* DON'T use an explicit default value of `null`.
* DON'T use `true` or `false` in equality operations.
* AVOID `late` variables if you need to check whether they are initialized.
* CONSIDER type promotion or null-check patterns for using nullable types.
##### Strings
* DO use adjacent strings to concatenate string literals.
* PREFER using interpolation to compose strings and values.
* AVOID using curly braces in interpolation when not needed.
##### Collections
* DO use collection literals when possible.
* DON'T use `.length` to see if a collection is empty.
* AVOID using `Iterable.forEach()` with a function literal.
* DON'T use `List.from()` unless you intend to change the type of the result.
* DO use `whereType()` to filter a collection by type.
* DON'T use `cast()` when a nearby operation will do.
* AVOID using `cast()`.
##### Functions
* DO use a function declaration to bind a function to a name.
* DON'T create a lambda when a tear-off will do.
##### Variables
* DO follow a consistent rule for `var` and `final` on local variables.
* AVOID storing what you can calculate.
##### Members
* DON'T wrap a field in a getter and setter unnecessarily.
* PREFER using a `final` field to make a read-only property.
* CONSIDER using `=>` for simple members.
* DON'T use `this.` except to redirect to a named constructor or to avoid shadowing.
* DO initialize fields at their declaration when possible.
##### Constructors
* DO use initializing formals when possible.
* DON'T use `late` when a constructor initializer list will do.
* DO use `;` instead of `{}` for empty constructor bodies.
* DON'T use `new`.
* DON'T use `const` redundantly.
##### Error handling
* AVOID catches without `on` clauses.
* DON'T discard errors from catches without `on` clauses.
* DO throw objects that implement `Error` only for programmatic errors.
* DON'T explicitly catch `Error` or types that implement it.
* DO use `rethrow` to rethrow a caught exception.
##### Asynchrony
* PREFER async/await over using raw futures.
* DON'T use `async` when it has no useful effect.
* CONSIDER using higher-order methods to transform a stream.
* AVOID using Completer directly.
* DO test for `Future<T>` when disambiguating a `FutureOr<T>` whose type argument could be `Object`.
#### Design
##### Names
* DO use terms consistently.
* AVOID abbreviations.
* PREFER putting the most descriptive noun last.
* CONSIDER making the code read like a sentence.
* PREFER a noun phrase for a non-boolean property or variable.
* PREFER a non-imperative verb phrase for a boolean property or variable.
* CONSIDER omitting the verb for a named boolean parameter.
* PREFER the "positive" name for a boolean property or variable.
* PREFER an imperative verb phrase for a function or method whose main purpose is a side effect.
* PREFER a noun phrase or non-imperative verb phrase for a function or method if returning a value is its primary purpose.
* CONSIDER an imperative verb phrase for a function or method if you want to draw attention to the work it performs.
* AVOID starting a method name with `get`.
* PREFER naming a method `to...()` if it copies the object's state to a new object.
* PREFER naming a method `as...()` if it returns a different representation backed by the original object.
* AVOID describing the parameters in the function's or method's name.
* DO follow existing mnemonic conventions when naming type parameters.
##### Libraries
* PREFER making declarations private.
* CONSIDER declaring multiple classes in the same library.
##### Classes and mixins
* AVOID defining a one-member abstract class when a simple function will do.
* AVOID defining a class that contains only static members.
* AVOID extending a class that isn't intended to be subclassed.
* DO use class modifiers to control if your class can be extended.
* AVOID implementing a class that isn't intended to be an interface.
* DO use class modifiers to control if your class can be an interface.
* PREFER defining a pure `mixin` or pure `class` to a `mixin class`.
##### Constructors
* CONSIDER making your constructor `const` if the class supports it.
##### Members
* PREFER making fields and top-level variables `final`.
* DO use getters for operations that conceptually access properties.
* DO use setters for operations that conceptually change properties.
* DON'T define a setter without a corresponding getter.
* AVOID using runtime type tests to fake overloading.
* AVOID public `late final` fields without initializers.
* AVOID returning nullable `Future`, `Stream`, and collection types.
* AVOID returning `this` from methods just to enable a fluent interface.
##### Types
* DO type annotate variables without initializers.
* DO type annotate fields and top-level variables if the type isn't obvious.
* DON'T redundantly type annotate initialized local variables.
* DO annotate return types on function declarations.
* DO annotate parameter types on function declarations.
* DON'T annotate inferred parameter types on function expressions.
* DON'T type annotate initializing formals.
* DO write type arguments on generic invocations that aren't inferred.
* DON'T write type arguments on generic invocations that are inferred.
* AVOID writing incomplete generic types.
* DO annotate with `dynamic` instead of letting inference fail.
* PREFER signatures in function type annotations.
* DON'T specify a return type for a setter.
* DON'T use the legacy typedef syntax.
* PREFER inline function types over typedefs.
* PREFER using function type syntax for parameters.
* AVOID using `dynamic` unless you want to disable static checking.
* DO use `Future<void>` as the return type of asynchronous members that do not produce values.
* AVOID using `FutureOr<T>` as a return type.
##### Parameters
* AVOID positional boolean parameters.
* AVOID optional positional parameters if the user may want to omit earlier parameters.
* AVOID mandatory parameters that accept a special "no argument" value.
* DO use inclusive start and exclusive end parameters to accept a range.
##### Equality
* DO override `hashCode` if you override `==`.
* DO make your `==` operator obey the mathematical rules of equality.
* AVOID defining custom equality for mutable classes.
* DON'T make the parameter to `==` nullable.
---
## Flutter Architecture Recommendations
This page presents architecture best practices, why they matter, and
whether we recommend them for your Flutter application.
You should treat these recommendations as recommendations,
and not steadfast rules, and you should
adapt them to your app's unique requirements.
The best practices on this page have a priority,
which reflects how strongly the Flutter team recommends it.
* **Strongly recommend:** You should always implement this recommendation if
you're starting to build a new application. You should strongly consider
refactoring an existing app to implement this practice unless doing so would
fundamentally clash with your current approach.
* **Recommend**: This practice will likely improve your app.
* **Conditional**: This practice can improve your app in certain circumstances.
### Separation of concerns
You should separate your app into a UI layer and a data layer. Within those layers, you should further separate logic into classes by responsibility.
#### Use clearly defined data and UI layers.
**Strongly recommend**
Separation of concerns is the most important architectural principle.
The data layer exposes application data to the rest of the app, and contains most of the business logic in your application.
The UI layer displays application data and listens for user events from users. The UI layer contains separate classes for UI logic and widgets.
#### Use the repository pattern in the data layer.
**Strongly recommend**
The repository pattern is a software design pattern that isolates the data access logic from the rest of the application.
It creates an abstraction layer between the application's business logic and the underlying data storage mechanisms (databases, APIs, file systems, etc.).
In practice, this means creating Repository classes and Service classes.
#### Use ViewModels and Views in the UI layer. (MVVM)
**Strongly recommend**
Separation of concerns is the most important architectural principle.
This particular separation makes your code much less error prone because your widgets remain "dumb".
#### Use `ChangeNotifiers` and `Listenables` to handle widget updates.
**Conditional**
> There are many options to handle state-management, and ultimately the decision comes down to personal preference.
The `ChangeNotifier` API is part of the Flutter SDK, and is a convenient way to have your widgets observe changes in your ViewModels.
#### Do not put logic in widgets.
**Strongly recommend**
Logic should be encapsulated in methods on the ViewModel. The only logic a view should contain is:
* Simple if-statements to show and hide widgets based on a flag or nullable field in the ViewModel
* Animation logic that relies on the widget to calculate
* Layout logic based on device information, like screen size or orientation.
* Simple routing logic
#### Use a domain layer.
**Conditional**
> Use in apps with complex logic requirements.
A domain layer is only needed if your application has exceeding complex logic that crowds your ViewModels,
or if you find yourself repeating logic in ViewModels.
In very large apps, use-cases are useful, but in most apps they add unnecessary overhead.
### Handling data
Handling data with care makes your code easier to understand, less error prone, and
prevents malformed or unexpected data from being created.
#### Use unidirectional data flow.
**Strongly recommend**
Data updates should only flow from the data layer to the UI layer.
Interactions in the UI layer are sent to the data layer where they're processed.
#### Use `Commands` to handle events from user interaction.
**Recommend**
Commands prevent rendering errors in your app, and standardize how the UI layer sends events to the data layer.
#### Use immutable data models.
**Strongly recommend**
Immutable data is crucial in ensuring that any necessary changes occur only in the proper place, usually the data or domain layer.
Because immutable objects can't be modified after creation, you must create a new instance to reflect changes.
This process prevents accidental updates in the UI layer and supports a clear, unidirectional data flow.
#### Use freezed or built_value to generate immutable data models.
**Recommend**
You can use packages to help generate useful functionality in your data models, `freezed` or `built_value`.
These can generate common model methods like JSON ser/des, deep equality checking and copy methods.
These code generation packages can add significant build time to your applications if you have a lot of models.
#### Create separate API models and domain models.
**Conditional**
> Use in large apps.
Using separate models adds verbosity, but prevents complexity in ViewModels and use-cases.
### App structure
Well organized code benefits both the health of the app itself, and the team working on the code.
#### Use dependency injection.
**Strongly recommend**
Dependency injection prevents your app from having globally accessible objects, which makes your code less error prone.
We recommend you use the `provider` package to handle dependency injection.
#### Use `go_router` for navigation.
**Recommend**
Go_router is the preferred way to write 90% of Flutter applications.
There are some specific use-cases that go_router doesn't solve,
in which case you can use the `Flutter Navigator API` directly or try other packages found on `pub.dev`.
#### Use standardized naming conventions for classes, files and directories.
**Recommend**
We recommend naming classes for the architectural component they represent.
For example, you may have the following classes:
* HomeViewModel
* HomeScreen
* UserRepository
* ClientApiService
For clarity, we do not recommend using names that can be confused with objects from the Flutter SDK.
For example, you should put your shared widgets in a directory called `ui/core/`,
rather than a directory called `/widgets`.
#### Use abstract repository classes
**Strongly recommend**
Repository classes are the sources of truth for all data in your app,
and facilitate communication with external APIs.
Creating abstract repository classes allows you to create different implementations,
which can be used for different app environments, such as "development" and "staging".
### Testing
Good testing practices makes your app flexible.
It also makes it straightforward and low risk to add new logic and new UI.
#### Test architectural components separately, and together.
**Strongly recommend**
* Write unit tests for every service, repository and ViewModel class. These tests should test the logic of every method individually.
* Write widget tests for views. Testing routing and dependency injection are particularly important.
#### Make fakes for testing (and write code that takes advantage of fakes.)
**Strongly recommend**
Fakes aren't concerned with the inner workings of any given method as much
as they're concerned with inputs and outputs. If you have this in mind while writing application code,
you're forced to write modular, lightweight functions and classes with well defined inputs and outputs.
### Deploying Firebase
You should not use Firebase CLI. You have to ask the user for deploying or modifying something.

View File

@@ -1,6 +1,6 @@
/// Configuration de la version de l'application /// Configuration de la version de l'application
class AppVersion { class AppVersion {
static const String version = '1.1.7'; static const String version = '1.1.14';
/// Retourne la version complète de l'application /// Retourne la version complète de l'application
static String get fullVersion => 'v$version'; static String get fullVersion => 'v$version';

View File

@@ -1,54 +1,128 @@
import 'package:flutter/foundation.dart' show kIsWeb; import 'dart:js_interop';
import 'package:flutter/services.dart'; import 'package:web/web.dart' as web;
import 'package:audioplayers/audioplayers.dart';
import 'package:em2rp/utils/debug_log.dart'; import 'package:em2rp/utils/debug_log.dart';
/// Service pour émettre des feedbacks sonores lors des interactions /// Service pour émettre des feedbacks sonores lors des interactions (Web)
class AudioFeedbackService { class AudioFeedbackService {
static final AudioPlayer _player = AudioPlayer(); static bool _isInitialized = false;
static bool _audioUnlocked = false;
/// Initialiser le service
static Future<void> _initialize() async {
if (_isInitialized) return;
try {
DebugLog.info('[AudioFeedbackService] Initializing audio service for Web...');
_isInitialized = true;
} catch (e) {
DebugLog.error('[AudioFeedbackService] Error initializing audio', e);
}
}
/// Débloquer l'audio (à appeler lors de la première interaction utilisateur)
static Future<void> unlockAudio() async {
if (_audioUnlocked) {
DebugLog.info('[AudioFeedbackService] Audio already unlocked');
return;
}
try {
if (!_isInitialized) await _initialize();
DebugLog.info('[AudioFeedbackService] Attempting to unlock audio...');
// Créer un audio temporaire et le jouer avec volume 0
final tempAudio = web.HTMLAudioElement();
tempAudio.src = 'assets/assets/sounds/ok.mp3';
tempAudio.volume = 0.01; // Volume très faible mais pas 0
tempAudio.preload = 'auto';
try {
await tempAudio.play().toDart;
await Future.delayed(const Duration(milliseconds: 100));
tempAudio.pause();
_audioUnlocked = true;
DebugLog.info('[AudioFeedbackService] ✓ Audio unlocked successfully');
} catch (e) {
DebugLog.warning('[AudioFeedbackService] ⚠ Could not unlock audio: $e');
DebugLog.warning('[AudioFeedbackService] User interaction may be required');
}
} catch (e) {
DebugLog.error('[AudioFeedbackService] Error unlocking audio', e);
}
}
/// Créer et jouer un son
static Future<void> _playSound(String assetPath, double volume) async {
try {
if (!_isInitialized) await _initialize();
DebugLog.info('[AudioFeedbackService] Attempting to play: $assetPath (volume: $volume)');
// Créer un nouvel élément audio à chaque fois
final audio = web.HTMLAudioElement();
audio.src = assetPath;
audio.volume = volume;
audio.preload = 'auto';
// Ajouter des événements pour debug
audio.onloadeddata = ((web.Event event) {
DebugLog.info('[AudioFeedbackService] Audio data loaded: $assetPath');
}.toJS);
audio.onerror = ((web.Event event) {
DebugLog.error('[AudioFeedbackService] ✗ Audio error for $assetPath: ${audio.error}');
}.toJS);
audio.onplay = ((web.Event event) {
DebugLog.info('[AudioFeedbackService] Audio started playing');
}.toJS);
audio.onended = ((web.Event event) {
DebugLog.info('[AudioFeedbackService] Audio finished playing');
}.toJS);
try {
// Essayer de jouer
await audio.play().toDart;
DebugLog.info('[AudioFeedbackService] ✓ Sound played successfully');
} catch (e) {
DebugLog.error('[AudioFeedbackService] ✗ Play failed: $e');
// Si c'est un problème d'autoplay, essayer de débloquer
if (!_audioUnlocked) {
DebugLog.info('[AudioFeedbackService] Trying to unlock audio on error...');
_audioUnlocked = false; // Forcer le déblocage
await unlockAudio();
// Réessayer une fois après déblocage
try {
final retryAudio = web.HTMLAudioElement();
retryAudio.src = assetPath;
retryAudio.volume = volume;
await retryAudio.play().toDart;
DebugLog.info('[AudioFeedbackService] ✓ Sound played on retry');
} catch (retryError) {
DebugLog.error('[AudioFeedbackService] ✗ Retry also failed: $retryError');
}
}
}
} catch (e) {
DebugLog.error('[AudioFeedbackService] Error in _playSound', e);
}
}
/// Jouer un son de succès /// Jouer un son de succès
static Future<void> playSuccessBeep() async { static Future<void> playSuccessBeep() async {
try { await _playSound('assets/assets/sounds/ok.mp3', 1.0);
if (kIsWeb) {
// Sur Web, utiliser le chemin absolu
await _player.play(UrlSource('assets/sounds/ok.mp3'));
} else {
// Sur mobile/desktop, utiliser AssetSource
await _player.play(AssetSource('sounds/ok.mp3'));
}
await HapticFeedback.lightImpact();
} catch (e) {
DebugLog.error('[AudioFeedbackService] Error playing success beep', e);
}
} }
/// Jouer un son d'erreur /// Jouer un son d'erreur
static Future<void> playErrorBeep() async { static Future<void> playErrorBeep() async {
try { await _playSound('assets/assets/sounds/error.mp3', 0.8);
if (kIsWeb) {
// Sur Web, utiliser le chemin absolu
await _player.play(UrlSource('assets/sounds/error.mp3'));
} else {
// Sur mobile/desktop, utiliser AssetSource
await _player.play(AssetSource('sounds/error.mp3'));
}
await HapticFeedback.heavyImpact();
} catch (e) {
DebugLog.error('[AudioFeedbackService] Error playing error beep', e);
}
} }
/// Jouer une vibration haptique (si disponible) /// Jouer un feedback complet (son uniquement, sans vibration)
static Future<void> playHapticFeedback() async {
try {
await HapticFeedback.mediumImpact();
} catch (e) {
DebugLog.error('[AudioFeedbackService] Error playing haptic feedback', e);
}
}
/// Jouer un feedback complet (son + vibration)
static Future<void> playFullFeedback({bool isSuccess = true}) async { static Future<void> playFullFeedback({bool isSuccess = true}) async {
if (isSuccess) { if (isSuccess) {
await playSuccessBeep(); await playSuccessBeep();
@@ -59,6 +133,12 @@ class AudioFeedbackService {
/// Nettoyer les ressources /// Nettoyer les ressources
static Future<void> dispose() async { static Future<void> dispose() async {
await _player.dispose(); try {
_isInitialized = false;
_audioUnlocked = false;
} catch (e) {
DebugLog.error('[AudioFeedbackService] Error disposing', e);
}
} }
} }

View File

@@ -1,60 +1,66 @@
import 'package:flutter_tts/flutter_tts.dart'; import 'dart:js_interop';
import 'package:web/web.dart' as web;
import 'package:em2rp/utils/debug_log.dart'; import 'package:em2rp/utils/debug_log.dart';
/// Service de synthèse vocale pour lire des textes à haute voix /// Service de synthèse vocale pour lire des textes à haute voix (Web)
class TextToSpeechService { class TextToSpeechService {
static final FlutterTts _tts = FlutterTts();
static bool _isInitialized = false; static bool _isInitialized = false;
static bool _voicesLoaded = false;
static List<web.SpeechSynthesisVoice> _cachedVoices = [];
/// Initialiser le service TTS /// Initialiser le service TTS
static Future<void> initialize() async { static Future<void> initialize() async {
if (_isInitialized) return; if (_isInitialized) return;
try { try {
await _tts.setLanguage('fr-FR'); _isInitialized = true;
await _tts.setSpeechRate(0.7); // Vitesse normale
await _tts.setVolume(1.0);
await _tts.setPitch(0.7); // Pitch plus bas pour une voix masculine
// Tenter de sélectionner une voix masculine si disponible final synthesis = web.window.speechSynthesis;
try {
final voices = await _tts.getVoices;
if (voices != null && voices is List) {
// Chercher une voix française masculine
final maleVoice = voices.firstWhere(
(voice) {
final voiceMap = voice as Map;
final name = voiceMap['name']?.toString().toLowerCase() ?? '';
final locale = voiceMap['locale']?.toString().toLowerCase() ?? '';
// Rechercher des voix françaises masculines // Essayer de charger les voix immédiatement
return locale.startsWith('fr') && _cachedVoices = synthesis.getVoices().toDart;
(name.contains('male') || name.contains('homme') ||
name.contains('thomas') || name.contains('paul'));
},
orElse: () => null,
);
if (maleVoice != null) { if (_cachedVoices.isNotEmpty) {
final voiceMap = maleVoice as Map; _voicesLoaded = true;
await _tts.setVoice({ DebugLog.info('[TextToSpeechService] Service initialized with ${_cachedVoices.length} voices');
'name': voiceMap['name'], return;
'locale': voiceMap['locale'],
});
DebugLog.info('[TextToSpeechService] Voix masculine sélectionnée: ${voiceMap['name']}');
}
}
} catch (e) {
DebugLog.info('[TextToSpeechService] Impossible de sélectionner une voix spécifique, utilisation de la voix par défaut');
} }
_isInitialized = true; // Sur certains navigateurs (Firefox notamment), les voix se chargent de manière asynchrone
DebugLog.info('[TextToSpeechService] Service initialisé avec voix masculine'); DebugLog.info('[TextToSpeechService] Waiting for voices to load asynchronously...');
// Attendre l'événement voiceschanged (si supporté)
final voicesLoaded = await _waitForVoices(synthesis);
if (voicesLoaded) {
_cachedVoices = synthesis.getVoices().toDart;
_voicesLoaded = true;
DebugLog.info('[TextToSpeechService] ✓ Voices loaded asynchronously: ${_cachedVoices.length}');
} else {
DebugLog.warning('[TextToSpeechService] ⚠ No voices found after initialization');
}
} catch (e) { } catch (e) {
DebugLog.error('[TextToSpeechService] Erreur lors de l\'initialisation', e); DebugLog.error('[TextToSpeechService] Erreur lors de l\'initialisation', e);
} }
} }
/// Attendre le chargement des voix (avec timeout)
static Future<bool> _waitForVoices(web.SpeechSynthesis synthesis) async {
// Essayer plusieurs fois avec des délais croissants
for (int attempt = 0; attempt < 5; attempt++) {
await Future.delayed(Duration(milliseconds: 100 * (attempt + 1)));
final voices = synthesis.getVoices().toDart;
if (voices.isNotEmpty) {
return true;
}
DebugLog.info('[TextToSpeechService] Attempt ${attempt + 1}/5: No voices yet');
}
return false;
}
/// Lire un texte à haute voix /// Lire un texte à haute voix
static Future<void> speak(String text) async { static Future<void> speak(String text) async {
if (!_isInitialized) { if (!_isInitialized) {
@@ -62,12 +68,148 @@ class TextToSpeechService {
} }
try { try {
// Arrêter toute lecture en cours final synthesis = web.window.speechSynthesis;
await _tts.stop();
// Lire le nouveau texte DebugLog.info('[TextToSpeechService] Speaking requested: "$text"');
await _tts.speak(text);
DebugLog.info('[TextToSpeechService] Lecture: $text'); // Arrêter toute lecture en cours
synthesis.cancel();
// Attendre un peu pour que le cancel soit effectif
await Future.delayed(const Duration(milliseconds: 50));
// Créer une nouvelle utterance
final utterance = web.SpeechSynthesisUtterance(text);
utterance.lang = 'fr-FR';
utterance.rate = 0.7;
utterance.pitch = 0.7;
utterance.volume = 1.0;
// Récupérer les voix (depuis le cache ou re-charger)
var voices = _cachedVoices;
// Si le cache est vide, essayer de recharger
if (voices.isEmpty) {
DebugLog.info('[TextToSpeechService] Cache empty, reloading voices...');
voices = synthesis.getVoices().toDart;
// Sur Firefox/Linux, les voix peuvent ne pas être disponibles immédiatement
if (voices.isEmpty && !_voicesLoaded) {
DebugLog.info('[TextToSpeechService] Waiting for voices with multiple attempts...');
// Essayer plusieurs fois avec des délais
for (int i = 0; i < 3; i++) {
await Future.delayed(Duration(milliseconds: 100 * (i + 1)));
voices = synthesis.getVoices().toDart;
if (voices.isNotEmpty) {
DebugLog.info('[TextToSpeechService] ✓ Voices loaded on attempt ${i + 1}');
break;
}
}
}
// Mettre à jour le cache
if (voices.isNotEmpty) {
_cachedVoices = voices;
_voicesLoaded = true;
}
}
DebugLog.info('[TextToSpeechService] Available voices: ${voices.length}');
if (voices.isNotEmpty) {
web.SpeechSynthesisVoice? selectedVoice;
// Lister TOUTES les voix françaises pour debug
final frenchVoices = <web.SpeechSynthesisVoice>[];
for (final voice in voices) {
final lang = voice.lang.toLowerCase();
if (lang.startsWith('fr')) {
frenchVoices.add(voice);
DebugLog.info('[TextToSpeechService] French: ${voice.name} (${voice.lang}) ${voice.localService ? 'LOCAL' : 'REMOTE'}');
}
}
if (frenchVoices.isEmpty) {
DebugLog.warning('[TextToSpeechService] ⚠ NO French voices found!');
DebugLog.info('[TextToSpeechService] Available languages:');
for (final voice in voices.take(5)) {
DebugLog.info('[TextToSpeechService] - ${voice.name} (${voice.lang})');
}
}
// Stratégie de sélection: préférer les voix LOCALES (plus fiables sur Linux)
for (final voice in frenchVoices) {
if (voice.localService) {
selectedVoice = voice;
DebugLog.info('[TextToSpeechService] ✓ Selected LOCAL French voice: ${voice.name}');
break;
}
}
// Si pas de voix locale, chercher une voix masculine
if (selectedVoice == null) {
for (final voice in frenchVoices) {
final name = voice.name.toLowerCase();
if (name.contains('male') ||
name.contains('homme') ||
name.contains('thomas') ||
name.contains('paul') ||
name.contains('bernard')) {
selectedVoice = voice;
DebugLog.info('[TextToSpeechService] Selected male voice: ${voice.name}');
break;
}
}
}
// Fallback: première voix française
selectedVoice ??= frenchVoices.isNotEmpty ? frenchVoices.first : null;
if (selectedVoice != null) {
utterance.voice = selectedVoice;
utterance.lang = selectedVoice.lang; // Utiliser la langue de la voix
DebugLog.info('[TextToSpeechService] Final voice: ${selectedVoice.name} (${selectedVoice.lang})');
} else {
DebugLog.warning('[TextToSpeechService] No French voice, using default with lang=fr-FR');
}
} else {
DebugLog.warning('[TextToSpeechService] ⚠ NO voices available at all!');
DebugLog.warning('[TextToSpeechService] On Linux: install speech-dispatcher and espeak-ng');
}
// Ajouter des événements pour le debug
utterance.onstart = (web.SpeechSynthesisEvent event) {
DebugLog.info('[TextToSpeechService] ✓ Speech started');
}.toJS;
utterance.onend = (web.SpeechSynthesisEvent event) {
DebugLog.info('[TextToSpeechService] ✓ Speech ended');
}.toJS;
utterance.onerror = (web.SpeechSynthesisErrorEvent event) {
DebugLog.error('[TextToSpeechService] ✗ Speech error: ${event.error}');
// Messages spécifiques pour aider au diagnostic
if (event.error == 'synthesis-failed') {
DebugLog.error('[TextToSpeechService] ⚠ SYNTHESIS FAILED - Common on Linux');
DebugLog.error('[TextToSpeechService] Possible causes:');
DebugLog.error('[TextToSpeechService] 1. speech-dispatcher not installed/running');
DebugLog.error('[TextToSpeechService] 2. espeak or espeak-ng not installed');
DebugLog.error('[TextToSpeechService] 3. No TTS engine configured');
DebugLog.error('[TextToSpeechService] Fix: sudo apt-get install speech-dispatcher espeak-ng');
DebugLog.error('[TextToSpeechService] Then restart browser');
} else if (event.error == 'network') {
DebugLog.error('[TextToSpeechService] Network error - online voice unavailable');
} else if (event.error == 'audio-busy') {
DebugLog.error('[TextToSpeechService] Audio system is busy');
}
}.toJS;
// Lire le texte
synthesis.speak(utterance);
DebugLog.info('[TextToSpeechService] Speech command sent');
} catch (e) { } catch (e) {
DebugLog.error('[TextToSpeechService] Erreur lors de la lecture', e); DebugLog.error('[TextToSpeechService] Erreur lors de la lecture', e);
} }
@@ -76,7 +218,7 @@ class TextToSpeechService {
/// Arrêter la lecture en cours /// Arrêter la lecture en cours
static Future<void> stop() async { static Future<void> stop() async {
try { try {
await _tts.stop(); web.window.speechSynthesis.cancel();
} catch (e) { } catch (e) {
DebugLog.error('[TextToSpeechService] Erreur lors de l\'arrêt', e); DebugLog.error('[TextToSpeechService] Erreur lors de l\'arrêt', e);
} }
@@ -85,8 +227,7 @@ class TextToSpeechService {
/// Vérifier si le service est en train de lire /// Vérifier si le service est en train de lire
static Future<bool> isSpeaking() async { static Future<bool> isSpeaking() async {
try { try {
// FlutterTts ne fournit pas directement cette info, on retourne false par défaut return web.window.speechSynthesis.speaking;
return false;
} catch (e) { } catch (e) {
return false; return false;
} }
@@ -95,10 +236,9 @@ class TextToSpeechService {
/// Nettoyer les ressources /// Nettoyer les ressources
static Future<void> dispose() async { static Future<void> dispose() async {
try { try {
await _tts.stop(); web.window.speechSynthesis.cancel();
} catch (e) { } catch (e) {
DebugLog.error('[TextToSpeechService] Erreur lors du nettoyage', e); DebugLog.error('[TextToSpeechService] Erreur lors du nettoyage', e);
} }
} }
} }

View File

@@ -21,6 +21,7 @@ import 'package:em2rp/views/widgets/event_preparation/add_equipment_to_event_dia
import 'package:em2rp/utils/debug_log.dart'; import 'package:em2rp/utils/debug_log.dart';
import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart'; import 'package:em2rp/views/widgets/equipment/missing_equipment_dialog.dart';
import 'package:em2rp/utils/colors.dart'; import 'package:em2rp/utils/colors.dart';
import 'package:em2rp/views/widgets/common/audio_diagnostic_button.dart';
/// Type d'étape de préparation /// Type d'étape de préparation
enum PreparationStep { enum PreparationStep {
@@ -73,6 +74,10 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
final TextEditingController _manualCodeController = TextEditingController(); final TextEditingController _manualCodeController = TextEditingController();
final FocusNode _manualCodeFocusNode = FocusNode(); final FocusNode _manualCodeFocusNode = FocusNode();
// 🆕 File d'attente pour traiter les scans séquentiellement
final List<String> _scanQueue = [];
bool _isProcessingQueue = false;
// Détermine l'étape actuelle selon le statut de l'événement // Détermine l'étape actuelle selon le statut de l'événement
PreparationStep get _currentStep { PreparationStep get _currentStep {
final prep = _currentEvent.preparationStatus ?? PreparationStatus.notStarted; final prep = _currentEvent.preparationStatus ?? PreparationStatus.notStarted;
@@ -119,6 +124,9 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
// Initialiser le service de synthèse vocale // Initialiser le service de synthèse vocale
TextToSpeechService.initialize(); TextToSpeechService.initialize();
// Initialiser et débloquer l'audio (pour éviter les problèmes d'autoplay)
AudioFeedbackService.unlockAudio();
// Vérification de sécurité et chargement après le premier frame // Vérification de sécurité et chargement après le premier frame
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (_isCurrentStepCompleted()) { if (_isCurrentStepCompleted()) {
@@ -827,13 +835,33 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
Future<void> _handleManualCodeEntry(String code) async { Future<void> _handleManualCodeEntry(String code) async {
if (code.trim().isEmpty) return; if (code.trim().isEmpty) return;
await _handleScannedCode(code.trim()); // Ajouter le code à la file d'attente
_scanQueue.add(code.trim());
// Effacer le champ après traitement // Effacer le champ immédiatement pour permettre le prochain scan
_manualCodeController.clear(); _manualCodeController.clear();
// Maintenir le focus sur le champ pour permettre une saisie continue // Maintenir le focus sur le champ pour permettre une saisie continue
_manualCodeFocusNode.requestFocus(); _manualCodeFocusNode.requestFocus();
// Démarrer le traitement de la file si pas déjà en cours
if (!_isProcessingQueue) {
_processQueue();
}
}
/// Traite la file d'attente des scans un par un
Future<void> _processQueue() async {
if (_isProcessingQueue) return;
_isProcessingQueue = true;
while (_scanQueue.isNotEmpty) {
final code = _scanQueue.removeAt(0);
await _handleScannedCode(code);
}
_isProcessingQueue = false;
} }
/// Obtenir les quantités actuelles selon l'étape /// Obtenir les quantités actuelles selon l'étape
@@ -1199,6 +1227,9 @@ class _EventPreparationPageState extends State<EventPreparationPage> with Single
appBar: AppBar( appBar: AppBar(
title: Text(stepTitle), title: Text(stepTitle),
backgroundColor: AppColors.bleuFonce, backgroundColor: AppColors.bleuFonce,
actions: const [
AudioDiagnosticButton(),
],
), ),
body: Stack( body: Stack(
children: [ children: [

View File

@@ -0,0 +1,147 @@
import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;
import 'package:em2rp/services/audio_feedback_service.dart';
import 'package:em2rp/services/text_to_speech_service.dart';
import 'package:em2rp/utils/debug_log.dart';
/// Bouton de diagnostic pour tester l'audio et le TTS
class AudioDiagnosticButton extends StatelessWidget {
const AudioDiagnosticButton({super.key});
Future<void> _testAudio(BuildContext context) async {
try {
DebugLog.info('[AudioDiagnostic] ========== AUDIO TEST START ==========');
DebugLog.info('[AudioDiagnostic] User Agent: ${web.window.navigator.userAgent}');
DebugLog.info('[AudioDiagnostic] Platform: ${web.window.navigator.platform}');
// Débloquer l'audio
DebugLog.info('[AudioDiagnostic] Step 1: Unlocking audio...');
await AudioFeedbackService.unlockAudio();
await Future.delayed(const Duration(milliseconds: 500));
// Tester le son de succès
DebugLog.info('[AudioDiagnostic] Step 2: Playing success beep...');
await AudioFeedbackService.playSuccessBeep();
await Future.delayed(const Duration(milliseconds: 1000));
// Tester le son d'erreur
DebugLog.info('[AudioDiagnostic] Step 3: Playing error beep...');
await AudioFeedbackService.playErrorBeep();
DebugLog.info('[AudioDiagnostic] ========== AUDIO TEST END ==========');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Test audio terminé - Vérifiez la console (F12)'),
backgroundColor: Colors.green,
duration: Duration(seconds: 3),
),
);
}
} catch (e) {
DebugLog.error('[AudioDiagnostic] Error during audio test', e);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur audio: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
Future<void> _testTTS(BuildContext context) async {
try {
DebugLog.info('[AudioDiagnostic] ========== TTS TEST START ==========');
DebugLog.info('[AudioDiagnostic] User Agent: ${web.window.navigator.userAgent}');
DebugLog.info('[AudioDiagnostic] Platform: ${web.window.navigator.platform}');
DebugLog.info('[AudioDiagnostic] Language: ${web.window.navigator.language}');
await TextToSpeechService.initialize();
await Future.delayed(const Duration(milliseconds: 500));
DebugLog.info('[AudioDiagnostic] Speaking test phrase...');
await TextToSpeechService.speak('Test de synthèse vocale. Un, deux, trois.');
DebugLog.info('[AudioDiagnostic] ========== TTS TEST END ==========');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Test TTS terminé - Vérifiez la console (F12)'),
backgroundColor: Colors.green,
duration: Duration(seconds: 3),
),
);
}
} catch (e) {
DebugLog.error('[AudioDiagnostic] Error during TTS test', e);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erreur TTS: $e'),
backgroundColor: Colors.red,
),
);
}
}
}
@override
Widget build(BuildContext context) {
return PopupMenuButton<String>(
icon: const Icon(Icons.bug_report, color: Colors.grey),
tooltip: 'Diagnostic Audio/TTS',
onSelected: (value) async {
switch (value) {
case 'audio':
await _testAudio(context);
break;
case 'tts':
await _testTTS(context);
break;
case 'both':
await _testAudio(context);
await Future.delayed(const Duration(milliseconds: 1000));
await _testTTS(context);
break;
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'audio',
child: Row(
children: [
Icon(Icons.volume_up, size: 20),
SizedBox(width: 8),
Text('Test Audio'),
],
),
),
const PopupMenuItem(
value: 'tts',
child: Row(
children: [
Icon(Icons.record_voice_over, size: 20),
SizedBox(width: 8),
Text('Test TTS'),
],
),
),
const PopupMenuItem(
value: 'both',
child: Row(
children: [
Icon(Icons.play_circle, size: 20),
SizedBox(width: 8),
Text('Test Audio + TTS'),
],
),
),
],
);
}
}

View File

@@ -61,9 +61,6 @@ dependencies:
# Notifications # Notifications
flutter_local_notifications: ^19.2.1 flutter_local_notifications: ^19.2.1
# Audio & TTS
audioplayers: ^6.1.0
flutter_tts: ^4.2.0
# Export/Import # Export/Import
csv: ^6.0.0 csv: ^6.0.0

View File

@@ -0,0 +1,71 @@
*// Script de test pour valider l'audio et le TTS dans la console du navigateur
// À copier-coller dans la console pour diagnostic
console.log('========== TEST AUDIO ET TTS ==========');
// Test 1 : Vérifier les voix disponibles
console.log('\n1⃣ Test des voix de synthèse :');
const synth = window.speechSynthesis;
const voices = synth.getVoices();
console.log(` ✓ Nombre de voix : ${voices.length}`);
const frenchVoices = voices.filter(v => v.lang.startsWith('fr'));
console.log(` ✓ Voix françaises : ${frenchVoices.length}`);
if (frenchVoices.length > 0) {
console.log(' ✓ Voix françaises disponibles :');
frenchVoices.forEach(v => {
console.log(` - ${v.name} (${v.lang}) ${v.localService ? 'LOCAL' : 'REMOTE'}`);
});
} else {
console.warn(' ⚠ AUCUNE voix française trouvée !');
}
// Test 2 : Tester le TTS
console.log('\n2⃣ Test du TTS (dans 1 seconde) :');
setTimeout(() => {
const utterance = new SpeechSynthesisUtterance('Test de synthèse vocale');
utterance.lang = 'fr-FR';
utterance.rate = 0.7;
utterance.pitch = 0.7;
utterance.volume = 1.0;
if (frenchVoices.length > 0) {
utterance.voice = frenchVoices[0];
console.log(` ✓ Utilisation de la voix : ${frenchVoices[0].name}`);
}
utterance.onstart = () => console.log(' ✓ TTS démarré');
utterance.onend = () => console.log(' ✓ TTS terminé');
utterance.onerror = (e) => console.error(' ✗ Erreur TTS:', e);
synth.speak(utterance);
console.log(' → Lecture en cours...');
}, 1000);
// Test 3 : Tester l'audio (dans 3 secondes)
console.log('\n3⃣ Test de l\'audio (dans 3 secondes) :');
setTimeout(() => {
const audio = new Audio('assets/assets/sounds/ok.mp3');
audio.volume = 1.0;
audio.onloadeddata = () => console.log(' ✓ Audio chargé');
audio.onplay = () => console.log(' ✓ Audio en lecture');
audio.onended = () => console.log(' ✓ Audio terminé');
audio.onerror = (e) => {
console.error(' ✗ Erreur audio:', audio.error);
console.error(' ✗ Code erreur:', audio.error?.code);
console.error(' ✗ Message:', audio.error?.message);
};
audio.play().then(() => {
console.log(' → Lecture audio démarrée');
}).catch(e => {
console.error(' ✗ Échec du play():', e);
});
}, 3000);
console.log('\n⏳ Tests en cours... Attendez les résultats ci-dessus');
console.log('========================================\n');

View File

@@ -1,7 +1,7 @@
{ {
"version": "1.1.7", "version": "1.1.14",
"updateUrl": "https://app.em2events.fr", "updateUrl": "https://app.em2events.fr",
"forceUpdate": true, "forceUpdate": true,
"releaseNotes": "Ajout de la gestion des maintenance et synthèse vocale", "releaseNotes": "Ajout de la gestion des maintenance et synthèse vocale",
"timestamp": "2026-02-24T12:39:57.675Z" "timestamp": "2026-03-03T10:13:12.014Z"
} }