diff --git a/lib/main.dart b/lib/main.dart index 10634895..f08d9fe4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'dart:async'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/gestures.dart'; @@ -452,6 +451,9 @@ class SetupScreen extends StatefulWidget { class _SetupScreenState extends State { String _statusMessage = ''; + // Per-server connection status: serverId -> (name, connected?) + final Map _serverStatus = {}; + @override void initState() { super.initState(); @@ -517,6 +519,15 @@ class _SetupScreenState extends State { _setStatus(t.common.connectingToServers); + // Populate per-server status for splash display + if (mounted) { + setState(() { + for (final server in servers) { + _serverStatus[server.clientIdentifier] = (server.name, null); + } + }); + } + try { final result = await ServerConnectionOrchestrator.connectAndInitialize( servers: servers, @@ -524,6 +535,16 @@ class _SetupScreenState extends State { librariesProvider: context.read(), syncService: context.read(), clientIdentifier: storage.getClientIdentifier(), + onServerStatus: (serverId, success) { + if (mounted) { + setState(() { + final existing = _serverStatus[serverId]; + if (existing != null) { + _serverStatus[serverId] = (existing.$1, success); + } + }); + } + }, ); if (!mounted) return; @@ -554,30 +575,81 @@ class _SetupScreenState extends State { } } + Widget _buildStatusText(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + child: Text( + _statusMessage, + key: ValueKey(_statusMessage), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ); + } + + Widget _buildServerStatusList(BuildContext context) { + if (_serverStatus.isEmpty) return const SizedBox.shrink(); + final textTheme = Theme.of(context).textTheme; + final dimColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.5); + const coralColor = Color(0xFFE5A00D); + const successColor = Color(0xFF4CAF50); + const failColor = Color(0xFFEF5350); + + return Column( + mainAxisSize: MainAxisSize.min, + children: _serverStatus.entries.map((entry) { + final (name, connected) = entry.value; + final Widget statusIcon; + if (connected == null) { + statusIcon = const SizedBox( + width: 12, height: 12, + child: CircularProgressIndicator(strokeWidth: 1.5, color: coralColor), + ); + } else if (connected) { + statusIcon = const Icon(Icons.check_circle, size: 14, color: successColor); + } else { + statusIcon = const Icon(Icons.cancel, size: 14, color: failColor); + } + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + statusIcon, + const SizedBox(width: 8), + Text(name, style: textTheme.bodySmall?.copyWith(color: dimColor)), + ], + ), + ); + }).toList(), + ); + } + @override Widget build(BuildContext context) { + const coralColor = Color(0xFFE5A00D); return ColoredBox( color: Theme.of(context).scaffoldBackgroundColor, child: Stack( children: [ - // Icon dead-center, matching Android 12+ splash position. - // 192dp accounts for the 16% inset in ic_launcher.xml. Center(child: SvgPicture.asset('assets/plezy_adaptive_foreground.svg', width: 288, height: 288)), - // Status text below center, independent of icon position. Positioned( - left: 0, - right: 0, - bottom: MediaQuery.of(context).size.height * 0.5 - 140, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - child: Text( - _statusMessage, - key: ValueKey(_statusMessage), - textAlign: TextAlign.center, - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6)), - ), + left: 0, right: 0, + bottom: MediaQuery.of(context).size.height * 0.5 - 170, + child: _buildStatusText(context), + ), + Positioned( + left: 0, right: 0, + top: MediaQuery.of(context).size.height * 0.5 + 180, + child: Center( + child: _serverStatus.isEmpty + ? const SizedBox( + width: 20, height: 20, + child: CircularProgressIndicator(strokeWidth: 2, color: coralColor), + ) + : _buildServerStatusList(context), ), ), ], diff --git a/lib/services/server_connection_orchestrator.dart b/lib/services/server_connection_orchestrator.dart index 753a6b99..3cc2d690 100644 --- a/lib/services/server_connection_orchestrator.dart +++ b/lib/services/server_connection_orchestrator.dart @@ -37,6 +37,7 @@ class ServerConnectionOrchestrator { required OfflineWatchSyncService syncService, String? clientIdentifier, Duration timeout = ConnectionTimeouts.connectAll, + void Function(String serverId, bool success)? onServerStatus, }) async { appLogger.i('Connecting to ${servers.length} servers...'); @@ -44,6 +45,12 @@ class ServerConnectionOrchestrator { servers, clientIdentifier: clientIdentifier, timeout: timeout, + onServerConnected: onServerStatus != null + ? (serverId, _) => onServerStatus(serverId, true) + : null, + onServerFailed: onServerStatus != null + ? (serverId, _) => onServerStatus(serverId, false) + : null, ); PlexClient? firstClient;