From cc9a94de629346309d3f1326163c86f85d727173 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 00:44:06 +0000 Subject: [PATCH] feat: Add settings to control play button visibility on home shelves This commit introduces new functionality to customize the visibility of the play button on home page shelves. Key changes: - Added `HomePageSettings` to `AppSettings` to store your preferences: - `showPlayButtonOnContinueShelves`: Controls visibility on "Continue Listening" and "Continue Series" shelves (default: true). - `showPlayButtonOnAllShelves`: Controls visibility on other shelves (default: false). - Modified `BookHomeShelf` to respect these settings when rendering books. - Created a new "Home Page Settings" page under "Appearance" in App Settings, allowing you to toggle these two options. - Added comprehensive unit and widget tests to cover the new settings model, the conditional logic in `BookHomeShelf`, and the functionality of the new settings page. --- lib/router/constants.dart | 5 + lib/router/router.dart | 8 + lib/settings/models/app_settings.dart | 12 ++ lib/settings/view/app_settings_page.dart | 10 ++ .../view/home_page_settings_page.dart | 53 ++++++ lib/shared/widgets/shelves/book_shelf.dart | 11 ++ test/settings/app_settings_test.dart | 51 ++++++ .../view/home_page_settings_page_test.dart | 138 ++++++++++++++ test/widgets/shelves/book_shelf_test.dart | 169 ++++++++++++++++++ 9 files changed, 457 insertions(+) create mode 100644 lib/settings/view/home_page_settings_page.dart create mode 100644 test/settings/app_settings_test.dart create mode 100644 test/settings/view/home_page_settings_page_test.dart create mode 100644 test/widgets/shelves/book_shelf_test.dart diff --git a/lib/router/constants.dart b/lib/router/constants.dart index f36d706..f01ce24 100644 --- a/lib/router/constants.dart +++ b/lib/router/constants.dart @@ -52,6 +52,11 @@ class Routes { name: 'shakeDetectorSettings', parentRoute: settings, ); + static const homePageSettings = _SimpleRoute( + pathName: 'home-page', + name: 'homePageSettings', + parentRoute: settings, + ); // search and explore static const search = _SimpleRoute( diff --git a/lib/router/router.dart b/lib/router/router.dart index 5e9f71e..eda348e 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -18,6 +18,7 @@ import 'package:vaani/settings/view/notification_settings_page.dart'; import 'package:vaani/settings/view/player_settings_page.dart'; import 'package:vaani/settings/view/shake_detector_settings_page.dart'; import 'package:vaani/settings/view/theme_settings_page.dart'; +import 'package:vaani/settings/view/home_page_settings_page.dart'; import 'scaffold_with_nav_bar.dart'; import 'transitions/slide.dart'; @@ -213,6 +214,13 @@ class MyAppRouter { const ShakeDetectorSettingsPage(), ), ), + GoRoute( + path: Routes.homePageSettings.pathName, + name: Routes.homePageSettings.name, + pageBuilder: defaultPageBuilder( + const HomePageSettingsPage(), + ), + ), ], ), GoRoute( diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index e486ac4..bd9f8dd 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -19,12 +19,24 @@ class AppSettings with _$AppSettings { @Default(NotificationSettings()) NotificationSettings notificationSettings, @Default(ShakeDetectionSettings()) ShakeDetectionSettings shakeDetectionSettings, + @Default(HomePageSettings()) HomePageSettings homePageSettings, }) = _AppSettings; factory AppSettings.fromJson(Map json) => _$AppSettingsFromJson(json); } +@freezed +class HomePageSettings with _$HomePageSettings { + const factory HomePageSettings({ + @Default(true) bool showPlayButtonOnContinueShelves, + @Default(false) bool showPlayButtonOnAllShelves, + }) = _HomePageSettings; + + factory HomePageSettings.fromJson(Map json) => + _$HomePageSettingsFromJson(json); +} + @freezed class ThemeSettings with _$ThemeSettings { const factory ThemeSettings({ diff --git a/lib/settings/view/app_settings_page.dart b/lib/settings/view/app_settings_page.dart index aebe867..3731e5b 100644 --- a/lib/settings/view/app_settings_page.dart +++ b/lib/settings/view/app_settings_page.dart @@ -120,6 +120,16 @@ class AppSettingsPage extends HookConsumerWidget { context.pushNamed(Routes.notificationSettings.name); }, ), + SettingsTile.navigation( + leading: const Icon(Icons.home_filled), + title: const Text('Home Page Settings'), + description: const Text( + 'Customize the home page shelves', + ), + onPressed: (context) { + context.pushNamed(Routes.homePageSettings.name); + }, + ), ], ), diff --git a/lib/settings/view/home_page_settings_page.dart b/lib/settings/view/home_page_settings_page.dart new file mode 100644 index 0000000..99f892a --- /dev/null +++ b/lib/settings/view/home_page_settings_page.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:settings_ui/settings_ui.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/widgets/simple_settings_page.dart'; + +class HomePageSettingsPage extends HookConsumerWidget { + const HomePageSettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSettings = ref.watch(appSettingsProvider); + final appSettingsNotifier = ref.read(appSettingsProvider.notifier); + + return SimpleSettingsPage( + title: 'Home Page Settings', + child: SettingsList( + sections: [ + SettingsSection( + tiles: [ + SettingsTile.switchTile( + title: const Text('Show play button on continue shelves'), + initialValue: appSettings.homePageSettings.showPlayButtonOnContinueShelves, + onToggle: (value) { + appSettingsNotifier.update( + appSettings.copyWith( + homePageSettings: appSettings.homePageSettings.copyWith( + showPlayButtonOnContinueShelves: value, + ), + ), + ); + }, + ), + SettingsTile.switchTile( + title: const Text('Show play button on all shelves'), + initialValue: appSettings.homePageSettings.showPlayButtonOnAllShelves, + onToggle: (value) { + appSettingsNotifier.update( + appSettings.copyWith( + homePageSettings: appSettings.homePageSettings.copyWith( + showPlayButtonOnAllShelves: value, + ), + ), + ); + }, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index 4490b99..4712e0f 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -32,6 +32,16 @@ class BookHomeShelf extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final appSettings = ref.watch(appSettingsProvider); + final homePageSettings = appSettings.homePageSettings; + + final bool showPlayButton; + if (title == 'Continue Listening' || title == 'Continue Series') { + showPlayButton = homePageSettings.showPlayButtonOnContinueShelves; + } else { + showPlayButton = homePageSettings.showPlayButtonOnAllShelves; + } + return SimpleHomeShelf( title: title, children: shelf.entities @@ -41,6 +51,7 @@ class BookHomeShelf extends HookConsumerWidget { item: item, key: ValueKey(shelf.id + item.id), heroTagSuffix: shelf.id, + showPlayButton: showPlayButton, ), _ => Container(), }, diff --git a/test/settings/app_settings_test.dart b/test/settings/app_settings_test.dart new file mode 100644 index 0000000..5550dea --- /dev/null +++ b/test/settings/app_settings_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:vaani/settings/models/app_settings.dart'; + +void main() { + group('AppSettings', () { + test('initializes with default HomePageSettings', () { + const appSettings = AppSettings(); + expect(appSettings.homePageSettings.showPlayButtonOnContinueShelves, isTrue); + expect(appSettings.homePageSettings.showPlayButtonOnAllShelves, isFalse); + }); + + test('HomePageSettings can be updated', () { + const initialSettings = AppSettings(); + final updatedSettings = initialSettings.copyWith( + homePageSettings: initialSettings.homePageSettings.copyWith( + showPlayButtonOnContinueShelves: false, + showPlayButtonOnAllShelves: true, + ), + ); + + expect(updatedSettings.homePageSettings.showPlayButtonOnContinueShelves, isFalse); + expect(updatedSettings.homePageSettings.showPlayButtonOnAllShelves, isTrue); + }); + + test('HomePageSettings are correctly serialized and deserialized', () { + const originalSettings = AppSettings( + homePageSettings: HomePageSettings( + showPlayButtonOnContinueShelves: false, + showPlayButtonOnAllShelves: true, + ), + ); + + final json = originalSettings.toJson(); + final deserializedSettings = AppSettings.fromJson(json); + + expect(deserializedSettings.homePageSettings.showPlayButtonOnContinueShelves, isFalse); + expect(deserializedSettings.homePageSettings.showPlayButtonOnAllShelves, isTrue); + expect(deserializedSettings, originalSettings); + }); + + test('Default AppSettings serialization and deserialization', () { + const originalSettings = AppSettings(); + final json = originalSettings.toJson(); + final deserializedSettings = AppSettings.fromJson(json); + + expect(deserializedSettings.homePageSettings.showPlayButtonOnContinueShelves, isTrue); + expect(deserializedSettings.homePageSettings.showPlayButtonOnAllShelves, isFalse); + expect(deserializedSettings, originalSettings); + }); + }); +} diff --git a/test/settings/view/home_page_settings_page_test.dart b/test/settings/view/home_page_settings_page_test.dart new file mode 100644 index 0000000..778f03b --- /dev/null +++ b/test/settings/view/home_page_settings_page_test.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:settings_ui/settings_ui.dart'; +import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/view/home_page_settings_page.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Helper function to pump HomePageSettingsPage + Future pumpHomePageSettingsPage({ + required WidgetTester tester, + required AppSettings initialAppSettings, + required ProviderContainer container, + }) async { + await tester.pumpWidget( + UncontrolledProviderScope( + container: container, + child: const MaterialApp( + home: HomePageSettingsPage(), + ), + ), + ); + await tester.pumpAndSettle(); + } + + group('HomePageSettingsPage Widget Tests', () { + late ProviderContainer container; + late AppSettingsNotifier appSettingsNotifier; + + setUp(() { + // Initialize with default AppSettings + appSettingsNotifier = AppSettingsNotifier(const AppSettings()); + container = ProviderContainer( + overrides: [ + appSettingsProvider.overrideWithValue(appSettingsNotifier), + ], + ); + }); + + tearDown(() { + container.dispose(); + }); + + testWidgets('Initial State: Switches reflect default AppSettings', + (WidgetTester tester) async { + await pumpHomePageSettingsPage( + tester: tester, + initialAppSettings: const AppSettings(), // Not strictly needed here as it's in setUp + container: container, + ); + + // Find the SettingsList + final settingsListFinder = find.byType(SettingsList); + expect(settingsListFinder, findsOneWidget); + + // Find the switch tiles + final switchTileFinders = find.byType(SettingsTile); + expect(switchTileFinders, findsNWidgets(2)); + + final continueShelvesSwitch = tester.widget( + find.descendant( + of: settingsListFinder, + matching: find.widgetWithText(SettingsTile, 'Show play button on continue shelves'), + ), + ); + expect(continueShelvesSwitch.initialValue, isTrue); + + final allShelvesSwitch = tester.widget( + find.descendant( + of: settingsListFinder, + matching: find.widgetWithText(SettingsTile, 'Show play button on all shelves'), + ), + ); + expect(allShelvesSwitch.initialValue, isFalse); + }); + + testWidgets('Toggle Switches: Updates AppSettings in provider', + (WidgetTester tester) async { + await pumpHomePageSettingsPage( + tester: tester, + initialAppSettings: const AppSettings(), + container: container, + ); + + // Initial state check (from AppSettingsNotifier directly) + expect( + container.read(appSettingsProvider).homePageSettings.showPlayButtonOnContinueShelves, + isTrue, + ); + expect( + container.read(appSettingsProvider).homePageSettings.showPlayButtonOnAllShelves, + isFalse, + ); + + // Find and tap the first switch (Continue Shelves) + final continueShelvesSwitchFinder = find.widgetWithText(SettingsTile, 'Show play button on continue shelves'); + expect(continueShelvesSwitchFinder, findsOneWidget); + await tester.tap(continueShelvesSwitchFinder); + await tester.pumpAndSettle(); // Allow state to update + + // Verify the AppSettingsProvider was updated + expect( + container.read(appSettingsProvider).homePageSettings.showPlayButtonOnContinueShelves, + isFalse, + ); + + // Find and tap the second switch (All Shelves) + final allShelvesSwitchFinder = find.widgetWithText(SettingsTile, 'Show play button on all shelves'); + expect(allShelvesSwitchFinder, findsOneWidget); + await tester.tap(allShelvesSwitchFinder); + await tester.pumpAndSettle(); // Allow state to update + + // Verify the AppSettingsProvider was updated + expect( + container.read(appSettingsProvider).homePageSettings.showPlayButtonOnAllShelves, + isTrue, + ); + + // Tap them again to ensure they toggle back + await tester.tap(continueShelvesSwitchFinder); + await tester.pumpAndSettle(); + expect( + container.read(appSettingsProvider).homePageSettings.showPlayButtonOnContinueShelves, + isTrue, + ); + + await tester.tap(allShelvesSwitchFinder); + await tester.pumpAndSettle(); + expect( + container.read(appSettingsProvider).homePageSettings.showPlayButtonOnAllShelves, + isFalse, + ); + }); + }); +} diff --git a/test/widgets/shelves/book_shelf_test.dart b/test/widgets/shelves/book_shelf_test.dart new file mode 100644 index 0000000..d86d70d --- /dev/null +++ b/test/widgets/shelves/book_shelf_test.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart'; +import 'package:vaani/api/image_provider.dart'; +import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; + +import 'book_shelf_test.mocks.dart'; + +// Mocks +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Helper function to create a mock LibraryItem + MockLibraryItem _createMockLibraryItem(String id) { + final mockItem = MockLibraryItem(); + final mockMedia = MockMedia(); + final mockBookMinified = MockBookMinified(); + final mockBookMetadataMinified = MockBookMetadataMinified(); + + when(mockItem.id).thenReturn(id); + when(mockItem.mediaType).thenReturn(MediaType.book); + when(mockItem.media).thenReturn(mockMedia); + when(mockMedia.asBookMinified).thenReturn(mockBookMinified); + when(mockBookMinified.metadata).thenReturn(mockBookMetadataMinified); + when(mockBookMetadataMinified.title).thenReturn('Test Book Title'); + when(mockBookMetadataMinified.authorName).thenReturn('Test Author'); + return mockItem; + } + + // Helper function to create a mock LibraryItemShelf + MockLibraryItemShelf _createMockLibraryItemShelf(List items) { + final mockShelf = MockLibraryItemShelf(); + when(mockShelf.id).thenReturn('test-shelf-id'); + when(mockShelf.entities).thenReturn(items); + return mockShelf; + } + + // Helper function to pump BookHomeShelf with specific settings + Future pumpBookHomeShelf({ + required WidgetTester tester, + required String title, + required LibraryItemShelf shelf, + required AppSettings appSettings, + }) async { + final mockCoverImageNotifier = MockCoverImageNotifier(); + when(mockCoverImageNotifier.build(any)).thenAnswer((_) => Uint8List(0)); + when(mockCoverImageNotifier.future).thenAnswer((_) async => Uint8List(0)); + + + await tester.pumpWidget( + ProviderScope( + overrides: [ + appSettingsProvider.overrideWithValue( + AppSettingsNotifier(appSettings), + ), + // Mock the coverImageProvider to avoid network calls or file system access + coverImageProvider(any).overrideWith((ref, id) => mockCoverImageNotifier), + ], + child: MaterialApp( + home: Scaffold( + body: BookHomeShelf( + title: title, + shelf: shelf, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); // Let animations and futures settle + } + + // Function to find BookOnShelf and check its showPlayButton property + bool getShowPlayButtonForBookOnShelf(WidgetTester tester, String itemId) { + final bookOnShelfFinder = find.byWidgetPredicate( + (widget) => widget is BookOnShelf && widget.item.id == itemId); + expect(bookOnShelfFinder, findsOneWidget); + final bookOnShelfWidget = tester.widget(bookOnShelfFinder); + return bookOnShelfWidget.showPlayButton; + } + + group('BookHomeShelf Play Button Visibility', () { + late MockLibraryItem mockItem1; + late MockLibraryItemShelf mockShelf; + + setUp(() { + mockItem1 = _createMockLibraryItem('item1'); + mockShelf = _createMockLibraryItemShelf([mockItem1]); + }); + + testWidgets('Continue Shelves - Default: showPlayButton should be true', + (WidgetTester tester) async { + const appSettings = AppSettings(); // Default settings + + await pumpBookHomeShelf( + tester: tester, + title: 'Continue Listening', + shelf: mockShelf, + appSettings: appSettings, + ); + + expect(getShowPlayButtonForBookOnShelf(tester, 'item1'), isTrue); + }); + + testWidgets('Continue Shelves - Hidden: showPlayButton should be false', + (WidgetTester tester) async { + const appSettings = AppSettings( + homePageSettings: HomePageSettings( + showPlayButtonOnContinueShelves: false, + showPlayButtonOnAllShelves: false, // Keep others default + ), + ); + + await pumpBookHomeShelf( + tester: tester, + title: 'Continue Series', // Another continue shelf title + shelf: mockShelf, + appSettings: appSettings, + ); + + expect(getShowPlayButtonForBookOnShelf(tester, 'item1'), isFalse); + }); + + testWidgets('Other Shelves - Default: showPlayButton should be false', + (WidgetTester tester) async { + const appSettings = AppSettings(); // Default settings + + await pumpBookHomeShelf( + tester: tester, + title: 'Discover', + shelf: mockShelf, + appSettings: appSettings, + ); + + expect(getShowPlayButtonForBookOnShelf(tester, 'item1'), isFalse); + }); + + testWidgets('Other Shelves - Shown: showPlayButton should be true', + (WidgetTester tester) async { + const appSettings = AppSettings( + homePageSettings: HomePageSettings( + showPlayButtonOnContinueShelves: true, // Keep others default + showPlayButtonOnAllShelves: true, + ), + ); + + await pumpBookHomeShelf( + tester: tester, + title: 'New Releases', + shelf: mockShelf, + appSettings: appSettings, + ); + + expect(getShowPlayButtonForBookOnShelf(tester, 'item1'), isTrue); + }); + }); +}