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.
This commit is contained in:
google-labs-jules[bot] 2025-05-22 00:44:06 +00:00
parent 23e5d73bea
commit cc9a94de62
9 changed files with 457 additions and 0 deletions

View file

@ -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(

View file

@ -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(

View file

@ -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<String, dynamic> json) =>
_$AppSettingsFromJson(json);
}
@freezed
class HomePageSettings with _$HomePageSettings {
const factory HomePageSettings({
@Default(true) bool showPlayButtonOnContinueShelves,
@Default(false) bool showPlayButtonOnAllShelves,
}) = _HomePageSettings;
factory HomePageSettings.fromJson(Map<String, dynamic> json) =>
_$HomePageSettingsFromJson(json);
}
@freezed
class ThemeSettings with _$ThemeSettings {
const factory ThemeSettings({

View file

@ -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);
},
),
],
),

View file

@ -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,
),
),
);
},
),
],
),
],
),
);
}
}

View file

@ -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(),
},

View file

@ -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);
});
});
}

View file

@ -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<void> 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<SettingsTile>(
find.descendant(
of: settingsListFinder,
matching: find.widgetWithText(SettingsTile, 'Show play button on continue shelves'),
),
);
expect(continueShelvesSwitch.initialValue, isTrue);
final allShelvesSwitch = tester.widget<SettingsTile>(
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,
);
});
});
}

View file

@ -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<LibraryItem>(),
MockSpec<Media>(),
MockSpec<BookMinified>(),
MockSpec<BookMetadataMinified>(),
MockSpec<LibraryItemShelf>(),
MockSpec<CoverImageNotifier>(),
])
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<LibraryItem> 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<void> 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<BookOnShelf>(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);
});
});
}