Compare commits

...
15 Commits
Author SHA1 Message Date
Your NameandClaude Opus 4.5 bd3a2f98e8 Bump version to 2.6.1-beta
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 15:12:39 +00:00
Your NameandClaude Opus 4.5 516de13894 Fix nav bar flash during first-time user onboarding
Add selectedPlayer check to nav bar visibility condition to prevent
it from showing during the ~1.2s gap between connection and player
selection for first-time users.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 14:35:57 +00:00
Your NameandClaude Opus 4.5 d7188a8784 Fix nav bar flash and backdrop color mismatch
1. Removed Selector for nav bar visibility - Selector only rebuilds when
   isConnected changes, but shouldShowWelcomeBackdrop is local state that
   can change independently. Now using simple if condition.

2. Changed backdrop color from colorScheme.surface to Color(0xFF1a1a1a)
   to match the connection screen exactly, ensuring seamless transition.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 14:18:05 +00:00
Your NameandClaude Opus 4.5 c03ea6a14b Hide bottom nav bar during welcome screen
The nav bar was appearing briefly before the welcome backdrop because
it only checked isConnected, not the welcome screen state. Now it also
hides when shouldShowWelcomeBackdrop is true.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 14:00:13 +00:00
Your NameandClaude Opus 4.5 d418af45f1 Fix LoginScreen to wait for player selection for first-time users
When a user logs in via LoginScreen (fresh install), the navigation to
HomeScreen was happening immediately on connection - before the player
was selected. This caused the home screen to flash before the welcome
overlay could appear.

For first-time users (hasCompletedOnboarding=false), LoginScreen now
waits for provider.selectedPlayer to be set before navigating, matching
the logic in AppStartup.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 13:44:13 +00:00
Your NameandClaude Opus 4.5 ac05381038 Show welcome backdrop immediately using computed condition
The backdrop was only showing when _isHintModeActive was true, but that
required setState which happens in a post-frame callback - causing a
1-frame gap where the home screen was visible.

Fix: Compute shouldShowWelcomeBackdrop synchronously in build():
- True when first-time user is ready (settings loaded + connected + player)
- True when already in hint mode
- This shows the backdrop IMMEDIATELY in the same frame as HomeScreen
- Welcome content (logo, text) still fades in via animation after

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 13:26:35 +00:00
Your NameandClaude Opus 4.5 4d29e32651 Fix welcome screen not triggering after connection
The welcome screen check in build() was missing - when the provider state
changes (connection happens), build() runs via context.watch(), but we
weren't checking if conditions were met for the welcome screen.

Added the check in build() with a post-frame callback to trigger the
welcome screen when:
- Settings are loaded
- First-time user (not completed onboarding)
- Connected with player selected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 13:12:46 +00:00
Your NameandClaude Opus 4.5 91795976ff Fix welcome screen race condition by making AppStartup the gate
The home screen was briefly visible between connection and welcome screens
because AppStartup and GlobalPlayerOverlay were racing - they loaded settings
and detected connection independently with no synchronization.

Solution: Make AppStartup the single source of truth for the transition.
- Load onboarding settings FIRST (before connection check)
- For first-time users: wait for connected + player selected
- For returning users: wait for connected only
- Remove preemptive backdrop from GlobalPlayerOverlay (no longer needed)

This eliminates the race by design - the dark "Connecting..." screen stays
visible until the welcome overlay is ready to appear immediately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 12:54:51 +00:00
Your NameandClaude Opus 4.5 484000bc6e Show backdrop during settings load to prevent home screen flash
Add _settingsLoaded flag to track async settings initialization.
Show solid backdrop when connected but settings not yet loaded,
ensuring the grey "Connecting..." background stays visible until
welcome screen is ready.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:23:16 +00:00
Your NameandClaude Opus 4.5 e373053f03 Fix welcome screen flash by using synchronous provider check
Use context.watch() directly in build method instead of nested Selector
to ensure the preemptive backdrop renders in the same frame as home screen.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:05:19 +00:00
Your NameandClaude Opus 4.5 31ef8edaf0 Fix preemptive backdrop not triggering rebuild
The _waitingForConnection flag was set without setState(), so the
preemptive backdrop condition was never re-evaluated. Now it properly
triggers a rebuild so the backdrop shows immediately when connected.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 10:43:47 +00:00
Your NameandClaude Opus 4.5 ff568ebf56 Add preemptive backdrop for seamless welcome transition
When connection is established, a solid backdrop now appears instantly
in the same render frame, covering the home screen before the animated
welcome screen starts. This prevents any flash of the home screen.

