From bc34d963295938ef9827d89c38f0fa4af192fd01 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 18 May 2026 23:03:31 -0400 Subject: [PATCH] fix(player): request POST_NOTIFICATIONS; auto-minimize player on teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device-surfaced on physical Android 13+ (worked on emulator): A) The media notification never appeared because the app never requested POST_NOTIFICATIONS at runtime — the manifest declares it and the foreground service is correct, but Android 13+ denies it by default until asked. Add permission_handler ^12.0.1 and request Permission.notification once at startup (post-first-frame, Platform.isAndroid-guarded; no-op on <13 / once decided). B) When the #52 idle/dismiss teardown nulled mediaItem while the full NowPlayingScreen was open, it stranded the user on an empty "Nothing playing." Scaffold. Now post-frame maybePop() so it auto-minimizes (the mini bar is already gone). pubspec.lock + db.g.dart regenerated by CI/build. Co-Authored-By: Claude Opus 4.7 (1M context) --- flutter_client/lib/app.dart | 11 +++++++++++ flutter_client/lib/player/now_playing_screen.dart | 8 ++++++++ flutter_client/pubspec.yaml | 3 +++ 3 files changed, 22 insertions(+) diff --git a/flutter_client/lib/app.dart b/flutter_client/lib/app.dart index 2c9a5160..40f4ce8c 100644 --- a/flutter_client/lib/app.dart +++ b/flutter_client/lib/app.dart @@ -1,5 +1,8 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'cache/cache_filler.dart'; import 'cache/metadata_prefetcher.dart'; @@ -77,6 +80,14 @@ class _MinstrelAppState extends ConsumerState { // probe → offlineProvider. Read here to start the poller; S4 // gates system-playlist play + Shuffle-all on it. ref.read(offlineProvider); + // POST_NOTIFICATIONS (Android 13+) is denied-by-default until + // requested; without it the media notification is silently + // suppressed on physical devices. One-shot, post-first-frame so + // it never blocks launch; no-op on <13 / once already decided. + if (Platform.isAndroid) { + // ignore: unawaited_futures + Permission.notification.request(); + } }); } diff --git a/flutter_client/lib/player/now_playing_screen.dart b/flutter_client/lib/player/now_playing_screen.dart index 95817b8f..9301e477 100644 --- a/flutter_client/lib/player/now_playing_screen.dart +++ b/flutter_client/lib/player/now_playing_screen.dart @@ -222,6 +222,14 @@ class _NowPlayingScreenState extends ConsumerState { _displayedDominant = null; _pendingPreloadId = null; }); + // Session was torn down (#52 idle/dismiss) while the full + // player was open. Don't strand the user on an empty + // "Nothing playing." screen — minimize back (the mini bar is + // already gone too). maybePop is a no-op if this is somehow + // the root route. + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) Navigator.of(context).maybePop(); + }); } return; } diff --git a/flutter_client/pubspec.yaml b/flutter_client/pubspec.yaml index 0c425aeb..30c98a22 100644 --- a/flutter_client/pubspec.yaml +++ b/flutter_client/pubspec.yaml @@ -25,6 +25,9 @@ dependencies: # Lucide icon set (design system mandates Lucide, not Material). # Icons exposed as LucideIcons. IconData usable in Icon(). flutter_lucide: ^1.11.0 + # Runtime POST_NOTIFICATIONS request (Android 13+ denies-by-default + # until asked; the media notification is suppressed without it). + permission_handler: ^12.0.1 google_fonts: ^8.1.0 # 10.x conflicts with flutter_secure_storage 10.x on win32. Hold at 8.3.1 # until either lib bumps win32 to 6.x.