import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/api_client.dart' show baseUrl; import '../providers/auth_provider.dart'; String protectedMediaUrl(String value) { final trimmed = value.trim(); if (trimmed.isEmpty) return trimmed; final absolute = Uri.tryParse(trimmed); final path = absolute?.path ?? trimmed.split('?').first; final legacyIndex = path.toLowerCase().indexOf('/uploads/users/'); if (legacyIndex >= 0) { final fileName = Uri.decodeComponent(path.split('/').last); return '$baseUrl/api/files/content/${Uri.encodeComponent(fileName)}'; } if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { return trimmed; } if (trimmed.startsWith('/')) return '$baseUrl$trimmed'; return '$baseUrl/$trimmed'; } bool mediaRequiresAuthentication(String url) { final normalizedBase = baseUrl.endsWith('/') ? baseUrl : '$baseUrl/'; return url == baseUrl || url.startsWith(normalizedBase); } class AuthenticatedNetworkImage extends ConsumerStatefulWidget { final String imageUrl; final BoxFit? fit; final double? width; final double? height; final ImageLoadingBuilder? loadingBuilder; final ImageErrorWidgetBuilder? errorBuilder; const AuthenticatedNetworkImage({ super.key, required this.imageUrl, this.fit, this.width, this.height, this.loadingBuilder, this.errorBuilder, }); @override ConsumerState createState() => _AuthenticatedNetworkImageState(); } class _AuthenticatedNetworkImageState extends ConsumerState { late String _resolvedUrl; Future? _bytes; @override void initState() { super.initState(); _configure(); } @override void didUpdateWidget(covariant AuthenticatedNetworkImage oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.imageUrl != widget.imageUrl) _configure(); } void _configure() { _resolvedUrl = protectedMediaUrl(widget.imageUrl); _bytes = mediaRequiresAuthentication(_resolvedUrl) ? _loadProtectedBytes(_resolvedUrl) : null; } Future _loadProtectedBytes(String url) async { final response = await ref .read(apiClientProvider) .dio .get>( url, options: Options(responseType: ResponseType.bytes), ); return Uint8List.fromList(response.data ?? const []); } @override Widget build(BuildContext context) { final bytes = _bytes; if (bytes == null) { return Image.network( _resolvedUrl, fit: widget.fit, width: widget.width, height: widget.height, loadingBuilder: widget.loadingBuilder, errorBuilder: widget.errorBuilder, ); } return FutureBuilder( future: bytes, builder: (context, snapshot) { if (snapshot.hasError) { return widget.errorBuilder?.call( context, snapshot.error!, snapshot.stackTrace, ) ?? const Icon(Icons.broken_image_outlined); } final data = snapshot.data; if (data == null) { return const Center(child: CircularProgressIndicator(strokeWidth: 2)); } return Image.memory( data, fit: widget.fit, width: widget.width, height: widget.height, errorBuilder: widget.errorBuilder, ); }, ); } }