1 Commits
Author SHA1 Message Date
neckfireandClaude Opus 4.8 fb4ab95f13 feat(ui): la carte reprend l'image importée en fond (voile pour lisibilité)
Build APK / build (push) Successful in 2m49s
- ServerCard: fond = image (MemoryImage) + scrim dégradé si présente, sinon
  dégradé thématique ; vignette d'icône masquée quand image de fond.
- servers_screen: fetch des bytes image (cache par id, vidé au refresh) → passe
  à la carte ; plus de FutureBuilder iconUrl inutile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 13:00:10 +02:00
2 changed files with 48 additions and 13 deletions
+16 -3
View File
@@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../models.dart'; import '../models.dart';
@@ -27,6 +29,15 @@ class _ServersScreenState extends State<ServersScreen> {
List<GameServer>? _servers; List<GameServer>? _servers;
String? _error; String? _error;
final _busy = <String>{}; final _busy = <String>{};
final _imgCache = <String, Uint8List?>{};
Future<Uint8List?> _image(GameServer s) async {
if (!s.hasImage) return null;
if (_imgCache.containsKey(s.id)) return _imgCache[s.id];
final b = await widget.api.imageBytes(s.id).catchError((_) => null);
_imgCache[s.id] = b;
return b;
}
@override @override
void initState() { void initState() {
@@ -41,6 +52,7 @@ class _ServersScreenState extends State<ServersScreen> {
if (mounted) setState(() { if (mounted) setState(() {
_servers = servers; _servers = servers;
_error = null; _error = null;
_imgCache.clear(); // recharge les images (elles ont pu changer)
}); });
} on UnauthorizedException { } on UnauthorizedException {
await widget.auth.logout(); await widget.auth.logout();
@@ -191,11 +203,12 @@ class _ServersScreenState extends State<ServersScreen> {
Widget _card(GameServer s) => Padding( Widget _card(GameServer s) => Padding(
padding: const EdgeInsets.only(bottom: 14), padding: const EdgeInsets.only(bottom: 14),
child: FutureBuilder<String>( child: FutureBuilder<Uint8List?>(
future: widget.api.iconUrl(s.id), future: _image(s),
builder: (_, snap) => ServerCard( builder: (_, snap) => ServerCard(
server: s, server: s,
iconUrl: snap.data ?? '', iconUrl: '',
imageBytes: snap.data,
busy: _busy.contains(s.id), busy: _busy.contains(s.id),
onToggle: () => _toggle(s), onToggle: () => _toggle(s),
onTap: () => _openDetail(s), onTap: () => _openDetail(s),
+25 -3
View File
@@ -1,3 +1,5 @@
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -11,6 +13,7 @@ class ServerCard extends StatelessWidget {
required this.busy, required this.busy,
required this.onToggle, required this.onToggle,
this.onTap, this.onTap,
this.imageBytes,
}); });
final GameServer server; final GameServer server;
@@ -18,31 +21,49 @@ class ServerCard extends StatelessWidget {
final bool busy; final bool busy;
final VoidCallback onToggle; final VoidCallback onToggle;
final VoidCallback? onTap; final VoidCallback? onTap;
final Uint8List? imageBytes;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final t = server.theme; final t = server.theme;
final badge = server.statusBadge; final badge = server.statusBadge;
final hasImg = imageBytes != null;
return Card( return Card(
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
child: Container( child: Ink(
decoration: BoxDecoration( decoration: hasImg
? BoxDecoration(
image: DecorationImage(image: MemoryImage(imageBytes!), fit: BoxFit.cover),
)
: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
colors: [t.start, t.end], colors: [t.start, t.end],
), ),
), ),
child: Container(
decoration: hasImg
? const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0x66000000), Color(0xCC000000)],
),
)
: null,
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
if (!hasImg) ...[
_Icon(server: server, iconUrl: iconUrl, fallback: t.icon), _Icon(server: server, iconUrl: iconUrl, fallback: t.icon),
const SizedBox(width: 14), const SizedBox(width: 14),
],
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -107,6 +128,7 @@ class ServerCard extends StatelessWidget {
), ),
), ),
), ),
),
); );
} }
} }
@@ -124,7 +146,7 @@ class _Icon extends StatelessWidget {
child: SizedBox( child: SizedBox(
width: 52, width: 52,
height: 52, height: 52,
child: (server.hasIcon) child: (server.hasIcon && iconUrl.isNotEmpty)
? Image.network(iconUrl, ? Image.network(iconUrl,
fit: BoxFit.cover, fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _fallbackBox()) errorBuilder: (_, __, ___) => _fallbackBox())