From 21fd51d62c76c0c141cf310e4921b67f3ecf307d Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:18:26 +0700 Subject: [PATCH] feat(autoplay): enable personalization by default and confirm learning resets --- lib/models/settings.dart | 2 +- lib/models/settings.g.dart | 2 +- .../settings/playback_settings_page.dart | 27 ++++++++++++++++++- test/autoplay_personalization_test.dart | 27 +++++++++---------- test/autoplay_settings_test.dart | 19 +++++++++---- 5 files changed, 54 insertions(+), 23 deletions(-) diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 2b28a21b..1ce31299 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -198,7 +198,7 @@ class AppSettings { this.usbAllowFixedVolume = false, this.dapExclusive = false, this.autoplay = false, - this.autoplayPersonalization = false, + this.autoplayPersonalization = true, this.isFirstLaunch = true, this.checkForUpdates = true, this.updateChannel = 'stable', diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 23b4c43e..28489597 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -59,7 +59,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( usbAllowFixedVolume: json['usbAllowFixedVolume'] as bool? ?? false, dapExclusive: json['dapExclusive'] as bool? ?? false, autoplay: json['autoplay'] as bool? ?? false, - autoplayPersonalization: json['autoplayPersonalization'] as bool? ?? false, + autoplayPersonalization: json['autoplayPersonalization'] as bool? ?? true, isFirstLaunch: json['isFirstLaunch'] as bool? ?? true, checkForUpdates: json['checkForUpdates'] as bool? ?? true, updateChannel: json['updateChannel'] as String? ?? 'stable', diff --git a/lib/screens/settings/playback_settings_page.dart b/lib/screens/settings/playback_settings_page.dart index 4c6af630..80dc1e75 100644 --- a/lib/screens/settings/playback_settings_page.dart +++ b/lib/screens/settings/playback_settings_page.dart @@ -47,6 +47,31 @@ class _PlaybackSettingsPageState extends ConsumerState { false; } + /// Learning cannot be restored once cleared, so a stray tap must not wipe it. + Future _confirmResetLearning() async { + final confirmed = + await showAppDialog( + context: context, + builder: (context) => AppAlertDialog( + title: Text(context.l10n.autoplayResetLearningConfirmTitle), + content: Text(context.l10n.autoplayResetLearningConfirmMessage), + actions: [ + AppDialogAction( + onPressed: () => Navigator.pop(context, false), + child: Text(context.l10n.dialogCancel), + ), + AppDialogAction( + isDestructive: true, + onPressed: () => Navigator.pop(context, true), + child: Text(context.l10n.autoplayResetLearningConfirm), + ), + ], + ), + ) ?? + false; + if (confirmed) await resetAutoplayLearning(); + } + @override Widget build(BuildContext context) { final settings = ref.watch(settingsProvider); @@ -160,7 +185,7 @@ class _PlaybackSettingsPageState extends ConsumerState { SettingsItem( icon: Icons.restart_alt, title: context.l10n.autoplayResetLearning, - onTap: resetAutoplayLearning, + onTap: _confirmResetLearning, showDivider: false, ), ], diff --git a/test/autoplay_personalization_test.dart b/test/autoplay_personalization_test.dart index bd684aff..d97bbbdd 100644 --- a/test/autoplay_personalization_test.dart +++ b/test/autoplay_personalization_test.dart @@ -112,21 +112,18 @@ void main() { '${durations[(durations.length * .95).floor()]} us; model parameters 65536 bytes.', ); }); - test( - 'cold start is neutral and personalization is an independent opt-in', - () { - final model = AutoplayPreferenceModel(); - expect(model.preference(_rock, at: _at), 0.5); - expect(model.rankAdjustment(_rock, previous: _jazz, at: _at), 0); - expect(AppSettings.fromJson({}).autoplayPersonalization, isFalse); - final settings = const AppSettings().copyWith( - autoplayPersonalization: true, - ); - final restored = AppSettings.fromJson(settings.toJson()); - expect(restored.autoplayPersonalization, isTrue); - expect(restored.autoplay, isFalse); - }, - ); + test('cold start is neutral and personalization is on independently', () { + final model = AutoplayPreferenceModel(); + expect(model.preference(_rock, at: _at), 0.5); + expect(model.rankAdjustment(_rock, previous: _jazz, at: _at), 0); + expect(AppSettings.fromJson({}).autoplayPersonalization, isTrue); + final settings = const AppSettings().copyWith( + autoplayPersonalization: false, + ); + final restored = AppSettings.fromJson(settings.toJson()); + expect(restored.autoplayPersonalization, isFalse); + expect(restored.autoplay, isFalse); + }); test('learning favors engagement, transfers genre and handles context', () { final model = AutoplayPreferenceModel(); diff --git a/test/autoplay_settings_test.dart b/test/autoplay_settings_test.dart index 13b8f27f..a782a17a 100644 --- a/test/autoplay_settings_test.dart +++ b/test/autoplay_settings_test.dart @@ -50,24 +50,33 @@ void main() { scrollable: find.byType(Scrollable).first, ); await tester.pumpAndSettle(); - expect(find.text('Reset recommendation learning'), findsNothing); + // On by default; the reset action belongs to the enabled state. + expect(container.read(settingsProvider).autoplayPersonalization, isTrue); + expect(find.text('Reset recommendation learning'), findsOneWidget); + // A stray tap only asks; cancelling keeps what was learned. + await tester.tap(find.text('Reset recommendation learning')); + await tester.pumpAndSettle(); + expect(find.text('Reset recommendation learning?'), findsOneWidget); + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + expect(find.text('Reset recommendation learning?'), findsNothing); await tester.tap(label); await tester.pumpAndSettle(); - expect(container.read(settingsProvider).autoplayPersonalization, isTrue); + expect(container.read(settingsProvider).autoplayPersonalization, isFalse); expect(container.read(settingsProvider).autoplay, isFalse); final prefs = await SharedPreferences.getInstance(); expect( AppSettings.fromJson( jsonDecode(prefs.getString('app_settings')!) as Map, ).autoplayPersonalization, - isTrue, + isFalse, ); - expect(find.text('Reset recommendation learning'), findsOneWidget); + expect(find.text('Reset recommendation learning'), findsNothing); await tester.ensureVisible(label); await tester.pumpAndSettle(); await tester.tap(label); await tester.pumpAndSettle(); - expect(container.read(settingsProvider).autoplayPersonalization, isFalse); + expect(container.read(settingsProvider).autoplayPersonalization, isTrue); final l10n = AppLocalizations.of(tester.element(label)); expect( SettingsSearchCatalog(l10n).playback.any(