diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 0a5e5bd9..47fff5af 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -158,6 +158,7 @@ "bufferSizeWarning": "${heap}MB memory available. A ${size}MB buffer may affect playback.", "defaultQualityTitle": "Default Quality", "defaultQualityDescription": "Used when starting playback. Lower values reduce bandwidth.", + "musicQualityTitle": "Music Quality", "subtitleStyling": "Subtitle Styling", "subtitleStylingDescription": "Customize subtitle appearance", "smallSkipDuration": "Small Skip Duration", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 9dcc6abc..ab69baae 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 16 -/// Strings: 22706 (1419 per locale) +/// Strings: 22707 (1419 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index fdebdf9c..56a04568 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -588,6 +588,9 @@ class TranslationsSettingsEn { /// en: 'Used when starting playback. Lower values reduce bandwidth.' String get defaultQualityDescription => 'Used when starting playback. Lower values reduce bandwidth.'; + /// en: 'Music Quality' + String get musicQualityTitle => 'Music Quality'; + /// en: 'Subtitle Styling' String get subtitleStyling => 'Subtitle Styling'; @@ -5111,6 +5114,7 @@ extension on Translations { 'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap}MB memory available. A ${size}MB buffer may affect playback.', 'settings.defaultQualityTitle' => 'Default Quality', 'settings.defaultQualityDescription' => 'Used when starting playback. Lower values reduce bandwidth.', + 'settings.musicQualityTitle' => 'Music Quality', 'settings.subtitleStyling' => 'Subtitle Styling', 'settings.subtitleStylingDescription' => 'Customize subtitle appearance', 'settings.smallSkipDuration' => 'Small Skip Duration', @@ -5474,9 +5478,9 @@ extension on Translations { 'messages.removedFromContinueWatching' => 'Removed from Continue Watching', 'messages.errorLoading' => ({required Object error}) => 'Error: ${error}', 'messages.streamInterrupted' => 'The stream was interrupted. Press play or seek to retry.', - 'messages.fileInfoNotAvailable' => 'File information not available', _ => null, } ?? switch (path) { + 'messages.fileInfoNotAvailable' => 'File information not available', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Error loading file info: ${error}', 'messages.errorLoadingSeries' => 'Error loading series', 'messages.musicNotSupported' => 'Music playback is not yet supported', @@ -5988,9 +5992,9 @@ extension on Translations { 'downloads.manage' => 'Manage', 'downloads.tvShows' => 'TV Shows', 'downloads.movies' => 'Movies', - 'downloads.music' => 'Music', _ => null, } ?? switch (path) { + 'downloads.music' => 'Music', 'downloads.tracksQueued' => ({required Object count}) => '${count} tracks queued for download', 'downloads.noDownloads' => 'No downloads yet', 'downloads.noDownloadsDescription' => 'Downloaded content will appear here for offline viewing', diff --git a/lib/screens/settings/playback_settings_screen.dart b/lib/screens/settings/playback_settings_screen.dart index e482211d..fc6f6c41 100644 --- a/lib/screens/settings/playback_settings_screen.dart +++ b/lib/screens/settings/playback_settings_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../i18n/strings.g.dart'; +import '../../models/audio_quality_preset.dart'; import '../../models/transcode_quality_preset.dart'; import '../../mpv/player/platform/player_android.dart'; import '../../utils/quality_preset_labels.dart'; @@ -91,6 +92,7 @@ class _PlaybackSettingsScreenState extends State { if (exoActive) _dvConversionModeTile(), _bufferSizeTile(), _defaultQualityTile(), + _musicQualityTile(), ], ), @@ -443,6 +445,19 @@ class _PlaybackSettingsScreenState extends State { encode: (p) => p, ); + Widget _musicQualityTile() => SettingSelectionTile( + pref: SettingsService.musicQualityPreset, + icon: Symbols.music_note_rounded, + title: t.settings.musicQualityTitle, + subtitleBuilder: _musicQualityLabel, + options: AudioQualityPreset.values.map((p) => DialogOption(value: p, title: _musicQualityLabel(p))).toList(), + decode: (p) => p, + encode: (p) => p, + ); + + String _musicQualityLabel(AudioQualityPreset preset) => + preset.isOriginal ? t.videoControls.qualityOriginal : '${preset.bitrateKbps} kbps'; + Widget _mpvConfigTile() => SettingNavigationTile( icon: Symbols.tune_rounded, title: t.mpvConfig.title, diff --git a/test/screens/settings/playback_settings_screen_test.dart b/test/screens/settings/playback_settings_screen_test.dart new file mode 100644 index 00000000..3e7da7e4 --- /dev/null +++ b/test/screens/settings/playback_settings_screen_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/i18n/strings.g.dart'; +import 'package:plezy/models/audio_quality_preset.dart'; +import 'package:plezy/screens/settings/playback_settings_screen.dart'; +import 'package:plezy/services/settings_service.dart'; +import 'package:plezy/theme/mono_theme.dart'; + +import '../../test_helpers/prefs.dart'; + +void main() { + setUp(() async { + resetSharedPreferencesForTest(initialAsync: {'music_quality_preset': 'medium'}); + SettingsService.resetForTesting(); + await SettingsService.getInstance(); + LocaleSettings.setLocaleSync(AppLocale.en); + }); + + testWidgets('shows and changes the persisted music quality', (tester) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(1000, 1400); + addTearDown(tester.view.resetDevicePixelRatio); + addTearDown(tester.view.resetPhysicalSize); + + await tester.pumpWidget(MaterialApp(theme: monoTheme(dark: true), home: const PlaybackSettingsScreen())); + await tester.pumpAndSettle(); + + final title = find.text('Music Quality'); + await tester.scrollUntilVisible(title, 500, scrollable: find.byType(Scrollable).first); + + final tile = find.widgetWithText(ListTile, 'Music Quality'); + expect(find.descendant(of: tile, matching: find.text('192 kbps')), findsOneWidget); + + await tester.tap(title); + await tester.pumpAndSettle(); + await tester.tap(find.text('128 kbps')); + await tester.pumpAndSettle(); + + final settings = SettingsService.instance; + expect(settings.read(SettingsService.musicQualityPreset), AudioQualityPreset.low); + expect(settings.prefs.getString(SettingsService.musicQualityPreset.key), 'low'); + expect(find.descendant(of: tile, matching: find.text('128 kbps')), findsOneWidget); + }); +} diff --git a/test/services/settings_service_test.dart b/test/services/settings_service_test.dart index 063a0fec..87010efc 100644 --- a/test/services/settings_service_test.dart +++ b/test/services/settings_service_test.dart @@ -1,6 +1,8 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:plezy/models/audio_quality_preset.dart'; import 'package:plezy/models/hotkey_model.dart'; +import 'package:plezy/services/base_shared_preferences_service.dart'; import 'package:plezy/services/settings_service.dart'; import 'package:plezy/services/trackers/tracker_constants.dart'; import 'package:plezy/utils/platform_detector.dart'; @@ -145,6 +147,23 @@ void main() { }); }); + group('SettingsService music quality', () { + test('defaults to original and persists changes by enum name', () async { + var settings = await SettingsService.getInstance(); + + expect(settings.read(SettingsService.musicQualityPreset), AudioQualityPreset.original); + + await settings.write(SettingsService.musicQualityPreset, AudioQualityPreset.medium); + expect(settings.prefs.getString(SettingsService.musicQualityPreset.key), 'medium'); + + BaseSharedPreferencesService.resetForTesting(); + SettingsService.resetForTesting(); + settings = await SettingsService.getInstance(); + + expect(settings.read(SettingsService.musicQualityPreset), AudioQualityPreset.medium); + }); + }); + group('SettingsService platform gates', () { test('audio passthrough stays available on desktop and Apple TV', () { expect(PlatformDetector.supportsAudioPassthrough(), isTrue);