Flow: login → solid backdrop (instant) → blur + welcome content (animated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 10:18:53 +00:00
Your NameandClaude Opus 4.5 ee6f00d56b Fix welcome screen to only show after successful login
The welcome screen was incorrectly starting immediately on first use,
even before the user had logged in. Now it correctly:

1. Shows login screen first (with hidden nav bar)
2. Waits for successful connection
3. Then shows welcome screen (covering home screen)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 10:04:05 +00:00
Your NameandClaude Opus 4.5 fff5c91b5c Improve welcome screen transition and hide nav bar on login
1. Hide bottom nav bar when not connected (fixes login screen issue)
2. Start welcome screen immediately on first use (blur + logo)
3. Show mini player hints only after connection is established

This ensures a smooth transition from login to home - the welcome
blur covers the home screen immediately, preventing any flash.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 09:47:35 +00:00
Your NameandClaude Opus 4.5 cd27aee7b8 Fix welcome screen showing before server connection
On fresh install, welcome screen was starting immediately even when
user hadn't configured server address/login yet. This meant the hints
were pointing to a mini player that wasn't visible.

Now the welcome screen waits until the user is connected with a
selected player before showing the onboarding experience.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 09:29:14 +00:00
4 changed files with 184 additions and 40 deletions
+54 -5
View File
@@ -348,7 +348,16 @@ class _SystemUIWrapperState extends State<SystemUIWrapper> {
}
}
/// Startup widget that checks if user is logged in and auto-connects
/// Startup widget that checks if user is logged in and auto-connects.
///
/// CRITICAL: This widget is the single source of truth for the startup transition.
/// It loads settings FIRST (before connection), then waits for the appropriate
/// conditions before showing HomeScreen. This eliminates the race condition between
/// AppStartup and GlobalPlayerOverlay that caused the home screen flash.
///
/// For first-time users: Holds dark screen until connected + player selected,
/// so the welcome overlay can appear seamlessly.
/// For returning users: Transitions as soon as connection is established.
class AppStartup extends StatefulWidget {
const AppStartup({super.key});
@@ -361,9 +370,32 @@ class _AppStartupState extends State<AppStartup> {
String? _savedServerUrl;
bool _connectionAttempted = false;
// Welcome system state - loaded FIRST before connection check
bool _settingsLoaded = false;
bool _hasCompletedOnboarding = true; // Default to true (no welcome needed)
@override
void initState() {
super.initState();
_loadSettingsFirst();
}
/// Load settings BEFORE checking connection.
/// This ensures we know if this is a first-time user before transitioning.
Future<void> _loadSettingsFirst() async {
// Load onboarding state first - this is critical for determining
// when to transition to HomeScreen
_hasCompletedOnboarding = await SettingsService.getHasCompletedOnboarding();
if (!mounted) return;
setState(() {
_settingsLoaded = true;
});
_logger.log('🚀 AppStartup: Settings loaded, hasCompletedOnboarding=$_hasCompletedOnboarding');
// Now check connection
_checkAndConnect();
}
@@ -388,14 +420,30 @@ class _AppStartupState extends State<AppStartup> {
// Just wait for it to complete or timeout
_logger.log('🚀 AppStartup: Waiting for provider auto-connection to $serverUrl');
// For first-time users, we need to wait for both connection AND player selection
// so the welcome overlay can appear immediately without home screen flash.
// For returning users, just wait for connection.
final needsWelcome = !_hasCompletedOnboarding;
// Give the provider time to connect (it restores credentials and connects in _initialize)
// Check connection state periodically
for (var i = 0; i < 20; i++) {
await Future.delayed(const Duration(milliseconds: 250));
if (provider.isConnected) {
_logger.log('🚀 AppStartup: Connection established');
break;
if (needsWelcome) {
// First-time user: wait for connection + player selection
if (provider.isConnected && provider.selectedPlayer != null) {
_logger.log('🚀 AppStartup: First-time user ready (connected + player selected)');
break;
}
} else {
// Returning user: just wait for connection
if (provider.isConnected) {
_logger.log('🚀 AppStartup: Connection established');
break;
}
}
if (provider.connectionState == MAConnectionState.error) {
_logger.log('🚀 AppStartup: Connection failed with error');
break;
@@ -422,7 +470,8 @@ class _AppStartupState extends State<AppStartup> {
@override
Widget build(BuildContext context) {
// Show loading while checking settings or connecting
if (!_connectionAttempted || _isConnecting) {
// CRITICAL: Don't transition until settings are loaded - this prevents the race
if (!_settingsLoaded || !_connectionAttempted || _isConnecting) {
return Scaffold(
backgroundColor: const Color(0xFF1a1a1a),
body: Center(
+12
View File
@@ -443,6 +443,18 @@ class _LoginScreenState extends State<LoginScreen> {
}
if (provider.isConnected) {
// For first-time users, wait for player selection so the welcome
// overlay can appear immediately without home screen flash.
// This matches the logic in AppStartup._checkAndConnect().
final hasCompletedOnboarding = await SettingsService.getHasCompletedOnboarding();
if (!hasCompletedOnboarding) {
// First-time user: wait for connection + player selection
for (int i = 0; i < 20; i++) {
await Future.delayed(const Duration(milliseconds: 250));
if (provider.selectedPlayer != null) break;
}
}
// Navigate to home screen
if (mounted) {
// Ensure keyboard is closed before navigating
+117 -34
View File
@@ -167,9 +167,15 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
final _bounceOffsetNotifier = ValueNotifier<double>(0.0);
// Hint system state
// NOTE: AppStartup now handles the settings loading gate. It waits for:
// - First-time users: connected + player selected before showing HomeScreen
// - Returning users: connected before showing HomeScreen
// This eliminates the race condition that caused home screen flash.
bool _showHints = true;
bool _hasCompletedOnboarding = false; // First-use welcome screen
bool _hintTriggered = false; // Prevent multiple triggers per session
bool _settingsLoaded = false; // True once settings have been loaded from storage
bool _miniPlayerHintsReady = false; // True once connected with player (can show hints)
final _hintOpacityNotifier = ValueNotifier<double>(0.0);
// Welcome content fade-in animation
@@ -234,12 +240,66 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
Future<void> _loadHintSettings() async {
_showHints = await SettingsService.getShowHints();
_hasCompletedOnboarding = await SettingsService.getHasCompletedOnboarding();
// Show welcome screen only on first use (not completed onboarding yet)
if (!_hasCompletedOnboarding && mounted && !_hintTriggered) {
_startWelcomeScreen();
if (!mounted) return;
setState(() {
_settingsLoaded = true;
});
// If first-time user and already connected (AppStartup waited for us),
// start welcome screen immediately - no race condition possible
if (!_hasCompletedOnboarding && !_hintTriggered) {
final provider = context.read<MusicAssistantProvider>();
if (provider.isConnected && provider.selectedPlayer != null) {
_miniPlayerHintsReady = true;
_startWelcomeScreen();
_startMiniPlayerBounce();
}
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Check if we should enable mini player hints now that we have connection
_checkConnectionForMiniPlayerHints();
}
/// Check connection state and start welcome screen if appropriate.
/// This is called from multiple places:
/// 1. didChangeDependencies() - when widget first builds
/// 2. _loadHintSettings() - when settings finish loading
/// 3. build() - when provider state changes (via post-frame callback)
void _checkConnectionForMiniPlayerHints() {
if (_hintTriggered || !mounted || !_settingsLoaded) return;
if (_hasCompletedOnboarding) return; // Not first-time user
final provider = context.read<MusicAssistantProvider>();
if (provider.isConnected && provider.selectedPlayer != null) {
// Connected with a player - start welcome screen now
_miniPlayerHintsReady = true;
_startWelcomeScreen();
// Start the bounce animation now that mini player is visible
_startMiniPlayerBounce();
}
}
/// Start the bounce animation for mini player hints
void _startMiniPlayerBounce() {
if (!_isHintModeActive || !mounted) return;
_doubleBounceController.reset();
_doubleBounceController.forward();
// Repeat bounce every 2 seconds
_hintBounceTimer?.cancel();
_hintBounceTimer = Timer.periodic(const Duration(seconds: 2), (_) {
if (_isHintModeActive && mounted) {
_doubleBounceController.reset();
_doubleBounceController.forward();
}
});
}
/// Start the welcome screen with fade-in animation
void _startWelcomeScreen() {
if (_hintTriggered) return;
@@ -250,24 +310,10 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
_isHintModeActive = true;
});
// Fade in welcome content
// Fade in welcome content (logo + title)
_welcomeFadeController.forward();
// Start bounce animation after fade completes
_welcomeFadeController.addStatusListener((status) {
if (status == AnimationStatus.completed && mounted) {
_doubleBounceController.reset();
_doubleBounceController.forward();
// Repeat bounce every 2 seconds
_hintBounceTimer = Timer.periodic(const Duration(seconds: 2), (_) {
if (_isHintModeActive && mounted) {
_doubleBounceController.reset();
_doubleBounceController.forward();
}
});
}
});
// Mini player bounce is started separately once connected via _startMiniPlayerBounce()
}
@override
@@ -362,6 +408,36 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
// Watch provider to trigger rebuilds when connection state changes
final provider = context.watch<MusicAssistantProvider>();
// Compute whether this is a first-time user ready for welcome screen.
// This is SYNCHRONOUS - no setState needed - so it works in the same frame.
final isFirstTimeUserReady = _settingsLoaded &&
!_hasCompletedOnboarding &&
provider.isConnected &&
provider.selectedPlayer != null;
// Show welcome backdrop if:
// 1. Already in hint mode (_isHintModeActive), OR
// 2. First-time user ready but hint mode not started yet (!_hintTriggered)
// This ensures backdrop appears IMMEDIATELY when HomeScreen renders,
// preventing the flash. Welcome content fades in after via animation.
final shouldShowWelcomeBackdrop = _isHintModeActive ||
(isFirstTimeUserReady && !_hintTriggered);
// Trigger hint mode if conditions are met (for animations, bounce, etc)
if (isFirstTimeUserReady && !_hintTriggered) {
// Schedule for post-frame to avoid setState during build
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && !_hintTriggered) {
_miniPlayerHintsReady = true;
_startWelcomeScreen();
_startMiniPlayerBounce();
}
});
}
// Handle back gesture at top level - dismiss hint mode or device list if visible
return PopScope(
canPop: !_isRevealVisible && !_isHintModeActive,
@@ -383,14 +459,18 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
child: widget.child,
),
// Global persistent bottom navigation bar - positioned at bottom
Positioned(
left: 0,
right: 0,
bottom: 0,
child: ListenableBuilder(
listenable: navigationProvider,
builder: (context, _) {
return Consumer<ThemeProvider>(
// Hide when: not connected, no player selected, OR showing welcome screen
// For first-time users, selectedPlayer becomes non-null at the same time
// as shouldShowWelcomeBackdrop becomes true, preventing any gap.
if (provider.isConnected && provider.selectedPlayer != null && !shouldShowWelcomeBackdrop)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: ListenableBuilder(
listenable: navigationProvider,
builder: (context, _) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, _) {
// Use adaptive primary color for bottom nav when adaptive theme is enabled
final sourceColor = themeProvider.adaptiveTheme
@@ -479,9 +559,9 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
);
},
);
},
},
),
),
),
// Blur backdrop for device selector (reveal mode) - static, no animation
if (_isRevealVisible && !_isHintModeActive)
Positioned.fill(
@@ -497,8 +577,10 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
),
),
// Blur backdrop for hint/welcome mode - animated fade from solid to semi-transparent
if (_isHintModeActive)
// Welcome/hint backdrop - shows IMMEDIATELY when first-time user is ready.
// Uses computed `shouldShowWelcomeBackdrop` which is true BEFORE _isHintModeActive,
// preventing the 1-frame gap where home screen would be visible.
if (shouldShowWelcomeBackdrop)
Positioned.fill(
child: TweenAnimationBuilder<double>(
// Hold solid for 2s, then fade to 0.5 over 1s
@@ -509,7 +591,8 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 8.0, sigmaY: 8.0),
child: Container(
color: colorScheme.surface.withOpacity(opacity),
// Use same dark color as connection screen for seamless transition
color: const Color(0xFF1a1a1a).withOpacity(opacity),
),
);
},
@@ -553,8 +636,8 @@ class _GlobalPlayerOverlayState extends State<GlobalPlayerOverlay>
),
),
// Bottom section: Hint text and Skip button (positioned relative to mini player)
if (_isHintModeActive)
// Bottom section: Hint text and Skip button (only show once mini player is visible)
if (_isHintModeActive && _miniPlayerHintsReady)
Positioned(
left: 24,
right: 24,
+1 -1
View File
@@ -1,7 +1,7 @@
name: ensemble
description: A minimalistic Music Assistant client built with Flutter
publish_to: 'none'
version: 2.6.0-beta+28
version: 2.6.1-beta+29
environment:
sdk: '>=3.0.0 <4.0.0'