diff --git a/CHANGELOG.md b/CHANGELOG.md index 516d79062..bf2dca6c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - **Calmer motion and chrome** — the staggered entrance skips to the final frame under reduced motion, decorative art is excluded from the accessibility tree, and route-scoped system-bar styling keeps icon contrast correct after light screens are popped. ### Settings & Navigation +- **Online artist and album information toggle** — Settings → Integrations → Metadata can hide Apple Music biographies, similar artists, top songs, album notes, and new artist-portrait downloads on artist and album pages. The preference is independent of post-scan album identification, and existing saved portraits, local descriptions, and library artwork stay untouched. - **Bottom Bar Width** — a new 70–100% slider in Settings → Interface → Bottom Bar scales the navigation bar inward from its usual margins and centers it, while keeping each item at least 44 dp; reset returns to full width. - **Mini player as its own card** — the mini player now always sits above the frosted navigation bar instead of merging into it, aligns to the standard bar width, and keeps its gradient, border, and shadow in separated and collapsed layouts. diff --git a/lib/features/albums/screens/album_detail_screen.dart b/lib/features/albums/screens/album_detail_screen.dart index 2d3a67621..e46a15d04 100644 --- a/lib/features/albums/screens/album_detail_screen.dart +++ b/lib/features/albums/screens/album_detail_screen.dart @@ -367,11 +367,15 @@ class _AlbumDetailScreenState extends ConsumerState } void _showMore() { - final appleMusic = ref - .read( - appleMusicAlbumProvider((widget.albumName, widget.albumArtist)), - ) - .value; + final pageInfoEnabled = + ref.read(appleMusicPageInfoEnabledProvider).value ?? true; + final appleMusic = pageInfoEnabled + ? ref + .read( + appleMusicAlbumProvider((widget.albumName, widget.albumArtist)), + ) + .value + : null; DetailMoreSheet.show( context, items: [ @@ -397,17 +401,18 @@ class _AlbumDetailScreenState extends ConsumerState initialAlbum: widget.albumName, ), ), - if (appleMusic != null) + if (pageInfoEnabled && appleMusic != null) DetailMoreSheetItem( icon: LucideIcons.externalLink, label: l10n.englishForFilipino.openInAppleMusic, onTap: () => _openAppleMusic(_albumAppleMusicUri(appleMusic)), ), - DetailMoreSheetItem( - icon: LucideIcons.refreshCw, - label: l10n.englishForFilipino.refreshAppleMusic, - onTap: _refreshAppleMusic, - ), + if (pageInfoEnabled) + DetailMoreSheetItem( + icon: LucideIcons.refreshCw, + label: l10n.englishForFilipino.refreshAppleMusic, + onTap: _refreshAppleMusic, + ), DetailMoreSheetItem( icon: LucideIcons.moonStar, label: l10n.englishForFilipino.sleepTimer, @@ -502,11 +507,14 @@ class _AlbumDetailScreenState extends ConsumerState builder: (context, animatedBg, _) { final resolvedBg = animatedBg ?? AppColors.background; final prefs = ref.watch(appPreferencesProvider); - final appleMusic = ref + final pageInfoEnabled = + ref.watch(appleMusicPageInfoEnabledProvider).value ?? true; + final remoteAlbum = ref .watch( appleMusicAlbumProvider((widget.albumName, widget.albumArtist)), ) .value; + final appleMusic = pageInfoEnabled ? remoteAlbum : null; final albumNotes = appleMusic?.notes?.trim(); return Stack( children: [ diff --git a/lib/features/artists/screens/artist_detail_screen.dart b/lib/features/artists/screens/artist_detail_screen.dart index 0f6404fc4..b0152ac72 100644 --- a/lib/features/artists/screens/artist_detail_screen.dart +++ b/lib/features/artists/screens/artist_detail_screen.dart @@ -155,21 +155,30 @@ class _ArtistDetailScreenState extends ConsumerState await _applyAppleMusicArtistArt(); } + Future _pageInfoEnabled() async { + final cached = ref.read(appleMusicPageInfoEnabledProvider).value; + if (cached != null) return cached; + return ref.read(appleMusicSettingsProvider).pageInfoEnabled(); + } + /// Last-resort artist image: the Apple Music page image, downloaded once and /// saved to the artist repository so it stays offline afterwards. Future _applyAppleMusicArtistArt() async { if (_artistArt != null && _artistArt!.isNotEmpty) return; + if (!await _pageInfoEnabled() || !mounted) return; final data = await ref.read( appleMusicArtistProvider(widget.artistName).future, ); final imageUrl = data?.info?.imageUrl; - if (!mounted || imageUrl == null || imageUrl.isEmpty) return; + if (!await _pageInfoEnabled() || !mounted) return; + if (imageUrl == null || imageUrl.isEmpty) return; - final bytes = await AppleMusicMetadataService.instance.fetchArtworkBytes( - imageUrl, - ); - if (!mounted || bytes == null) return; + final bytes = await ref + .read(appleMusicMetadataServiceProvider) + .fetchArtworkBytes(imageUrl); + // The toggle may have been switched off while the portrait was in flight. + if (!await _pageInfoEnabled() || !mounted || bytes == null) return; try { final support = await getApplicationSupportDirectory(); @@ -178,7 +187,7 @@ class _ArtistDetailScreenState extends ConsumerState final name = widget.artistName.hashCode.toRadixString(16); final file = File('${dir.path}/$name.jpg'); await file.writeAsBytes(bytes); - if (!mounted) return; + if (!await _pageInfoEnabled() || !mounted) return; setState(() { _artistArt = file.path; _artistArtSourcePath = null; @@ -426,9 +435,11 @@ class _ArtistDetailScreenState extends ConsumerState } void _showMore() { - final appleMusic = ref - .read(appleMusicArtistProvider(widget.artistName)) - .value; + final pageInfoEnabled = + ref.read(appleMusicPageInfoEnabledProvider).value ?? true; + final appleMusic = pageInfoEnabled + ? ref.read(appleMusicArtistProvider(widget.artistName)).value + : null; DetailMoreSheet.show( context, items: [ @@ -444,17 +455,18 @@ class _ArtistDetailScreenState extends ConsumerState label: l10n.englishForFilipino.addToPlaylist, onTap: () => AddToPlaylistSheet.showSongs(context, widget.songs), ), - if (appleMusic != null) + if (pageInfoEnabled && appleMusic != null) DetailMoreSheetItem( icon: LucideIcons.externalLink, label: l10n.englishForFilipino.openInAppleMusic, onTap: () => _openAppleMusic(_artistAppleMusicUri(appleMusic)), ), - DetailMoreSheetItem( - icon: LucideIcons.refreshCw, - label: l10n.englishForFilipino.refreshAppleMusic, - onTap: _refreshAppleMusic, - ), + if (pageInfoEnabled) + DetailMoreSheetItem( + icon: LucideIcons.refreshCw, + label: l10n.englishForFilipino.refreshAppleMusic, + onTap: _refreshAppleMusic, + ), DetailMoreSheetItem( icon: LucideIcons.moonStar, label: l10n.englishForFilipino.sleepTimer, @@ -588,9 +600,12 @@ class _ArtistDetailScreenState extends ConsumerState builder: (context, animatedBg, _) { final resolvedBg = animatedBg ?? AppColors.background; final prefs = ref.watch(appPreferencesProvider); - final appleMusic = ref + final pageInfoEnabled = + ref.watch(appleMusicPageInfoEnabledProvider).value ?? true; + final remoteArtist = ref .watch(appleMusicArtistProvider(widget.artistName)) .value; + final appleMusic = pageInfoEnabled ? remoteArtist : null; final appleBio = appleMusic?.info?.biography?.trim(); final similarArtists = appleMusic?.info?.similarArtists ?? diff --git a/lib/features/settings/data/settings_search_catalog.dart b/lib/features/settings/data/settings_search_catalog.dart index b0cf5d8ab..1485b4371 100644 --- a/lib/features/settings/data/settings_search_catalog.dart +++ b/lib/features/settings/data/settings_search_catalog.dart @@ -413,9 +413,17 @@ List buildSettingsSearchCatalog() { ('lastFm', l10n.lastFm), ('listenbrainz', l10n.listenbrainz), ], keywords: 'scrobble scrobbling last.fm listenbrainz login'), - _section('metadata', l10n.metadata, [ - ('autoIdentifyUntaggedAlbums', l10n.autoIdentifyUntaggedAlbums), - ], keywords: 'apple music album identification tags'), + _section( + 'metadata', + l10n.metadata, + [ + ('onlineArtistAlbumInformation', l10n.onlineArtistAlbumInformation), + ('autoIdentifyUntaggedAlbums', l10n.autoIdentifyUntaggedAlbums), + ], + keywords: + 'apple music artist information biography similar artists ' + 'top songs album notes portrait identification tags', + ), ], ), for (var tab = 0; tab < 4; tab++) diff --git a/lib/features/settings/widgets/apple_music_settings_tile.dart b/lib/features/settings/widgets/apple_music_settings_tile.dart index e5784fc82..c05f5e516 100644 --- a/lib/features/settings/widgets/apple_music_settings_tile.dart +++ b/lib/features/settings/widgets/apple_music_settings_tile.dart @@ -1,19 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flick/features/settings/widgets/settings_widgets.dart'; +import 'package:flick/providers/apple_music_provider.dart'; import 'package:flick/services/apple_music/apple_music_settings.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import 'package:flick/l10n/settings_l10n.dart'; -class AppleMusicSettingsTile extends StatefulWidget { +class AppleMusicSettingsTile extends ConsumerStatefulWidget { const AppleMusicSettingsTile({super.key}); @override - State createState() => _AppleMusicSettingsTileState(); + ConsumerState createState() => + _AppleMusicSettingsTileState(); } -class _AppleMusicSettingsTileState extends State { +class _AppleMusicSettingsTileState + extends ConsumerState { final _settings = AppleMusicSettings(); - bool _enabled = false; + bool _autoEnrichEnabled = false; @override void initState() { @@ -24,23 +28,42 @@ class _AppleMusicSettingsTileState extends State { Future _load() async { final enabled = await _settings.autoEnrichEnabled(); if (!mounted) return; - setState(() => _enabled = enabled); + setState(() => _autoEnrichEnabled = enabled); } - Future _toggle(bool value) async { - setState(() => _enabled = value); + Future _toggleAutoEnrich(bool value) async { + setState(() => _autoEnrichEnabled = value); await _settings.setAutoEnrichEnabled(value); } + Future _togglePageInfo(bool value) { + return ref + .read(appleMusicPageInfoEnabledProvider.notifier) + .setEnabled(value); + } + @override Widget build(BuildContext context) { - return ToggleSetting( - icon: LucideIcons.sparkles, - title: l10n.englishForFilipino.autoIdentifyUntaggedAlbums, - subtitle: - l10n.afterScanningMatchFilesWithMissing, - value: _enabled, - onChanged: _toggle, + final pageInfoEnabled = + ref.watch(appleMusicPageInfoEnabledProvider).value ?? true; + return Column( + children: [ + ToggleSetting( + icon: LucideIcons.globe, + title: l10n.englishForFilipino.onlineArtistAlbumInformation, + subtitle: l10n.showAppleMusicBiographiesRecommendationsAnd, + value: pageInfoEnabled, + onChanged: _togglePageInfo, + ), + const SettingsDivider(), + ToggleSetting( + icon: LucideIcons.sparkles, + title: l10n.englishForFilipino.autoIdentifyUntaggedAlbums, + subtitle: l10n.afterScanningMatchFilesWithMissing, + value: _autoEnrichEnabled, + onChanged: _toggleAutoEnrich, + ), + ], ); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 34c1f36be..216340a7f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -7079,6 +7079,10 @@ "@onboardingWelcomeTitle": { "description": "lib/features/onboarding/screens/onboarding_screen.dart:30" }, + "onlineArtistAlbumInformation": "Online artist & album information", + "@onlineArtistAlbumInformation": { + "description": "lib/features/settings/widgets/apple_music_settings_tile.dart" + }, "onlineResults": "Online Results", "@onlineResults": { "description": "lib/features/songs/widgets/album_art_picker_bottom_sheet.dart:365" @@ -9062,6 +9066,10 @@ "@showAlbumArtworkOnTheMini": { "description": "lib/features/settings/screens/widget_settings_screen.dart:201" }, + "showAppleMusicBiographiesRecommendationsAnd": "Show Apple Music biographies, similar artists, top songs, album notes, and artist portraits.", + "@showAppleMusicBiographiesRecommendationsAnd": { + "description": "lib/features/settings/widgets/apple_music_settings_tile.dart" + }, "showArtist": "Show artist", "@showArtist": { "description": "lib/features/player/widgets/player_layout_sheet.dart:256" diff --git a/lib/l10n/app_tl.arb b/lib/l10n/app_tl.arb index 972f210de..ea7bcca8d 100644 --- a/lib/l10n/app_tl.arb +++ b/lib/l10n/app_tl.arb @@ -1366,6 +1366,7 @@ "onboardingReadyTitle": "Handa\nka na", "onboardingWelcomeBody": "Maganda at mabilis na music player\npara sa lokal mong aklatan.\nWalang ads. Walang tracking. Musika lang.", "onboardingWelcomeTitle": "Kumusta! Greetings from Flick!", + "onlineArtistAlbumInformation": "Impormasyon sa artist at album online", "onlineResults": "Mga resulta online", "onlyLibraryEntriesAreRemovedYour": "Mga tala lamang sa aklatan ang aalisin — mananatili sa imbakan ang iyong mga audio file.", "onlyReReadFilesThatAre": "Basahin lamang muli ang mga bago o nabagong file.", @@ -1801,6 +1802,7 @@ "showAlbum2": "Ipakita ang album", "showAlbumArtwork": "Ipakita ang larawan ng album.", "showAlbumArtworkOnTheMini": "Ipakita ang larawan ng album sa mini player", + "showAppleMusicBiographiesRecommendationsAnd": "Ipakita ang talambuhay, katulad na artist, nangungunang kanta, tala ng album, at larawan ng artist mula sa Apple Music.", "showArtist": "Ipakita ang kumanta", "showArtist2": "Ipakita ang kumanta", "showArtistBelowSongTitle": "Ipakita ang kumanta sa ilalim ng pamagat ng kanta", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index ef82a72f4..1838d83f9 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1519,6 +1519,7 @@ "onboardingReadyTitle": "一切\n就绪", "onboardingWelcomeBody": "一款为本地音乐库打造的\n漂亮、快速播放器。\n无广告,无追踪,只有音乐。", "onboardingWelcomeTitle": "欢迎使用\nFlick 播放器", + "onlineArtistAlbumInformation": "在线歌手与专辑信息", "onlineResults": "在线结果", "onlyLibraryEntriesAreRemovedYour": "仅移除音乐库条目——音频文件仍保留在磁盘上。", "onlyReReadFilesThatAre": "只重新读取新增或有改动的文件。", @@ -1951,6 +1952,7 @@ "showAlbum2": "显示专辑", "showAlbumArtwork": "显示专辑封面。", "showAlbumArtworkOnTheMini": "在迷你播放器上显示专辑封面", + "showAppleMusicBiographiesRecommendationsAnd": "显示 Apple Music 的简介、相似歌手、热门歌曲、专辑介绍和歌手照片。", "showArtist": "显示艺术家", "showArtist2": "显示艺术家", "showArtistBelowSongTitle": "在歌曲标题下显示艺术家", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index fdf2731ea..52da05d92 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -9166,6 +9166,12 @@ abstract class AppLocalizations { /// **'Welcome to\nFlick Player'** String get onboardingWelcomeTitle; + /// lib/features/settings/widgets/apple_music_settings_tile.dart + /// + /// In en, this message translates to: + /// **'Online artist & album information'** + String get onlineArtistAlbumInformation; + /// lib/features/songs/widgets/album_art_picker_bottom_sheet.dart:365 /// /// In en, this message translates to: @@ -11758,6 +11764,12 @@ abstract class AppLocalizations { /// **'Show album artwork on the mini player'** String get showAlbumArtworkOnTheMini; + /// lib/features/settings/widgets/apple_music_settings_tile.dart + /// + /// In en, this message translates to: + /// **'Show Apple Music biographies, similar artists, top songs, album notes, and artist portraits.'** + String get showAppleMusicBiographiesRecommendationsAnd; + /// lib/features/player/widgets/player_layout_sheet.dart:256 /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index b5209e94a..d8cc2c473 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -5362,6 +5362,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get onboardingWelcomeTitle => 'Welcome to\nFlick Player'; + @override + String get onlineArtistAlbumInformation => + 'Online artist & album information'; + @override String get onlineResults => 'Online Results'; @@ -6880,6 +6884,10 @@ class AppLocalizationsEn extends AppLocalizations { String get showAlbumArtworkOnTheMini => 'Show album artwork on the mini player'; + @override + String get showAppleMusicBiographiesRecommendationsAnd => + 'Show Apple Music biographies, similar artists, top songs, album notes, and artist portraits.'; + @override String get showArtist => 'Show artist'; diff --git a/lib/l10n/generated/app_localizations_tl.dart b/lib/l10n/generated/app_localizations_tl.dart index a79078d26..91b2a9bca 100644 --- a/lib/l10n/generated/app_localizations_tl.dart +++ b/lib/l10n/generated/app_localizations_tl.dart @@ -5419,6 +5419,10 @@ class AppLocalizationsTl extends AppLocalizations { @override String get onboardingWelcomeTitle => 'Kumusta! Greetings from Flick!'; + @override + String get onlineArtistAlbumInformation => + 'Impormasyon sa artist at album online'; + @override String get onlineResults => 'Mga resulta online'; @@ -6958,6 +6962,10 @@ class AppLocalizationsTl extends AppLocalizations { String get showAlbumArtworkOnTheMini => 'Ipakita ang larawan ng album sa mini player'; + @override + String get showAppleMusicBiographiesRecommendationsAnd => + 'Ipakita ang talambuhay, katulad na artist, nangungunang kanta, tala ng album, at larawan ng artist mula sa Apple Music.'; + @override String get showArtist => 'Ipakita ang kumanta'; diff --git a/lib/l10n/generated/app_localizations_zh.dart b/lib/l10n/generated/app_localizations_zh.dart index 5023d9c87..491e52691 100644 --- a/lib/l10n/generated/app_localizations_zh.dart +++ b/lib/l10n/generated/app_localizations_zh.dart @@ -5009,6 +5009,9 @@ class AppLocalizationsZh extends AppLocalizations { @override String get onboardingWelcomeTitle => '欢迎使用\nFlick 播放器'; + @override + String get onlineArtistAlbumInformation => '在线歌手与专辑信息'; + @override String get onlineResults => '在线结果'; @@ -6411,6 +6414,10 @@ class AppLocalizationsZh extends AppLocalizations { @override String get showAlbumArtworkOnTheMini => '在迷你播放器上显示专辑封面'; + @override + String get showAppleMusicBiographiesRecommendationsAnd => + '显示 Apple Music 的简介、相似歌手、热门歌曲、专辑介绍和歌手照片。'; + @override String get showArtist => '显示艺术家'; diff --git a/lib/providers/apple_music_provider.dart b/lib/providers/apple_music_provider.dart index 91415dd26..98663cfc3 100644 --- a/lib/providers/apple_music_provider.dart +++ b/lib/providers/apple_music_provider.dart @@ -1,6 +1,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:flick/services/apple_music/apple_music_metadata_service.dart'; +import 'package:flick/services/apple_music/apple_music_settings.dart'; part 'apple_music_provider.g.dart'; @@ -9,18 +10,57 @@ AppleMusicMetadataService appleMusicMetadataService(Ref ref) { return AppleMusicMetadataService.instance; } +@Riverpod(keepAlive: true) +AppleMusicSettings appleMusicSettings(Ref ref) { + return AppleMusicSettings(); +} + +/// Whether artist and album pages may load Apple Music page information. +/// Keep-alive so the persisted preference is read once and shared by every +/// open detail page and the settings toggle. +@Riverpod(keepAlive: true) +class AppleMusicPageInfoEnabledNotifier + extends _$AppleMusicPageInfoEnabledNotifier { + @override + Future build() { + return ref.watch(appleMusicSettingsProvider).pageInfoEnabled(); + } + + Future setEnabled(bool value) async { + await ref.read(appleMusicSettingsProvider).setPageInfoEnabled(value); + if (!ref.mounted) return; + state = AsyncData(value); + } +} + /// Apple Music metadata for an artist page. Auto-disposes when the page /// closes; durable caching lives in [AppleMusicMetadataService]. @riverpod class AppleMusicArtistNotifier extends _$AppleMusicArtistNotifier { @override - Future build(String artistName) => _load(); + Future build(String artistName) async { + if (!await ref.watch(appleMusicPageInfoEnabledProvider.future)) { + return null; + } + return _load(); + } /// Reloads from Apple Music. Returns true when fresh data arrived; on /// failure the previous value is kept so the page does not blank out. Future refresh() async { + if (!await _pageInfoEnabled()) { + _clear(); + return false; + } final previous = state; final next = await AsyncValue.guard(() => _load(refresh: true)); + if (!ref.mounted) return false; + // The user may switch page information off while the request is in + // flight. Do not let its late result restore hidden content. + if (!await _pageInfoEnabled()) { + _clear(); + return false; + } if ((next.hasError || next.value == null) && previous.hasValue) { state = previous; return false; @@ -29,6 +69,14 @@ class AppleMusicArtistNotifier extends _$AppleMusicArtistNotifier { return next.value != null; } + Future _pageInfoEnabled() { + return ref.read(appleMusicSettingsProvider).pageInfoEnabled(); + } + + void _clear() { + if (ref.mounted) state = const AsyncData(null); + } + Future _load({bool refresh = false}) { return ref .read(appleMusicMetadataServiceProvider) @@ -40,7 +88,10 @@ class AppleMusicArtistNotifier extends _$AppleMusicArtistNotifier { @riverpod class AppleMusicAlbumNotifier extends _$AppleMusicAlbumNotifier { @override - Future build((String, String) key) { + Future build((String, String) key) async { + if (!await ref.watch(appleMusicPageInfoEnabledProvider.future)) { + return null; + } final (album, artist) = key; return _load(album: album, artist: artist); } @@ -48,11 +99,22 @@ class AppleMusicAlbumNotifier extends _$AppleMusicAlbumNotifier { /// Reloads from Apple Music. Returns true when fresh data arrived; on /// failure the previous value is kept so the page does not blank out. Future refresh() async { + if (!await _pageInfoEnabled()) { + _clear(); + return false; + } final previous = state; final (album, artist) = key; final next = await AsyncValue.guard( () => _load(album: album, artist: artist, refresh: true), ); + if (!ref.mounted) return false; + // The user may switch page information off while the request is in + // flight. Do not let its late result restore hidden content. + if (!await _pageInfoEnabled()) { + _clear(); + return false; + } if ((next.hasError || next.value == null) && previous.hasValue) { state = previous; return false; @@ -61,6 +123,14 @@ class AppleMusicAlbumNotifier extends _$AppleMusicAlbumNotifier { return next.value != null; } + Future _pageInfoEnabled() { + return ref.read(appleMusicSettingsProvider).pageInfoEnabled(); + } + + void _clear() { + if (ref.mounted) state = const AsyncData(null); + } + Future _load({ required String album, required String artist, diff --git a/lib/providers/apple_music_provider.g.dart b/lib/providers/apple_music_provider.g.dart index 820cb063a..b2fdde2d0 100644 --- a/lib/providers/apple_music_provider.g.dart +++ b/lib/providers/apple_music_provider.g.dart @@ -57,6 +57,117 @@ final class AppleMusicMetadataServiceProvider String _$appleMusicMetadataServiceHash() => r'2ba21aa8053b9fe91c8c014b245bebf32b2a3dab'; +@ProviderFor(appleMusicSettings) +final appleMusicSettingsProvider = AppleMusicSettingsProvider._(); + +final class AppleMusicSettingsProvider + extends + $FunctionalProvider< + AppleMusicSettings, + AppleMusicSettings, + AppleMusicSettings + > + with $Provider { + AppleMusicSettingsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'appleMusicSettingsProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$appleMusicSettingsHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + AppleMusicSettings create(Ref ref) { + return appleMusicSettings(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AppleMusicSettings value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$appleMusicSettingsHash() => + r'9503a051107dd9de72c031eed153abdaa98269f7'; + +/// Whether artist and album pages may load Apple Music page information. +/// Keep-alive so the persisted preference is read once and shared by every +/// open detail page and the settings toggle. + +@ProviderFor(AppleMusicPageInfoEnabledNotifier) +final appleMusicPageInfoEnabledProvider = + AppleMusicPageInfoEnabledNotifierProvider._(); + +/// Whether artist and album pages may load Apple Music page information. +/// Keep-alive so the persisted preference is read once and shared by every +/// open detail page and the settings toggle. +final class AppleMusicPageInfoEnabledNotifierProvider + extends $AsyncNotifierProvider { + /// Whether artist and album pages may load Apple Music page information. + /// Keep-alive so the persisted preference is read once and shared by every + /// open detail page and the settings toggle. + AppleMusicPageInfoEnabledNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'appleMusicPageInfoEnabledProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => + _$appleMusicPageInfoEnabledNotifierHash(); + + @$internal + @override + AppleMusicPageInfoEnabledNotifier create() => + AppleMusicPageInfoEnabledNotifier(); +} + +String _$appleMusicPageInfoEnabledNotifierHash() => + r'46b6964c43c7874f976f0eea5377dc9edcbd25ed'; + +/// Whether artist and album pages may load Apple Music page information. +/// Keep-alive so the persisted preference is read once and shared by every +/// open detail page and the settings toggle. + +abstract class _$AppleMusicPageInfoEnabledNotifier + extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, bool>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, bool>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} + /// Apple Music metadata for an artist page. Auto-disposes when the page /// closes; durable caching lives in [AppleMusicMetadataService]. @@ -111,7 +222,7 @@ final class AppleMusicArtistNotifierProvider } String _$appleMusicArtistNotifierHash() => - r'8ac727b9403b1a687fffeda9de0f249d557e6743'; + r'b1e56596d9893d13855ba17ca517bf7e0c69a583'; /// Apple Music metadata for an artist page. Auto-disposes when the page /// closes; durable caching lives in [AppleMusicMetadataService]. @@ -222,7 +333,7 @@ final class AppleMusicAlbumNotifierProvider } String _$appleMusicAlbumNotifierHash() => - r'0575473b3d0b18f286ea326914c98e96f832d90a'; + r'52f048d76d305e098089c2ea3d3d39b5cb2ac23a'; /// Apple Music metadata for an album page, keyed by `(album, artist)`. diff --git a/lib/services/apple_music/apple_music_settings.dart b/lib/services/apple_music/apple_music_settings.dart index 2ec8f20ff..493f67b81 100644 --- a/lib/services/apple_music/apple_music_settings.dart +++ b/lib/services/apple_music/apple_music_settings.dart @@ -2,6 +2,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class AppleMusicSettings { static const _keyAutoEnrich = 'apple_music_auto_enrich'; + static const _keyPageInfo = 'apple_music_page_info_enabled'; Future autoEnrichEnabled() async { final prefs = await SharedPreferences.getInstance(); @@ -12,4 +13,16 @@ class AppleMusicSettings { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(_keyAutoEnrich, value); } + + /// Whether artist and album pages may fetch Apple Music page information + /// (biographies, similar artists, top songs, album notes, and portraits). + Future pageInfoEnabled() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_keyPageInfo) ?? true; + } + + Future setPageInfoEnabled(bool value) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_keyPageInfo, value); + } } diff --git a/test/providers/apple_music_provider_test.dart b/test/providers/apple_music_provider_test.dart index 7cd399de6..71d8750a4 100644 --- a/test/providers/apple_music_provider_test.dart +++ b/test/providers/apple_music_provider_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -5,6 +6,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:flick/providers/apple_music_provider.dart'; import 'package:flick/services/apple_music/apple_music_cache.dart'; @@ -52,9 +54,12 @@ const _albumNotesHtml = ''; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late Directory cacheDir; setUp(() async { + SharedPreferences.setMockInitialValues({}); cacheDir = await Directory.systemTemp.createTemp('apple_music_provider_'); }); @@ -70,9 +75,7 @@ void main() { cache: AppleMusicCache.forDirectory(cacheDir), ); return ProviderContainer( - overrides: [ - appleMusicMetadataServiceProvider.overrideWithValue(service), - ], + overrides: [appleMusicMetadataServiceProvider.overrideWithValue(service)], ); } @@ -226,4 +229,177 @@ void main() { expect(data?.notes, 'A trip hop landmark.'); expect(data?.tracks.single.trackName, 'Mysterons'); }); + + test('page information disabled: build makes no requests', () async { + SharedPreferences.setMockInitialValues({ + 'apple_music_page_info_enabled': false, + }); + var requests = 0; + final client = MockClient((request) async { + requests++; + return _json({}, 404); + }); + final container = containerWith(client); + addTearDown(container.dispose); + + final provider = appleMusicArtistProvider('Portishead'); + final sub = container.listen(provider, (_, __) {}); + addTearDown(sub.close); + + expect(await container.read(provider.future), isNull); + expect(requests, 0); + }); + + test('album page information disabled: build makes no requests', () async { + SharedPreferences.setMockInitialValues({ + 'apple_music_page_info_enabled': false, + }); + var requests = 0; + final client = MockClient((request) async { + requests++; + return _json({}, 404); + }); + final container = containerWith(client); + addTearDown(container.dispose); + + final provider = appleMusicAlbumProvider(('Dummy', 'Portishead')); + final sub = container.listen(provider, (_, __) {}); + addTearDown(sub.close); + + expect(await container.read(provider.future), isNull); + expect(requests, 0); + }); + + test( + 'disabling page information hides cached data and blocks refresh', + () async { + var requests = 0; + final client = MockClient((request) async { + requests++; + if (request.url.path == '/search') { + return _json({ + 'resultCount': 1, + 'results': [_artistResult()], + }); + } + if (request.url.path == '/lookup') { + return _json({ + 'resultCount': 1, + 'results': [_trackResult('Glory Box')], + }); + } + return _json({}, 404); + }); + final container = containerWith(client); + addTearDown(container.dispose); + + final provider = appleMusicArtistProvider('Portishead'); + final sub = container.listen(provider, (_, __) {}); + addTearDown(sub.close); + + await container.read(provider.future); + final loadedRequests = requests; + expect(container.read(provider).value, isNotNull); + + await container + .read(appleMusicPageInfoEnabledProvider.notifier) + .setEnabled(false); + expect(await container.read(provider.future), isNull); + + final updated = await container.read(provider.notifier).refresh(); + expect(updated, isFalse); + expect(requests, loadedRequests); + expect(container.read(provider).value, isNull); + }, + ); + + test('re-enabling page information resumes from cache', () async { + var requests = 0; + final client = MockClient((request) async { + requests++; + if (request.url.path == '/search') { + return _json({ + 'resultCount': 1, + 'results': [_artistResult()], + }); + } + if (request.url.path == '/lookup') { + return _json({ + 'resultCount': 1, + 'results': [_trackResult('Glory Box')], + }); + } + return _json({}, 404); + }); + final container = containerWith(client); + addTearDown(container.dispose); + + final provider = appleMusicArtistProvider('Portishead'); + final sub = container.listen(provider, (_, __) {}); + addTearDown(sub.close); + + await container.read(provider.future); + final loadedRequests = requests; + + final settings = container.read(appleMusicPageInfoEnabledProvider.notifier); + await settings.setEnabled(false); + expect(await container.read(provider.future), isNull); + + await settings.setEnabled(true); + final data = await container.read(provider.future); + expect(data?.topSongs.single.trackName, 'Glory Box'); + expect(requests, loadedRequests); + }); + + test( + 'disabling page information during refresh discards the late result', + () async { + final pending = Completer(); + var searchCalls = 0; + final client = MockClient((request) async { + if (request.url.path == '/search') { + searchCalls++; + if (searchCalls > 1) return pending.future; + return _json({ + 'resultCount': 1, + 'results': [_artistResult()], + }); + } + if (request.url.path == '/lookup') { + return _json({ + 'resultCount': 1, + 'results': [_trackResult('Glory Box')], + }); + } + return _json({}, 404); + }); + final container = containerWith(client); + addTearDown(container.dispose); + + final provider = appleMusicArtistProvider('Portishead'); + final sub = container.listen(provider, (_, __) {}); + addTearDown(sub.close); + + await container.read(provider.future); + final refresh = container.read(provider.notifier).refresh(); + for (var i = 0; i < 100 && searchCalls < 2; i++) { + await Future.delayed(const Duration(milliseconds: 1)); + } + expect(searchCalls, 2); + + await container + .read(appleMusicPageInfoEnabledProvider.notifier) + .setEnabled(false); + pending.complete( + _json({ + 'resultCount': 1, + 'results': [_artistResult()], + }), + ); + + expect(await refresh, isFalse); + expect(await container.read(provider.future), isNull); + expect(container.read(provider).value, isNull); + }, + ); } diff --git a/test/services/apple_music_settings_test.dart b/test/services/apple_music_settings_test.dart new file mode 100644 index 000000000..13795cee4 --- /dev/null +++ b/test/services/apple_music_settings_test.dart @@ -0,0 +1,26 @@ +import 'package:flick/services/apple_music/apple_music_settings.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + SharedPreferences.setMockInitialValues({}); + }); + + test('page information defaults on and persists independently', () async { + final settings = AppleMusicSettings(); + + expect(await settings.pageInfoEnabled(), isTrue); + expect(await settings.autoEnrichEnabled(), isFalse); + + await settings.setPageInfoEnabled(false); + expect(await settings.pageInfoEnabled(), isFalse); + expect(await settings.autoEnrichEnabled(), isFalse); + + await settings.setAutoEnrichEnabled(true); + expect(await settings.pageInfoEnabled(), isFalse); + expect(await settings.autoEnrichEnabled(), isTrue); + }); +}