Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19de0c2874 | |||
| 1dc298c111 | |||
| e605335339 | |||
| 22a4649bfc | |||
| 01a1ac148e | |||
| bc34d96329 | |||
| 8e7660c05e | |||
| eaddb2478a |
@@ -83,19 +83,31 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
# Discover THIS job's Postgres service container via the
|
# Discover THIS job's Postgres service container via the
|
||||||
# mounted docker socket. Scope by the job-name filter so the
|
# mounted docker socket. act_runner attaches the job
|
||||||
# operator's dev compose `minstrel-postgres-*` (same image,
|
# container and its service container(s) to a shared per-job
|
||||||
# same daemon) can never match. Require exactly one — abort
|
# network, so scope discovery to a postgres that sits on a
|
||||||
# loudly otherwise; a wrong target would truncate real data.
|
# network THIS job container is also on. The old
|
||||||
PG_LIST=$(docker ps --filter "name=integration" --filter "ancestor=postgres:16-alpine" --format '{{.ID}} {{.Names}}')
|
# `--filter name=integration` matched EVERY concurrent
|
||||||
echo "candidates: ${PG_LIST:-<none>}"
|
# integration run's postgres (a dev push + the main-merge run
|
||||||
PG_COUNT=$(printf '%s\n' "$PG_LIST" | grep -c . || true)
|
# overlap → 2 candidates → false "expected exactly 1" abort).
|
||||||
test "$PG_COUNT" = "1" || { echo "FATAL: expected exactly 1 postgres service container, got $PG_COUNT"; exit 1; }
|
# The operator's dev compose `minstrel-postgres-*` is never on
|
||||||
PG_ID=$(printf '%s' "$PG_LIST" | awk '{print $1}')
|
# this job's network; skip it explicitly as belt-and-suspenders
|
||||||
PG_NAME=$(printf '%s' "$PG_LIST" | awk '{print $2}')
|
# (a wrong target would truncate real data).
|
||||||
case "$PG_NAME" in
|
SELF=$(cat /etc/hostname)
|
||||||
*minstrel-postgres*|*_postgres_*) echo "FATAL: matched the dev compose container ($PG_NAME), refusing"; exit 1 ;;
|
SELF_NETS=$(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$SELF")
|
||||||
esac
|
test -n "$SELF_NETS"
|
||||||
|
echo "self ($SELF) networks: $SELF_NETS"
|
||||||
|
PG_ID=""
|
||||||
|
PG_NAME=""
|
||||||
|
for cid in $(docker ps --filter "ancestor=postgres:16-alpine" -q); do
|
||||||
|
nm=$(docker inspect -f '{{.Name}}' "$cid" | sed 's#^/##')
|
||||||
|
case "$nm" in *minstrel-postgres*|*_postgres_*) continue ;; esac
|
||||||
|
for net in $(docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$cid"); do
|
||||||
|
case " $SELF_NETS " in *" $net "*) PG_ID="$cid"; PG_NAME="$nm"; break 2 ;; esac
|
||||||
|
done
|
||||||
|
done
|
||||||
|
test -n "$PG_ID" || { echo "FATAL: no postgres service container on this job's network (self nets: $SELF_NETS)"; exit 1; }
|
||||||
|
echo "selected postgres: $PG_ID $PG_NAME"
|
||||||
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG_ID")
|
PG_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$PG_ID")
|
||||||
test -n "$PG_IP"
|
test -n "$PG_IP"
|
||||||
export MINSTREL_TEST_DATABASE_URL="postgres://minstrel:minstrel@${PG_IP}:5432/minstrel_test?sslmode=disable"
|
export MINSTREL_TEST_DATABASE_URL="postgres://minstrel:minstrel@${PG_IP}:5432/minstrel_test?sslmode=disable"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
import 'cache/cache_filler.dart';
|
import 'cache/cache_filler.dart';
|
||||||
import 'cache/metadata_prefetcher.dart';
|
import 'cache/metadata_prefetcher.dart';
|
||||||
@@ -77,6 +80,14 @@ class _MinstrelAppState extends ConsumerState<MinstrelApp> {
|
|||||||
// probe → offlineProvider. Read here to start the poller; S4
|
// probe → offlineProvider. Read here to start the poller; S4
|
||||||
// gates system-playlist play + Shuffle-all on it.
|
// gates system-playlist play + Shuffle-all on it.
|
||||||
ref.read(offlineProvider);
|
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();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -724,31 +724,18 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl
|
|||||||
// with up-to-date shuffleMode/repeatMode fields.
|
// with up-to-date shuffleMode/repeatMode fields.
|
||||||
void _broadcastState(PlaybackEvent? event) {
|
void _broadcastState(PlaybackEvent? event) {
|
||||||
final playing = _player.playing;
|
final playing = _player.playing;
|
||||||
// Favorite control: only when a track is playing and the LikeBridge
|
// No custom favorite MediaControl here. audio_service builds a
|
||||||
// is wired. Icon/label toggle by current like state; tapping it
|
// PlaybackStateCompat.CustomAction for it and throws
|
||||||
// routes to customAction('minstrel.favorite'). Implemented as a
|
// "You must specify an icon resource id to build a CustomAction"
|
||||||
// MediaControl.custom (NOT setRating — that path is broken upstream,
|
// on real builds (the androidIcon doesn't resolve to a usable id),
|
||||||
// audio_service #376, and regressed the Pixel Watch). Kept out of
|
// and that exception aborts the ENTIRE media notification — no
|
||||||
// androidCompactActionIndices so the compact/lock view is unchanged.
|
// tray or Wear controls at all. Removed; like/favorite remains
|
||||||
final favTrackId = mediaItem.value?.id;
|
// available in-app and via the standard lock-screen surface.
|
||||||
final favBridge = _likeBridge;
|
|
||||||
final showFav = favTrackId != null && favBridge != null;
|
|
||||||
final favLiked = favTrackId != null &&
|
|
||||||
favBridge != null &&
|
|
||||||
favBridge.isTrackLiked(favTrackId);
|
|
||||||
playbackState.add(PlaybackState(
|
playbackState.add(PlaybackState(
|
||||||
controls: [
|
controls: [
|
||||||
MediaControl.skipToPrevious,
|
MediaControl.skipToPrevious,
|
||||||
if (playing) MediaControl.pause else MediaControl.play,
|
if (playing) MediaControl.pause else MediaControl.play,
|
||||||
MediaControl.skipToNext,
|
MediaControl.skipToNext,
|
||||||
if (showFav)
|
|
||||||
MediaControl.custom(
|
|
||||||
androidIcon: favLiked
|
|
||||||
? 'drawable/ic_stat_favorite'
|
|
||||||
: 'drawable/ic_stat_favorite_border',
|
|
||||||
label: favLiked ? 'Unfavorite' : 'Favorite',
|
|
||||||
name: 'minstrel.favorite',
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
// androidCompactActionIndices tells the system which controls
|
// androidCompactActionIndices tells the system which controls
|
||||||
// appear in the collapsed/lock-screen view. Without this, some
|
// appear in the collapsed/lock-screen view. Without this, some
|
||||||
|
|||||||
@@ -222,6 +222,14 @@ class _NowPlayingScreenState extends ConsumerState<NowPlayingScreen> {
|
|||||||
_displayedDominant = null;
|
_displayedDominant = null;
|
||||||
_pendingPreloadId = 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: minstrel
|
name: minstrel
|
||||||
description: Minstrel mobile client
|
description: Minstrel mobile client
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 2026.5.19+10
|
version: 2026.5.19+12
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.5.0 <4.0.0'
|
sdk: '>=3.5.0 <4.0.0'
|
||||||
@@ -25,6 +25,9 @@ dependencies:
|
|||||||
# Lucide icon set (design system mandates Lucide, not Material).
|
# Lucide icon set (design system mandates Lucide, not Material).
|
||||||
# Icons exposed as LucideIcons.<snake_case> IconData usable in Icon().
|
# Icons exposed as LucideIcons.<snake_case> IconData usable in Icon().
|
||||||
flutter_lucide: ^1.11.0
|
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
|
google_fonts: ^8.1.0
|
||||||
# 10.x conflicts with flutter_secure_storage 10.x on win32. Hold at 8.3.1
|
# 10.x conflicts with flutter_secure_storage 10.x on win32. Hold at 8.3.1
|
||||||
# until either lib bumps win32 to 6.x.
|
# until either lib bumps win32 to 6.x.
|
||||||
|
|||||||
Reference in New Issue
Block a user