From ea078f1ac56b584cb5833e2bc9424479fe5f610f Mon Sep 17 00:00:00 2001 From: lymnyx Date: Mon, 12 Feb 2024 19:33:56 +0100 Subject: [PATCH 01/33] Added playback speed option in song menu. --- lib/components/AlbumScreen/song_menu.dart | 21 ++++++++++++-- lib/components/PlayerScreen/queue_list.dart | 8 +++-- lib/l10n/app_en.arb | 4 +++ lib/models/finamp_models.dart | 5 ++++ lib/services/finamp_settings_helper.dart | 8 +++++ .../music_player_background_task.dart | 5 ++++ lib/services/queue_service.dart | 29 +++++++++++++++++++ 7 files changed, 75 insertions(+), 5 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index db1969357..6d7948b23 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -225,10 +225,11 @@ class _SongMenuState extends State { ), if (widget.showPlaybackControls) StreamBuilder( - stream: Rx.combineLatest2( + stream: Rx.combineLatest3( _queueService.getPlaybackOrderStream(), _queueService.getLoopModeStream(), - (a, b) => PlaybackBehaviorInfo(a, b)), + _queueService.getPlaybackSpeedStream(), + (a, b, c) => PlaybackBehaviorInfo(a, b, c)), builder: (context, snapshot) { if (!snapshot.hasData) return const SliverToBoxAdapter(); @@ -249,6 +250,7 @@ class _SongMenuState extends State { ?.playbackOrderShuffledButtonLabel ?? "Shuffling", }; + final playbackSpeedTooltip = AppLocalizations.of(context)?.playbackSpeedButtonLabel ?? "Playback speed"; const loopModeIcons = { FinampLoopMode.none: TablerIcons.repeat, FinampLoopMode.one: TablerIcons.repeat_once, @@ -331,6 +333,21 @@ class _SongMenuState extends State { ); }, ), + PlaybackAction( + icon: TablerIcons.brand_speedtest, + onPressed: () async { + _queueService.setPlaybackSpeed(playbackBehavior.speed % 3.5 + 0.5); + }, + tooltip: "$playbackSpeedTooltip (${playbackBehavior.speed})", + iconColor: + playbackBehavior.speed == 1.0 + ? Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white + : iconColor, + ), PlaybackAction( icon: loopModeIcons[playbackBehavior.loop]!, onPressed: () async { diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index 44d34198e..30b83f49e 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -1029,8 +1029,9 @@ Future setFavourite(FinampQueueItem track, BuildContext context) async { class PlaybackBehaviorInfo { final FinampPlaybackOrder order; final FinampLoopMode loop; + final double speed; - PlaybackBehaviorInfo(this.order, this.loop); + PlaybackBehaviorInfo(this.order, this.loop, this.speed); } class QueueSectionHeader extends SliverPersistentHeaderDelegate { @@ -1053,10 +1054,11 @@ class QueueSectionHeader extends SliverPersistentHeaderDelegate { final queueService = GetIt.instance(); return StreamBuilder( - stream: Rx.combineLatest2( + stream: Rx.combineLatest3( queueService.getPlaybackOrderStream(), queueService.getLoopModeStream(), - (a, b) => PlaybackBehaviorInfo(a, b)), + queueService.getPlaybackSpeedStream(), + (a, b, c) => PlaybackBehaviorInfo(a, b, c)), builder: (context, snapshot) { PlaybackBehaviorInfo? info = snapshot.data; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1ced42f19..26a8e3e02 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -778,6 +778,10 @@ "@playbackOrderShuffledButtonLabel": { "description": "Label for the button that toggles the playback order between linear/in-order and shuffle, while the queue is in shuffle mode" }, + "playbackSpeedButtonLabel": "Playback speed", + "@playbackSpeedButtonLabel": { + "description": "Label for the button that changes playback speed" + }, "loopModeNoneButtonLabel": "Looping off", "@loopModeNoneButtonLabel": { "description": "Label for the button that toggles the loop mode between off, loop one, and loop all, while the queue is in loop off mode" diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 6900becab..0c8bbc5e4 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -63,6 +63,7 @@ const _bufferDurationSeconds = 600; const _tabOrder = TabContentType.values; const _swipeInsertQueueNext = false; const _defaultLoopMode = FinampLoopMode.all; +const _defaultPlaybackSpeed = 1.0; const _autoLoadLastQueueOnStartup = true; @HiveType(typeId: 28) @@ -96,6 +97,7 @@ class FinampSettings { required this.tabSortBy, required this.tabSortOrder, this.loopMode = _defaultLoopMode, + this.playbackSpeed = _defaultPlaybackSpeed, this.tabOrder = _tabOrder, this.autoloadLastQueueOnStartup = _autoLoadLastQueueOnStartup, this.hasCompletedBlurhashImageMigration = true, @@ -205,6 +207,9 @@ class FinampSettings { @HiveField(28, defaultValue: _autoLoadLastQueueOnStartup) bool autoloadLastQueueOnStartup; + @HiveField(29, defaultValue: _defaultPlaybackSpeed) + double playbackSpeed; + static Future create() async { final internalSongDir = await getInternalSongDir(); final downloadLocation = DownloadLocation.create( diff --git a/lib/services/finamp_settings_helper.dart b/lib/services/finamp_settings_helper.dart index 1c18b217f..b3fdf2f52 100644 --- a/lib/services/finamp_settings_helper.dart +++ b/lib/services/finamp_settings_helper.dart @@ -220,6 +220,14 @@ class FinampSettingsHelper { .put("FinampSettings", finampSettingsTemp); } + /// Set the playbackSpeed property + static void setPlaybackSpeed(double speed) { + FinampSettings finampSettingsTemp = finampSettings; + finampSettingsTemp.playbackSpeed = speed; + Hive.box("FinampSettings") + .put("FinampSettings", finampSettingsTemp); + } + static void setHasCompletedBlurhashImageMigrationIdFix( bool hasCompletedBlurhashImageMigrationIdFix) { FinampSettings finampSettingsTemp = finampSettings; diff --git a/lib/services/music_player_background_task.dart b/lib/services/music_player_background_task.dart index 118759bce..cc1f8efe4 100644 --- a/lib/services/music_player_background_task.dart +++ b/lib/services/music_player_background_task.dart @@ -111,6 +111,11 @@ class MusicPlayerBackgroundTask extends BaseAudioHandler { return _player.play(); } + @override + Future setSpeed(final double speed) async { + return _player.setSpeed(speed); + } + @override Future pause() => _player.pause(); diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index f8711b700..c2a19f217 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -48,6 +48,7 @@ class QueueService { FinampPlaybackOrder _playbackOrder = FinampPlaybackOrder.linear; FinampLoopMode _loopMode = FinampLoopMode.none; + double _playbackSpeed = 1.0; final _currentTrackStream = BehaviorSubject.seeded(null); final _queueStream = BehaviorSubject.seeded(null); @@ -56,6 +57,8 @@ class QueueService { BehaviorSubject.seeded(FinampPlaybackOrder.linear); final _loopModeStream = BehaviorSubject.seeded(FinampLoopMode.none); + final _playbackSpeedStream = + BehaviorSubject.seeded(1.0); // external queue state @@ -81,6 +84,9 @@ class QueueService { loopMode = finampSettings.loopMode; _queueServiceLogger.info("Restored loop mode to $loopMode from settings"); + playbackSpeed = finampSettings.playbackSpeed; + _queueServiceLogger.info("Restored playback speed to $playbackSpeed from settings"); + _shuffleOrder = NextUpShuffleOrder(queueService: this); _queueAudioSource = ConcatenatingAudioSource( children: [], @@ -720,6 +726,10 @@ class QueueService { return _loopModeStream; } + BehaviorSubject getPlaybackSpeedStream() { + return _playbackSpeedStream; + } + BehaviorSubject getCurrentTrackStream() { return _currentTrackStream; } @@ -728,6 +738,21 @@ class QueueService { return _currentTrack; } + set playbackSpeed(double speed) { + _playbackSpeed = speed; + + _playbackSpeedStream.add(speed); + + _audioHandler.setSpeed(speed); + + FinampSettingsHelper.setPlaybackSpeed(playbackSpeed); + + _queueServiceLogger.fine( + "Playback speed set to ${FinampSettingsHelper.finampSettings.playbackSpeed}"); + } + + double get playbackSpeed => _playbackSpeed; + set loopMode(FinampLoopMode mode) { _loopMode = mode; @@ -786,6 +811,10 @@ class QueueService { } } + void setPlaybackSpeed(double) { + playbackSpeed = double; + } + Logger get queueServiceLogger => _queueServiceLogger; void _logQueues({String message = ""}) { From f3602c13618d25210682aacea325cd6d374b5f9f Mon Sep 17 00:00:00 2001 From: lymnyx Date: Tue, 13 Feb 2024 01:49:26 +0100 Subject: [PATCH 02/33] Added setting in 'Layout & Theme' with 'automatic' as default. --- lib/components/AlbumScreen/song_menu.dart | 222 ++++++++++-------- ...layback_speed_type_dropdown_list_tile.dart | 37 +++ lib/l10n/app_en.arb | 10 + lib/main.dart | 1 + lib/models/finamp_models.dart | 48 ++++ lib/models/finamp_models.g.dart | 57 ++++- lib/models/jellyfin_models.dart | 5 +- lib/screens/layout_settings_screen.dart | 2 + lib/services/finamp_settings_helper.dart | 7 + lib/services/queue_service.dart | 6 +- 10 files changed, 295 insertions(+), 100 deletions(-) create mode 100644 lib/components/LayoutSettingsScreen/content_playback_speed_type_dropdown_list_tile.dart diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 6d7948b23..b81360d9a 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -176,6 +176,9 @@ class _SongMenuState extends State { Theme.of(context).iconTheme.color ?? Colors.white; + // Makes sure that widget doesn't just disappear after press while menu is visible + var speedWidgetWasVisible = false; + return Stack(children: [ DraggableScrollableSheet( snap: true, @@ -250,7 +253,10 @@ class _SongMenuState extends State { ?.playbackOrderShuffledButtonLabel ?? "Shuffling", }; - final playbackSpeedTooltip = AppLocalizations.of(context)?.playbackSpeedButtonLabel ?? "Playback speed"; + final playbackSpeedTooltip = + AppLocalizations.of(context) + ?.playbackSpeedButtonLabel ?? + "Playback speed"; const loopModeIcons = { FinampLoopMode.none: TablerIcons.repeat, FinampLoopMode.one: TablerIcons.repeat_once, @@ -268,103 +274,135 @@ class _SongMenuState extends State { "Looping all", }; - return SliverCrossAxisGroup( - // return SliverGrid.count( - // crossAxisCount: 3, - // mainAxisSpacing: 40, - // children: [ - slivers: [ + var sliverArray = [ + PlaybackAction( + icon: playbackOrderIcons[playbackBehavior.order]!, + onPressed: () async { + _queueService.togglePlaybackOrder(); + }, + tooltip: + playbackOrderTooltips[playbackBehavior.order]!, + iconColor: playbackBehavior.order == + FinampPlaybackOrder.shuffled + ? iconColor + : Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white, + ), + ValueListenableBuilder( + valueListenable: _audioHandler.sleepTimer, + builder: (context, timerValue, child) { + final remainingMinutes = + (_audioHandler.sleepTimerRemaining.inSeconds / + 60.0) + .ceil(); + return PlaybackAction( + icon: timerValue != null + ? TablerIcons.hourglass_high + : TablerIcons.hourglass_empty, + onPressed: () async { + if (timerValue != null) { + showDialog( + context: context, + builder: (context) => + const SleepTimerCancelDialog(), + ); + } else { + await showDialog( + context: context, + builder: (context) => + const SleepTimerDialog(), + ); + } + }, + tooltip: timerValue != null + ? AppLocalizations.of(context) + ?.sleepTimerRemainingTime( + remainingMinutes) ?? + "Sleeping in $remainingMinutes minutes" + : AppLocalizations.of(context)! + .sleepTimerTooltip, + iconColor: timerValue != null + ? iconColor + : Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white, + ); + }, + ), + // [Playback speed widget will be added here if conditions are met] + PlaybackAction( + icon: loopModeIcons[playbackBehavior.loop]!, + onPressed: () async { + _queueService.toggleLoopMode(); + }, + tooltip: loopModeTooltips[playbackBehavior.loop]!, + iconColor: + playbackBehavior.loop == FinampLoopMode.none + ? Theme.of(context) + .textTheme + .bodyMedium + ?.color ?? + Colors.white + : iconColor, + ), + ]; + + var seemsLikeAudiobook = false; + if (FinampSettingsHelper.finampSettings + .contentPlaybackSpeedType.index == + 0) { + var genres = widget.item.genreItems!; + + for (var i = 0; i < genres.length; i++) { + if (["audiobook", "speech"] + .contains(genres[i].name?.toLowerCase())) { + seemsLikeAudiobook = true; + } + } + } + + // Adding the playback speed widget + if (speedWidgetWasVisible || + playbackBehavior.speed != 1.0 || + FinampSettingsHelper.finampSettings + .contentPlaybackSpeedType.index == + 1 || + FinampSettingsHelper.finampSettings + .contentPlaybackSpeedType.index == + 0 && + seemsLikeAudiobook) { + speedWidgetWasVisible = true; + sliverArray.insertAll(2, [ PlaybackAction( - icon: playbackOrderIcons[playbackBehavior.order]!, + icon: TablerIcons.brand_speedtest, onPressed: () async { - _queueService.togglePlaybackOrder(); + _queueService.setPlaybackSpeed( + playbackBehavior.speed % 3.5 + 0.5); }, - tooltip: playbackOrderTooltips[ - playbackBehavior.order]!, - iconColor: playbackBehavior.order == - FinampPlaybackOrder.shuffled - ? iconColor - : Theme.of(context) + tooltip: + "$playbackSpeedTooltip (${playbackBehavior.speed})", + iconColor: playbackBehavior.speed == 1.0 + ? Theme.of(context) .textTheme .bodyMedium ?.color ?? - Colors.white, - ), - ValueListenableBuilder( - valueListenable: _audioHandler.sleepTimer, - builder: (context, timerValue, child) { - final remainingMinutes = (_audioHandler - .sleepTimerRemaining.inSeconds / - 60.0) - .ceil(); - return PlaybackAction( - icon: timerValue != null - ? TablerIcons.hourglass_high - : TablerIcons.hourglass_empty, - onPressed: () async { - if (timerValue != null) { - showDialog( - context: context, - builder: (context) => - const SleepTimerCancelDialog(), - ); - } else { - await showDialog( - context: context, - builder: (context) => - const SleepTimerDialog(), - ); - } - }, - tooltip: timerValue != null - ? AppLocalizations.of(context) - ?.sleepTimerRemainingTime( - remainingMinutes) ?? - "Sleeping in $remainingMinutes minutes" - : AppLocalizations.of(context)! - .sleepTimerTooltip, - iconColor: timerValue != null - ? iconColor - : Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white, - ); - }, - ), - PlaybackAction( - icon: TablerIcons.brand_speedtest, - onPressed: () async { - _queueService.setPlaybackSpeed(playbackBehavior.speed % 3.5 + 0.5); - }, - tooltip: "$playbackSpeedTooltip (${playbackBehavior.speed})", - iconColor: - playbackBehavior.speed == 1.0 - ? Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white - : iconColor, - ), - PlaybackAction( - icon: loopModeIcons[playbackBehavior.loop]!, - onPressed: () async { - _queueService.toggleLoopMode(); - }, - tooltip: loopModeTooltips[playbackBehavior.loop]!, - iconColor: - playbackBehavior.loop == FinampLoopMode.none - ? Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white - : iconColor, - ), - ], - ); + Colors.white + : iconColor, + ) + ]); + } + + return SliverCrossAxisGroup( + // return SliverGrid.count( + // crossAxisCount: 3, + // mainAxisSpacing: 40, + // children: [ + slivers: sliverArray); }, ), SliverPadding( diff --git a/lib/components/LayoutSettingsScreen/content_playback_speed_type_dropdown_list_tile.dart b/lib/components/LayoutSettingsScreen/content_playback_speed_type_dropdown_list_tile.dart new file mode 100644 index 000000000..8ee8dd3a3 --- /dev/null +++ b/lib/components/LayoutSettingsScreen/content_playback_speed_type_dropdown_list_tile.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:hive/hive.dart'; + +import '../../models/finamp_models.dart'; +import '../../services/finamp_settings_helper.dart'; + +class ContentPlaybackSpeedTypeDropdownListTile extends StatelessWidget { + const ContentPlaybackSpeedTypeDropdownListTile({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: FinampSettingsHelper.finampSettingsListener, + builder: (_, box, __) { + return ListTile( + title: Text(AppLocalizations.of(context)!.playbackSpeedType), + subtitle: Text(AppLocalizations.of(context)!.playbackSpeedTypeSubtitle), + trailing: DropdownButton( + value: box.get("FinampSettings")?.contentPlaybackSpeedType, + items: ContentPlaybackSpeedType.values + .map((e) => DropdownMenuItem( + value: e, + child: Text(e.toLocalisedString(context)), + )) + .toList(), + onChanged: (value) { + if (value != null) { + FinampSettingsHelper.setContentPlaybackSpeedType(value); + } + }, + ), + ); + }, + ); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 26a8e3e02..a8d26180f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -443,6 +443,16 @@ "@list": {}, "grid": "Grid", "@grid": {}, + "playbackSpeedType": "Playback Speed Type", + "@playbackSpeedType": {}, + "playbackSpeedTypeSubtitle": "When and whether the playback speed widget is displayed in the song menu", + "@playbackSpeedTypeSubtitle": {}, + "automatic": "Automatic", + "@automatic": {}, + "on": "On", + "@on": {}, + "off": "Off", + "@off": {}, "portrait": "Portrait", "@portrait": {}, "landscape": "Landscape", diff --git a/lib/main.dart b/lib/main.dart index 14fdfc188..941999161 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -186,6 +186,7 @@ Future setupHive() async { Hive.registerAdapter(SortByAdapter()); Hive.registerAdapter(SortOrderAdapter()); Hive.registerAdapter(ContentViewTypeAdapter()); + Hive.registerAdapter(ContentPlaybackSpeedTypeAdapter()); Hive.registerAdapter(DownloadedImageAdapter()); Hive.registerAdapter(ThemeModeAdapter()); Hive.registerAdapter(LocaleAdapter()); diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 0c8bbc5e4..6ac7c690e 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -51,6 +51,7 @@ const _androidStopForegroundOnPauseDefault = false; const _isFavouriteDefault = false; const _songShuffleItemCountDefault = 250; const _contentViewType = ContentViewType.list; +const _contentPlaybackSpeedType = ContentPlaybackSpeedType.automatic; const _contentGridViewCrossAxisCountPortrait = 2; const _contentGridViewCrossAxisCountLandscape = 3; const _showTextOnGridView = true; @@ -83,6 +84,7 @@ class FinampSettings { this.sortOrder = SortOrder.ascending, this.songShuffleItemCount = _songShuffleItemCountDefault, this.contentViewType = _contentViewType, + this.contentPlaybackSpeedType = _contentPlaybackSpeedType, this.contentGridViewCrossAxisCountPortrait = _contentGridViewCrossAxisCountPortrait, this.contentGridViewCrossAxisCountLandscape = @@ -210,6 +212,10 @@ class FinampSettings { @HiveField(29, defaultValue: _defaultPlaybackSpeed) double playbackSpeed; + /// The content playback speed type defining how and whether to display the playbackSpeed widget in the song menu + @HiveField(30, defaultValue: _contentPlaybackSpeedType) + ContentPlaybackSpeedType contentPlaybackSpeedType; + static Future create() async { final internalSongDir = await getInternalSongDir(); final downloadLocation = DownloadLocation.create( @@ -972,3 +978,45 @@ enum SavedQueueState { pendingSave, } +@HiveType(typeId: 63) +enum ContentPlaybackSpeedType { + @HiveField(0) + automatic, + @HiveField(1) + on, + @HiveField(2) + off; + + /// Human-readable version of this enum. I've written longer descriptions on + /// enums like [TabContentType], and I can't be bothered to copy and paste it + /// again. + @override + @Deprecated("Use toLocalisedString when possible") + String toString() => _humanReadableName(this); + + String toLocalisedString(BuildContext context) => + _humanReadableLocalisedName(this, context); + + String _humanReadableName(ContentPlaybackSpeedType contentPlaybackSpeedType) { + switch (contentPlaybackSpeedType) { + case ContentPlaybackSpeedType.automatic: + return "Automatic"; + case ContentPlaybackSpeedType.on: + return "On"; + case ContentPlaybackSpeedType.off: + return "Off"; + } + } + + String _humanReadableLocalisedName( + ContentPlaybackSpeedType contentPlaybackSpeedType, BuildContext context) { + switch (contentPlaybackSpeedType) { + case ContentPlaybackSpeedType.automatic: + return AppLocalizations.of(context)!.automatic; + case ContentPlaybackSpeedType.on: + return AppLocalizations.of(context)!.on; + case ContentPlaybackSpeedType.off: + return AppLocalizations.of(context)!.off; + } + } +} diff --git a/lib/models/finamp_models.g.dart b/lib/models/finamp_models.g.dart index bbb1592e2..5138f56a5 100644 --- a/lib/models/finamp_models.g.dart +++ b/lib/models/finamp_models.g.dart @@ -80,6 +80,9 @@ class FinampSettingsAdapter extends TypeAdapter { contentViewType: fields[10] == null ? ContentViewType.list : fields[10] as ContentViewType, + contentPlaybackSpeedType: fields[30] == null + ? ContentPlaybackSpeedType.automatic + : fields[30] as ContentPlaybackSpeedType, contentGridViewCrossAxisCountPortrait: fields[11] == null ? 2 : fields[11] as int, contentGridViewCrossAxisCountLandscape: @@ -103,6 +106,7 @@ class FinampSettingsAdapter extends TypeAdapter { loopMode: fields[27] == null ? FinampLoopMode.all : fields[27] as FinampLoopMode, + playbackSpeed: fields[29] == null ? 1.0 : fields[29] as double, tabOrder: fields[22] == null ? [ TabContentType.albums, @@ -127,7 +131,7 @@ class FinampSettingsAdapter extends TypeAdapter { @override void write(BinaryWriter writer, FinampSettings obj) { writer - ..writeByte(29) + ..writeByte(31) ..writeByte(0) ..write(obj.isOffline) ..writeByte(1) @@ -185,7 +189,11 @@ class FinampSettingsAdapter extends TypeAdapter { ..writeByte(27) ..write(obj.loopMode) ..writeByte(28) - ..write(obj.autoloadLastQueueOnStartup); + ..write(obj.autoloadLastQueueOnStartup) + ..writeByte(29) + ..write(obj.playbackSpeed) + ..writeByte(30) + ..write(obj.contentPlaybackSpeedType); } @override @@ -1229,6 +1237,51 @@ class SavedQueueStateAdapter extends TypeAdapter { typeId == other.typeId; } +class ContentPlaybackSpeedTypeAdapter + extends TypeAdapter { + @override + final int typeId = 63; + + @override + ContentPlaybackSpeedType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return ContentPlaybackSpeedType.automatic; + case 1: + return ContentPlaybackSpeedType.on; + case 2: + return ContentPlaybackSpeedType.off; + default: + return ContentPlaybackSpeedType.automatic; + } + } + + @override + void write(BinaryWriter writer, ContentPlaybackSpeedType obj) { + switch (obj) { + case ContentPlaybackSpeedType.automatic: + writer.writeByte(0); + break; + case ContentPlaybackSpeedType.on: + writer.writeByte(1); + break; + case ContentPlaybackSpeedType.off: + writer.writeByte(2); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ContentPlaybackSpeedTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + // ************************************************************************** // JsonSerializableGenerator // ************************************************************************** diff --git a/lib/models/jellyfin_models.dart b/lib/models/jellyfin_models.dart index 8db31cb8b..d799f6dbb 100644 --- a/lib/models/jellyfin_models.dart +++ b/lib/models/jellyfin_models.dart @@ -2200,6 +2200,7 @@ class BaseItemDto { /// Whether or not the item is an artist bool get isArtist => type == "MusicArtist"; + /// The first primary blurhash of this item. String? get blurHash => imageBlurHashes?.primary?.values.first; @@ -3659,7 +3660,7 @@ class QuickConnectState { @HiveField(1) String? secret; - + @HiveField(2) String? code; @@ -3690,7 +3691,6 @@ class QuickConnectState { ) @HiveType(typeId: 43) class ClientDiscoveryResponse { - ClientDiscoveryResponse({ this.address, this.id, @@ -3712,5 +3712,4 @@ class ClientDiscoveryResponse { factory ClientDiscoveryResponse.fromJson(Map json) => _$ClientDiscoveryResponseFromJson(json); - } diff --git a/lib/screens/layout_settings_screen.dart b/lib/screens/layout_settings_screen.dart index 2c025262a..3225c73cd 100644 --- a/lib/screens/layout_settings_screen.dart +++ b/lib/screens/layout_settings_screen.dart @@ -5,6 +5,7 @@ import '../components/LayoutSettingsScreen/theme_selector.dart'; import 'tabs_settings_screen.dart'; import '../components/LayoutSettingsScreen/content_grid_view_cross_axis_count_list_tile.dart'; import '../components/LayoutSettingsScreen/content_view_type_dropdown_list_tile.dart'; +import '../components/LayoutSettingsScreen/content_playback_speed_type_dropdown_list_tile.dart'; import '../components/LayoutSettingsScreen/show_text_on_grid_view_selector.dart'; import '../components/LayoutSettingsScreen/show_cover_as_player_background_selector.dart'; import '../components/LayoutSettingsScreen/hide_song_artists_if_same_as_album_artists_selector.dart'; @@ -28,6 +29,7 @@ class LayoutSettingsScreen extends StatelessWidget { const ShowTextOnGridViewSelector(), const ShowCoverAsPlayerBackgroundSelector(), const HideSongArtistsIfSameAsAlbumArtistsSelector(), + const ContentPlaybackSpeedTypeDropdownListTile(), const ThemeSelector(), const Divider(), ListTile( diff --git a/lib/services/finamp_settings_helper.dart b/lib/services/finamp_settings_helper.dart index b3fdf2f52..7b41447aa 100644 --- a/lib/services/finamp_settings_helper.dart +++ b/lib/services/finamp_settings_helper.dart @@ -147,6 +147,13 @@ class FinampSettingsHelper { .put("FinampSettings", finampSettingsTemp); } + static void setContentPlaybackSpeedType(ContentPlaybackSpeedType contentPlaybackSpeedType) { + FinampSettings finampSettingsTemp = finampSettings; + finampSettingsTemp.contentPlaybackSpeedType = contentPlaybackSpeedType; + Hive.box("FinampSettings") + .put("FinampSettings", finampSettingsTemp); + } + static void setShowTextOnGridView(bool showTextOnGridView) { FinampSettings finampSettingsTemp = finampSettings; finampSettingsTemp.showTextOnGridView = showTextOnGridView; diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index c2a19f217..a540fa37c 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -57,8 +57,7 @@ class QueueService { BehaviorSubject.seeded(FinampPlaybackOrder.linear); final _loopModeStream = BehaviorSubject.seeded(FinampLoopMode.none); - final _playbackSpeedStream = - BehaviorSubject.seeded(1.0); + final _playbackSpeedStream = BehaviorSubject.seeded(1.0); // external queue state @@ -85,7 +84,8 @@ class QueueService { _queueServiceLogger.info("Restored loop mode to $loopMode from settings"); playbackSpeed = finampSettings.playbackSpeed; - _queueServiceLogger.info("Restored playback speed to $playbackSpeed from settings"); + _queueServiceLogger + .info("Restored playback speed to $playbackSpeed from settings"); _shuffleOrder = NextUpShuffleOrder(queueService: this); _queueAudioSource = ConcatenatingAudioSource( From c9ca6f46ffba6f4fa10f81062c1158fb865f833b Mon Sep 17 00:00:00 2001 From: lymnyx Date: Tue, 13 Feb 2024 02:11:55 +0100 Subject: [PATCH 03/33] Moved up genre checking for efficiency. --- lib/components/AlbumScreen/song_menu.dart | 26 +++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index b81360d9a..cbc8ca084 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -179,6 +179,18 @@ class _SongMenuState extends State { // Makes sure that widget doesn't just disappear after press while menu is visible var speedWidgetWasVisible = false; + var seemsLikeAudiobook = false; + if (FinampSettingsHelper.finampSettings.contentPlaybackSpeedType.index == + 0) { + var genres = widget.item.genreItems!; + + for (var i = 0; i < genres.length; i++) { + if (["audiobook", "speech"].contains(genres[i].name?.toLowerCase())) { + seemsLikeAudiobook = true; + } + } + } + return Stack(children: [ DraggableScrollableSheet( snap: true, @@ -352,20 +364,6 @@ class _SongMenuState extends State { ), ]; - var seemsLikeAudiobook = false; - if (FinampSettingsHelper.finampSettings - .contentPlaybackSpeedType.index == - 0) { - var genres = widget.item.genreItems!; - - for (var i = 0; i < genres.length; i++) { - if (["audiobook", "speech"] - .contains(genres[i].name?.toLowerCase())) { - seemsLikeAudiobook = true; - } - } - } - // Adding the playback speed widget if (speedWidgetWasVisible || playbackBehavior.speed != 1.0 || From 2f098bd2165a701992f887a36eb49ad31beb0f07 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 13 Feb 2024 19:30:25 +0100 Subject: [PATCH 04/33] restructure settings --- lib/components/AlbumScreen/song_menu.dart | 4 +- ...ontrol_visibility_dropdown_list_tile.dart} | 12 ++--- .../TabsSettingsScreen/hide_tab_toggle.dart | 4 +- lib/l10n/app_en.arb | 16 +++++-- lib/main.dart | 3 ++ lib/models/finamp_models.dart | 16 +++---- lib/models/finamp_models.g.dart | 8 ++-- .../customization_settings_screen.dart | 45 +++++++++++++++++++ lib/screens/layout_settings_screen.dart | 26 ++++++----- lib/screens/settings_screen.dart | 12 ++--- lib/screens/tabs_settings_screen.dart | 2 +- 11 files changed, 105 insertions(+), 43 deletions(-) rename lib/components/LayoutSettingsScreen/{content_playback_speed_type_dropdown_list_tile.dart => CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart} (78%) rename lib/components/{ => LayoutSettingsScreen}/TabsSettingsScreen/hide_tab_toggle.dart (90%) create mode 100644 lib/screens/customization_settings_screen.dart diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index cbc8ca084..0696f20e5 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -379,8 +379,8 @@ class _SongMenuState extends State { PlaybackAction( icon: TablerIcons.brand_speedtest, onPressed: () async { - _queueService.setPlaybackSpeed( - playbackBehavior.speed % 3.5 + 0.5); + _queueService.setPlaybackSpeed(clampDouble(playbackBehavior.speed % 3.5 + 0.5, 1.0, 4.0) + ); }, tooltip: "$playbackSpeedTooltip (${playbackBehavior.speed})", diff --git a/lib/components/LayoutSettingsScreen/content_playback_speed_type_dropdown_list_tile.dart b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart similarity index 78% rename from lib/components/LayoutSettingsScreen/content_playback_speed_type_dropdown_list_tile.dart rename to lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart index 8ee8dd3a3..08bfccc68 100644 --- a/lib/components/LayoutSettingsScreen/content_playback_speed_type_dropdown_list_tile.dart +++ b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:hive/hive.dart'; -import '../../models/finamp_models.dart'; -import '../../services/finamp_settings_helper.dart'; +import '../../../models/finamp_models.dart'; +import '../../../services/finamp_settings_helper.dart'; -class ContentPlaybackSpeedTypeDropdownListTile extends StatelessWidget { - const ContentPlaybackSpeedTypeDropdownListTile({Key? key}) : super(key: key); +class PlaybackSpeedControlVisibilityDropdownListTile extends StatelessWidget { + const PlaybackSpeedControlVisibilityDropdownListTile({Key? key}) : super(key: key); @override Widget build(BuildContext context) { @@ -14,8 +14,8 @@ class ContentPlaybackSpeedTypeDropdownListTile extends StatelessWidget { valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (_, box, __) { return ListTile( - title: Text(AppLocalizations.of(context)!.playbackSpeedType), - subtitle: Text(AppLocalizations.of(context)!.playbackSpeedTypeSubtitle), + title: Text(AppLocalizations.of(context)!.playbackSpeedControlSetting), + subtitle: Text(AppLocalizations.of(context)!.playbackSpeedControlSettingSubtitle), trailing: DropdownButton( value: box.get("FinampSettings")?.contentPlaybackSpeedType, items: ContentPlaybackSpeedType.values diff --git a/lib/components/TabsSettingsScreen/hide_tab_toggle.dart b/lib/components/LayoutSettingsScreen/TabsSettingsScreen/hide_tab_toggle.dart similarity index 90% rename from lib/components/TabsSettingsScreen/hide_tab_toggle.dart rename to lib/components/LayoutSettingsScreen/TabsSettingsScreen/hide_tab_toggle.dart index ddae160d1..78037ef18 100644 --- a/lib/components/TabsSettingsScreen/hide_tab_toggle.dart +++ b/lib/components/LayoutSettingsScreen/TabsSettingsScreen/hide_tab_toggle.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; -import '../../services/finamp_settings_helper.dart'; -import '../../models/finamp_models.dart'; +import '../../../services/finamp_settings_helper.dart'; +import '../../../models/finamp_models.dart'; class HideTabToggle extends StatelessWidget { const HideTabToggle({ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a8d26180f..f4e8cb981 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -443,12 +443,20 @@ "@list": {}, "grid": "Grid", "@grid": {}, - "playbackSpeedType": "Playback Speed Type", - "@playbackSpeedType": {}, - "playbackSpeedTypeSubtitle": "When and whether the playback speed widget is displayed in the song menu", - "@playbackSpeedTypeSubtitle": {}, + "customizationSettingsTitle": "Customization", + "@customizationSettingsTitle": { + "description": "Title for the customization settings screen" + }, + "playbackSpeedControlSetting": "Playback Speed Visibility", + "@playbackSpeedControlSetting": {}, + "playbackSpeedControlSettingSubtitle": "Whether the playback speed controls are shown in the player screen menu", + "@playbackSpeedControlSettingSubtitle": {}, "automatic": "Automatic", "@automatic": {}, + "shown": "Shown", + "@shown": {}, + "hidden": "Hidden", + "@hidden": {}, "on": "On", "@on": {}, "off": "Off", diff --git a/lib/main.dart b/lib/main.dart index 941999161..2a9c3de47 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -36,6 +36,7 @@ import 'screens/add_to_playlist_screen.dart'; import 'screens/album_screen.dart'; import 'screens/artist_screen.dart'; import 'screens/audio_service_settings_screen.dart'; +import 'screens/customization_settings_screen.dart'; import 'screens/downloads_error_screen.dart'; import 'screens/downloads_screen.dart'; import 'screens/downloads_settings_screen.dart'; @@ -361,6 +362,8 @@ class Finamp extends StatelessWidget { const InteractionSettingsScreen(), TabsSettingsScreen.routeName: (context) => const TabsSettingsScreen(), + CustomizationSettingsScreen.routeName: (context) => + const CustomizationSettingsScreen(), LayoutSettingsScreen.routeName: (context) => const LayoutSettingsScreen(), LanguageSelectionScreen.routeName: (context) => diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 6ac7c690e..6f2fdb981 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -983,9 +983,9 @@ enum ContentPlaybackSpeedType { @HiveField(0) automatic, @HiveField(1) - on, + visible, @HiveField(2) - off; + hidden; /// Human-readable version of this enum. I've written longer descriptions on /// enums like [TabContentType], and I can't be bothered to copy and paste it @@ -1001,9 +1001,9 @@ enum ContentPlaybackSpeedType { switch (contentPlaybackSpeedType) { case ContentPlaybackSpeedType.automatic: return "Automatic"; - case ContentPlaybackSpeedType.on: + case ContentPlaybackSpeedType.visible: return "On"; - case ContentPlaybackSpeedType.off: + case ContentPlaybackSpeedType.hidden: return "Off"; } } @@ -1013,10 +1013,10 @@ enum ContentPlaybackSpeedType { switch (contentPlaybackSpeedType) { case ContentPlaybackSpeedType.automatic: return AppLocalizations.of(context)!.automatic; - case ContentPlaybackSpeedType.on: - return AppLocalizations.of(context)!.on; - case ContentPlaybackSpeedType.off: - return AppLocalizations.of(context)!.off; + case ContentPlaybackSpeedType.visible: + return AppLocalizations.of(context)!.shown; + case ContentPlaybackSpeedType.hidden: + return AppLocalizations.of(context)!.hidden; } } } diff --git a/lib/models/finamp_models.g.dart b/lib/models/finamp_models.g.dart index 5138f56a5..b849d635b 100644 --- a/lib/models/finamp_models.g.dart +++ b/lib/models/finamp_models.g.dart @@ -1248,9 +1248,9 @@ class ContentPlaybackSpeedTypeAdapter case 0: return ContentPlaybackSpeedType.automatic; case 1: - return ContentPlaybackSpeedType.on; + return ContentPlaybackSpeedType.visible; case 2: - return ContentPlaybackSpeedType.off; + return ContentPlaybackSpeedType.hidden; default: return ContentPlaybackSpeedType.automatic; } @@ -1262,10 +1262,10 @@ class ContentPlaybackSpeedTypeAdapter case ContentPlaybackSpeedType.automatic: writer.writeByte(0); break; - case ContentPlaybackSpeedType.on: + case ContentPlaybackSpeedType.visible: writer.writeByte(1); break; - case ContentPlaybackSpeedType.off: + case ContentPlaybackSpeedType.hidden: writer.writeByte(2); break; } diff --git a/lib/screens/customization_settings_screen.dart b/lib/screens/customization_settings_screen.dart new file mode 100644 index 000000000..292f86ed7 --- /dev/null +++ b/lib/screens/customization_settings_screen.dart @@ -0,0 +1,45 @@ +import 'package:finamp/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart'; +import 'package:finamp/services/finamp_settings_helper.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../components/LayoutSettingsScreen/TabsSettingsScreen/hide_tab_toggle.dart'; + +class CustomizationSettingsScreen extends StatefulWidget { + const CustomizationSettingsScreen({Key? key}) : super(key: key); + + static const routeName = "/settings/customization"; + + @override + State createState() => _CustomizationSettingsScreenState(); +} + +class _CustomizationSettingsScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(AppLocalizations.of(context)!.customizationSettingsTitle), + actions: [ + // TODO add button to reset to defaults + // IconButton( + // onPressed: () { + // setState(() { + // FinampSettingsHelper.resetTabs(); + // }); + // }, + // icon: const Icon(Icons.refresh), + // tooltip: AppLocalizations.of(context)!.resetTabs, + // ) + ], + ), + body: Scrollbar( + child: ListView( + children: [ + const PlaybackSpeedControlVisibilityDropdownListTile(), + ], + ), + ), + ); + } +} diff --git a/lib/screens/layout_settings_screen.dart b/lib/screens/layout_settings_screen.dart index 3225c73cd..3ce009ba7 100644 --- a/lib/screens/layout_settings_screen.dart +++ b/lib/screens/layout_settings_screen.dart @@ -1,11 +1,12 @@ +import 'package:finamp/screens/customization_settings_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import '../components/LayoutSettingsScreen/theme_selector.dart'; import 'tabs_settings_screen.dart'; import '../components/LayoutSettingsScreen/content_grid_view_cross_axis_count_list_tile.dart'; import '../components/LayoutSettingsScreen/content_view_type_dropdown_list_tile.dart'; -import '../components/LayoutSettingsScreen/content_playback_speed_type_dropdown_list_tile.dart'; import '../components/LayoutSettingsScreen/show_text_on_grid_view_selector.dart'; import '../components/LayoutSettingsScreen/show_cover_as_player_background_selector.dart'; import '../components/LayoutSettingsScreen/hide_song_artists_if_same_as_album_artists_selector.dart'; @@ -23,21 +24,26 @@ class LayoutSettingsScreen extends StatelessWidget { ), body: ListView( children: [ - const ContentViewTypeDropdownListTile(), - for (final type in ContentGridViewCrossAxisCountType.values) - ContentGridViewCrossAxisCountListTile(type: type), - const ShowTextOnGridViewSelector(), - const ShowCoverAsPlayerBackgroundSelector(), - const HideSongArtistsIfSameAsAlbumArtistsSelector(), - const ContentPlaybackSpeedTypeDropdownListTile(), - const ThemeSelector(), - const Divider(), + ListTile( + leading: const Icon(TablerIcons.sparkles), + title: Text(AppLocalizations.of(context)!.customizationSettingsTitle), + onTap: () => + Navigator.of(context).pushNamed(CustomizationSettingsScreen.routeName), + ), ListTile( leading: const Icon(Icons.tab), title: Text(AppLocalizations.of(context)!.tabs), onTap: () => Navigator.of(context).pushNamed(TabsSettingsScreen.routeName), ), + const Divider(), + const ThemeSelector(), + const ContentViewTypeDropdownListTile(), + for (final type in ContentGridViewCrossAxisCountType.values) + ContentGridViewCrossAxisCountListTile(type: type), + const ShowTextOnGridViewSelector(), + const ShowCoverAsPlayerBackgroundSelector(), + const HideSongArtistsIfSameAsAlbumArtistsSelector(), ], ), ); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 553eeffca..8d9164ea5 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -51,12 +51,6 @@ class SettingsScreen extends StatelessWidget { onTap: () => Navigator.of(context) .pushNamed(TranscodingSettingsScreen.routeName), ), - ListTile( - leading: const Icon(Icons.folder), - title: Text(AppLocalizations.of(context)!.downloadLocations), - onTap: () => Navigator.of(context) - .pushNamed(DownloadsSettingsScreen.routeName), - ), ListTile( leading: const Icon(Icons.music_note), title: Text(AppLocalizations.of(context)!.audioService), @@ -75,6 +69,12 @@ class SettingsScreen extends StatelessWidget { onTap: () => Navigator.of(context) .pushNamed(LayoutSettingsScreen.routeName), ), + ListTile( + leading: const Icon(Icons.folder), + title: Text(AppLocalizations.of(context)!.downloadLocations), + onTap: () => Navigator.of(context) + .pushNamed(DownloadsSettingsScreen.routeName), + ), ListTile( leading: const Icon(Icons.library_music), title: Text(AppLocalizations.of(context)!.selectMusicLibraries), diff --git a/lib/screens/tabs_settings_screen.dart b/lib/screens/tabs_settings_screen.dart index 0e47bb495..90ecf0dd5 100644 --- a/lib/screens/tabs_settings_screen.dart +++ b/lib/screens/tabs_settings_screen.dart @@ -2,7 +2,7 @@ import 'package:finamp/services/finamp_settings_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import '../components/TabsSettingsScreen/hide_tab_toggle.dart'; +import '../components/LayoutSettingsScreen/TabsSettingsScreen/hide_tab_toggle.dart'; class TabsSettingsScreen extends StatefulWidget { const TabsSettingsScreen({Key? key}) : super(key: key); From 52fd6182a899b1e06a1a6f4e750055d6e973dc67 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Thu, 15 Feb 2024 20:22:45 +0100 Subject: [PATCH 05/33] Text positioning. --- lib/components/AlbumScreen/song_menu.dart | 41 ++++++++++++++++++----- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 0696f20e5..ac21d026c 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -378,12 +378,14 @@ class _SongMenuState extends State { sliverArray.insertAll(2, [ PlaybackAction( icon: TablerIcons.brand_speedtest, + iconValue: playbackBehavior.speed.toString(), onPressed: () async { - _queueService.setPlaybackSpeed(clampDouble(playbackBehavior.speed % 3.5 + 0.5, 1.0, 4.0) - ); + _queueService.setPlaybackSpeed(clampDouble( + playbackBehavior.speed % 3.5 + 0.5, + 1.0, + 4.0)); }, - tooltip: - "$playbackSpeedTooltip (${playbackBehavior.speed})", + tooltip: "$playbackSpeedTooltip", iconColor: playbackBehavior.speed == 1.0 ? Theme.of(context) .textTheme @@ -911,12 +913,14 @@ class PlaybackAction extends StatelessWidget { const PlaybackAction({ super.key, required this.icon, + this.iconValue, required this.onPressed, required this.tooltip, required this.iconColor, }); final IconData icon; + final String? iconValue; final Function() onPressed; final String tooltip; final Color iconColor; @@ -927,11 +931,30 @@ class PlaybackAction extends StatelessWidget { child: IconButton( icon: Column( children: [ - Icon( - icon, - color: iconColor, - size: 32, - weight: 1.0, + SizedBox( + width: 35, + height: 35, + child: Stack( + alignment: Alignment.topCenter, + children: [ + Icon( + icon, + color: iconColor, + size: 35 + (iconValue != null ? -11 : 0), + weight: 1.0, + ), + Positioned( + top: 18, + child: Text( + iconValue ?? "", + style: TextStyle( + fontWeight: FontWeight.w800, + color: iconColor, + ), + ), + ), + ], + ), ), const SizedBox(height: 12), SizedBox( From 6d7f8b494e4aa5506fd586645154cafbe6dbf2da Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 15 Feb 2024 22:07:15 +0100 Subject: [PATCH 06/33] adjust PlaybackAction layout --- lib/components/AlbumScreen/song_menu.dart | 24 ++++++++--------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index ac21d026c..6031b256e 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -378,7 +378,7 @@ class _SongMenuState extends State { sliverArray.insertAll(2, [ PlaybackAction( icon: TablerIcons.brand_speedtest, - iconValue: playbackBehavior.speed.toString(), + value: playbackBehavior.speed.toString(), onPressed: () async { _queueService.setPlaybackSpeed(clampDouble( playbackBehavior.speed % 3.5 + 0.5, @@ -398,10 +398,6 @@ class _SongMenuState extends State { } return SliverCrossAxisGroup( - // return SliverGrid.count( - // crossAxisCount: 3, - // mainAxisSpacing: 40, - // children: [ slivers: sliverArray); }, ), @@ -913,14 +909,14 @@ class PlaybackAction extends StatelessWidget { const PlaybackAction({ super.key, required this.icon, - this.iconValue, + this.value, required this.onPressed, required this.tooltip, required this.iconColor, }); final IconData icon; - final String? iconValue; + final String? value; final Function() onPressed; final String tooltip; final Color iconColor; @@ -933,30 +929,26 @@ class PlaybackAction extends StatelessWidget { children: [ SizedBox( width: 35, - height: 35, + height: 46, child: Stack( alignment: Alignment.topCenter, children: [ Icon( icon, color: iconColor, - size: 35 + (iconValue != null ? -11 : 0), + size: 35, weight: 1.0, ), Positioned( - top: 18, + bottom: -2, child: Text( - iconValue ?? "", - style: TextStyle( - fontWeight: FontWeight.w800, - color: iconColor, - ), + value ?? "", + style: Theme.of(context).textTheme.labelSmall, ), ), ], ), ), - const SizedBox(height: 12), SizedBox( height: 2 * 12 * 1.4 + 2, child: Align( From bb0b272a4d5d77d245ca50189c8a84f8d9a42432 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Thu, 15 Feb 2024 22:30:03 +0100 Subject: [PATCH 07/33] Album length check for automatic detection. --- lib/components/AlbumScreen/song_menu.dart | 108 +++++++++++++--------- 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 6031b256e..859201d7c 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -170,6 +170,36 @@ class _SongMenuState extends State { } } + Future seemsLikeAudiobook(currentSpeed) async { + if (currentSpeed != 1.0 || + FinampSettingsHelper.finampSettings.contentPlaybackSpeedType.index == + 1) { + return true; + } + if (FinampSettingsHelper.finampSettings.contentPlaybackSpeedType.index == + 0) { + var genres = widget.item.genres!; + + for (var i = 0; i < genres.length; i++) { + if (["audiobook", "speech"].contains(genres[i].toLowerCase())) { + return true; + } + } + + try { + var parent = + await _jellyfinApiHelper.getItemById(widget.item.parentId!); + if (parent.runTimeTicks! > 72e9.toInt()) { + return true; + } + } catch (e) { + errorSnackbar(e, context); + } + } + + return false; + } + @override Widget build(BuildContext context) { final iconColor = _imageTheme?.primary ?? @@ -179,18 +209,6 @@ class _SongMenuState extends State { // Makes sure that widget doesn't just disappear after press while menu is visible var speedWidgetWasVisible = false; - var seemsLikeAudiobook = false; - if (FinampSettingsHelper.finampSettings.contentPlaybackSpeedType.index == - 0) { - var genres = widget.item.genreItems!; - - for (var i = 0; i < genres.length; i++) { - if (["audiobook", "speech"].contains(genres[i].name?.toLowerCase())) { - seemsLikeAudiobook = true; - } - } - } - return Stack(children: [ DraggableScrollableSheet( snap: true, @@ -364,41 +382,39 @@ class _SongMenuState extends State { ), ]; - // Adding the playback speed widget - if (speedWidgetWasVisible || - playbackBehavior.speed != 1.0 || - FinampSettingsHelper.finampSettings - .contentPlaybackSpeedType.index == - 1 || - FinampSettingsHelper.finampSettings - .contentPlaybackSpeedType.index == - 0 && - seemsLikeAudiobook) { - speedWidgetWasVisible = true; - sliverArray.insertAll(2, [ - PlaybackAction( - icon: TablerIcons.brand_speedtest, - value: playbackBehavior.speed.toString(), - onPressed: () async { - _queueService.setPlaybackSpeed(clampDouble( - playbackBehavior.speed % 3.5 + 0.5, - 1.0, - 4.0)); - }, - tooltip: "$playbackSpeedTooltip", - iconColor: playbackBehavior.speed == 1.0 - ? Theme.of(context) - .textTheme - .bodyMedium - ?.color ?? - Colors.white - : iconColor, - ) - ]); + final speedWidget = PlaybackAction( + icon: TablerIcons.brand_speedtest, + value: playbackBehavior.speed.toString(), + onPressed: () async { + _queueService.setPlaybackSpeed(clampDouble( + playbackBehavior.speed % 3.5 + 0.5, 1.0, 4.0)); + }, + tooltip: playbackSpeedTooltip, + iconColor: playbackBehavior.speed == 1.0 + ? Theme.of(context).textTheme.bodyMedium?.color ?? + Colors.white + : iconColor, + ); + + if (speedWidgetWasVisible) { + sliverArray.insertAll(2, [speedWidget]); + return SliverCrossAxisGroup( + slivers: sliverArray, + ); } - - return SliverCrossAxisGroup( - slivers: sliverArray); + return FutureBuilder( + future: seemsLikeAudiobook(playbackBehavior.speed), + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.data == true) { + speedWidgetWasVisible = true; + sliverArray.insertAll(2, [speedWidget]); + } + return SliverCrossAxisGroup( + slivers: sliverArray, + ); + }); }, ), SliverPadding( From efb44f6820eddd3896aea02a5395c172c3a6e3a6 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Sun, 18 Feb 2024 22:24:34 +0100 Subject: [PATCH 08/33] Added speed menu --- lib/components/AlbumScreen/preset_chip.dart | 118 ++++++++++++++++ lib/components/AlbumScreen/song_menu.dart | 38 ++++-- lib/components/AlbumScreen/speed_menu.dart | 134 +++++++++++++++++++ lib/components/Buttons/simple_button.dart | 16 ++- lib/components/PlayerScreen/artist_chip.dart | 21 +-- lib/l10n/app_en.arb | 9 +- lib/services/queue_service.dart | 10 +- 7 files changed, 315 insertions(+), 31 deletions(-) create mode 100644 lib/components/AlbumScreen/preset_chip.dart create mode 100644 lib/components/AlbumScreen/speed_menu.dart diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart new file mode 100644 index 000000000..b41f93299 --- /dev/null +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -0,0 +1,118 @@ +import 'dart:ffi'; + +import 'package:finamp/color_schemes.g.dart'; +import 'package:flutter/material.dart'; +import 'package:finamp/services/queue_service.dart'; +import 'package:get_it/get_it.dart'; + +const _radius = Radius.circular(4); +const _borderRadius = BorderRadius.all(_radius); +const _height = 36.0; +final _defaultBackgroundColour = Colors.white.withOpacity(0.1); +final _queueService = GetIt.instance(); + +class PresetChips extends StatefulWidget { + const PresetChips({ + Key? key, + required this.type, + required this.values, + required this.activeValue, + this.onTap, + this.mainColour, + this.onPressed, + }) : super(key: key); + + // for future preset types other than "speed" + final String type; + + final List values; + final double activeValue; + final Function()? onTap; + final Color? mainColour; // used for different background colours + final Function()? onPressed; + + @override + State createState() => _PresetChipsState(); +} + +class _PresetChipsState extends State { + @override + Widget build(BuildContext context) { + var nowActiveValue = widget.activeValue; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + crossAxisAlignment: WrapCrossAlignment.center, + children: List.generate(widget.values.length, (index) { + final currentValue = widget.values[index]; + var newValue = "x$currentValue"; + + return PresetChip( + value: newValue, + backgroundColour: currentValue == nowActiveValue + ? widget.mainColour?.withOpacity(0.4) + : widget.mainColour?.withOpacity(0.1), + onTap: () { + setState(() { + nowActiveValue = currentValue; + }); + _queueService.setPlaybackSpeed(currentValue); + widget.onPressed!(); + }, + ); + }), + ), + ), + ); + } +} + +class PresetChip extends StatelessWidget { + const PresetChip({ + Key? key, + this.value = "", + this.onTap, + this.backgroundColour, + }) : super(key: key); + + final String value; + final void Function()? onTap; + final Color? backgroundColour; + + @override + Widget build(BuildContext context) { + final backgroundColor = backgroundColour ?? _defaultBackgroundColour; + final color = Theme.of(context).textTheme.bodySmall?.color ?? Colors.white; + + return SizedBox( + height: _height, + child: Material( + color: backgroundColor, + borderRadius: _borderRadius, + child: InkWell( + onTap: onTap, + borderRadius: _borderRadius, + child: Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 220), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + value, + style: TextStyle( + color: color, overflow: TextOverflow.ellipsis), + softWrap: false, + overflow: TextOverflow.ellipsis, + ), + ), + ), + )), + ), + ); + } +} diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 859201d7c..d66e7e896 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -32,6 +32,7 @@ import '../PlayerScreen/artist_chip.dart'; import '../album_image.dart'; import '../error_snackbar.dart'; import 'song_list_tile.dart'; +import 'speed_menu.dart'; Future showModalSongMenu({ required BuildContext context, @@ -125,6 +126,10 @@ class _SongMenuState extends State { ColorScheme? _imageTheme; ImageProvider? _imageProvider; + // Makes sure that widget doesn't just disappear after press while menu is visible + var speedWidgetWasVisible = false; + var showSpeedMenu = false; + @override void initState() { super.initState(); @@ -132,6 +137,9 @@ class _SongMenuState extends State { widget.playerScreenTheme; // use player screen theme if provided } + final _speedInputController = TextEditingController( + text: FinampSettingsHelper.finampSettings.playbackSpeed.toString()); + /// Sets the item's favourite on the Jellyfin server. Future toggleFavorite() async { try { @@ -170,7 +178,7 @@ class _SongMenuState extends State { } } - Future seemsLikeAudiobook(currentSpeed) async { + Future shouldShowSpeedWidget(currentSpeed) async { if (currentSpeed != 1.0 || FinampSettingsHelper.finampSettings.contentPlaybackSpeedType.index == 1) { @@ -200,15 +208,19 @@ class _SongMenuState extends State { return false; } + void toggleSpeedMenu() { + setState(() { + showSpeedMenu = !showSpeedMenu; + }); + Vibrate.feedback(FeedbackType.success); + } + @override Widget build(BuildContext context) { final iconColor = _imageTheme?.primary ?? Theme.of(context).iconTheme.color ?? Colors.white; - // Makes sure that widget doesn't just disappear after press while menu is visible - var speedWidgetWasVisible = false; - return Stack(children: [ DraggableScrollableSheet( snap: true, @@ -285,8 +297,9 @@ class _SongMenuState extends State { }; final playbackSpeedTooltip = AppLocalizations.of(context) - ?.playbackSpeedButtonLabel ?? - "Playback speed"; + ?.playbackSpeedButtonLabel( + playbackBehavior.speed) ?? + "Playing at x${playbackBehavior.speed} speed"; const loopModeIcons = { FinampLoopMode.none: TablerIcons.repeat, FinampLoopMode.one: TablerIcons.repeat_once, @@ -384,10 +397,8 @@ class _SongMenuState extends State { final speedWidget = PlaybackAction( icon: TablerIcons.brand_speedtest, - value: playbackBehavior.speed.toString(), onPressed: () async { - _queueService.setPlaybackSpeed(clampDouble( - playbackBehavior.speed % 3.5 + 0.5, 1.0, 4.0)); + toggleSpeedMenu(); }, tooltip: playbackSpeedTooltip, iconColor: playbackBehavior.speed == 1.0 @@ -403,7 +414,8 @@ class _SongMenuState extends State { ); } return FutureBuilder( - future: seemsLikeAudiobook(playbackBehavior.speed), + future: + shouldShowSpeedWidget(playbackBehavior.speed), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done && @@ -417,6 +429,12 @@ class _SongMenuState extends State { }); }, ), + SliverToBoxAdapter( + child: Visibility( + visible: showSpeedMenu, + child: SpeedMenu(iconColor: iconColor), + ), + ), SliverPadding( padding: const EdgeInsets.only(left: 8.0), sliver: SliverList( diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart new file mode 100644 index 000000000..d6550bcc1 --- /dev/null +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -0,0 +1,134 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:get_it/get_it.dart'; + +import 'package:finamp/services/queue_service.dart'; +import '../../services/finamp_settings_helper.dart'; +import '../Buttons/simple_button.dart'; +import 'preset_chip.dart'; + +final _queueService = GetIt.instance(); +final presets = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0]; + +class SpeedMenu extends StatefulWidget { + const SpeedMenu({ + Key? key, + required this.iconColor, + }) : super(key: key); + + final Color iconColor; + + @override + State createState() => _SpeedMenuState(); +} + +class _SpeedMenuState extends State { + final _textController = TextEditingController( + text: FinampSettingsHelper.finampSettings.playbackSpeed.toString()); + final _formKey = GlobalKey(); + + var currentSpeed = FinampSettingsHelper.finampSettings.playbackSpeed; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: widget.iconColor.withOpacity(0.1), + ), + margin: EdgeInsets.only(top: 10.0, bottom: 10.0, left: 10.0, right: 10.0), + child: Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), + child: Stack( + alignment: Alignment.topLeft, + children: [ + Padding( + padding: EdgeInsets.only(left: 16.0), + child: PresetChips( + type: 'speed', + mainColour: widget.iconColor, + values: presets, + activeValue: currentSpeed, + onPressed: () { + setState(() { + currentSpeed = + FinampSettingsHelper.finampSettings.playbackSpeed; + _textController.text = currentSpeed.toString(); + }); + })), + Padding( + padding: EdgeInsets.only(top: 45.0, left: 15.0, right: 15.0), + child: Form( + key: _formKey, + child: Row( + children: [ + SimpleButton( + text: "Reset", + icon: TablerIcons.arrow_back_up_double, + iconColor: widget.iconColor, + onPressed: () { + _queueService.setPlaybackSpeed(1.0); + setState(() { + currentSpeed = 1.0; + _textController.text = currentSpeed.toString(); + }); + }, + ), + Expanded( + child: Container( + margin: EdgeInsets.symmetric( + vertical: 0.0, horizontal: 20.0), + child: TextFormField( + controller: _textController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + validator: (value) { + if (value == null || value.isEmpty) { + return AppLocalizations.of(context)!.required; + } + + if (double.tryParse(value) == null) { + return AppLocalizations.of(context)! + .invalidNumber; + } + return null; + }, + onSaved: (value) { + final roundedDouble = + min(max(double.parse(value!), 0), 5) + .toStringAsFixed(2); + final valueDouble = double.parse(roundedDouble); + + _textController.text = roundedDouble; + + _queueService.setPlaybackSpeed(valueDouble); + + setState(() { + currentSpeed = valueDouble; + }); + }, + ), + ), + ), + SimpleButton( + text: "Apply", + icon: TablerIcons.check, + iconColor: widget.iconColor, + onPressed: () { + if (_formKey.currentState?.validate() == true) { + _formKey.currentState!.save(); + } + }, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/Buttons/simple_button.dart b/lib/components/Buttons/simple_button.dart index 2f144ed1f..fcfa14d99 100644 --- a/lib/components/Buttons/simple_button.dart +++ b/lib/components/Buttons/simple_button.dart @@ -1,5 +1,3 @@ - - import 'package:finamp/color_schemes.g.dart'; import 'package:flutter/material.dart'; @@ -7,8 +5,14 @@ class SimpleButton extends StatelessWidget { final String text; final IconData icon; final void Function() onPressed; + final Color? iconColor; - const SimpleButton({super.key, required this.text, required this.icon, required this.onPressed}); + const SimpleButton( + {super.key, + required this.text, + required this.icon, + required this.onPressed, + this.iconColor}); @override Widget build(BuildContext context) { @@ -33,10 +37,12 @@ class SimpleButton extends StatelessWidget { Icon( icon, size: 20, - color: jellyfinBlueColor, + color: iconColor ?? jellyfinBlueColor, weight: 1.5, ), - const SizedBox(width: 6,), + const SizedBox( + width: 6, + ), Text( text, style: TextStyle( diff --git a/lib/components/PlayerScreen/artist_chip.dart b/lib/components/PlayerScreen/artist_chip.dart index 4f16851cd..20125bc86 100644 --- a/lib/components/PlayerScreen/artist_chip.dart +++ b/lib/components/PlayerScreen/artist_chip.dart @@ -29,9 +29,9 @@ class ArtistChips extends StatelessWidget { @override Widget build(BuildContext context) { + final artists = + useAlbumArtist ? baseItem?.albumArtists : baseItem?.artistItems; - final artists = useAlbumArtist ? baseItem?.albumArtists : baseItem?.artistItems; - return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: SingleChildScrollView( @@ -103,12 +103,15 @@ class _ArtistChipState extends State { return FutureBuilder( future: _artistChipFuture, builder: (context, snapshot) { - final backgroundColor = widget.backgroundColor ?? _defaultBackgroundColour; - final color = widget.color ?? Theme.of(context).textTheme.bodySmall?.color ?? Colors.white; + final backgroundColor = + widget.backgroundColor ?? _defaultBackgroundColour; + final color = widget.color ?? + Theme.of(context).textTheme.bodySmall?.color ?? + Colors.white; return _ArtistChipContent( - item: snapshot.data ?? widget.artist!, - backgroundColor: backgroundColor, - color: color, + item: snapshot.data ?? widget.artist!, + backgroundColor: backgroundColor, + color: color, ); }); } @@ -165,9 +168,7 @@ class _ArtistChipContent extends StatelessWidget { child: Text( name ?? AppLocalizations.of(context)!.unknownArtist, style: TextStyle( - color: color, - overflow: TextOverflow.ellipsis - ), + color: color, overflow: TextOverflow.ellipsis), softWrap: false, overflow: TextOverflow.ellipsis, ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 22573c145..2ef6cb81d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -796,9 +796,14 @@ "@playbackOrderShuffledButtonLabel": { "description": "Label for the button that toggles the playback order between linear/in-order and shuffle, while the queue is in shuffle mode" }, - "playbackSpeedButtonLabel": "Playback speed", + "playbackSpeedButtonLabel": "Playing at x{speed} speed", "@playbackSpeedButtonLabel": { - "description": "Label for the button that changes playback speed" + "description": "Label for the button that changes playback speed, {speed} is the current playback speed.", + "placeholders": { + "speed": { + "type": "double" + } + } }, "loopModeNoneButtonLabel": "Looping off", "@loopModeNoneButtonLabel": { diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index 90902b9c2..934dea3f1 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -450,7 +450,8 @@ class QueueService { for (int i = 0; i < itemList.length; i++) { jellyfin_models.BaseItemDto item = itemList[i]; try { - MediaItem mediaItem = await _generateMediaItem(item, source.contextLufs); + MediaItem mediaItem = + await _generateMediaItem(item, source.contextLufs); newItems.add(FinampQueueItem( item: mediaItem, source: source, @@ -811,8 +812,8 @@ class QueueService { } } - void setPlaybackSpeed(double) { - playbackSpeed = double; + void setPlaybackSpeed(double speed) { + playbackSpeed = speed; } Logger get queueServiceLogger => _queueServiceLogger; @@ -850,7 +851,8 @@ class QueueService { /// [contextLufs] is the LUFS of the context that the song is being played in, e.g. the album /// Should only be used when the tracks within that context come from the same source, e.g. the same album (or maybe artist?). Usually makes no sense for playlists. - Future _generateMediaItem(jellyfin_models.BaseItemDto item, double? contextLufs) async { + Future _generateMediaItem( + jellyfin_models.BaseItemDto item, double? contextLufs) async { const uuid = Uuid(); final downloadedSong = _downloadsHelper.getDownloadedSong(item.id); From 7607a05d72b92ffcd55a40787ed827b81f602e00 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Sun, 18 Feb 2024 23:00:02 +0100 Subject: [PATCH 09/33] Minor clean-up --- lib/components/AlbumScreen/preset_chip.dart | 3 --- lib/components/AlbumScreen/speed_menu.dart | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index b41f93299..d2c202ba3 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -1,6 +1,3 @@ -import 'dart:ffi'; - -import 'package:finamp/color_schemes.g.dart'; import 'package:flutter/material.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:get_it/get_it.dart'; diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index d6550bcc1..076e081e6 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -45,7 +45,7 @@ class _SpeedMenuState extends State { alignment: Alignment.topLeft, children: [ Padding( - padding: EdgeInsets.only(left: 16.0), + padding: EdgeInsets.symmetric(horizontal: 16.0), child: PresetChips( type: 'speed', mainColour: widget.iconColor, @@ -78,8 +78,7 @@ class _SpeedMenuState extends State { ), Expanded( child: Container( - margin: EdgeInsets.symmetric( - vertical: 0.0, horizontal: 20.0), + margin: EdgeInsets.symmetric(horizontal: 20.0), child: TextFormField( controller: _textController, keyboardType: TextInputType.number, From 79a9ac1cd5b971ea73abe9f46c27cb599ac5682e Mon Sep 17 00:00:00 2001 From: lymnyx Date: Mon, 19 Feb 2024 14:42:13 +0100 Subject: [PATCH 10/33] Feedback, i18n and improved speed parsing --- lib/components/AlbumScreen/preset_chip.dart | 26 ++++++++------- lib/components/AlbumScreen/song_menu.dart | 23 +++++++++----- lib/components/AlbumScreen/speed_menu.dart | 35 ++++++++++++++++----- lib/components/PlayerScreen/queue_list.dart | 9 +++--- lib/l10n/app_en.arb | 6 ++++ 5 files changed, 68 insertions(+), 31 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index d2c202ba3..459a0a1ca 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -54,6 +54,8 @@ class _PresetChipsState extends State { backgroundColour: currentValue == nowActiveValue ? widget.mainColour?.withOpacity(0.4) : widget.mainColour?.withOpacity(0.1), + isTextBold: currentValue == 1.0, + width: 55.0, onTap: () { setState(() { nowActiveValue = currentValue; @@ -72,14 +74,18 @@ class _PresetChipsState extends State { class PresetChip extends StatelessWidget { const PresetChip({ Key? key, + required this.width, this.value = "", this.onTap, this.backgroundColour, + this.isTextBold, }) : super(key: key); + final double width; final String value; final void Function()? onTap; final Color? backgroundColour; + final bool? isTextBold; @override Widget build(BuildContext context) { @@ -87,6 +93,7 @@ class PresetChip extends StatelessWidget { final color = Theme.of(context).textTheme.bodySmall?.color ?? Colors.white; return SizedBox( + width: width, height: _height, child: Material( color: backgroundColor, @@ -95,18 +102,15 @@ class PresetChip extends StatelessWidget { onTap: onTap, borderRadius: _borderRadius, child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 220), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - value, - style: TextStyle( - color: color, overflow: TextOverflow.ellipsis), - softWrap: false, + child: Text( + value, + style: TextStyle( + color: color, overflow: TextOverflow.ellipsis, - ), - ), + fontWeight: + isTextBold! ? FontWeight.w700 : FontWeight.normal), + softWrap: false, + overflow: TextOverflow.ellipsis, ), )), ), diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index d66e7e896..07e46ddf5 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -129,6 +129,7 @@ class _SongMenuState extends State { // Makes sure that widget doesn't just disappear after press while menu is visible var speedWidgetWasVisible = false; var showSpeedMenu = false; + final dragController = DraggableScrollableController(); @override void initState() { @@ -137,9 +138,6 @@ class _SongMenuState extends State { widget.playerScreenTheme; // use player screen theme if provided } - final _speedInputController = TextEditingController( - text: FinampSettingsHelper.finampSettings.playbackSpeed.toString()); - /// Sets the item's favourite on the Jellyfin server. Future toggleFavorite() async { try { @@ -212,9 +210,19 @@ class _SongMenuState extends State { setState(() { showSpeedMenu = !showSpeedMenu; }); + scrollToExtent(dragController, showSpeedMenu ? 0.8 : 0.6); Vibrate.feedback(FeedbackType.success); } + scrollToExtent( + DraggableScrollableController scrollController, double percentage) { + scrollController.animateTo( + percentage, + duration: Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + } + @override Widget build(BuildContext context) { final iconColor = _imageTheme?.primary ?? @@ -223,6 +231,7 @@ class _SongMenuState extends State { return Stack(children: [ DraggableScrollableSheet( + controller: dragController, snap: true, snapSizes: widget.showPlaybackControls ? const [0.6] : const [0.45], initialChildSize: widget.showPlaybackControls ? 0.6 : 0.45, @@ -397,7 +406,7 @@ class _SongMenuState extends State { final speedWidget = PlaybackAction( icon: TablerIcons.brand_speedtest, - onPressed: () async { + onPressed: () { toggleSpeedMenu(); }, tooltip: playbackSpeedTooltip, @@ -429,12 +438,10 @@ class _SongMenuState extends State { }); }, ), - SliverToBoxAdapter( - child: Visibility( - visible: showSpeedMenu, + if (showSpeedMenu) + SliverToBoxAdapter( child: SpeedMenu(iconColor: iconColor), ), - ), SliverPadding( padding: const EdgeInsets.only(left: 8.0), sliver: SliverList( diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index 076e081e6..4e3c54faf 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; +import 'package:flutter_vibrate/flutter_vibrate.dart'; import 'package:get_it/get_it.dart'; import 'package:finamp/services/queue_service.dart'; @@ -16,9 +17,11 @@ class SpeedMenu extends StatefulWidget { const SpeedMenu({ Key? key, required this.iconColor, + this.scrollFunction, }) : super(key: key); final Color iconColor; + final Function()? scrollFunction; @override State createState() => _SpeedMenuState(); @@ -31,6 +34,21 @@ class _SpeedMenuState extends State { var currentSpeed = FinampSettingsHelper.finampSettings.playbackSpeed; + InputDecoration inputFieldDecoration() { + return InputDecoration( + filled: true, + fillColor: widget.iconColor.withOpacity(0.1), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), + label: Center(child: Text(AppLocalizations.of(context)!.speed)), + floatingLabelBehavior: FloatingLabelBehavior.never, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(16), + ), + ); + } + @override Widget build(BuildContext context) { return Container( @@ -59,13 +77,13 @@ class _SpeedMenuState extends State { }); })), Padding( - padding: EdgeInsets.only(top: 45.0, left: 15.0, right: 15.0), + padding: EdgeInsets.only(top: 50.0, left: 15.0, right: 15.0), child: Form( key: _formKey, child: Row( children: [ SimpleButton( - text: "Reset", + text: AppLocalizations.of(context)!.reset, icon: TablerIcons.arrow_back_up_double, iconColor: widget.iconColor, onPressed: () { @@ -83,6 +101,7 @@ class _SpeedMenuState extends State { controller: _textController, keyboardType: TextInputType.number, textAlign: TextAlign.center, + decoration: inputFieldDecoration(), validator: (value) { if (value == null || value.isEmpty) { return AppLocalizations.of(context)!.required; @@ -95,12 +114,12 @@ class _SpeedMenuState extends State { return null; }, onSaved: (value) { - final roundedDouble = - min(max(double.parse(value!), 0), 5) - .toStringAsFixed(2); - final valueDouble = double.parse(roundedDouble); + final valueDouble = + (min(max(double.parse(value!), 0), 5) * 100) + .roundToDouble() / + 100; - _textController.text = roundedDouble; + _textController.text = valueDouble.toString(); _queueService.setPlaybackSpeed(valueDouble); @@ -112,7 +131,7 @@ class _SpeedMenuState extends State { ), ), SimpleButton( - text: "Apply", + text: AppLocalizations.of(context)!.apply, icon: TablerIcons.check, iconColor: widget.iconColor, onPressed: () { diff --git a/lib/components/PlayerScreen/queue_list.dart b/lib/components/PlayerScreen/queue_list.dart index cb9732f6a..0e6782915 100644 --- a/lib/components/PlayerScreen/queue_list.dart +++ b/lib/components/PlayerScreen/queue_list.dart @@ -170,16 +170,17 @@ class _QueueListState extends State { isRecentTracksExpanded: isRecentTracksExpanded, previousTracksHeaderKey: widget.previousTracksHeaderKey, onTap: () { - final oldBottomOffset = widget.scrollController.position.extentAfter; + final oldBottomOffset = + widget.scrollController.position.extentAfter; late StreamSubscription subscription; subscription = isRecentTracksExpanded.stream.listen((expanded) { final previousTracks = _queueService.getQueue().previousTracks; // a random delay isn't a great solution, but I'm not sure how to do this properly - Future.delayed(Duration(milliseconds: expanded ? 5: 50), () { + Future.delayed(Duration(milliseconds: expanded ? 5 : 50), () { widget.scrollController.jumpTo( widget.scrollController.position.maxScrollExtent - - oldBottomOffset - (previousTracks.isNotEmpty ? 100.0 : 0.0) - ); + oldBottomOffset - + (previousTracks.isNotEmpty ? 100.0 : 0.0)); }); subscription.cancel(); }); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2ef6cb81d..4eb227659 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -457,6 +457,12 @@ "@shown": {}, "hidden": "Hidden", "@hidden": {}, + "speed": "Speed", + "@speed": {}, + "reset": "Reset", + "@reset": {}, + "apply": "Apply", + "@apply": {}, "on": "On", "@on": {}, "off": "Off", From f01e35821c4661fdf1c6b5c4c074798ab6bd7af7 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Mon, 19 Feb 2024 21:49:16 +0100 Subject: [PATCH 11/33] small UI improvements --- lib/components/AlbumScreen/preset_chip.dart | 55 ++++++++++----------- lib/components/AlbumScreen/song_menu.dart | 8 +-- lib/components/AlbumScreen/speed_menu.dart | 10 ++-- 3 files changed, 35 insertions(+), 38 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index 459a0a1ca..154e05d22 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -37,35 +37,32 @@ class _PresetChipsState extends State { Widget build(BuildContext context) { var nowActiveValue = widget.activeValue; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - crossAxisAlignment: WrapCrossAlignment.center, - children: List.generate(widget.values.length, (index) { - final currentValue = widget.values[index]; - var newValue = "x$currentValue"; - - return PresetChip( - value: newValue, - backgroundColour: currentValue == nowActiveValue - ? widget.mainColour?.withOpacity(0.4) - : widget.mainColour?.withOpacity(0.1), - isTextBold: currentValue == 1.0, - width: 55.0, - onTap: () { - setState(() { - nowActiveValue = currentValue; - }); - _queueService.setPlaybackSpeed(currentValue); - widget.onPressed!(); - }, - ); - }), - ), + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + crossAxisAlignment: WrapCrossAlignment.center, + children: List.generate(widget.values.length, (index) { + final currentValue = widget.values[index]; + var newValue = "x$currentValue"; + + return PresetChip( + value: newValue, + backgroundColour: currentValue == nowActiveValue + ? widget.mainColour?.withOpacity(0.4) + : widget.mainColour?.withOpacity(0.1), + isTextBold: currentValue == 1.0, + width: 55.0, + onTap: () { + setState(() { + nowActiveValue = currentValue; + }); + _queueService.setPlaybackSpeed(currentValue); + widget.onPressed!(); + }, + ); + }), ), ); } diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 07e46ddf5..3cbcf1e9b 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -210,16 +210,16 @@ class _SongMenuState extends State { setState(() { showSpeedMenu = !showSpeedMenu; }); - scrollToExtent(dragController, showSpeedMenu ? 0.8 : 0.6); - Vibrate.feedback(FeedbackType.success); + scrollToExtent(dragController, showSpeedMenu ? 0.9 : 0.6); + Vibrate.feedback(FeedbackType.selection); } scrollToExtent( DraggableScrollableController scrollController, double percentage) { scrollController.animateTo( percentage, - duration: Duration(milliseconds: 500), - curve: Curves.easeInOut, + duration: Duration(milliseconds: 350), + curve: Curves.easeOutCubic, ); } diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index 4e3c54faf..865ab4c6b 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -58,12 +58,12 @@ class _SpeedMenuState extends State { ), margin: EdgeInsets.only(top: 10.0, bottom: 10.0, left: 10.0, right: 10.0), child: Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), - child: Stack( - alignment: Alignment.topLeft, + padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 16.0), + padding: EdgeInsets.symmetric(horizontal: 12.0), child: PresetChips( type: 'speed', mainColour: widget.iconColor, @@ -77,7 +77,7 @@ class _SpeedMenuState extends State { }); })), Padding( - padding: EdgeInsets.only(top: 50.0, left: 15.0, right: 15.0), + padding: EdgeInsets.only(top: 8.0, left: 15.0, right: 15.0), child: Form( key: _formKey, child: Row( From 8e9602d30848eacf1b94844813114ee41293583d Mon Sep 17 00:00:00 2001 From: lymnyx Date: Mon, 19 Feb 2024 22:30:52 +0100 Subject: [PATCH 12/33] Reverting position when collapsing menu --- lib/components/AlbumScreen/song_menu.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 3cbcf1e9b..6a1528873 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -210,17 +210,21 @@ class _SongMenuState extends State { setState(() { showSpeedMenu = !showSpeedMenu; }); - scrollToExtent(dragController, showSpeedMenu ? 0.9 : 0.6); + scrollToExtent(dragController, showSpeedMenu ? 0.9 : null); Vibrate.feedback(FeedbackType.selection); } + var oldExtent = 0.0; + scrollToExtent( - DraggableScrollableController scrollController, double percentage) { + DraggableScrollableController scrollController, double? percentage) { + var currentSize = scrollController.size; scrollController.animateTo( - percentage, + percentage ?? oldExtent, duration: Duration(milliseconds: 350), curve: Curves.easeOutCubic, ); + oldExtent = currentSize; } @override From 6d3938414e7b1427adf125b890a383bf3d621151 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Mon, 19 Feb 2024 23:18:26 +0100 Subject: [PATCH 13/33] Improvements to scrollToExtent() --- lib/components/AlbumScreen/song_menu.dart | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 6a1528873..d08287323 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -206,24 +206,29 @@ class _SongMenuState extends State { return false; } + final inputStep = 0.9; + var oldExtent = 0.0; + void toggleSpeedMenu() { setState(() { showSpeedMenu = !showSpeedMenu; }); - scrollToExtent(dragController, showSpeedMenu ? 0.9 : null); + scrollToExtent(dragController, showSpeedMenu ? inputStep : null); Vibrate.feedback(FeedbackType.selection); } - var oldExtent = 0.0; - scrollToExtent( DraggableScrollableController scrollController, double? percentage) { var currentSize = scrollController.size; - scrollController.animateTo( - percentage ?? oldExtent, - duration: Duration(milliseconds: 350), - curve: Curves.easeOutCubic, - ); + if (percentage != null && + (percentage != inputStep || currentSize < percentage) || + scrollController.size == inputStep) { + scrollController.animateTo( + percentage ?? oldExtent, + duration: Duration(milliseconds: 350), + curve: Curves.easeOutCubic, + ); + } oldExtent = currentSize; } From 1cfc5558ca0da3907725f41ec76a0ef3d18a69c1 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Tue, 20 Feb 2024 01:38:39 +0100 Subject: [PATCH 14/33] Scroll to active preset --- lib/components/AlbumScreen/preset_chip.dart | 93 ++++++++++++++------- 1 file changed, 65 insertions(+), 28 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index 154e05d22..600f89af9 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:get_it/get_it.dart'; @@ -28,43 +29,79 @@ class PresetChips extends StatefulWidget { final Color? mainColour; // used for different background colours final Function()? onPressed; + final chipWidth = 55.0; + @override State createState() => _PresetChipsState(); } class _PresetChipsState extends State { + final _controller = ScrollController(); + var scrolledAlready = false; + + scrollToActivePreset(double currentValue, double maxWidth) { + if (!_controller.hasClients) return false; + var offset = + (widget.chipWidth + 8.0) * widget.values.indexOf(currentValue) - + maxWidth / 2 + + widget.chipWidth / 2; + + offset = min( + max(0, offset), maxWidth - _controller.position.maxScrollExtent + 15); + + _controller.animateTo( + offset, + duration: Duration(milliseconds: 350), + curve: Curves.easeOutCubic, + ); + } + @override Widget build(BuildContext context) { var nowActiveValue = widget.activeValue; - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - crossAxisAlignment: WrapCrossAlignment.center, - children: List.generate(widget.values.length, (index) { - final currentValue = widget.values[index]; - var newValue = "x$currentValue"; - - return PresetChip( - value: newValue, - backgroundColour: currentValue == nowActiveValue - ? widget.mainColour?.withOpacity(0.4) - : widget.mainColour?.withOpacity(0.1), - isTextBold: currentValue == 1.0, - width: 55.0, - onTap: () { - setState(() { - nowActiveValue = currentValue; - }); - _queueService.setPlaybackSpeed(currentValue); - widget.onPressed!(); - }, - ); - }), - ), - ); + return LayoutBuilder(builder: (context, constraints) { + return SingleChildScrollView( + controller: _controller, + scrollDirection: Axis.horizontal, + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + crossAxisAlignment: WrapCrossAlignment.center, + children: List.generate(widget.values.length, (index) { + final currentValue = widget.values[index]; + var newValue = "x$currentValue"; + + if (currentValue == nowActiveValue) { + if (scrolledAlready) { + scrollToActivePreset(currentValue, constraints.maxWidth); + } else { + Future.delayed(Duration(milliseconds: 200), () { + scrollToActivePreset(currentValue, constraints.maxWidth); + }); + scrolledAlready = true; + } + } + + return PresetChip( + value: newValue, + backgroundColour: currentValue == nowActiveValue + ? widget.mainColour?.withOpacity(0.4) + : widget.mainColour?.withOpacity(0.1), + isTextBold: currentValue == 1.0, + width: widget.chipWidth, + onTap: () { + setState(() { + nowActiveValue = currentValue; + }); + _queueService.setPlaybackSpeed(currentValue); + widget.onPressed!(); + }, + ); + }), + ), + ); + }); } } From 1ff59cd490a48d9529a40bc1e2a34d9dca666386 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Tue, 20 Feb 2024 07:59:48 +0100 Subject: [PATCH 15/33] animate speed menu --- lib/components/AlbumScreen/song_menu.dart | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index d08287323..555b2c157 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -34,6 +34,10 @@ import '../error_snackbar.dart'; import 'song_list_tile.dart'; import 'speed_menu.dart'; +const Duration songMenuDefaultAnimationDuration = Duration(milliseconds: 350); +const Curve songMenuDefaultInCurve = Curves.easeOutCubic; +const Curve songMenuDefaultOutCurve = Curves.easeInCubic; + Future showModalSongMenu({ required BuildContext context, required BaseItemDto item, @@ -225,8 +229,8 @@ class _SongMenuState extends State { scrollController.size == inputStep) { scrollController.animateTo( percentage ?? oldExtent, - duration: Duration(milliseconds: 350), - curve: Curves.easeOutCubic, + duration: songMenuDefaultAnimationDuration, + curve: songMenuDefaultInCurve, ); } oldExtent = currentSize; @@ -447,10 +451,17 @@ class _SongMenuState extends State { }); }, ), - if (showSpeedMenu) - SliverToBoxAdapter( - child: SpeedMenu(iconColor: iconColor), + SliverToBoxAdapter( + child: AnimatedSwitcher( + duration: songMenuDefaultAnimationDuration, + switchInCurve: songMenuDefaultInCurve, + switchOutCurve: songMenuDefaultOutCurve, + transitionBuilder: (child, animation) { + return SizeTransition(sizeFactor: animation, child: child); + }, + child: showSpeedMenu ? SpeedMenu(iconColor: iconColor) : null, ), + ), SliverPadding( padding: const EdgeInsets.only(left: 8.0), sliver: SliverList( From 5271c8a8383c3d326fd8922d2a61ceb6662f5c99 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Tue, 20 Feb 2024 17:15:12 +0100 Subject: [PATCH 16/33] Review changes 1 --- lib/components/AlbumScreen/preset_chip.dart | 7 +-- lib/components/AlbumScreen/song_menu.dart | 55 +++++++------------ lib/components/AlbumScreen/speed_menu.dart | 31 +++++------ ...control_visibility_dropdown_list_tile.dart | 19 ++++--- lib/l10n/app_en.arb | 38 ++++++++----- lib/main.dart | 2 +- lib/models/finamp_models.dart | 33 ++++++----- lib/models/finamp_models.g.dart | 32 +++++------ lib/services/finamp_settings_helper.dart | 5 +- lib/services/queue_service.dart | 4 -- 10 files changed, 111 insertions(+), 115 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index 600f89af9..4352b78a7 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -7,7 +7,6 @@ const _radius = Radius.circular(4); const _borderRadius = BorderRadius.all(_radius); const _height = 36.0; final _defaultBackgroundColour = Colors.white.withOpacity(0.1); -final _queueService = GetIt.instance(); class PresetChips extends StatefulWidget { const PresetChips({ @@ -36,8 +35,9 @@ class PresetChips extends StatefulWidget { } class _PresetChipsState extends State { + final _queueService = GetIt.instance(); final _controller = ScrollController(); - var scrolledAlready = false; + bool scrolledAlready = false; scrollToActivePreset(double currentValue, double maxWidth) { if (!_controller.hasClients) return false; @@ -95,7 +95,7 @@ class _PresetChipsState extends State { nowActiveValue = currentValue; }); _queueService.setPlaybackSpeed(currentValue); - widget.onPressed!(); + widget.onPressed?.call(); }, ); }), @@ -144,7 +144,6 @@ class PresetChip extends StatelessWidget { fontWeight: isTextBold! ? FontWeight.w700 : FontWeight.normal), softWrap: false, - overflow: TextOverflow.ellipsis, ), )), ), diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 04ada8e44..cd730f8bc 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -129,8 +129,8 @@ class _SongMenuState extends State { ImageProvider? _imageProvider; // Makes sure that widget doesn't just disappear after press while menu is visible - var speedWidgetWasVisible = false; - var showSpeedMenu = false; + bool speedWidgetWasVisible = false; + bool showSpeedMenu = false; final dragController = DraggableScrollableController(); @override @@ -178,13 +178,13 @@ class _SongMenuState extends State { } } - Future shouldShowSpeedWidget(currentSpeed) async { + Future shouldShowSpeedControls(currentSpeed) async { if (currentSpeed != 1.0 || - FinampSettingsHelper.finampSettings.contentPlaybackSpeedType.index == + FinampSettingsHelper.finampSettings.playbackSpeedVisibility.index == 1) { return true; } - if (FinampSettingsHelper.finampSettings.contentPlaybackSpeedType.index == + if (FinampSettingsHelper.finampSettings.playbackSpeedVisibility.index == 0) { var genres = widget.item.genres!; @@ -197,6 +197,7 @@ class _SongMenuState extends State { try { var parent = await _jellyfinApiHelper.getItemById(widget.item.parentId!); + // 72e9 = 120 minutes if (parent.runTimeTicks! > 72e9.toInt()) { return true; } @@ -323,11 +324,9 @@ class _SongMenuState extends State { ?.playbackOrderShuffledButtonLabel ?? "Shuffling", }; - final playbackSpeedTooltip = - AppLocalizations.of(context) - ?.playbackSpeedButtonLabel( - playbackBehavior.speed) ?? - "Playing at x${playbackBehavior.speed} speed"; + final playbackSpeedTooltip = AppLocalizations.of( + context)! + .playbackSpeedButtonLabel(playbackBehavior.speed); const loopModeIcons = { FinampLoopMode.none: TablerIcons.repeat, FinampLoopMode.one: TablerIcons.repeat_once, @@ -443,7 +442,7 @@ class _SongMenuState extends State { } return FutureBuilder( future: - shouldShowSpeedWidget(playbackBehavior.speed), + shouldShowSpeedControls(playbackBehavior.speed), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done && @@ -463,9 +462,12 @@ class _SongMenuState extends State { switchInCurve: songMenuDefaultInCurve, switchOutCurve: songMenuDefaultOutCurve, transitionBuilder: (child, animation) { - return SizeTransition(sizeFactor: animation, child: child); + return SizeTransition( + sizeFactor: animation, child: child); }, - child: showSpeedMenu ? SpeedMenu(iconColor: iconColor) : null, + child: showSpeedMenu + ? SpeedMenu(iconColor: iconColor) + : null, ), ), SliverPadding( @@ -1056,28 +1058,13 @@ class PlaybackAction extends StatelessWidget { child: IconButton( icon: Column( children: [ - SizedBox( - width: 35, - height: 46, - child: Stack( - alignment: Alignment.topCenter, - children: [ - Icon( - icon, - color: iconColor, - size: 35, - weight: 1.0, - ), - Positioned( - bottom: -2, - child: Text( - value ?? "", - style: Theme.of(context).textTheme.labelSmall, - ), - ), - ], - ), + Icon( + icon, + color: iconColor, + size: 35, + weight: 1.0, ), + const SizedBox(height: 9), SizedBox( height: 2 * 12 * 1.4 + 2, child: Align( diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index 865ab4c6b..dc12f863a 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -49,6 +49,17 @@ class _SpeedMenuState extends State { ); } + saveSpeedInput(value) { + final valueDouble = + (min(max(double.parse(value!), 0), 5) * 100).roundToDouble() / 100; + + _textController.text = valueDouble.toString(); + _queueService.setPlaybackSpeed(valueDouble); + setState(() { + currentSpeed = valueDouble; + }); + } + @override Widget build(BuildContext context) { return Container( @@ -77,7 +88,7 @@ class _SpeedMenuState extends State { }); })), Padding( - padding: EdgeInsets.only(top: 8.0, left: 15.0, right: 15.0), + padding: EdgeInsets.only(top: 8.0, left: 25.0, right: 25.0), child: Form( key: _formKey, child: Row( @@ -96,7 +107,7 @@ class _SpeedMenuState extends State { ), Expanded( child: Container( - margin: EdgeInsets.symmetric(horizontal: 20.0), + padding: EdgeInsets.symmetric(horizontal: 40.0), child: TextFormField( controller: _textController, keyboardType: TextInputType.number, @@ -113,20 +124,8 @@ class _SpeedMenuState extends State { } return null; }, - onSaved: (value) { - final valueDouble = - (min(max(double.parse(value!), 0), 5) * 100) - .roundToDouble() / - 100; - - _textController.text = valueDouble.toString(); - - _queueService.setPlaybackSpeed(valueDouble); - - setState(() { - currentSpeed = valueDouble; - }); - }, + onSaved: (value) => saveSpeedInput(value), + onFieldSubmitted: (value) => saveSpeedInput(value), ), ), ), diff --git a/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart index 08bfccc68..6c3e8dd2f 100644 --- a/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart +++ b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart @@ -6,7 +6,8 @@ import '../../../models/finamp_models.dart'; import '../../../services/finamp_settings_helper.dart'; class PlaybackSpeedControlVisibilityDropdownListTile extends StatelessWidget { - const PlaybackSpeedControlVisibilityDropdownListTile({Key? key}) : super(key: key); + const PlaybackSpeedControlVisibilityDropdownListTile({Key? key}) + : super(key: key); @override Widget build(BuildContext context) { @@ -14,19 +15,21 @@ class PlaybackSpeedControlVisibilityDropdownListTile extends StatelessWidget { valueListenable: FinampSettingsHelper.finampSettingsListener, builder: (_, box, __) { return ListTile( - title: Text(AppLocalizations.of(context)!.playbackSpeedControlSetting), - subtitle: Text(AppLocalizations.of(context)!.playbackSpeedControlSettingSubtitle), - trailing: DropdownButton( - value: box.get("FinampSettings")?.contentPlaybackSpeedType, - items: ContentPlaybackSpeedType.values - .map((e) => DropdownMenuItem( + title: + Text(AppLocalizations.of(context)!.playbackSpeedControlSetting), + subtitle: Text(AppLocalizations.of(context)! + .playbackSpeedControlSettingSubtitle), + trailing: DropdownButton( + value: box.get("FinampSettings")?.playbackSpeedVisibility, + items: PlaybackSpeedVisibility.values + .map((e) => DropdownMenuItem( value: e, child: Text(e.toLocalisedString(context)), )) .toList(), onChanged: (value) { if (value != null) { - FinampSettingsHelper.setContentPlaybackSpeedType(value); + FinampSettingsHelper.setPlaybackSpeedVisibility(value); } }, ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 2829d84c8..f379ea963 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -455,25 +455,37 @@ "description": "Title for the customization settings screen" }, "playbackSpeedControlSetting": "Playback Speed Visibility", - "@playbackSpeedControlSetting": {}, + "@playbackSpeedControlSetting": { + "description": "Title of the visibility setting of the playback speed controls" + }, "playbackSpeedControlSettingSubtitle": "Whether the playback speed controls are shown in the player screen menu", - "@playbackSpeedControlSettingSubtitle": {}, + "@playbackSpeedControlSettingSubtitle": { + "description": "Subtitle for the playback speed visibility setting" + }, "automatic": "Automatic", - "@automatic": {}, + "@automatic": { + "description": "Used as an option in the playback speed visibility settings" + }, "shown": "Shown", - "@shown": {}, + "@shown": { + "description": "Used as an option in the playback speed visibility settings" + }, "hidden": "Hidden", - "@hidden": {}, + "@hidden": { + "description": "Used as an option in the playback speed visibility settings" + }, "speed": "Speed", - "@speed": {}, + "@speed": { + "description": "Used as a placeholder in the input of the playback speed menu" + }, "reset": "Reset", - "@reset": {}, + "@reset": { + "description": "Used for a button in the playback speed menu" + }, "apply": "Apply", - "@apply": {}, - "on": "On", - "@on": {}, - "off": "Off", - "@off": {}, + "@apply": { + "description": "Used for a button in the playback speed menu" + }, "portrait": "Portrait", "@portrait": {}, "landscape": "Landscape", @@ -807,7 +819,7 @@ }, "playbackSpeedButtonLabel": "Playing at x{speed} speed", "@playbackSpeedButtonLabel": { - "description": "Label for the button that changes playback speed, {speed} is the current playback speed.", + "description": "Label for the button that toggles visibility of the playback speed menu, {speed} is the current playback speed.", "placeholders": { "speed": { "type": "double" diff --git a/lib/main.dart b/lib/main.dart index f9c8b7957..676fcb82d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -178,7 +178,7 @@ Future setupHive() async { Hive.registerAdapter(SortByAdapter()); Hive.registerAdapter(SortOrderAdapter()); Hive.registerAdapter(ContentViewTypeAdapter()); - Hive.registerAdapter(ContentPlaybackSpeedTypeAdapter()); + Hive.registerAdapter(PlaybackSpeedVisibilityAdapter()); Hive.registerAdapter(DownloadedImageAdapter()); Hive.registerAdapter(ThemeModeAdapter()); Hive.registerAdapter(LocaleAdapter()); diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 006304173..8beaaf7a5 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -73,7 +73,7 @@ const _replayGainTargetLufsDefault = -14.0; const _replayGainNormalizationFactorDefault = 1.0; const _replayGainModeDefault = ReplayGainMode.hybrid; const _contentViewType = ContentViewType.list; -const _contentPlaybackSpeedType = ContentPlaybackSpeedType.automatic; +const _playbackSpeedVisibility = PlaybackSpeedVisibility.automatic; const _contentGridViewCrossAxisCountPortrait = 2; const _contentGridViewCrossAxisCountLandscape = 3; const _showTextOnGridView = true; @@ -114,7 +114,7 @@ class FinampSettings { this.replayGainNormalizationFactor = _replayGainNormalizationFactorDefault, this.replayGainMode = _replayGainModeDefault, this.contentViewType = _contentViewType, - this.contentPlaybackSpeedType = _contentPlaybackSpeedType, + this.playbackSpeedVisibility = _playbackSpeedVisibility, this.contentGridViewCrossAxisCountPortrait = _contentGridViewCrossAxisCountPortrait, this.contentGridViewCrossAxisCountLandscape = @@ -309,9 +309,9 @@ class FinampSettings { @HiveField(47, defaultValue: _defaultPlaybackSpeed) double playbackSpeed; - /// The content playback speed type defining how and whether to display the playbackSpeed widget in the song menu - @HiveField(48, defaultValue: _contentPlaybackSpeedType) - ContentPlaybackSpeedType contentPlaybackSpeedType; + /// The content playback speed type defining how and whether to display the playback speed controls in the song menu + @HiveField(48, defaultValue: _playbackSpeedVisibility) + PlaybackSpeedVisibility playbackSpeedVisibility; static Future create() async { final downloadLocation = await DownloadLocation.create( @@ -1668,7 +1668,7 @@ enum TranscodeDownloadsSetting { } @HiveType(typeId: 67) -enum ContentPlaybackSpeedType { +enum PlaybackSpeedVisibility { @HiveField(0) automatic, @HiveField(1) @@ -1686,27 +1686,26 @@ enum ContentPlaybackSpeedType { String toLocalisedString(BuildContext context) => _humanReadableLocalisedName(this, context); - String _humanReadableName(ContentPlaybackSpeedType contentPlaybackSpeedType) { - switch (contentPlaybackSpeedType) { - case ContentPlaybackSpeedType.automatic: + String _humanReadableName(PlaybackSpeedVisibility playbackSpeedVisibility) { + switch (playbackSpeedVisibility) { + case PlaybackSpeedVisibility.automatic: return "Automatic"; - case ContentPlaybackSpeedType.visible: + case PlaybackSpeedVisibility.visible: return "On"; - case ContentPlaybackSpeedType.hidden: + case PlaybackSpeedVisibility.hidden: return "Off"; } } String _humanReadableLocalisedName( - ContentPlaybackSpeedType contentPlaybackSpeedType, BuildContext context) { - switch (contentPlaybackSpeedType) { - case ContentPlaybackSpeedType.automatic: + PlaybackSpeedVisibility playbackSpeedVisibility, BuildContext context) { + switch (playbackSpeedVisibility) { + case PlaybackSpeedVisibility.automatic: return AppLocalizations.of(context)!.automatic; - case ContentPlaybackSpeedType.visible: + case PlaybackSpeedVisibility.visible: return AppLocalizations.of(context)!.shown; - case ContentPlaybackSpeedType.hidden: + case PlaybackSpeedVisibility.hidden: return AppLocalizations.of(context)!.hidden; } } - } diff --git a/lib/models/finamp_models.g.dart b/lib/models/finamp_models.g.dart index cb90aff48..d9f1c8f00 100644 --- a/lib/models/finamp_models.g.dart +++ b/lib/models/finamp_models.g.dart @@ -88,9 +88,9 @@ class FinampSettingsAdapter extends TypeAdapter { contentViewType: fields[10] == null ? ContentViewType.list : fields[10] as ContentViewType, - contentPlaybackSpeedType: fields[48] == null - ? ContentPlaybackSpeedType.automatic - : fields[48] as ContentPlaybackSpeedType, + playbackSpeedVisibility: fields[48] == null + ? PlaybackSpeedVisibility.automatic + : fields[48] as PlaybackSpeedVisibility, contentGridViewCrossAxisCountPortrait: fields[11] == null ? 2 : fields[11] as int, contentGridViewCrossAxisCountLandscape: @@ -256,7 +256,7 @@ class FinampSettingsAdapter extends TypeAdapter { ..writeByte(47) ..write(obj.playbackSpeed) ..writeByte(48) - ..write(obj.contentPlaybackSpeedType); + ..write(obj.playbackSpeedVisibility); } @override @@ -1506,35 +1506,35 @@ class TranscodeDownloadsSettingAdapter typeId == other.typeId; } -class ContentPlaybackSpeedTypeAdapter - extends TypeAdapter { +class PlaybackSpeedVisibilityAdapter + extends TypeAdapter { @override final int typeId = 67; @override - ContentPlaybackSpeedType read(BinaryReader reader) { + PlaybackSpeedVisibility read(BinaryReader reader) { switch (reader.readByte()) { case 0: - return ContentPlaybackSpeedType.automatic; + return PlaybackSpeedVisibility.automatic; case 1: - return ContentPlaybackSpeedType.visible; + return PlaybackSpeedVisibility.visible; case 2: - return ContentPlaybackSpeedType.hidden; + return PlaybackSpeedVisibility.hidden; default: - return ContentPlaybackSpeedType.automatic; + return PlaybackSpeedVisibility.automatic; } } @override - void write(BinaryWriter writer, ContentPlaybackSpeedType obj) { + void write(BinaryWriter writer, PlaybackSpeedVisibility obj) { switch (obj) { - case ContentPlaybackSpeedType.automatic: + case PlaybackSpeedVisibility.automatic: writer.writeByte(0); break; - case ContentPlaybackSpeedType.visible: + case PlaybackSpeedVisibility.visible: writer.writeByte(1); break; - case ContentPlaybackSpeedType.hidden: + case PlaybackSpeedVisibility.hidden: writer.writeByte(2); break; } @@ -1546,7 +1546,7 @@ class ContentPlaybackSpeedTypeAdapter @override bool operator ==(Object other) => identical(this, other) || - other is ContentPlaybackSpeedTypeAdapter && + other is PlaybackSpeedVisibilityAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/lib/services/finamp_settings_helper.dart b/lib/services/finamp_settings_helper.dart index 8a995de98..a3cc5032f 100644 --- a/lib/services/finamp_settings_helper.dart +++ b/lib/services/finamp_settings_helper.dart @@ -184,9 +184,10 @@ class FinampSettingsHelper { .put("FinampSettings", finampSettingsTemp); } - static void setContentPlaybackSpeedType(ContentPlaybackSpeedType contentPlaybackSpeedType) { + static void setPlaybackSpeedVisibility( + PlaybackSpeedVisibility playbackSpeedVisibility) { FinampSettings finampSettingsTemp = finampSettings; - finampSettingsTemp.contentPlaybackSpeedType = contentPlaybackSpeedType; + finampSettingsTemp.playbackSpeedVisibility = playbackSpeedVisibility; Hive.box("FinampSettings") .put("FinampSettings", finampSettingsTemp); } diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index cd65af08e..ca0fc3b2b 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -743,13 +743,9 @@ class QueueService { set playbackSpeed(double speed) { _playbackSpeed = speed; - _playbackSpeedStream.add(speed); - _audioHandler.setSpeed(speed); - FinampSettingsHelper.setPlaybackSpeed(playbackSpeed); - _queueServiceLogger.fine( "Playback speed set to ${FinampSettingsHelper.finampSettings.playbackSpeed}"); } From a8bcaa0ef3a95cd365c7c4812cb2105085e9831c Mon Sep 17 00:00:00 2001 From: lymnyx Date: Wed, 21 Feb 2024 15:44:33 +0100 Subject: [PATCH 17/33] Review changes 2 --- lib/components/AlbumScreen/preset_chip.dart | 76 ++++++++++--------- lib/components/AlbumScreen/song_menu.dart | 2 +- lib/components/AlbumScreen/speed_menu.dart | 34 ++++----- ...control_visibility_dropdown_list_tile.dart | 45 ++++++++++- lib/l10n/app_en.arb | 7 +- .../customization_settings_screen.dart | 19 +++-- lib/services/finamp_settings_helper.dart | 8 ++ 7 files changed, 121 insertions(+), 70 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index 4352b78a7..8d0986dcb 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -8,6 +8,10 @@ const _borderRadius = BorderRadius.all(_radius); const _height = 36.0; final _defaultBackgroundColour = Colors.white.withOpacity(0.1); +enum PresetTypes { + speed, +} + class PresetChips extends StatefulWidget { const PresetChips({ Key? key, @@ -16,17 +20,16 @@ class PresetChips extends StatefulWidget { required this.activeValue, this.onTap, this.mainColour, - this.onPressed, + this.onPresetSelected, }) : super(key: key); - // for future preset types other than "speed" - final String type; + final PresetTypes type; final List values; final double activeValue; final Function()? onTap; final Color? mainColour; // used for different background colours - final Function()? onPressed; + final Function()? onPresetSelected; final chipWidth = 55.0; @@ -56,10 +59,38 @@ class _PresetChipsState extends State { ); } + generatePresetChip(value, BoxConstraints constraints) { + // Scroll to the active preset + if (value == widget.activeValue) { + if (scrolledAlready) { + scrollToActivePreset(value, constraints.maxWidth); + } else { + Future.delayed(Duration(milliseconds: 200), () { + scrollToActivePreset(value, constraints.maxWidth); + }); + scrolledAlready = true; + } + } + + var stringValue = "x$value"; + + return PresetChip( + value: stringValue, + backgroundColour: value == widget.activeValue + ? widget.mainColour?.withOpacity(0.4) + : widget.mainColour?.withOpacity(0.1), + isTextBold: value == 1.0, + width: widget.chipWidth, + onTap: () { + setState(() {}); + _queueService.setPlaybackSpeed(value); + widget.onPresetSelected?.call(); + }, + ); + } + @override Widget build(BuildContext context) { - var nowActiveValue = widget.activeValue; - return LayoutBuilder(builder: (context, constraints) { return SingleChildScrollView( controller: _controller, @@ -68,37 +99,8 @@ class _PresetChipsState extends State { spacing: 8.0, runSpacing: 8.0, crossAxisAlignment: WrapCrossAlignment.center, - children: List.generate(widget.values.length, (index) { - final currentValue = widget.values[index]; - var newValue = "x$currentValue"; - - if (currentValue == nowActiveValue) { - if (scrolledAlready) { - scrollToActivePreset(currentValue, constraints.maxWidth); - } else { - Future.delayed(Duration(milliseconds: 200), () { - scrollToActivePreset(currentValue, constraints.maxWidth); - }); - scrolledAlready = true; - } - } - - return PresetChip( - value: newValue, - backgroundColour: currentValue == nowActiveValue - ? widget.mainColour?.withOpacity(0.4) - : widget.mainColour?.withOpacity(0.1), - isTextBold: currentValue == 1.0, - width: widget.chipWidth, - onTap: () { - setState(() { - nowActiveValue = currentValue; - }); - _queueService.setPlaybackSpeed(currentValue); - widget.onPressed?.call(); - }, - ); - }), + children: List.generate(widget.values.length, + (index) => generatePresetChip(widget.values[index], constraints)), ), ); }); diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index cd730f8bc..d6ee0786d 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -374,7 +374,7 @@ class _SongMenuState extends State { : TablerIcons.hourglass_empty, onPressed: () async { if (timerValue != null) { - showDialog( + await showDialog( context: context, builder: (context) => const SleepTimerCancelDialog(), diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index dc12f863a..f311263a5 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -32,8 +32,6 @@ class _SpeedMenuState extends State { text: FinampSettingsHelper.finampSettings.playbackSpeed.toString()); final _formKey = GlobalKey(); - var currentSpeed = FinampSettingsHelper.finampSettings.playbackSpeed; - InputDecoration inputFieldDecoration() { return InputDecoration( filled: true, @@ -49,15 +47,17 @@ class _SpeedMenuState extends State { ); } - saveSpeedInput(value) { + void saveSpeedInput(value) { final valueDouble = (min(max(double.parse(value!), 0), 5) * 100).roundToDouble() / 100; - _textController.text = valueDouble.toString(); _queueService.setPlaybackSpeed(valueDouble); - setState(() { - currentSpeed = valueDouble; - }); + setState(() {}); + } + + void refreshInputText() { + _textController.text = + FinampSettingsHelper.finampSettings.playbackSpeed.toString(); } @override @@ -76,16 +76,14 @@ class _SpeedMenuState extends State { Padding( padding: EdgeInsets.symmetric(horizontal: 12.0), child: PresetChips( - type: 'speed', + type: PresetTypes.speed, mainColour: widget.iconColor, values: presets, - activeValue: currentSpeed, - onPressed: () { - setState(() { - currentSpeed = - FinampSettingsHelper.finampSettings.playbackSpeed; - _textController.text = currentSpeed.toString(); - }); + activeValue: + FinampSettingsHelper.finampSettings.playbackSpeed, + onPresetSelected: () { + setState(() {}); + refreshInputText(); })), Padding( padding: EdgeInsets.only(top: 8.0, left: 25.0, right: 25.0), @@ -99,10 +97,8 @@ class _SpeedMenuState extends State { iconColor: widget.iconColor, onPressed: () { _queueService.setPlaybackSpeed(1.0); - setState(() { - currentSpeed = 1.0; - _textController.text = currentSpeed.toString(); - }); + setState(() {}); + refreshInputText(); }, ), Expanded( diff --git a/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart index 6c3e8dd2f..c109a0ecc 100644 --- a/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart +++ b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:hive/hive.dart'; @@ -17,8 +18,48 @@ class PlaybackSpeedControlVisibilityDropdownListTile extends StatelessWidget { return ListTile( title: Text(AppLocalizations.of(context)!.playbackSpeedControlSetting), - subtitle: Text(AppLocalizations.of(context)! - .playbackSpeedControlSettingSubtitle), + subtitle: RichText( + text: TextSpan( + children: [ + TextSpan( + text: AppLocalizations.of(context)! + .playbackSpeedControlSettingSubtitle, + style: Theme.of(context).textTheme.bodyMedium, + ), + const TextSpan(text: "\n"), + // tappable "more info" text + TextSpan( + text: AppLocalizations.of(context)!.moreInfo, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.w500, + ), + recognizer: TapGestureRecognizer() + ..onTap = () { + showGeneralDialog( + context: context, + pageBuilder: (context, anim1, anim2) { + return AlertDialog( + title: Text(AppLocalizations.of(context)! + .playbackSpeedControlSetting), + content: Text(AppLocalizations.of(context)! + .playbackSpeedControlSettingDescription), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: + Text(AppLocalizations.of(context)!.close), + ), + ], + ); + }); + }, + ), + ], + ), + ), trailing: DropdownButton( value: box.get("FinampSettings")?.playbackSpeedVisibility, items: PlaybackSpeedVisibility.values diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f379ea963..bf8e5589e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -462,6 +462,10 @@ "@playbackSpeedControlSettingSubtitle": { "description": "Subtitle for the playback speed visibility setting" }, + "playbackSpeedControlSettingDescription": "Automatic: Finamp tries to identify whether the media you are playing is an audiobook (based on genre and album length). If Finamp thinks it is, it will show the playback speed controls.\n\nShown: The playback speed controls are available regardless of what kind of media you are playing.\n\nHidden: The playback speed controls in the song menu are hidden.", + "@playbackSpeedControlSettingDescription": { + "description": "Description for the dropdown that selects the replay gain mode, shown in a dialog that opens when the user presses the info icon next to the dropdown" + }, "automatic": "Automatic", "@automatic": { "description": "Used as an option in the playback speed visibility settings" @@ -480,7 +484,7 @@ }, "reset": "Reset", "@reset": { - "description": "Used for a button in the playback speed menu" + "description": "Used for buttons in the playback speed menu and the settings" }, "apply": "Apply", "@apply": { @@ -929,6 +933,7 @@ "close": "Close", "showUncensoredLogMessage": "This log contains your login information. Show?", "resetTabs": "Reset tabs", + "resetToDefaults": "Reset to defaults", "noMusicLibrariesTitle": "No Music Libraries", "@noMusicLibrariesTitle": { "description": "Title for message that shows on the views screen when no music libraries could be found." diff --git a/lib/screens/customization_settings_screen.dart b/lib/screens/customization_settings_screen.dart index 292f86ed7..1bb3db00b 100644 --- a/lib/screens/customization_settings_screen.dart +++ b/lib/screens/customization_settings_screen.dart @@ -21,16 +21,15 @@ class _CustomizationSettingsScreenState extends State("FinampSettings") + .put("FinampSettings", finampSettingsTemp); + } + static void setSwipeInsertQueueNext(bool swipeInsertQueueNext) { FinampSettings finampSettingsTemp = finampSettings; finampSettingsTemp.swipeInsertQueueNext = swipeInsertQueueNext; From 6dee8a6f53bfdcd89d88a08b58e9d58d23d2472a Mon Sep 17 00:00:00 2001 From: lymnyx Date: Wed, 21 Feb 2024 19:48:43 +0100 Subject: [PATCH 18/33] Update input text on save --- lib/components/AlbumScreen/speed_menu.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index f311263a5..2a19ff9a1 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -53,6 +53,8 @@ class _SpeedMenuState extends State { _queueService.setPlaybackSpeed(valueDouble); setState(() {}); + + refreshInputText(); } void refreshInputText() { From a9257e7b2f0046a0d79c8a568037e7622ed3aa4c Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 22 Feb 2024 11:46:34 +0100 Subject: [PATCH 19/33] optimize speed input layout --- lib/components/AlbumScreen/song_menu.dart | 12 +++--- lib/components/AlbumScreen/speed_menu.dart | 47 ++++++++++++---------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 8810a1208..3e1b226d4 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -132,12 +132,17 @@ class _SongMenuState extends State { bool speedWidgetWasVisible = false; bool showSpeedMenu = false; final dragController = DraggableScrollableController(); + double initialSheetExtent = 0.0; + double inputStep = 0.9; + double oldExtent = 0.0; @override void initState() { super.initState(); _imageTheme = widget.playerScreenTheme; // use player screen theme if provided + initialSheetExtent = widget.showPlaybackControls ? 0.6 : 0.45; + oldExtent = initialSheetExtent; } /// Sets the item's favourite on the Jellyfin server. @@ -218,9 +223,6 @@ class _SongMenuState extends State { return false; } - final inputStep = 0.9; - var oldExtent = 0.0; - void toggleSpeedMenu() { setState(() { showSpeedMenu = !showSpeedMenu; @@ -229,7 +231,7 @@ class _SongMenuState extends State { Vibrate.feedback(FeedbackType.selection); } - scrollToExtent( + void scrollToExtent( DraggableScrollableController scrollController, double? percentage) { var currentSize = scrollController.size; if (percentage != null && @@ -263,7 +265,7 @@ class _SongMenuState extends State { controller: dragController, snap: true, snapSizes: widget.showPlaybackControls ? const [0.6] : const [0.45], - initialChildSize: widget.showPlaybackControls ? 0.6 : 0.45, + initialChildSize: initialSheetExtent, minChildSize: 0.15, expand: false, builder: (context, scrollController) { diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index 2a19ff9a1..e45233df0 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -1,5 +1,6 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; import 'package:flutter_vibrate/flutter_vibrate.dart'; @@ -40,6 +41,9 @@ class _SpeedMenuState extends State { const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), label: Center(child: Text(AppLocalizations.of(context)!.speed)), floatingLabelBehavior: FloatingLabelBehavior.never, + constraints: const BoxConstraints( + maxWidth: 125, + ), border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.circular(16), @@ -92,6 +96,7 @@ class _SpeedMenuState extends State { child: Form( key: _formKey, child: Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ SimpleButton( text: AppLocalizations.of(context)!.reset, @@ -103,28 +108,26 @@ class _SpeedMenuState extends State { refreshInputText(); }, ), - Expanded( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 40.0), - child: TextFormField( - controller: _textController, - keyboardType: TextInputType.number, - textAlign: TextAlign.center, - decoration: inputFieldDecoration(), - validator: (value) { - if (value == null || value.isEmpty) { - return AppLocalizations.of(context)!.required; - } - - if (double.tryParse(value) == null) { - return AppLocalizations.of(context)! - .invalidNumber; - } - return null; - }, - onSaved: (value) => saveSpeedInput(value), - onFieldSubmitted: (value) => saveSpeedInput(value), - ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: TextFormField( + controller: _textController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: inputFieldDecoration(), + validator: (value) { + if (value == null || value.isEmpty) { + return AppLocalizations.of(context)!.required; + } + + if (double.tryParse(value) == null) { + return AppLocalizations.of(context)! + .invalidNumber; + } + return null; + }, + onSaved: (value) => saveSpeedInput(value), + onFieldSubmitted: (value) => saveSpeedInput(value), ), ), SimpleButton( From 8ddbc55f1998d66d67a7ce71d63520a738f81e2f Mon Sep 17 00:00:00 2001 From: lymnyx Date: Thu, 22 Feb 2024 16:37:40 +0100 Subject: [PATCH 20/33] ValueListenableBuilder instead of onPresetSelected() --- lib/components/AlbumScreen/preset_chip.dart | 3 - lib/components/AlbumScreen/speed_menu.dart | 141 ++++++++++---------- 2 files changed, 73 insertions(+), 71 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index 8d0986dcb..bacb1bdc1 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -20,7 +20,6 @@ class PresetChips extends StatefulWidget { required this.activeValue, this.onTap, this.mainColour, - this.onPresetSelected, }) : super(key: key); final PresetTypes type; @@ -29,7 +28,6 @@ class PresetChips extends StatefulWidget { final double activeValue; final Function()? onTap; final Color? mainColour; // used for different background colours - final Function()? onPresetSelected; final chipWidth = 55.0; @@ -84,7 +82,6 @@ class _PresetChipsState extends State { onTap: () { setState(() {}); _queueService.setPlaybackSpeed(value); - widget.onPresetSelected?.call(); }, ); } diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index e45233df0..096ac108f 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -32,6 +32,7 @@ class _SpeedMenuState extends State { final _textController = TextEditingController( text: FinampSettingsHelper.finampSettings.playbackSpeed.toString()); final _formKey = GlobalKey(); + final _settingsListener = FinampSettingsHelper.finampSettingsListener; InputDecoration inputFieldDecoration() { return InputDecoration( @@ -76,76 +77,80 @@ class _SpeedMenuState extends State { margin: EdgeInsets.only(top: 10.0, bottom: 10.0, left: 10.0, right: 10.0), child: Padding( padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: PresetChips( - type: PresetTypes.speed, - mainColour: widget.iconColor, - values: presets, - activeValue: - FinampSettingsHelper.finampSettings.playbackSpeed, - onPresetSelected: () { - setState(() {}); - refreshInputText(); - })), - Padding( - padding: EdgeInsets.only(top: 8.0, left: 25.0, right: 25.0), - child: Form( - key: _formKey, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SimpleButton( - text: AppLocalizations.of(context)!.reset, - icon: TablerIcons.arrow_back_up_double, - iconColor: widget.iconColor, - onPressed: () { - _queueService.setPlaybackSpeed(1.0); - setState(() {}); - refreshInputText(); - }, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: TextFormField( - controller: _textController, - keyboardType: TextInputType.number, - textAlign: TextAlign.center, - decoration: inputFieldDecoration(), - validator: (value) { - if (value == null || value.isEmpty) { - return AppLocalizations.of(context)!.required; - } - - if (double.tryParse(value) == null) { - return AppLocalizations.of(context)! - .invalidNumber; - } - return null; - }, - onSaved: (value) => saveSpeedInput(value), - onFieldSubmitted: (value) => saveSpeedInput(value), + child: ValueListenableBuilder( + valueListenable: _settingsListener, + builder: (BuildContext builder, value, Widget? child) { + _textController.text = + FinampSettingsHelper.finampSettings.playbackSpeed.toString(); + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: PresetChips( + type: PresetTypes.speed, + mainColour: widget.iconColor, + values: presets, + activeValue: + FinampSettingsHelper.finampSettings.playbackSpeed, + )), + Padding( + padding: EdgeInsets.only(top: 8.0, left: 25.0, right: 25.0), + child: Form( + key: _formKey, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SimpleButton( + text: AppLocalizations.of(context)!.reset, + icon: TablerIcons.arrow_back_up_double, + iconColor: widget.iconColor, + onPressed: () { + _queueService.setPlaybackSpeed(1.0); + }, + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12.0), + child: TextFormField( + controller: _textController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + decoration: inputFieldDecoration(), + validator: (value) { + if (value == null || value.isEmpty) { + return AppLocalizations.of(context)!.required; + } + + if (double.tryParse(value) == null) { + return AppLocalizations.of(context)! + .invalidNumber; + } + return null; + }, + onSaved: (value) => saveSpeedInput(value), + onFieldSubmitted: (value) => + saveSpeedInput(value), + ), + ), + child ?? const SizedBox(), + ], ), ), - SimpleButton( - text: AppLocalizations.of(context)!.apply, - icon: TablerIcons.check, - iconColor: widget.iconColor, - onPressed: () { - if (_formKey.currentState?.validate() == true) { - _formKey.currentState!.save(); - } - }, - ), - ], - ), - ), - ), - ], - ), + ), + ], + ); + }, + child: SimpleButton( + text: AppLocalizations.of(context)!.apply, + icon: TablerIcons.check, + iconColor: widget.iconColor, + onPressed: () { + if (_formKey.currentState?.validate() == true) { + _formKey.currentState!.save(); + } + }, + )), ), ); } From ec4ac44162b9c6a9c57704b4b1e7e00eecf4a249 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Thu, 22 Feb 2024 17:54:07 +0100 Subject: [PATCH 21/33] use TextButton for PresetChip, more layout improvements --- lib/components/AlbumScreen/preset_chip.dart | 60 +++++++++++---------- lib/components/AlbumScreen/speed_menu.dart | 13 +++-- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index bacb1bdc1..783543e0c 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -5,7 +5,6 @@ import 'package:get_it/get_it.dart'; const _radius = Radius.circular(4); const _borderRadius = BorderRadius.all(_radius); -const _height = 36.0; final _defaultBackgroundColour = Colors.white.withOpacity(0.1); enum PresetTypes { @@ -20,6 +19,9 @@ class PresetChips extends StatefulWidget { required this.activeValue, this.onTap, this.mainColour, + this.onPresetSelected, + this.chipWidth = 64.0, + this.chipHeight = 44.0, }) : super(key: key); final PresetTypes type; @@ -28,8 +30,10 @@ class PresetChips extends StatefulWidget { final double activeValue; final Function()? onTap; final Color? mainColour; // used for different background colours + final Function()? onPresetSelected; + final double chipWidth; + final double chipHeight; - final chipWidth = 55.0; @override State createState() => _PresetChipsState(); @@ -40,8 +44,8 @@ class _PresetChipsState extends State { final _controller = ScrollController(); bool scrolledAlready = false; - scrollToActivePreset(double currentValue, double maxWidth) { - if (!_controller.hasClients) return false; + void scrollToActivePreset(double currentValue, double maxWidth) { + if (!_controller.hasClients) return; var offset = (widget.chipWidth + 8.0) * widget.values.indexOf(currentValue) - maxWidth / 2 + @@ -57,7 +61,7 @@ class _PresetChipsState extends State { ); } - generatePresetChip(value, BoxConstraints constraints) { + PresetChip generatePresetChip(value, BoxConstraints constraints) { // Scroll to the active preset if (value == widget.activeValue) { if (scrolledAlready) { @@ -70,7 +74,7 @@ class _PresetChipsState extends State { } } - var stringValue = "x$value"; + final stringValue = "x$value"; return PresetChip( value: stringValue, @@ -79,9 +83,11 @@ class _PresetChipsState extends State { : widget.mainColour?.withOpacity(0.1), isTextBold: value == 1.0, width: widget.chipWidth, + height: widget.chipHeight, onTap: () { setState(() {}); _queueService.setPlaybackSpeed(value); + widget.onPresetSelected?.call(); }, ); } @@ -94,7 +100,6 @@ class _PresetChipsState extends State { scrollDirection: Axis.horizontal, child: Wrap( spacing: 8.0, - runSpacing: 8.0, crossAxisAlignment: WrapCrossAlignment.center, children: List.generate(widget.values.length, (index) => generatePresetChip(widget.values[index], constraints)), @@ -108,6 +113,7 @@ class PresetChip extends StatelessWidget { const PresetChip({ Key? key, required this.width, + required this.height, this.value = "", this.onTap, this.backgroundColour, @@ -115,6 +121,7 @@ class PresetChip extends StatelessWidget { }) : super(key: key); final double width; + final double height; final String value; final void Function()? onTap; final Color? backgroundColour; @@ -125,27 +132,24 @@ class PresetChip extends StatelessWidget { final backgroundColor = backgroundColour ?? _defaultBackgroundColour; final color = Theme.of(context).textTheme.bodySmall?.color ?? Colors.white; - return SizedBox( - width: width, - height: _height, - child: Material( - color: backgroundColor, - borderRadius: _borderRadius, - child: InkWell( - onTap: onTap, - borderRadius: _borderRadius, - child: Center( - child: Text( - value, - style: TextStyle( - color: color, - overflow: TextOverflow.ellipsis, - fontWeight: - isTextBold! ? FontWeight.w700 : FontWeight.normal), - softWrap: false, - ), - )), - ), + return TextButton( + style: TextButton.styleFrom( + backgroundColor: backgroundColor, + shape: const RoundedRectangleBorder(borderRadius: _borderRadius), + minimumSize: Size(width, height), + padding: const EdgeInsets.symmetric(horizontal: 2.0), + visualDensity: VisualDensity.compact, + ), + onPressed: onTap, + child: Text( + value, + style: TextStyle( + color: color, + overflow: TextOverflow.visible, + fontWeight: isTextBold! ? FontWeight.w700 : FontWeight.normal, + ), + softWrap: false, + ), ); } } diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index 096ac108f..549920b04 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -39,12 +39,14 @@ class _SpeedMenuState extends State { filled: true, fillColor: widget.iconColor.withOpacity(0.1), contentPadding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0), label: Center(child: Text(AppLocalizations.of(context)!.speed)), floatingLabelBehavior: FloatingLabelBehavior.never, constraints: const BoxConstraints( - maxWidth: 125, + maxWidth: 82, + maxHeight: 40, ), + isDense: true, border: OutlineInputBorder( borderSide: BorderSide.none, borderRadius: BorderRadius.circular(16), @@ -74,9 +76,9 @@ class _SpeedMenuState extends State { borderRadius: BorderRadius.circular(10), color: widget.iconColor.withOpacity(0.1), ), - margin: EdgeInsets.only(top: 10.0, bottom: 10.0, left: 10.0, right: 10.0), + margin: EdgeInsets.symmetric(horizontal: 10.0, vertical: 10.0), child: Padding( - padding: const EdgeInsets.only(top: 12.0, bottom: 12.0), + padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), child: ValueListenableBuilder( valueListenable: _settingsListener, builder: (BuildContext builder, value, Widget? child) { @@ -95,7 +97,7 @@ class _SpeedMenuState extends State { FinampSettingsHelper.finampSettings.playbackSpeed, )), Padding( - padding: EdgeInsets.only(top: 8.0, left: 25.0, right: 25.0), + padding: EdgeInsets.only(top: 6.0), child: Form( key: _formKey, child: Row( @@ -116,6 +118,7 @@ class _SpeedMenuState extends State { controller: _textController, keyboardType: TextInputType.number, textAlign: TextAlign.center, + cursorRadius: const Radius.circular(4), decoration: inputFieldDecoration(), validator: (value) { if (value == null || value.isEmpty) { From 1e65dec4422f6cadcf8f0ccc5d07ce1ba96aa3a1 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Thu, 22 Feb 2024 19:54:34 +0100 Subject: [PATCH 22/33] Improved preset scroll function --- lib/components/AlbumScreen/preset_chip.dart | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index 783543e0c..b486c178e 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -6,6 +6,7 @@ import 'package:get_it/get_it.dart'; const _radius = Radius.circular(4); const _borderRadius = BorderRadius.all(_radius); final _defaultBackgroundColour = Colors.white.withOpacity(0.1); +const _spacing = 8.0; enum PresetTypes { speed, @@ -34,7 +35,6 @@ class PresetChips extends StatefulWidget { final double chipWidth; final double chipHeight; - @override State createState() => _PresetChipsState(); } @@ -46,13 +46,13 @@ class _PresetChipsState extends State { void scrollToActivePreset(double currentValue, double maxWidth) { if (!_controller.hasClients) return; - var offset = - (widget.chipWidth + 8.0) * widget.values.indexOf(currentValue) - - maxWidth / 2 + - widget.chipWidth / 2; + var offset = widget.chipWidth * widget.values.indexOf(currentValue) + + widget.chipWidth / 2 - + maxWidth / 2 - + _spacing / 2; - offset = min( - max(0, offset), maxWidth - _controller.position.maxScrollExtent + 15); + offset = min(max(0, offset), + widget.chipWidth * (widget.values.length) - maxWidth - _spacing); _controller.animateTo( offset, @@ -99,7 +99,7 @@ class _PresetChipsState extends State { controller: _controller, scrollDirection: Axis.horizontal, child: Wrap( - spacing: 8.0, + spacing: _spacing, crossAxisAlignment: WrapCrossAlignment.center, children: List.generate(widget.values.length, (index) => generatePresetChip(widget.values[index], constraints)), @@ -139,17 +139,17 @@ class PresetChip extends StatelessWidget { minimumSize: Size(width, height), padding: const EdgeInsets.symmetric(horizontal: 2.0), visualDensity: VisualDensity.compact, - ), - onPressed: onTap, - child: Text( - value, - style: TextStyle( - color: color, - overflow: TextOverflow.visible, - fontWeight: isTextBold! ? FontWeight.w700 : FontWeight.normal, - ), - softWrap: false, - ), + ), + onPressed: onTap, + child: Text( + value, + style: TextStyle( + color: color, + overflow: TextOverflow.visible, + fontWeight: isTextBold! ? FontWeight.w700 : FontWeight.normal, + ), + softWrap: false, + ), ); } } From 9b8c9861f0acda42e656a1241950e6270eec0111 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Sun, 25 Feb 2024 19:29:01 +0100 Subject: [PATCH 23/33] Add missing space in Top Songs list tile --- lib/components/AlbumScreen/song_list_tile.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/components/AlbumScreen/song_list_tile.dart b/lib/components/AlbumScreen/song_list_tile.dart index 16aa9442f..3166fc5ae 100644 --- a/lib/components/AlbumScreen/song_list_tile.dart +++ b/lib/components/AlbumScreen/song_list_tile.dart @@ -178,7 +178,7 @@ class _SongListTileState extends ConsumerState if (widget.showPlayCount) TextSpan( text: - "· ${AppLocalizations.of(context)!.playCountValue(widget.item.userData?.playCount ?? 0)}", + " · ${AppLocalizations.of(context)!.playCountValue(widget.item.userData?.playCount ?? 0)}", style: TextStyle(color: Theme.of(context).disabledColor), ), @@ -333,11 +333,12 @@ class _SongListTileState extends ConsumerState if (!mounted) return false; GlobalSnackbar.message( - (scaffold) => FinampSettingsHelper.finampSettings.swipeInsertQueueNext - ? AppLocalizations.of(scaffold)! - .confirmAddToNextUp("track") - : AppLocalizations.of(scaffold)! - .confirmAddToQueue("track"), + (scaffold) => + FinampSettingsHelper.finampSettings.swipeInsertQueueNext + ? AppLocalizations.of(scaffold)! + .confirmAddToNextUp("track") + : AppLocalizations.of(scaffold)! + .confirmAddToQueue("track"), isConfirmation: true, ); From 117d06737df87bc4c6dd0f136d3e7ced90203368 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Tue, 27 Feb 2024 20:52:18 +0100 Subject: [PATCH 24/33] Very basic slider instead of input --- lib/components/AlbumScreen/preset_chip.dart | 17 +- lib/components/AlbumScreen/speed_menu.dart | 168 +++++++++++--------- 2 files changed, 100 insertions(+), 85 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index b486c178e..7ceedf44b 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -78,10 +78,13 @@ class _PresetChipsState extends State { return PresetChip( value: stringValue, - backgroundColour: value == widget.activeValue - ? widget.mainColour?.withOpacity(0.4) - : widget.mainColour?.withOpacity(0.1), - isTextBold: value == 1.0, + backgroundColour: + widget.mainColour?.withOpacity(value == widget.activeValue + ? 0.4 + : (value == 1.0) + ? 0.2 + : 0.1), + isPresetDefault: value == 1.0, width: widget.chipWidth, height: widget.chipHeight, onTap: () { @@ -117,7 +120,7 @@ class PresetChip extends StatelessWidget { this.value = "", this.onTap, this.backgroundColour, - this.isTextBold, + this.isPresetDefault, }) : super(key: key); final double width; @@ -125,7 +128,7 @@ class PresetChip extends StatelessWidget { final String value; final void Function()? onTap; final Color? backgroundColour; - final bool? isTextBold; + final bool? isPresetDefault; @override Widget build(BuildContext context) { @@ -146,7 +149,7 @@ class PresetChip extends StatelessWidget { style: TextStyle( color: color, overflow: TextOverflow.visible, - fontWeight: isTextBold! ? FontWeight.w700 : FontWeight.normal, + fontWeight: isPresetDefault! ? FontWeight.w700 : FontWeight.normal, ), softWrap: false, ), diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index 549920b04..e0d907dec 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -8,12 +8,73 @@ import 'package:get_it/get_it.dart'; import 'package:finamp/services/queue_service.dart'; import '../../services/finamp_settings_helper.dart'; -import '../Buttons/simple_button.dart'; import 'preset_chip.dart'; final _queueService = GetIt.instance(); final presets = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0]; +class SpeedSlider extends StatefulWidget { + const SpeedSlider({ + Key? key, + required this.iconColor, + required this.saveSpeedInput, + }) : super(key: key); + + final Color iconColor; + final Function saveSpeedInput; + + @override + State createState() => _SpeedSliderState(); +} + +class _SpeedSliderState extends State { + double? _dragValue; + + @override + Widget build(BuildContext context) { + return SliderTheme( + data: SliderTheme.of(context).copyWith( + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10), + trackHeight: 8.0, + inactiveTrackColor: widget.iconColor.withOpacity(0.35), + activeTrackColor: widget.iconColor.withOpacity(0.6), + showValueIndicator: ShowValueIndicator.always, + valueIndicatorColor: Color.lerp(Colors.black, widget.iconColor, 0.1), + valueIndicatorTextStyle: Theme.of(context).textTheme.labelLarge, + valueIndicatorShape: const RectangularSliderValueIndicatorShape(), + ), + child: ExcludeSemantics( + child: Slider( + min: 0.20, + max: 5.00, + value: + _dragValue ?? FinampSettingsHelper.finampSettings.playbackSpeed, + onChanged: (value) { + value = (value / 0.05).round() * 0.05; + setState(() { + _dragValue = value; + }); + }, + onChangeStart: (value) { + value = (value / 0.05).round() * 0.05; + setState(() { + _dragValue = value; + }); + }, + onChangeEnd: (value) { + _dragValue = null; + value = (value / 0.05).round() * 0.05; + widget.saveSpeedInput(value); + }, + label: + (_dragValue ?? FinampSettingsHelper.finampSettings.playbackSpeed) + .toStringAsFixed(2), + ), + ), + ); + } +} + class SpeedMenu extends StatefulWidget { const SpeedMenu({ Key? key, @@ -31,7 +92,6 @@ class SpeedMenu extends StatefulWidget { class _SpeedMenuState extends State { final _textController = TextEditingController( text: FinampSettingsHelper.finampSettings.playbackSpeed.toString()); - final _formKey = GlobalKey(); final _settingsListener = FinampSettingsHelper.finampSettingsListener; InputDecoration inputFieldDecoration() { @@ -54,9 +114,8 @@ class _SpeedMenuState extends State { ); } - void saveSpeedInput(value) { - final valueDouble = - (min(max(double.parse(value!), 0), 5) * 100).roundToDouble() / 100; + void saveSpeedInput(double value) { + final valueDouble = (min(max(value, 0), 5) * 100).roundToDouble() / 100; _queueService.setPlaybackSpeed(valueDouble); setState(() {}); @@ -80,80 +139,33 @@ class _SpeedMenuState extends State { child: Padding( padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), child: ValueListenableBuilder( - valueListenable: _settingsListener, - builder: (BuildContext builder, value, Widget? child) { - _textController.text = - FinampSettingsHelper.finampSettings.playbackSpeed.toString(); - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: PresetChips( - type: PresetTypes.speed, - mainColour: widget.iconColor, - values: presets, - activeValue: - FinampSettingsHelper.finampSettings.playbackSpeed, - )), - Padding( - padding: EdgeInsets.only(top: 6.0), - child: Form( - key: _formKey, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SimpleButton( - text: AppLocalizations.of(context)!.reset, - icon: TablerIcons.arrow_back_up_double, - iconColor: widget.iconColor, - onPressed: () { - _queueService.setPlaybackSpeed(1.0); - }, - ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12.0), - child: TextFormField( - controller: _textController, - keyboardType: TextInputType.number, - textAlign: TextAlign.center, - cursorRadius: const Radius.circular(4), - decoration: inputFieldDecoration(), - validator: (value) { - if (value == null || value.isEmpty) { - return AppLocalizations.of(context)!.required; - } - - if (double.tryParse(value) == null) { - return AppLocalizations.of(context)! - .invalidNumber; - } - return null; - }, - onSaved: (value) => saveSpeedInput(value), - onFieldSubmitted: (value) => - saveSpeedInput(value), - ), - ), - child ?? const SizedBox(), - ], - ), - ), + valueListenable: _settingsListener, + builder: (BuildContext builder, value, Widget? child) { + _textController.text = + FinampSettingsHelper.finampSettings.playbackSpeed.toString(); + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: PresetChips( + type: PresetTypes.speed, + mainColour: widget.iconColor, + values: presets, + activeValue: + FinampSettingsHelper.finampSettings.playbackSpeed, + )), + Padding( + padding: EdgeInsets.only(top: 6.0), + child: SpeedSlider( + iconColor: widget.iconColor, + saveSpeedInput: saveSpeedInput, ), - ], - ); - }, - child: SimpleButton( - text: AppLocalizations.of(context)!.apply, - icon: TablerIcons.check, - iconColor: widget.iconColor, - onPressed: () { - if (_formKey.currentState?.validate() == true) { - _formKey.currentState!.save(); - } - }, - )), + ), + ], + ); + }, + ), ), ); } From 07e974ffca75a89dd5cd482b89624ecf849ecf82 Mon Sep 17 00:00:00 2001 From: lymnyx Date: Wed, 6 Mar 2024 14:21:15 +0100 Subject: [PATCH 25/33] Add decrease/increase buttons. --- lib/components/AlbumScreen/speed_menu.dart | 78 +++++++++++++++++++--- lib/l10n/app_en.arb | 8 +++ 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index e0d907dec..f9e14268a 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -12,6 +12,10 @@ import 'preset_chip.dart'; final _queueService = GetIt.instance(); final presets = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0]; +const speedMin = 0.20; +const speedMax = 5.00; +const speedSliderStep = 0.05; +const speedButtonStep = 0.10; class SpeedSlider extends StatefulWidget { const SpeedSlider({ @@ -45,25 +49,25 @@ class _SpeedSliderState extends State { ), child: ExcludeSemantics( child: Slider( - min: 0.20, - max: 5.00, + min: speedMin, + max: speedMax, value: _dragValue ?? FinampSettingsHelper.finampSettings.playbackSpeed, onChanged: (value) { - value = (value / 0.05).round() * 0.05; + value = (value / speedSliderStep).round() * speedSliderStep; setState(() { _dragValue = value; }); }, onChangeStart: (value) { - value = (value / 0.05).round() * 0.05; + value = (value / speedSliderStep).round() * speedSliderStep; setState(() { _dragValue = value; }); }, onChangeEnd: (value) { _dragValue = null; - value = (value / 0.05).round() * 0.05; + value = (value / speedSliderStep).round() * speedSliderStep; widget.saveSpeedInput(value); }, label: @@ -156,11 +160,65 @@ class _SpeedMenuState extends State { FinampSettingsHelper.finampSettings.playbackSpeed, )), Padding( - padding: EdgeInsets.only(top: 6.0), - child: SpeedSlider( - iconColor: widget.iconColor, - saveSpeedInput: saveSpeedInput, - ), + padding: EdgeInsets.only(top: 6.0, left: 12.0, right: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon( + TablerIcons.minus, + color: widget.iconColor, + ), + onPressed: () { + final currentSpeed = FinampSettingsHelper + .finampSettings.playbackSpeed; + + if (currentSpeed > speedMin) { + _queueService.setPlaybackSpeed(max( + speedMin, + double.parse((currentSpeed - speedButtonStep) + .toStringAsFixed(2)))); + Vibrate.feedback(FeedbackType.success); + } else { + Vibrate.feedback(FeedbackType.error); + } + }, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(12.0), + tooltip: AppLocalizations.of(context)! + .playbackSpeedDecreaseLabel, + ), + Expanded( + child: SpeedSlider( + iconColor: widget.iconColor, + saveSpeedInput: saveSpeedInput, + ), + ), + IconButton( + icon: Icon( + TablerIcons.plus, + color: widget.iconColor, + ), + onPressed: () { + final currentSpeed = FinampSettingsHelper + .finampSettings.playbackSpeed; + + if (currentSpeed < speedMax) { + _queueService.setPlaybackSpeed(min( + speedMax, + double.parse((currentSpeed + speedButtonStep) + .toStringAsFixed(2)))); + Vibrate.feedback(FeedbackType.success); + } else { + Vibrate.feedback(FeedbackType.error); + } + }, + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(12.0), + tooltip: AppLocalizations.of(context)! + .playbackSpeedIncreaseLabel, + ), + ]), ), ], ); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ce14a9e0f..3833a7f9a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -842,6 +842,14 @@ } } }, + "playbackSpeedDecreaseLabel": "Decrease playback speed", + "@playbackSpeedDecreaseLabel": { + "description": "Label for the button in the speed menu that decreases the playback speed." + }, + "playbackSpeedIncreaseLabel": "Increase playback speed", + "@playbackSpeedIncreaseLabel": { + "description": "Label for the button in the speed menu that increases the playback speed." + }, "loopModeNoneButtonLabel": "Looping off", "@loopModeNoneButtonLabel": { "description": "Label for the button that toggles the loop mode between off, loop one, and loop all, while the queue is in loop off mode" From e88918cbdf6caa2b397282d24f600e0851236e43 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Sat, 30 Mar 2024 21:28:14 +0100 Subject: [PATCH 26/33] update slider design, add debouncing, add haptic feedback --- lib/components/AlbumScreen/preset_chip.dart | 9 ++-- lib/components/AlbumScreen/speed_menu.dart | 49 ++++++++++++++------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index 7ceedf44b..bdd8fd5c2 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -80,10 +80,11 @@ class _PresetChipsState extends State { value: stringValue, backgroundColour: widget.mainColour?.withOpacity(value == widget.activeValue - ? 0.4 + ? 0.6 : (value == 1.0) - ? 0.2 + ? 0.3 : 0.1), + isSelected: value == widget.activeValue, isPresetDefault: value == 1.0, width: widget.chipWidth, height: widget.chipHeight, @@ -120,6 +121,7 @@ class PresetChip extends StatelessWidget { this.value = "", this.onTap, this.backgroundColour, + this.isSelected, this.isPresetDefault, }) : super(key: key); @@ -128,6 +130,7 @@ class PresetChip extends StatelessWidget { final String value; final void Function()? onTap; final Color? backgroundColour; + final bool? isSelected; final bool? isPresetDefault; @override @@ -149,7 +152,7 @@ class PresetChip extends StatelessWidget { style: TextStyle( color: color, overflow: TextOverflow.visible, - fontWeight: isPresetDefault! ? FontWeight.w700 : FontWeight.normal, + fontWeight: isSelected! ? FontWeight.w800 : FontWeight.normal, ), softWrap: false, ), diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index f9e14268a..92e1cbf82 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -1,4 +1,6 @@ +import 'dart:async'; import 'dart:math'; +import 'package:finamp/services/feedback_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -11,9 +13,9 @@ import '../../services/finamp_settings_helper.dart'; import 'preset_chip.dart'; final _queueService = GetIt.instance(); -final presets = [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0]; -const speedMin = 0.20; -const speedMax = 5.00; +final presets = [0.75, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5]; +const speedMin = 0.35; +const speedMax = 2.50; const speedSliderStep = 0.05; const speedButtonStep = 0.10; @@ -33,19 +35,29 @@ class SpeedSlider extends StatefulWidget { class _SpeedSliderState extends State { double? _dragValue; + Timer? _debouncer; + + @override + void dispose() { + _debouncer?.cancel(); + super.dispose(); + } @override Widget build(BuildContext context) { return SliderTheme( data: SliderTheme.of(context).copyWith( - thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 10), - trackHeight: 8.0, - inactiveTrackColor: widget.iconColor.withOpacity(0.35), + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 24/2.0), + trackHeight: 24.0, + inactiveTrackColor: widget.iconColor.withOpacity(0.3), activeTrackColor: widget.iconColor.withOpacity(0.6), showValueIndicator: ShowValueIndicator.always, - valueIndicatorColor: Color.lerp(Colors.black, widget.iconColor, 0.1), + valueIndicatorColor: Color.lerp(Theme.of(context).cardColor, widget.iconColor, 0.6), valueIndicatorTextStyle: Theme.of(context).textTheme.labelLarge, valueIndicatorShape: const RectangularSliderValueIndicatorShape(), + tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 1.5), + activeTickMarkColor: widget.iconColor.withOpacity(0.9), + overlayShape: SliderComponentShape.noOverlay, // get rid of padding ), child: ExcludeSemantics( child: Slider( @@ -53,11 +65,19 @@ class _SpeedSliderState extends State { max: speedMax, value: _dragValue ?? FinampSettingsHelper.finampSettings.playbackSpeed, + // divisions: ((speedMax - speedMin) / speedSliderStep / 2).round(), onChanged: (value) { - value = (value / speedSliderStep).round() * speedSliderStep; - setState(() { - _dragValue = value; - }); + value = ((value / speedSliderStep).round() * speedSliderStep); + if (_dragValue != value) { + setState(() { + _dragValue = value; + }); + FeedbackHelper.feedback(FeedbackType.impact); + _debouncer?.cancel(); + _debouncer = Timer(const Duration(milliseconds: 150), () { + widget.saveSpeedInput(value); + }); + } }, onChangeStart: (value) { value = (value / speedSliderStep).round() * speedSliderStep; @@ -69,6 +89,7 @@ class _SpeedSliderState extends State { _dragValue = null; value = (value / speedSliderStep).round() * speedSliderStep; widget.saveSpeedInput(value); + FeedbackHelper.feedback(FeedbackType.selection); }, label: (_dragValue ?? FinampSettingsHelper.finampSettings.playbackSpeed) @@ -151,7 +172,7 @@ class _SpeedMenuState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding( - padding: EdgeInsets.symmetric(horizontal: 12.0), + padding: const EdgeInsets.symmetric(horizontal: 12.0), child: PresetChips( type: PresetTypes.speed, mainColour: widget.iconColor, @@ -160,7 +181,7 @@ class _SpeedMenuState extends State { FinampSettingsHelper.finampSettings.playbackSpeed, )), Padding( - padding: EdgeInsets.only(top: 6.0, left: 12.0, right: 12.0), + padding: const EdgeInsets.only(top: 8.0, left: 2.0, right: 2.0, bottom: 2.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -184,7 +205,6 @@ class _SpeedMenuState extends State { } }, visualDensity: VisualDensity.compact, - padding: const EdgeInsets.all(12.0), tooltip: AppLocalizations.of(context)! .playbackSpeedDecreaseLabel, ), @@ -214,7 +234,6 @@ class _SpeedMenuState extends State { } }, visualDensity: VisualDensity.compact, - padding: const EdgeInsets.all(12.0), tooltip: AppLocalizations.of(context)! .playbackSpeedIncreaseLabel, ), From 813b100a95fcb837fe2d8f27833fe5732d9c482f Mon Sep 17 00:00:00 2001 From: lymnyx Date: Sun, 31 Mar 2024 11:25:41 +0200 Subject: [PATCH 27/33] Speed and margin changes --- lib/components/AlbumScreen/speed_menu.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index 92e1cbf82..d79c08649 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -15,7 +15,7 @@ import 'preset_chip.dart'; final _queueService = GetIt.instance(); final presets = [0.75, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5]; const speedMin = 0.35; -const speedMax = 2.50; +const speedMax = 3.50; const speedSliderStep = 0.05; const speedButtonStep = 0.10; @@ -47,12 +47,13 @@ class _SpeedSliderState extends State { Widget build(BuildContext context) { return SliderTheme( data: SliderTheme.of(context).copyWith( - thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 24/2.0), + thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 24 / 2.0), trackHeight: 24.0, inactiveTrackColor: widget.iconColor.withOpacity(0.3), activeTrackColor: widget.iconColor.withOpacity(0.6), showValueIndicator: ShowValueIndicator.always, - valueIndicatorColor: Color.lerp(Theme.of(context).cardColor, widget.iconColor, 0.6), + valueIndicatorColor: + Color.lerp(Theme.of(context).cardColor, widget.iconColor, 0.6), valueIndicatorTextStyle: Theme.of(context).textTheme.labelLarge, valueIndicatorShape: const RectangularSliderValueIndicatorShape(), tickMarkShape: const RoundSliderTickMarkShape(tickMarkRadius: 1.5), @@ -181,7 +182,8 @@ class _SpeedMenuState extends State { FinampSettingsHelper.finampSettings.playbackSpeed, )), Padding( - padding: const EdgeInsets.only(top: 8.0, left: 2.0, right: 2.0, bottom: 2.0), + padding: const EdgeInsets.only( + top: 8.0, left: 12.0, right: 12.0, bottom: 2.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ From bb8cde9d1941ad348843fc57dad4f9320a602a5b Mon Sep 17 00:00:00 2001 From: lymnyx Date: Sun, 31 Mar 2024 11:47:45 +0200 Subject: [PATCH 28/33] More presets, less slider range --- lib/components/AlbumScreen/speed_menu.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index d79c08649..3eae4a9d3 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -13,9 +13,9 @@ import '../../services/finamp_settings_helper.dart'; import 'preset_chip.dart'; final _queueService = GetIt.instance(); -final presets = [0.75, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5]; -const speedMin = 0.35; -const speedMax = 3.50; +final presets = [0.75, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5]; +const speedSliderMin = 0.35; +const speedSliderMax = 2.50; const speedSliderStep = 0.05; const speedButtonStep = 0.10; @@ -62,11 +62,11 @@ class _SpeedSliderState extends State { ), child: ExcludeSemantics( child: Slider( - min: speedMin, - max: speedMax, + min: speedSliderMin, + max: speedSliderMax, value: _dragValue ?? FinampSettingsHelper.finampSettings.playbackSpeed, - // divisions: ((speedMax - speedMin) / speedSliderStep / 2).round(), + // divisions: ((speedSliderMax - speedSliderMin) / speedSliderStep / 2).round(), onChanged: (value) { value = ((value / speedSliderStep).round() * speedSliderStep); if (_dragValue != value) { @@ -196,9 +196,9 @@ class _SpeedMenuState extends State { final currentSpeed = FinampSettingsHelper .finampSettings.playbackSpeed; - if (currentSpeed > speedMin) { + if (currentSpeed > speedSliderMin) { _queueService.setPlaybackSpeed(max( - speedMin, + speedSliderMin, double.parse((currentSpeed - speedButtonStep) .toStringAsFixed(2)))); Vibrate.feedback(FeedbackType.success); @@ -225,9 +225,9 @@ class _SpeedMenuState extends State { final currentSpeed = FinampSettingsHelper .finampSettings.playbackSpeed; - if (currentSpeed < speedMax) { + if (currentSpeed < speedSliderMax) { _queueService.setPlaybackSpeed(min( - speedMax, + speedSliderMax, double.parse((currentSpeed + speedButtonStep) .toStringAsFixed(2)))); Vibrate.feedback(FeedbackType.success); From bd6c233bbad2b779977b77cb4be0370f63f9663b Mon Sep 17 00:00:00 2001 From: lymnyx Date: Sun, 31 Mar 2024 14:28:37 +0200 Subject: [PATCH 29/33] Slider value fix and small visual indicator for values above slider maximum --- lib/components/AlbumScreen/speed_menu.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index 3eae4a9d3..f0dc6de86 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'dart:ui'; import 'package:finamp/services/feedback_helper.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -50,7 +51,10 @@ class _SpeedSliderState extends State { thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 24 / 2.0), trackHeight: 24.0, inactiveTrackColor: widget.iconColor.withOpacity(0.3), - activeTrackColor: widget.iconColor.withOpacity(0.6), + activeTrackColor: + FinampSettingsHelper.finampSettings.playbackSpeed > speedSliderMax + ? widget.iconColor.withOpacity(0.4) + : widget.iconColor.withOpacity(0.6), showValueIndicator: ShowValueIndicator.always, valueIndicatorColor: Color.lerp(Theme.of(context).cardColor, widget.iconColor, 0.6), @@ -64,8 +68,9 @@ class _SpeedSliderState extends State { child: Slider( min: speedSliderMin, max: speedSliderMax, - value: - _dragValue ?? FinampSettingsHelper.finampSettings.playbackSpeed, + value: _dragValue ?? + clampDouble(FinampSettingsHelper.finampSettings.playbackSpeed, + speedSliderMin, speedSliderMax), // divisions: ((speedSliderMax - speedSliderMin) / speedSliderStep / 2).round(), onChanged: (value) { value = ((value / speedSliderStep).round() * speedSliderStep); From 10596bb77c7c7f8eaa0bc2104249aed4185ae161 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 3 May 2024 15:37:51 +0200 Subject: [PATCH 30/33] use metadata provider for determining if playback speed controls should be shown --- lib/components/AlbumScreen/song_menu.dart | 50 +++++++------------ .../current_track_metadata_provider.dart | 3 +- lib/services/metadata_provider.dart | 33 +++++++++++- 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/lib/components/AlbumScreen/song_menu.dart b/lib/components/AlbumScreen/song_menu.dart index 04ed4e054..d8f7503d9 100644 --- a/lib/components/AlbumScreen/song_menu.dart +++ b/lib/components/AlbumScreen/song_menu.dart @@ -8,6 +8,7 @@ import 'package:finamp/screens/artist_screen.dart'; import 'package:finamp/screens/blurred_player_screen_background.dart'; import 'package:finamp/services/current_track_metadata_provider.dart'; import 'package:finamp/services/feedback_helper.dart'; +import 'package:finamp/services/metadata_provider.dart'; import 'package:finamp/services/music_player_background_task.dart'; import 'package:finamp/services/queue_service.dart'; import 'package:flutter/material.dart'; @@ -203,37 +204,6 @@ class _SongMenuState extends ConsumerState { } } - Future shouldShowSpeedControls(currentSpeed) async { - if (currentSpeed != 1.0 || - FinampSettingsHelper.finampSettings.playbackSpeedVisibility.index == - 1) { - return true; - } - if (FinampSettingsHelper.finampSettings.playbackSpeedVisibility.index == - 0) { - var genres = widget.item.genres!; - - for (var i = 0; i < genres.length; i++) { - if (["audiobook", "speech"].contains(genres[i].toLowerCase())) { - return true; - } - } - - try { - var parent = - await _jellyfinApiHelper.getItemById(widget.item.parentId!); - // 72e9 = 120 minutes - if (parent.runTimeTicks! > 72e9.toInt()) { - return true; - } - } catch (e) { - GlobalSnackbar.error(e); - } - } - - return false; - } - void toggleSpeedMenu() { setState(() { showSpeedMenu = !showSpeedMenu; @@ -242,6 +212,20 @@ class _SongMenuState extends ConsumerState { Vibrate.feedback(FeedbackType.selection); } + Future shouldShowSpeedControls(double currentSpeed, MetadataProvider? metadata) async { + if (currentSpeed != 1.0 || + FinampSettingsHelper.finampSettings.playbackSpeedVisibility == + PlaybackSpeedVisibility.visible) { + return true; + } + + if (FinampSettingsHelper.finampSettings.playbackSpeedVisibility == PlaybackSpeedVisibility.automatic) { + return metadata?.qualifiesForPlaybackSpeedControl ?? false; + } + + return false; +} + void scrollToExtent( DraggableScrollableController scrollController, double? percentage) { var currentSize = scrollController.size; @@ -265,6 +249,8 @@ class _SongMenuState extends ConsumerState { Theme.of(context).iconTheme.color ?? Colors.white; + final metadata = ref.watch(currentTrackMetadataProvider).unwrapPrevious(); + final menuEntries = _menuEntries(context, iconColor); var stackHeight = widget.showPlaybackControls ? 255 : 155; stackHeight += menuEntries @@ -465,7 +451,7 @@ class _SongMenuState extends ConsumerState { } return FutureBuilder( future: - shouldShowSpeedControls(playbackBehavior.speed), + shouldShowSpeedControls(playbackBehavior.speed, metadata.value), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done && diff --git a/lib/services/current_track_metadata_provider.dart b/lib/services/current_track_metadata_provider.dart index 67a33e43a..a7ef59fbe 100644 --- a/lib/services/current_track_metadata_provider.dart +++ b/lib/services/current_track_metadata_provider.dart @@ -18,7 +18,7 @@ final currentTrackMetadataProvider = BaseItemDto? base = itemToPrecache.baseItem; if (base != null) { // only fetch lyrics for the current track - final request = MetadataRequest(item: base, queueItem: itemToPrecache, includeLyrics: true); + final request = MetadataRequest(item: base, queueItem: itemToPrecache, includeLyrics: true, checkIfSpeedControlNeeded: FinampSettingsHelper.finampSettings.playbackSpeedVisibility == PlaybackSpeedVisibility.automatic); unawaited(ref.watch(metadataProvider(request).future)); } } @@ -29,6 +29,7 @@ final currentTrackMetadataProvider = item: currentTrack!.baseItem!, queueItem: currentTrack, includeLyrics: true, + checkIfSpeedControlNeeded: FinampSettingsHelper.finampSettings.playbackSpeedVisibility == PlaybackSpeedVisibility.automatic, ); return ref.watch(metadataProvider(request)); } diff --git a/lib/services/metadata_provider.dart b/lib/services/metadata_provider.dart index dfcf63a56..9e8834132 100644 --- a/lib/services/metadata_provider.dart +++ b/lib/services/metadata_provider.dart @@ -17,23 +17,26 @@ class MetadataRequest { required this.item, this.queueItem, this.includeLyrics = false, + this.checkIfSpeedControlNeeded = false, }) : super(); final BaseItemDto item; final FinampQueueItem? queueItem; final bool includeLyrics; + final bool checkIfSpeedControlNeeded; @override bool operator ==(Object other) { return other is MetadataRequest && other.includeLyrics == includeLyrics && + other.checkIfSpeedControlNeeded == checkIfSpeedControlNeeded && other.item.id == item.id && other.queueItem?.id == queueItem?.id; } @override - int get hashCode => Object.hash(item.id, queueItem?.id, includeLyrics); + int get hashCode => Object.hash(item.id, queueItem?.id, includeLyrics, checkIfSpeedControlNeeded); } class MetadataProvider { @@ -41,11 +44,13 @@ class MetadataProvider { final MediaSourceInfo mediaSourceInfo; LyricDto? lyrics; bool isDownloaded; + bool qualifiesForPlaybackSpeedControl; MetadataProvider({ required this.mediaSourceInfo, this.lyrics, this.isDownloaded = false, + this.qualifiesForPlaybackSpeedControl = false, }); bool get hasLyrics => mediaSourceInfo.mediaStreams.any((e) => e.type == "Lyric"); @@ -135,6 +140,32 @@ final AutoDisposeFutureProviderFamily final metadata = MetadataProvider(mediaSourceInfo: playbackInfo, isDownloaded: localPlaybackInfo != null); + // check if item qualifies for having playback speed control available + if (request.checkIfSpeedControlNeeded) { + + for (final genre in request.item.genres ?? []) { + if (["audiobook", "podcast", "speech"].contains(genre.toLowerCase())) { + metadata.qualifiesForPlaybackSpeedControl = true; + break; + } + } + if (!metadata.qualifiesForPlaybackSpeedControl && metadata.mediaSourceInfo.runTimeTicks! > const Duration(minutes: 15).inMicroseconds * 10) { + // we might want playback speed control for long tracks (like podcasts or audiobook chapters) + metadata.qualifiesForPlaybackSpeedControl = true; + } else { + // check if "album" is long enough to qualify for playback speed control + try { + final parent = await jellyfinApiHelper.getItemById(request.item.parentId!); + if (parent.runTimeTicks! > const Duration(hours: 2).inMicroseconds * 10) { + metadata.qualifiesForPlaybackSpeedControl = true; + } + } catch(e) { + metadataProviderLogger.warning("Failed to check if '${request.item.name}' (${request.item.id}) qualifies for playback speed controls", e); + } + } + + } + if (request.includeLyrics && metadata.hasLyrics) { //!!! only use offline metadata if the app is in offline mode From 4c3683c3b5f1e93d9d6c88dc1fa0be39f704e326 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 3 May 2024 15:39:49 +0200 Subject: [PATCH 31/33] bump min album duration required for speed control to 3 hours --- lib/services/metadata_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/metadata_provider.dart b/lib/services/metadata_provider.dart index 9e8834132..eb044dfbd 100644 --- a/lib/services/metadata_provider.dart +++ b/lib/services/metadata_provider.dart @@ -156,7 +156,7 @@ final AutoDisposeFutureProviderFamily // check if "album" is long enough to qualify for playback speed control try { final parent = await jellyfinApiHelper.getItemById(request.item.parentId!); - if (parent.runTimeTicks! > const Duration(hours: 2).inMicroseconds * 10) { + if (parent.runTimeTicks! > const Duration(hours: 3).inMicroseconds * 10) { metadata.qualifiesForPlaybackSpeedControl = true; } } catch(e) { From da023c57610c55b0ca4ba6da8c4198e7652d8e09 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 3 May 2024 23:36:28 +0200 Subject: [PATCH 32/33] added conditional playback speed feature chip - will be shown whenever the playback speed isn't x1.0 --- lib/components/AlbumScreen/preset_chip.dart | 2 +- lib/components/AlbumScreen/speed_menu.dart | 10 +++---- .../PlayerScreen/feature_chips.dart | 26 +++++++++++++------ lib/l10n/app_en.arb | 9 +++++++ lib/services/queue_service.dart | 6 +---- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/lib/components/AlbumScreen/preset_chip.dart b/lib/components/AlbumScreen/preset_chip.dart index bdd8fd5c2..5c1f8ea20 100644 --- a/lib/components/AlbumScreen/preset_chip.dart +++ b/lib/components/AlbumScreen/preset_chip.dart @@ -90,7 +90,7 @@ class _PresetChipsState extends State { height: widget.chipHeight, onTap: () { setState(() {}); - _queueService.setPlaybackSpeed(value); + _queueService.playbackSpeed = value; widget.onPresetSelected?.call(); }, ); diff --git a/lib/components/AlbumScreen/speed_menu.dart b/lib/components/AlbumScreen/speed_menu.dart index f0dc6de86..1dbb8c346 100644 --- a/lib/components/AlbumScreen/speed_menu.dart +++ b/lib/components/AlbumScreen/speed_menu.dart @@ -148,7 +148,7 @@ class _SpeedMenuState extends State { void saveSpeedInput(double value) { final valueDouble = (min(max(value, 0), 5) * 100).roundToDouble() / 100; - _queueService.setPlaybackSpeed(valueDouble); + _queueService.playbackSpeed = valueDouble; setState(() {}); refreshInputText(); @@ -202,10 +202,10 @@ class _SpeedMenuState extends State { .finampSettings.playbackSpeed; if (currentSpeed > speedSliderMin) { - _queueService.setPlaybackSpeed(max( + _queueService.playbackSpeed = max( speedSliderMin, double.parse((currentSpeed - speedButtonStep) - .toStringAsFixed(2)))); + .toStringAsFixed(2))); Vibrate.feedback(FeedbackType.success); } else { Vibrate.feedback(FeedbackType.error); @@ -231,10 +231,10 @@ class _SpeedMenuState extends State { .finampSettings.playbackSpeed; if (currentSpeed < speedSliderMax) { - _queueService.setPlaybackSpeed(min( + _queueService.playbackSpeed = min( speedSliderMax, double.parse((currentSpeed + speedButtonStep) - .toStringAsFixed(2)))); + .toStringAsFixed(2))); Vibrate.feedback(FeedbackType.success); } else { Vibrate.feedback(FeedbackType.error); diff --git a/lib/components/PlayerScreen/feature_chips.dart b/lib/components/PlayerScreen/feature_chips.dart index ffd83f232..afa6d83a3 100644 --- a/lib/components/PlayerScreen/feature_chips.dart +++ b/lib/components/PlayerScreen/feature_chips.dart @@ -40,8 +40,18 @@ class FeatureState { int? get sampleRate => audioStream?.sampleRate; int? get bitDepth => audioStream?.bitDepth; - get features { - final features = []; + List get features { + final queueService = GetIt.instance(); + + final List features = []; + + if (queueService.playbackSpeed != 1.0) { + features.add( + FeatureProperties( + text: AppLocalizations.of(context)!.playbackSpeedFeatureText(queueService.playbackSpeed), + ), + ); + } // TODO this will likely be extremely outdated if offline, hide? if (currentTrack?.baseItem?.userData?.playCount != null) { @@ -149,8 +159,8 @@ class FeatureProperties { class FeatureChips extends ConsumerWidget { const FeatureChips({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -194,11 +204,11 @@ class FeatureChips extends ConsumerWidget { class Features extends StatelessWidget { const Features({ - Key? key, + super.key, required this.features, this.backgroundColor, this.color, - }) : super(key: key); + }); final FeatureState features; final Color? backgroundColor; @@ -225,11 +235,11 @@ class Features extends StatelessWidget { class _FeatureContent extends StatelessWidget { const _FeatureContent({ - Key? key, + super.key, required this.feature, required this.backgroundColor, this.color, - }) : super(key: key); + }); final FeatureProperties feature; final Color? backgroundColor; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1a5456a44..d0f9418a3 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -862,6 +862,15 @@ } } }, + "playbackSpeedFeatureText": "x{speed} speed", + "@playbackSpeedFeatureText": { + "description": "Label for the feature chip that is shown if the playback speed is different from x1, {speed} is the current playback speed.", + "placeholders": { + "speed": { + "type": "double" + } + } + }, "playbackSpeedDecreaseLabel": "Decrease playback speed", "@playbackSpeedDecreaseLabel": { "description": "Label for the button in the speed menu that decreases the playback speed." diff --git a/lib/services/queue_service.dart b/lib/services/queue_service.dart index bacdb3431..3397d9f21 100644 --- a/lib/services/queue_service.dart +++ b/lib/services/queue_service.dart @@ -843,10 +843,6 @@ class QueueService { } } - void setPlaybackSpeed(double speed) { - playbackSpeed = speed; - } - Logger get queueServiceLogger => _queueServiceLogger; int getActualIndexByLinearIndex(int linearIndex) { @@ -1159,4 +1155,4 @@ class NextUpShuffleOrder extends ShuffleOrder { void clear() { indices.clear(); } -} \ No newline at end of file +} From e84e953fe4e7cafcd0874c907defd5361405d8e9 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Fri, 3 May 2024 23:56:59 +0200 Subject: [PATCH 33/33] updated description in settings "more info" dialog to include more details --- ...control_visibility_dropdown_list_tile.dart | 3 ++- lib/l10n/app_en.arb | 20 ++++++++++++++++--- lib/services/metadata_provider.dart | 10 +++++++--- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart index c109a0ecc..10130d804 100644 --- a/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart +++ b/lib/components/LayoutSettingsScreen/CustomizationSettingsScreen/playback_speed_control_visibility_dropdown_list_tile.dart @@ -1,3 +1,4 @@ +import 'package:finamp/services/metadata_provider.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -43,7 +44,7 @@ class PlaybackSpeedControlVisibilityDropdownListTile extends StatelessWidget { title: Text(AppLocalizations.of(context)! .playbackSpeedControlSetting), content: Text(AppLocalizations.of(context)! - .playbackSpeedControlSettingDescription), + .playbackSpeedControlSettingDescription(MetadataProvider.speedControlLongTrackDuration.inMinutes, MetadataProvider.speedControlLongAlbumDuration.inHours, MetadataProvider.speedControlGenres.join(", "))), actions: [ TextButton( onPressed: () { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d0f9418a3..3088debc7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -470,9 +470,23 @@ "@playbackSpeedControlSettingSubtitle": { "description": "Subtitle for the playback speed visibility setting" }, - "playbackSpeedControlSettingDescription": "Automatic: Finamp tries to identify whether the media you are playing is an audiobook (based on genre and album length). If Finamp thinks it is, it will show the playback speed controls.\n\nShown: The playback speed controls are available regardless of what kind of media you are playing.\n\nHidden: The playback speed controls in the song menu are hidden.", + "playbackSpeedControlSettingDescription": "Automatic:\nFinamp tries to identify whether the track you are playing is a podcast or (part of) an audiobook. This is considered to be the case if the track is longer than {trackDuration} minutes, if the track's album is longer than {albumDuration} hours, or if the track has at least one of these genres assigned: {genreList}\nPlayback speed controls will then be shown in the player screen menu.\n\nShown:\nThe playback speed controls will always be shown in the player screen menu.\n\nHidden:\nThe playback speed controls in the player screen menu are always hidden.", "@playbackSpeedControlSettingDescription": { - "description": "Description for the dropdown that selects the replay gain mode, shown in a dialog that opens when the user presses the info icon next to the dropdown" + "description": "Description for the dropdown that selects the replay gain mode, shown in a dialog that opens when the user presses the info icon next to the dropdown", + "placeholders": { + "trackDuration": { + "type": "int", + "example": "30" + }, + "albumDuration": { + "type": "int", + "example": "3" + }, + "genreList": { + "type": "String", + "example": "Podcast, Audiobook" + } + } }, "automatic": "Automatic", "@automatic": { @@ -1030,7 +1044,7 @@ "@volumeNormalizationModeSelectorSubtitle": { "description": "Subtitle for the dropdown that selects the replay gain mode" }, - "volumeNormalizationModeSelectorDescription": "Hybrid (Track + Album): Track gain is used for regular playback, but if an album is playing (either because it's the main playback queue source, or because it was added to the queue at some point), the album gain is used instead.\n\nTrack-based: Track gain is always used, regardless of whether an album is playing or not.\n\nAlbums Only: Volume Normalization is only applied while playing albums (using the album gain), but not for individual tracks.", + "volumeNormalizationModeSelectorDescription": "Hybrid (Track + Album):\nTrack gain is used for regular playback, but if an album is playing (either because it's the main playback queue source, or because it was added to the queue at some point), the album gain is used instead.\n\nTrack-based:\nTrack gain is always used, regardless of whether an album is playing or not.\n\nAlbums Only:\nVolume Normalization is only applied while playing albums (using the album gain), but not for individual tracks.", "@volumeNormalizationModeSelectorDescription": { "description": "Description for the dropdown that selects the replay gain mode, shown in a dialog that opens when the user presses the info icon next to the dropdown" }, diff --git a/lib/services/metadata_provider.dart b/lib/services/metadata_provider.dart index eb044dfbd..05e74fc49 100644 --- a/lib/services/metadata_provider.dart +++ b/lib/services/metadata_provider.dart @@ -41,6 +41,10 @@ class MetadataRequest { class MetadataProvider { + static const speedControlGenres = ["audiobook", "podcast", "speech"]; + static const speedControlLongTrackDuration = Duration(minutes: 15); + static const speedControlLongAlbumDuration = Duration(hours: 3); + final MediaSourceInfo mediaSourceInfo; LyricDto? lyrics; bool isDownloaded; @@ -144,19 +148,19 @@ final AutoDisposeFutureProviderFamily if (request.checkIfSpeedControlNeeded) { for (final genre in request.item.genres ?? []) { - if (["audiobook", "podcast", "speech"].contains(genre.toLowerCase())) { + if (MetadataProvider.speedControlGenres.contains(genre.toLowerCase())) { metadata.qualifiesForPlaybackSpeedControl = true; break; } } - if (!metadata.qualifiesForPlaybackSpeedControl && metadata.mediaSourceInfo.runTimeTicks! > const Duration(minutes: 15).inMicroseconds * 10) { + if (!metadata.qualifiesForPlaybackSpeedControl && metadata.mediaSourceInfo.runTimeTicks! > MetadataProvider.speedControlLongTrackDuration.inMicroseconds * 10) { // we might want playback speed control for long tracks (like podcasts or audiobook chapters) metadata.qualifiesForPlaybackSpeedControl = true; } else { // check if "album" is long enough to qualify for playback speed control try { final parent = await jellyfinApiHelper.getItemById(request.item.parentId!); - if (parent.runTimeTicks! > const Duration(hours: 3).inMicroseconds * 10) { + if (parent.runTimeTicks! > MetadataProvider.speedControlLongAlbumDuration.inMicroseconds * 10) { metadata.qualifiesForPlaybackSpeedControl = true; } } catch(e) {