Skip to content

Commit 46d5340

Browse files
authored
feat: add radio favorites functionality and UI components (#101)
* feat: add radio favorites functionality and UI components * feat: refactor radio favorites list UI and add playing state indication * feat: update PlayerTrackInfo to display title for remote media * feat: update media item creation to use MediaItem constructor for better clarity * feat: enhance PlayerManager to use fallback media art and title for remote sources; update PlayerFullView layout for better responsiveness
1 parent 992c4d2 commit 46d5340

23 files changed

+540
-138
lines changed

lib/common/view/safe_network_image.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class SafeNetworkImage extends StatelessWidget {
2525
this.httpHeaders,
2626
this.errorListener,
2727
this.onImageLoaded,
28+
this.placeholder,
2829
});
2930

3031
final String? url;
@@ -37,6 +38,7 @@ class SafeNetworkImage extends StatelessWidget {
3738
final Map<String, String>? httpHeaders;
3839
final void Function(Object)? errorListener;
3940
final Function(ImageProvider imageProvider)? onImageLoaded;
41+
final Widget Function(BuildContext, String)? placeholder;
4042

4143
@override
4244
Widget build(BuildContext context) {
@@ -77,6 +79,7 @@ class SafeNetworkImage extends StatelessWidget {
7779
httpHeaders: httpHeaders,
7880
cacheManager: Platforms.isLinux ? XdgCacheManager() : null,
7981
imageUrl: url!,
82+
placeholder: placeholder,
8083
imageBuilder: (context, imageProvider) {
8184
onImageLoaded?.call(imageProvider);
8285
return Image(

lib/common/view/ui_constants.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,5 @@ var playerButtonStyle = IconButton.styleFrom(
3939
foregroundColor: Colors.white,
4040
backgroundColor: Colors.transparent,
4141
);
42+
43+
const kDefaultTileLeadingDimension = 40.0;

lib/extensions/media_x.dart

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ extension MediaX on Media {
1616
static final _audioMetadataCache = <String, AudioMetadata?>{};
1717
static final _audioAlbumArtUriCache = <String, Uri?>{};
1818
static final _uuidToMediaCache = <String, Media>{};
19+
static Media? getMediaByUuid(String uuid) => _uuidToMediaCache[uuid];
20+
static void addToCache(Media media) =>
21+
_uuidToMediaCache[media.stationId] = media;
1922
static final _mediaToStationCache = <Media, Station>{};
2023

2124
static Media fromStation(Station station) {
@@ -56,19 +59,30 @@ extension MediaX on Media {
5659
}
5760

5861
String get artist =>
59-
(isLocal ? _localMetadata?.artist : _mediaToStationCache[this]?.name) ??
62+
(isLocal
63+
? _localMetadata?.artist
64+
: '${_mediaToStationCache[this]?.bitrate ?? 0} kbps') ??
6065
'Unknown Artist';
6166

62-
String get album =>
67+
String get album => _localMetadata?.album ?? 'Unknown Album';
68+
69+
String get genre =>
6370
(isLocal
64-
? _localMetadata?.album
65-
: _mediaToStationCache[this]?.tags ?? '') ??
66-
'Unknown Album';
71+
? _localMetadata?.genres.join(', ')
72+
: _mediaToStationCache[this]?.tags) ??
73+
'Unknown Genre';
74+
75+
String? get remoteTagsFull =>
76+
isLocal ? null : genre.split(',').map((e) => e.trim()).join(', ');
77+
78+
String? getRemoteTags(int count) => isLocal
79+
? null
80+
: genre.split(',').map((e) => e.trim()).take(count).join(', ');
6781

6882
String get title =>
6983
(isLocal && _localMetadata?.title != null
7084
? _localMetadata?.title
71-
: _mediaToStationCache[this]?.language) ??
85+
: _mediaToStationCache[this]?.name.toString()) ??
7286
basenameWithoutExtension(uri.toString());
7387

7488
Duration? get duration => _localMetadata?.duration;

lib/l10n/app_en.arb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3155,5 +3155,11 @@
31553155
"favoriteRemoved": "Favorite removed",
31563156
"@favoriteRemoved": {},
31573157
"notSupportedByServer": "Not supported by server",
3158-
"@notSupportedByServer": {}
3158+
"@notSupportedByServer": {},
3159+
"radioStation": "Radio station",
3160+
"@radioStation": {},
3161+
"radioStations": "Radio stations",
3162+
"@radioStations": {},
3163+
"noRadioBrowserConnected": "No Radio Browser connected",
3164+
"@noRadioBrowserConnected": {}
31593165
}

lib/l10n/app_localizations.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4323,6 +4323,24 @@ abstract class AppLocalizations {
43234323
/// In en, this message translates to:
43244324
/// **'Not supported by server'**
43254325
String get notSupportedByServer;
4326+
4327+
/// No description provided for @radioStation.
4328+
///
4329+
/// In en, this message translates to:
4330+
/// **'Radio station'**
4331+
String get radioStation;
4332+
4333+
/// No description provided for @radioStations.
4334+
///
4335+
/// In en, this message translates to:
4336+
/// **'Radio stations'**
4337+
String get radioStations;
4338+
4339+
/// No description provided for @noRadioBrowserConnected.
4340+
///
4341+
/// In en, this message translates to:
4342+
/// **'No Radio Browser connected'**
4343+
String get noRadioBrowserConnected;
43264344
}
43274345

43284346
class _AppLocalizationsDelegate

lib/l10n/app_localizations_de.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2468,4 +2468,13 @@ class AppLocalizationsDe extends AppLocalizations {
24682468

24692469
@override
24702470
String get notSupportedByServer => 'Not supported by server';
2471+
2472+
@override
2473+
String get radioStation => 'Radio station';
2474+
2475+
@override
2476+
String get radioStations => 'Radio stations';
2477+
2478+
@override
2479+
String get noRadioBrowserConnected => 'No Radio Browser connected';
24712480
}

lib/l10n/app_localizations_en.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2468,4 +2468,13 @@ class AppLocalizationsEn extends AppLocalizations {
24682468

24692469
@override
24702470
String get notSupportedByServer => 'Not supported by server';
2471+
2472+
@override
2473+
String get radioStation => 'Radio station';
2474+
2475+
@override
2476+
String get radioStations => 'Radio stations';
2477+
2478+
@override
2479+
String get noRadioBrowserConnected => 'No Radio Browser connected';
24712480
}

lib/l10n/app_localizations_sv.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2482,4 +2482,13 @@ class AppLocalizationsSv extends AppLocalizations {
24822482

24832483
@override
24842484
String get notSupportedByServer => 'Not supported by server';
2485+
2486+
@override
2487+
String get radioStation => 'Radio station';
2488+
2489+
@override
2490+
String get radioStations => 'Radio stations';
2491+
2492+
@override
2493+
String get noRadioBrowserConnected => 'No Radio Browser connected';
24852494
}

lib/player/player_manager.dart

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,20 @@ class PlayerManager extends BaseAudioHandler with SeekHandler {
4646
String? remoteSourceArtUrl,
4747
String? remoteSourceTitle,
4848
}) {
49+
String? remoteMediaFallbackArt;
50+
String? remoteMediaFallbackTitle;
51+
52+
if (currentMedia != null && !currentMedia!.isLocal) {
53+
remoteMediaFallbackArt = mediaItem.value?.artUri?.toString();
54+
remoteMediaFallbackTitle = mediaItem.value?.title;
55+
}
4956
playerViewState.value = playerViewState.value.copyWith(
5057
fullMode: fullMode,
5158
showPlayerExplorer: showPlayerExplorer,
5259
explorerIndex: explorerIndex,
5360
color: color,
54-
remoteSourceArtUrl: remoteSourceArtUrl,
55-
remoteSourceTitle: remoteSourceTitle,
61+
remoteSourceArtUrl: remoteSourceArtUrl ?? remoteMediaFallbackArt,
62+
remoteSourceTitle: remoteSourceTitle ?? remoteMediaFallbackTitle,
5663
);
5764
}
5865

@@ -152,7 +159,10 @@ class PlayerManager extends BaseAudioHandler with SeekHandler {
152159
bool play = true,
153160
}) async {
154161
if (mediaList.isEmpty) return;
155-
updateViewMode(remoteSourceArtUrl: mediaList.firstOrNull?.remoteAlbumArt);
162+
updateViewMode(
163+
remoteSourceArtUrl: mediaList.firstOrNull?.remoteAlbumArt,
164+
remoteSourceTitle: mediaList.firstOrNull?.title,
165+
);
156166
await _player.open(Playlist(mediaList, index: index));
157167
}
158168

lib/player/view/player_album_art.dart

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,7 @@ class PlayerAlbumArt extends StatelessWidget {
4343
return SizedBox(
4444
width: dim,
4545
height: dim,
46-
child: PlayerRemoteSourceImage(
47-
height: dim,
48-
width: dim,
49-
fit: fit,
50-
fallBackIcon: const Icon(YaruIcons.music_note),
51-
errorIcon: const Icon(YaruIcons.music_note),
52-
),
46+
child: PlayerRemoteSourceImage(height: dim, width: dim, fit: fit),
5347
);
5448
}
5549

@@ -63,41 +57,52 @@ class PlayerRemoteSourceImage extends StatelessWidget with WatchItMixin {
6357
required this.height,
6458
required this.width,
6559
required this.fit,
66-
required this.fallBackIcon,
67-
required this.errorIcon,
6860
});
6961

7062
final double height;
7163
final double width;
7264
final BoxFit? fit;
73-
final Widget fallBackIcon;
74-
final Widget errorIcon;
7565

7666
@override
7767
Widget build(BuildContext context) {
78-
final theme = context.theme;
79-
8068
final remoteSourceArtUrl = watchValue(
8169
(PlayerManager p) =>
8270
p.playerViewState.select((e) => e.remoteSourceArtUrl),
8371
);
8472

85-
return Container(
86-
color: theme.cardColor.scale(
87-
lightness: theme.colorScheme.isLight ? -0.15 : 0.3,
73+
final color = watchValue(
74+
(PlayerManager p) => p.playerViewState.select((e) => e.color),
75+
);
76+
77+
return SafeNetworkImage(
78+
placeholder: (context, url) => Center(
79+
child: SizedBox(
80+
width: width * 0.3,
81+
height: width * 0.3,
82+
child: CircularProgressIndicator(
83+
color: color?.withValues(alpha: 0.5),
84+
valueColor: AlwaysStoppedAnimation<Color>(
85+
color ?? getPlayerIconColor(context.theme),
86+
),
87+
),
88+
),
89+
),
90+
onImageLoaded: di<PlayerManager>().setRemoteColorFromImageProvider,
91+
url: remoteSourceArtUrl,
92+
filterQuality: FilterQuality.medium,
93+
fit: fit ?? BoxFit.scaleDown,
94+
fallBackIcon: Icon(
95+
YaruIcons.music_note,
96+
size: width * 0.8,
97+
color: color ?? getPlayerIconColor(context.theme),
98+
),
99+
errorIcon: Icon(
100+
YaruIcons.music_note,
101+
size: width * 0.8,
102+
color: color ?? getPlayerIconColor(context.theme),
88103
),
89104
height: height,
90105
width: width,
91-
child: SafeNetworkImage(
92-
onImageLoaded: di<PlayerManager>().setRemoteColorFromImageProvider,
93-
url: remoteSourceArtUrl,
94-
filterQuality: FilterQuality.medium,
95-
fit: fit ?? BoxFit.scaleDown,
96-
fallBackIcon: fallBackIcon,
97-
errorIcon: errorIcon,
98-
height: height,
99-
width: width,
100-
),
101106
);
102107
}
103108
}

0 commit comments

Comments
 (0)