diff --git a/lib/main.dart b/lib/main.dart index 357679ded..665f396f9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,6 +34,7 @@ import 'player/player_service.dart'; import 'podcasts/download_model.dart'; import 'podcasts/podcast_model.dart'; import 'podcasts/podcast_service.dart'; +import 'radio/online_art_model.dart'; import 'radio/online_art_service.dart'; import 'radio/radio_model.dart'; import 'radio/radio_service.dart'; @@ -182,6 +183,12 @@ void registerServicesAndViewModels({ )..init(), dispose: (s) => s.dispose(), ) + ..registerLazySingleton( + () => OnlineArtModel( + onlineArtService: di(), + )..init(), + dispose: (s) => s.dispose(), + ) ..registerLazySingleton( () => AppModel( appVersion: version, @@ -207,7 +214,6 @@ void registerServicesAndViewModels({ ..registerLazySingleton( () => RadioModel( radioService: di(), - onlineArtService: di(), ), dispose: (s) => s.dispose(), ) diff --git a/lib/player/player_model.dart b/lib/player/player_model.dart index dcbec86b9..eb0894870 100644 --- a/lib/player/player_model.dart +++ b/lib/player/player_model.dart @@ -123,6 +123,9 @@ class PlayerModel extends SafeChangeNotifier { int getRadioHistoryLength({String? filter}) => filteredRadioHistory(filter: filter).length; + MpvMetaData? getMetadata(String? icyTitle) => + icyTitle == null ? null : _playerService.radioHistory[icyTitle]; + Iterable> filteredRadioHistory({ required String? filter, }) { diff --git a/lib/player/player_service.dart b/lib/player/player_service.dart index 055074e19..d2c0da2bf 100644 --- a/lib/player/player_service.dart +++ b/lib/player/player_service.dart @@ -81,13 +81,13 @@ class PlayerService { } if (validHistoryElement) { _addRadioHistoryElement( - icyTitle: mpvMetaData!.icyTitle.everyWordCapitalized, + icyTitle: mpvMetaData!.icyTitle, mpvMetaData: mpvMetaData!.copyWith( icyName: audio?.title?.trim() ?? _mpvMetaData?.icyName ?? '', ), ); - await _processParsedIcyTitle(mpvMetaData!.icyTitle.everyWordCapitalized); + await _processParsedIcyTitle(mpvMetaData!.icyTitle); } _propertiesChangedController.add(true); } diff --git a/lib/radio/online_art_model.dart b/lib/radio/online_art_model.dart new file mode 100644 index 000000000..5dc3cebd3 --- /dev/null +++ b/lib/radio/online_art_model.dart @@ -0,0 +1,26 @@ +import 'dart:async'; + +import 'package:safe_change_notifier/safe_change_notifier.dart'; + +import 'online_art_service.dart'; + +class OnlineArtModel extends SafeChangeNotifier { + OnlineArtModel({ + required OnlineArtService onlineArtService, + }) : _onlineArtService = onlineArtService; + + final OnlineArtService _onlineArtService; + StreamSubscription? _propertiesChangedSub; + String? getCover(String icyTitle) => _onlineArtService.get(icyTitle); + + void init() { + _propertiesChangedSub ??= + _onlineArtService.propertiesChanged.listen((_) => notifyListeners()); + } + + @override + Future dispose() async { + await _propertiesChangedSub?.cancel(); + super.dispose(); + } +} diff --git a/lib/radio/online_art_service.dart b/lib/radio/online_art_service.dart index 2e0c3b7f2..0d593138b 100644 --- a/lib/radio/online_art_service.dart +++ b/lib/radio/online_art_service.dart @@ -14,10 +14,14 @@ const _kCoverArtArchiveAddress = 'https://coverartarchive.org/release/'; class OnlineArtService { OnlineArtService({required Dio dio}) : _dio = dio; final Dio _dio; + final _propertiesChangedController = StreamController.broadcast(); + Stream get propertiesChanged => _propertiesChangedController.stream; + final _errorController = StreamController.broadcast(); + Stream get error => _errorController.stream; Future fetchAlbumArt(String icyTitle) async { _errorController.add(null); - return get(icyTitle) ?? + final albumArtUrl = get(icyTitle) ?? put( key: icyTitle, url: await compute( @@ -29,23 +33,24 @@ class OnlineArtService { return null; }), ); + _propertiesChangedController.add(true); + + return albumArtUrl; } - final _value = {}; + final _store = {}; String? put({required String key, String? url}) { - return _value.containsKey(key) - ? _value.update(key, (value) => url) - : _value.putIfAbsent(key, () => url); + return _store.containsKey(key) + ? _store.update(key, (value) => url) + : _store.putIfAbsent(key, () => url); } - String? get(String? icyTitle) => icyTitle == null ? null : _value[icyTitle]; - - final _errorController = StreamController.broadcast(); - Stream get error => _errorController.stream; + String? get(String? icyTitle) => icyTitle == null ? null : _store[icyTitle]; Future dispose() async { await _errorController.close(); + await _propertiesChangedController.close(); } } diff --git a/lib/radio/radio_model.dart b/lib/radio/radio_model.dart index 6f34e430d..189fa0b4b 100644 --- a/lib/radio/radio_model.dart +++ b/lib/radio/radio_model.dart @@ -4,20 +4,13 @@ import 'package:safe_change_notifier/safe_change_notifier.dart'; import '../common/data/audio.dart'; import '../l10n/l10n.dart'; -import 'online_art_service.dart'; import 'radio_service.dart'; class RadioModel extends SafeChangeNotifier { final RadioService _radioService; - final OnlineArtService _onlineArtService; - RadioModel({ - required RadioService radioService, - required OnlineArtService onlineArtService, - }) : _radioService = radioService, - _onlineArtService = onlineArtService; - - String? getCover(String icyTitle) => _onlineArtService.get(icyTitle); + RadioModel({required RadioService radioService}) + : _radioService = radioService; Future clickStation(Audio? station) async { if (station?.description != null) { diff --git a/lib/radio/view/icy_image.dart b/lib/radio/view/icy_image.dart index 119ccac65..ddd788d95 100644 --- a/lib/radio/view/icy_image.dart +++ b/lib/radio/view/icy_image.dart @@ -1,19 +1,18 @@ import 'package:flutter/material.dart'; import 'package:watch_it/watch_it.dart'; -import '../../common/data/mpv_meta_data.dart'; import '../../common/view/icons.dart'; import '../../common/view/mpv_metadata_dialog.dart'; import '../../common/view/safe_network_image.dart'; import '../../constants.dart'; import '../../l10n/l10n.dart'; import '../../player/player_model.dart'; -import '../radio_model.dart'; +import '../online_art_model.dart'; class IcyImage extends StatelessWidget with WatchItMixin { const IcyImage({ super.key, - required this.mpvMetaData, + required this.icyTitle, this.height = kAudioTrackWidth, this.width = kAudioTrackWidth, this.borderRadius, @@ -21,7 +20,7 @@ class IcyImage extends StatelessWidget with WatchItMixin { this.fit, }); - final MpvMetaData mpvMetaData; + final String? icyTitle; final double height, width; final BorderRadius? borderRadius; @@ -31,9 +30,12 @@ class IcyImage extends StatelessWidget with WatchItMixin { @override Widget build(BuildContext context) { final bR = borderRadius ?? BorderRadius.circular(4); - watchPropertyValue( - (PlayerModel m) => m.remoteImageUrl, - ); + final imageUrl = icyTitle == null + ? null + : watchPropertyValue( + (OnlineArtModel m) => m.getCover(icyTitle!), + ); + // watchPropertyValue((PlayerModel m) => m.remoteImageUrl); return Tooltip( message: context.l10n.metadata, @@ -41,18 +43,22 @@ class IcyImage extends StatelessWidget with WatchItMixin { borderRadius: bR, child: InkWell( borderRadius: bR, - onTap: () => showDialog( - context: context, - builder: (context) => MpvMetadataDialog( - image: di().getCover(mpvMetaData.icyTitle), - mpvMetaData: mpvMetaData, - ), - ), + onTap: () { + final metadata = di().getMetadata(icyTitle); + if (metadata == null) return; + showDialog( + context: context, + builder: (context) => MpvMetadataDialog( + image: imageUrl, + mpvMetaData: metadata, + ), + ); + }, child: SizedBox( height: height, width: width, child: SafeNetworkImage( - url: di().getCover(mpvMetaData.icyTitle), + url: imageUrl, fallBackIcon: fallBackWidget ?? Icon(Iconz.radio), filterQuality: FilterQuality.medium, fit: fit ?? BoxFit.fitHeight, diff --git a/lib/radio/view/radio_history_list.dart b/lib/radio/view/radio_history_list.dart index 9fcd14cf3..522b44658 100644 --- a/lib/radio/view/radio_history_list.dart +++ b/lib/radio/view/radio_history_list.dart @@ -53,12 +53,12 @@ class RadioHistoryList extends StatelessWidget with WatchItMixin { .elementAt(reversedIndex); return simpleList ? RadioHistoryTile.simple( - entry: e, + icyTitle: e.key, selected: current?.icyTitle != null && current?.icyTitle == e.value.icyTitle, ) : RadioHistoryTile( - entry: e, + icyTitle: e.key, selected: current?.icyTitle != null && current?.icyTitle == e.value.icyTitle, ); @@ -77,12 +77,14 @@ class SliverRadioHistoryList extends StatelessWidget with WatchItMixin { this.emptyMessage, this.padding, this.emptyIcon, + required this.allowNavigation, }); final String? filter; final Widget? emptyMessage; final Widget? emptyIcon; final EdgeInsetsGeometry? padding; + final bool allowNavigation; @override Widget build(BuildContext context) { @@ -109,9 +111,10 @@ class SliverRadioHistoryList extends StatelessWidget with WatchItMixin { .filteredRadioHistory(filter: filter) .elementAt(reversedIndex); return RadioHistoryTile( - entry: e, + icyTitle: e.key, selected: current?.icyTitle != null && current?.icyTitle == e.value.icyTitle, + allowNavigation: allowNavigation, ); }, childCount: length, diff --git a/lib/radio/view/radio_history_tile.dart b/lib/radio/view/radio_history_tile.dart index 588c8ad84..10d41dc8f 100644 --- a/lib/radio/view/radio_history_tile.dart +++ b/lib/radio/view/radio_history_tile.dart @@ -4,7 +4,6 @@ import 'package:yaru/constants.dart'; import 'package:yaru/yaru.dart'; import '../../common/data/audio.dart'; -import '../../common/data/mpv_meta_data.dart'; import '../../common/view/copy_clipboard_content.dart'; import '../../common/view/icons.dart'; import '../../common/view/mpv_metadata_dialog.dart'; @@ -15,7 +14,9 @@ import '../../extensions/build_context_x.dart'; import '../../extensions/theme_data_x.dart'; import '../../l10n/l10n.dart'; import '../../library/library_model.dart'; +import '../../player/player_model.dart'; import '../../search/search_model.dart'; +import '../online_art_model.dart'; import '../radio_model.dart'; import 'icy_image.dart'; import 'station_page.dart'; @@ -25,82 +26,86 @@ enum _RadioHistoryTileVariant { regular, simple } class RadioHistoryTile extends StatelessWidget { const RadioHistoryTile({ super.key, - required this.entry, + required this.icyTitle, required this.selected, + this.allowNavigation = true, }) : _variant = _RadioHistoryTileVariant.regular; const RadioHistoryTile.simple({ super.key, - required this.entry, + required this.icyTitle, required this.selected, + this.allowNavigation = false, }) : _variant = _RadioHistoryTileVariant.simple; final _RadioHistoryTileVariant _variant; - final MapEntry entry; + final String icyTitle; final bool selected; + final bool allowNavigation; @override Widget build(BuildContext context) { + final icyName = di().getMetadata(icyTitle)?.icyName; return switch (_variant) { _RadioHistoryTileVariant.simple => _SimpleRadioHistoryTile( - key: ValueKey(entry.value.icyTitle), - entry: entry, + key: ValueKey(icyTitle), + icyTitle: icyTitle, selected: selected, ), _RadioHistoryTileVariant.regular => ListTile( - key: ValueKey(entry.value.icyTitle), + key: ValueKey(icyTitle), selected: selected, selectedColor: context.theme.contrastyPrimary, contentPadding: const EdgeInsets.symmetric(horizontal: kYaruPagePadding), leading: IcyImage( - key: ValueKey(entry.value.icyTitle), + key: ValueKey(icyTitle), height: yaruStyled ? 34 : 40, width: yaruStyled ? 34 : 40, - mpvMetaData: entry.value, + icyTitle: icyTitle, ), trailing: IconButton( tooltip: context.l10n.metadata, - onPressed: () => showDialog( - context: context, - builder: (context) { - final image = di().getCover(entry.value.icyTitle); - return MpvMetadataDialog( - mpvMetaData: entry.value, + onPressed: () { + final image = di().getCover(icyTitle); + final metadata = di().getMetadata(icyTitle); + if (metadata == null) return; + showDialog( + context: context, + builder: (context) => MpvMetadataDialog( + mpvMetaData: metadata, image: image, - ); - }, - ), - icon: Icon( - Iconz.info, - ), + ), + ); + }, + icon: Icon(Iconz.info), ), title: TapAbleText( overflow: TextOverflow.visible, maxLines: 10, - text: entry.value.icyTitle, + text: icyTitle, onTap: () => showSnackBar( context: context, - content: CopyClipboardContent(text: entry.value.icyTitle), + content: CopyClipboardContent(text: icyTitle), ), ), subtitle: TapAbleText( - text: entry.value.icyName, - onTap: () async { - final libraryModel = di(); - if (libraryModel.selectedPageId == entry.value.icyUrl) return; - - await di().init(); - di().radioNameSearch(entry.value.icyName).then((v) { - if (v?.firstOrNull?.stationUUID != null) { - libraryModel.push( - builder: (_) => - StationPage(station: Audio.fromStation(v.first)), - pageId: v!.first.stationUUID, - ); - } - }); - }, + text: icyName ?? context.l10n.station, + onTap: !allowNavigation || icyName == null + ? null + : () async { + await di().init(); + di().radioNameSearch(icyName).then((v) { + if (v?.firstOrNull?.stationUUID != null) { + di().push( + builder: (_) => StationPage( + station: Audio.fromStation(v.first), + ), + pageId: v!.first.stationUUID, + ); + } + }); + }, ), ) }; @@ -110,17 +115,16 @@ class RadioHistoryTile extends StatelessWidget { class _SimpleRadioHistoryTile extends StatelessWidget { const _SimpleRadioHistoryTile({ super.key, - required this.entry, + required this.icyTitle, required this.selected, }); - final MapEntry entry; + final String icyTitle; final bool selected; @override Widget build(BuildContext context) { return ListTile( - key: ValueKey(entry.value.icyTitle), selected: selected, selectedColor: context.theme.colorScheme.onSurface, contentPadding: const EdgeInsets.symmetric(horizontal: kYaruPagePadding), @@ -135,14 +139,15 @@ class _SimpleRadioHistoryTile extends StatelessWidget { title: TapAbleText( overflow: TextOverflow.visible, maxLines: 10, - text: entry.value.icyTitle, + text: icyTitle, onTap: () => showSnackBar( context: context, - content: CopyClipboardContent(text: entry.value.icyTitle), + content: CopyClipboardContent(text: icyTitle), ), ), subtitle: Text( - entry.value.icyName, + di().getMetadata(icyTitle)?.icyName ?? + context.l10n.station, ), ); } diff --git a/lib/radio/view/station_page.dart b/lib/radio/view/station_page.dart index 406b6b3a1..f14b0f25e 100644 --- a/lib/radio/view/station_page.dart +++ b/lib/radio/view/station_page.dart @@ -106,6 +106,7 @@ class StationPage extends StatelessWidget with WatchItMixin { emptyMessage: const SizedBox.shrink(), emptyIcon: const SizedBox.shrink(), padding: radioHistoryListPadding, + allowNavigation: false, ), ), ], diff --git a/needs_translation.json b/needs_translation.json index 7e8748e9e..fac5080ad 100644 --- a/needs_translation.json +++ b/needs_translation.json @@ -573,21 +573,6 @@ "regionZimbabwe" ], - "es": [ - "downloadsDirectory", - "downloadsDirectoryDescription", - "downloadsChangeWarning", - "disconnectedFrom", - "useMoreAnimationsTitle", - "useMoreAnimationsDescription", - "onlineArtError", - "clicks", - "exposeOnlineHeadline", - "exposeToDiscordTitle", - "exposeToDiscordSubTitle", - "featureDisabledOnPlatform" - ], - "fr": [ "local", "downloadsDirectory",