Compare commits

..

2 Commits

Author SHA1 Message Date
Thomas Fransolet
a1701e216a Telecharger une ressource la telecharge, au lieu de l'ouvrir
L'attribut `download` d'un lien est ignore quand la cible est sur une autre
origine — Firebase Storage ici. Le navigateur se contentait donc d'ouvrir la
ressource dans un onglet, et le nom de fichier calcule par
`downloadFileNameFor` ne servait a rien.

Le fichier est d'abord recupere en blob et le lien pointe sur l'URL d'objet.
Repli sur l'ouverture en onglet si la requete echoue (CORS, reseau) : le
comportement d'avant, faute de mieux — a surveiller, c'est le chemin qui
reintroduit le bug sans rien dire.

Le telechargement groupe attend chaque fichier au lieu de lancer toute la
selection en parallele.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 16:55:06 +02:00
Thomas Fransolet
83eba7ff54 Editeurs de traduction : le curseur ne saute plus a chaque frappe
`QuillEditor.basic` fabrique un `FocusNode` et un `ScrollController` neufs a
chaque construction quand on ne lui en passe pas. Or chaque frappe reconstruit
ces widgets — compteur de caracteres, pastilles du rail de langues — donc
l'editeur perdait le focus des le premier caractere saisi.

Les deux conteneurs creent desormais leurs noeuds une fois, dans `initState`,
un par langue, et les liberent au `dispose`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 16:54:56 +02:00
4 changed files with 69 additions and 7 deletions

View File

@ -41,6 +41,12 @@ class TranslationInputAndResourceContainer extends StatefulWidget {
class _TranslationInputAndResourceContainerState
extends State<TranslationInputAndResourceContainer> {
late Map<String, QuillController> _controllers;
/// Meme piege que dans `translation_input_container` : sans `focusNode` ni
/// `scrollController`, `QuillEditor.basic` en cree de neufs a chaque
/// construction et le champ perd le curseur des qu'un rebuild survient.
late Map<String, FocusNode> _focusNodes;
late Map<String, ScrollController> _editorScrollControllers;
int _selected = 0;
bool _isEnforcingLimit = false;
bool _isTranslating = false;
@ -50,6 +56,14 @@ class _TranslationInputAndResourceContainerState
void initState() {
super.initState();
_controllers = _buildControllers();
_focusNodes = {
for (final translation in widget.newValues)
translation.language!: FocusNode(),
};
_editorScrollControllers = {
for (final translation in widget.newValues)
translation.language!: ScrollController(),
};
}
static const _emptyDelta = [{'insert': '\n'}];
@ -172,6 +186,12 @@ class _TranslationInputAndResourceContainerState
for (final c in _controllers.values) {
c.dispose();
}
for (final node in _focusNodes.values) {
node.dispose();
}
for (final c in _editorScrollControllers.values) {
c.dispose();
}
super.dispose();
}
@ -396,6 +416,8 @@ class _TranslationInputAndResourceContainerState
),
child: QuillEditor.basic(
controller: controller,
focusNode: _focusNodes[lang],
scrollController: _editorScrollControllers[lang],
config: const QuillEditorConfig(
scrollable: true,
expands: false,

View File

@ -47,6 +47,13 @@ class TranslationInputContainer extends StatefulWidget {
class _TranslationInputContainerState extends State<TranslationInputContainer> {
late Map<String, QuillController> _controllers;
late Map<String, TextEditingController> _textControllers;
/// `QuillEditor.basic` fabrique un `FocusNode` et un `ScrollController` neufs
/// a chaque construction quand on ne lui en passe pas : le curseur sautait de
/// l'editeur des la premiere frappe, puisque chaque frappe reconstruit ce
/// widget (compteur de caracteres, pastilles du rail).
late Map<String, FocusNode> _focusNodes;
late Map<String, ScrollController> _editorScrollControllers;
int _selected = 0;
bool _isEnforcingLimit = false;
bool _isTranslating = false;
@ -57,6 +64,14 @@ class _TranslationInputContainerState extends State<TranslationInputContainer> {
super.initState();
_controllers = widget.isHTML ? _buildControllers() : {};
_textControllers = widget.isHTML ? {} : _buildTextControllers();
_focusNodes = {
for (final translation in widget.newValues)
translation.language!: FocusNode(),
};
_editorScrollControllers = {
for (final translation in widget.newValues)
translation.language!: ScrollController(),
};
}
Map<String, TextEditingController> _buildTextControllers() {
@ -207,6 +222,12 @@ class _TranslationInputContainerState extends State<TranslationInputContainer> {
for (final c in _textControllers.values) {
c.dispose();
}
for (final node in _focusNodes.values) {
node.dispose();
}
for (final c in _editorScrollControllers.values) {
c.dispose();
}
super.dispose();
}
@ -467,6 +488,8 @@ class _TranslationInputContainerState extends State<TranslationInputContainer> {
),
child: QuillEditor.basic(
controller: controller,
focusNode: _focusNodes[lang],
scrollController: _editorScrollControllers[lang],
config: const QuillEditorConfig(
scrollable: true,
expands: false,

View File

@ -492,9 +492,9 @@ class _ResourceBodyGridState extends State<ResourceBodyGrid> {
TextButton.icon(
icon: const Icon(Icons.download, size: 16),
label: Text(l.download, style: const TextStyle(fontSize: 12.5)),
onPressed: () {
onPressed: () async {
for (final resource in selected) {
downloadResource(resource);
await downloadResource(resource);
}
},
),

View File

@ -8,12 +8,29 @@ import 'resource_formatting.dart';
///
/// L'ancienne modale forçait `'${label}.json'` quel que soit le type : un PNG
/// arrivait en `mon-image.json` et ne s'ouvrait nulle part sans renommage.
void downloadResource(ResourceDTO resource) {
///
/// Le fichier est d'abord récupéré en blob : l'attribut `download` d'un lien est
/// ignoré quand la cible est sur une autre origine (Firebase Storage ici), et le
/// navigateur se contentait d'ouvrir la ressource dans un onglet. Si la requête
/// échoue (CORS, réseau), on retombe sur l'ouverture dans un nouvel onglet.
Future<void> downloadResource(ResourceDTO resource) async {
final url = resource.url;
if (url == null) return;
final anchor = html.AnchorElement(href: url)
..download = downloadFileNameFor(resource)
..target = '_blank';
anchor.click();
final fileName = downloadFileNameFor(resource);
try {
final request = await html.HttpRequest.request(url, responseType: 'blob');
final blob = request.response as html.Blob;
final objectUrl = html.Url.createObjectUrlFromBlob(blob);
html.AnchorElement(href: objectUrl)
..download = fileName
..click();
html.Url.revokeObjectUrl(objectUrl);
} catch (_) {
html.AnchorElement(href: url)
..download = fileName
..target = '_blank'
..click();
}
}