import 'package:flutter/material.dart'; import 'package:p_tits_pas/services/api/tokenService.dart'; /// [Image.network] avec en-tête `Authorization` uniquement si l’URL n’est pas un fichier statique public. /// /// Les chemins `/uploads/...` sont servis sans auth (voir back + Traefik) : ne pas envoyer de Bearer, /// sinon en **cross-origin** (ex. admin Flutter web en local, API en prod) le navigateur peut bloquer /// la requête (CORS) et afficher l’icône « image cassée ». class AuthNetworkImage extends StatefulWidget { const AuthNetworkImage({ super.key, required this.url, this.width, this.height, this.fit = BoxFit.cover, this.alignment = Alignment.center, this.loadingBuilder, this.errorBuilder, }); final String url; final double? width; final double? height; final BoxFit fit; final AlignmentGeometry alignment; final ImageLoadingBuilder? loadingBuilder; final ImageErrorWidgetBuilder? errorBuilder; static bool isPublicUploadUrl(String url) { final u = url.toLowerCase(); return u.contains('/uploads/'); } @override State createState() => _AuthNetworkImageState(); } class _AuthNetworkImageState extends State { late final Future?> _headersFuture; @override void initState() { super.initState(); final isPublic = AuthNetworkImage.isPublicUploadUrl(widget.url); _headersFuture = isPublic ? Future?>.value(null) : _loadHeaders(); } static Future?> _loadHeaders() async { final t = await TokenService.getToken(); if (t == null || t.isEmpty) return null; return {'Authorization': 'Bearer $t'}; } ImageErrorWidgetBuilder _wrapErrorBuilder() { return (BuildContext context, Object error, StackTrace? stackTrace) { final inner = widget.errorBuilder; if (inner != null) { return inner(context, error, stackTrace); } return ColoredBox( color: Colors.grey.shade200, child: Center( child: Icon(Icons.broken_image_outlined, color: Colors.grey.shade500), ), ); }; } @override Widget build(BuildContext context) { final err = _wrapErrorBuilder(); if (AuthNetworkImage.isPublicUploadUrl(widget.url)) { return Image.network( widget.url, width: widget.width, height: widget.height, fit: widget.fit, alignment: widget.alignment, loadingBuilder: widget.loadingBuilder, errorBuilder: err, ); } return FutureBuilder?>( future: _headersFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return SizedBox( width: widget.width, height: widget.height, child: ColoredBox( color: Colors.grey.shade200, child: const Center( child: SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ), ), ), ); } final headers = snapshot.data; return Image.network( widget.url, width: widget.width, height: widget.height, fit: widget.fit, alignment: widget.alignment, headers: headers, loadingBuilder: widget.loadingBuilder, errorBuilder: err, ); }, ); } }