From aac06b38ee58c17e86950a7c30011393fcd81d87 Mon Sep 17 00:00:00 2001 From: Chaphasilor Date: Wed, 9 Oct 2024 22:49:13 +0200 Subject: [PATCH] add setting for hiding feature chips - also lays groundwork for planned further customization (including/excluding specific features, reordering) --- .../PlayerScreen/feature_chips.dart | 187 ++++++++++-------- lib/l10n/app_en.arb | 40 ++++ lib/models/finamp_models.dart | 132 ++++++++++++- lib/models/finamp_models.g.dart | 161 ++++++++++++++- .../customization_settings_screen.dart | 34 ++++ lib/screens/player_screen.dart | 4 + 6 files changed, 475 insertions(+), 83 deletions(-) diff --git a/lib/components/PlayerScreen/feature_chips.dart b/lib/components/PlayerScreen/feature_chips.dart index 29ef8a8e..3347fa7d 100644 --- a/lib/components/PlayerScreen/feature_chips.dart +++ b/lib/components/PlayerScreen/feature_chips.dart @@ -30,6 +30,9 @@ class FeatureState { final FinampSettings settings; final MetadataProvider? metadata; + FinampFeatureChipsConfiguration get configuration => + settings.featureChipsConfiguration; + bool get isDownloaded => metadata?.isDownloaded ?? false; bool get isTranscoding => !isDownloaded && (currentTrack?.item.extras?["shouldTranscode"] ?? false); @@ -63,102 +66,124 @@ class FeatureState { ), ); } + + for (var feature in configuration.features) { - // TODO this will likely be extremely outdated if offline, hide? - if (currentTrack?.baseItem?.userData?.playCount != null) { - features.add( - FeatureProperties( - text: AppLocalizations.of(context)! - .playCountValue(currentTrack!.baseItem!.userData?.playCount ?? 0), - ), - ); - } - - if (currentTrack?.baseItem?.people?.isNotEmpty ?? false) { - currentTrack?.baseItem?.people?.forEach((person) { - features.add( - FeatureProperties( - text: "${person.role}: ${person.name}", - ), - ); - }); - } - - if (currentTrack?.item.extras?["downloadedSongPath"] != null) { - features.add( - FeatureProperties( - text: AppLocalizations.of(context)!.playbackModeLocal, - ), - ); - } else { - if (isTranscoding) { + // TODO this will likely be extremely outdated if offline, hide? + if (feature == FinampFeatureChipType.playCount && + currentTrack?.baseItem?.userData?.playCount != null) { features.add( FeatureProperties( - text: AppLocalizations.of(context)!.playbackModeTranscoding, - ), - ); - } else { - features.add( - //TODO differentiate between direct streaming and direct playing - // const FeatureProperties( - // text: "Direct Streaming", - // ), - FeatureProperties( - text: AppLocalizations.of(context)!.playbackModeDirectPlaying, + type: feature, + text: AppLocalizations.of(context)!.playCountValue( + currentTrack!.baseItem!.userData?.playCount ?? 0), ), ); } - } - if (metadata?.mediaSourceInfo != null) { - if (bitrate != null) { - features.add( - FeatureProperties( - text: - "${container.toUpperCase()} @ ${AppLocalizations.of(context)!.kiloBitsPerSecondLabel(bitrate! ~/ 1000)}", - ), - ); + if (feature == FinampFeatureChipType.additionalPeople && + (currentTrack?.baseItem?.people?.isNotEmpty ?? false)) { + currentTrack?.baseItem?.people?.forEach((person) { + features.add( + FeatureProperties( + type: feature, + text: "${person.role}: ${person.name}", + ), + ); + }); } - if (bitDepth != null) { - features.add( - FeatureProperties( - text: AppLocalizations.of(context)!.numberAsBit(bitDepth!), - ), - ); + if (feature == FinampFeatureChipType.playbackMode) { + if (currentTrack?.item.extras?["downloadedSongPath"] != null) { + features.add( + FeatureProperties( + type: feature, + text: AppLocalizations.of(context)!.playbackModeLocal, + ), + ); + } else { + if (isTranscoding) { + features.add( + FeatureProperties( + type: feature, + text: AppLocalizations.of(context)!.playbackModeTranscoding, + ), + ); + } else { + features.add( + //TODO differentiate between direct streaming and direct playing + // const FeatureProperties( + // text: "Direct Streaming", + // ), + FeatureProperties( + type: feature, + text: AppLocalizations.of(context)!.playbackModeDirectPlaying, + ), + ); + } + } } - if (sampleRate != null) { - features.add( - FeatureProperties( - text: AppLocalizations.of(context)! - .numberAsKiloHertz(sampleRate! / 1000.0), - ), - ); + if (metadata?.mediaSourceInfo != null) { + if (feature == FinampFeatureChipType.codec || + feature == FinampFeatureChipType.bitRate) { + // only add this feature the first time + if (!features.any((f) => f.type == FinampFeatureChipType.codec)) { + features.add( + FeatureProperties( + type: feature, + text: + "${configuration.features.contains(FinampFeatureChipType.codec) ? container.toUpperCase() : ""}${configuration.features.contains(FinampFeatureChipType.codec) && configuration.features.contains(FinampFeatureChipType.bitRate) ? " @ " : ""}${configuration.features.contains(FinampFeatureChipType.bitRate) && bitrate != null ? AppLocalizations.of(context)!.kiloBitsPerSecondLabel(bitrate! ~/ 1000) : ""}", + ), + ); + } + } + + if (feature == FinampFeatureChipType.bitDepth && bitDepth != null) { + features.add( + FeatureProperties( + type: feature, + text: AppLocalizations.of(context)!.numberAsBit(bitDepth!), + ), + ); + } + + if (feature == FinampFeatureChipType.sampleRate && sampleRate != null) { + features.add( + FeatureProperties( + type: feature, + text: AppLocalizations.of(context)! + .numberAsKiloHertz(sampleRate! / 1000.0), + ), + ); + } + + if (feature == FinampFeatureChipType.size && size != null) { + features.add( + FeatureProperties( + type: feature, + text: FileSize.getSize(size), + ), + ); + } } - if (size != null) { - features.add( - FeatureProperties( - text: FileSize.getSize(size), - ), - ); + if (feature == FinampFeatureChipType.normalizationGain && + FinampSettingsHelper.finampSettings.volumeNormalizationActive) { + double? effectiveGainChange = + getEffectiveGainChange(currentTrack!.item, currentTrack!.baseItem); + if (effectiveGainChange != null) { + features.add( + FeatureProperties( + type: feature, + text: AppLocalizations.of(context)!.numberAsDecibel( + double.parse(effectiveGainChange.toStringAsFixed(1))), + ), + ); + } } - } - if (FinampSettingsHelper.finampSettings.volumeNormalizationActive) { - double? effectiveGainChange = - getEffectiveGainChange(currentTrack!.item, currentTrack!.baseItem); - if (effectiveGainChange != null) { - features.add( - FeatureProperties( - text: AppLocalizations.of(context)!.numberAsDecibel( - double.parse(effectiveGainChange.toStringAsFixed(1))), - ), - ); - } } - return features; } } @@ -166,9 +191,11 @@ class FeatureState { class FeatureProperties { const FeatureProperties({ required this.text, + this.type, }); final String text; + final FinampFeatureChipType? type; } class FeatureChips extends ConsumerWidget { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5660c2ec..a400fc36 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1728,5 +1728,45 @@ "nowPlayingBarTooltip": "Open Player Screen", "@nowPlayingBarTooltip": { "description": "Tooltip for the now playing bar at the bottom of the screen" + }, + "additionalPeople": "People", + "@additionalPeople": { + "description": "Label for the feature chips showing additional people in the credits of a track or album" + }, + "playbackMode": "Playback Mode", + "@playbackMode": { + "description": "Label for the feature chips showing the playback mode of a track. See [playbackModeLocal], [playbackModeDirectPlaying], and [playbackModeTranscoding]" + }, + "codec": "Codec", + "@codec": { + "description": "Label for the feature chips showing the codec of a track" + }, + "bitRate": "Bit Rate", + "@bitRate": { + "description": "Label for the feature chips showing the bit rate of a track" + }, + "bitDepth": "Bit Depth", + "@bitDepth": { + "description": "Label for the feature chips showing the bit depth of a track" + }, + "size": "Size", + "@size": { + "description": "Label for the feature chips showing the size (original file size or transcoded size, if available) of a track" + }, + "normalizationGain": "Gain", + "@normalizationGain": { + "description": "Label for the feature chips showing the normalization gain / LUFS offset of a track" + }, + "sampleRate": "Sample Rate", + "@sampleRate": { + "description": "Label for the feature chips showing the sample rate of a track" + }, + "showFeatureChipsToggleTitle": "Show Advanced Track Info", + "@showFeatureChipsToggleTitle": { + "description": "Title for the setting that controls if the feature chips showing advanced track info are shown on the player screen" + }, + "showFeatureChipsToggleSubtitle": "Show advanced track info like codec, bit rate, and more on the player screen.", + "@showFeatureChipsToggleSubtitle": { + "description": "Subtitle for the setting that controls if the feature chips showing advanced track info are shown on the player screen" } } diff --git a/lib/models/finamp_models.dart b/lib/models/finamp_models.dart index 41d13e3f..398973ad 100644 --- a/lib/models/finamp_models.dart +++ b/lib/models/finamp_models.dart @@ -113,6 +113,18 @@ const _showStopButtonOnMediaNotificationDefault = false; const _showSeekControlsOnMediaNotificationDefault = true; const _keepScreenOnOption = KeepScreenOnOption.whileLyrics; const _keepScreenOnWhilePluggedIn = true; +const _featureChipsConfigurationDefault = + FinampFeatureChipsConfiguration(enabled: true, features: [ + FinampFeatureChipType.playCount, + FinampFeatureChipType.additionalPeople, + FinampFeatureChipType.playbackMode, + FinampFeatureChipType.codec, + FinampFeatureChipType.bitRate, + FinampFeatureChipType.bitDepth, + FinampFeatureChipType.sampleRate, + FinampFeatureChipType.size, + FinampFeatureChipType.normalizationGain, +]); @HiveType(typeId: 28) class FinampSettings { @@ -196,7 +208,8 @@ class FinampSettings { this.showSeekControlsOnMediaNotification = _showSeekControlsOnMediaNotificationDefault, this.keepScreenOnOption = _keepScreenOnOption, - this.keepScreenOnWhilePluggedIn = _keepScreenOnWhilePluggedIn}); + this.keepScreenOnWhilePluggedIn = _keepScreenOnWhilePluggedIn, + this.featureChipsConfiguration = _featureChipsConfigurationDefault}); @HiveField(0, defaultValue: _isOfflineDefault) bool isOffline; @@ -433,6 +446,9 @@ class FinampSettings { @HiveField(73, defaultValue: _keepScreenOnWhilePluggedIn) bool keepScreenOnWhilePluggedIn; + @HiveField(74, defaultValue: _featureChipsConfigurationDefault) + FinampFeatureChipsConfiguration featureChipsConfiguration; + static Future create() async { final downloadLocation = await DownloadLocation.create( name: "Internal Storage", @@ -2234,3 +2250,117 @@ enum KeepScreenOnOption { } } } + +@HiveType(typeId: 73) +enum FinampFeatureChipType { + @HiveField(0) + playCount, + @HiveField(1) + additionalPeople, + @HiveField(2) + playbackMode, + @HiveField(3) + codec, + @HiveField(4) + bitRate, + @HiveField(5) + bitDepth, + @HiveField(6) + size, + @HiveField(7) + normalizationGain, + @HiveField(8) + sampleRate; + + /// Human-readable version of the [FinampFeatureChipType] + @override + @Deprecated("Use toLocalisedString when possible") + String toString() => _humanReadableName(this); + + String toLocalisedString(BuildContext context) => + _humanReadableLocalisedName(this, context); + + String _humanReadableName(FinampFeatureChipType featureChipType) { + switch (featureChipType) { + case FinampFeatureChipType.playCount: + return "Play Count"; + case FinampFeatureChipType.additionalPeople: + return "Additional People"; + case FinampFeatureChipType.playbackMode: + return "Playback Mode"; + case FinampFeatureChipType.codec: + return "codec"; + case FinampFeatureChipType.bitRate: + return "Bit Rate"; + case FinampFeatureChipType.bitDepth: + return "Bit Depth"; + case FinampFeatureChipType.size: + return "size"; + case FinampFeatureChipType.normalizationGain: + return "Normalization Gain"; + case FinampFeatureChipType.sampleRate: + return "Sample Rate"; + } + } + + String _humanReadableLocalisedName( + FinampFeatureChipType featureChipType, BuildContext context) { + switch (featureChipType) { + case FinampFeatureChipType.playCount: + return AppLocalizations.of(context)!.playCount; + case FinampFeatureChipType.additionalPeople: + return AppLocalizations.of(context)!.additionalPeople; + case FinampFeatureChipType.playbackMode: + return AppLocalizations.of(context)!.playbackMode; + case FinampFeatureChipType.codec: + return AppLocalizations.of(context)!.codec; + case FinampFeatureChipType.bitRate: + return AppLocalizations.of(context)!.bitRate; + case FinampFeatureChipType.bitDepth: + return AppLocalizations.of(context)!.bitDepth; + case FinampFeatureChipType.size: + return AppLocalizations.of(context)!.size; + case FinampFeatureChipType.normalizationGain: + return AppLocalizations.of(context)!.normalizationGain; + case FinampFeatureChipType.sampleRate: + return AppLocalizations.of(context)!.sampleRate; + } + } +} + +@JsonSerializable() +@HiveType(typeId: 74) +class FinampFeatureChipsConfiguration { + const FinampFeatureChipsConfiguration({ + required this.enabled, + required this.features, + }); + + @HiveField(0) + final bool enabled; + + @HiveField(1) + final List features; + + factory FinampFeatureChipsConfiguration.fromJson(Map json) => + _$FinampFeatureChipsConfigurationFromJson(json); + + Map toJson() => + _$FinampFeatureChipsConfigurationToJson(this); + + @override + String toString() { + return jsonEncode(toJson()); + } + + // implement copyWith + FinampFeatureChipsConfiguration copyWith({ + bool? enabled, + List? features, + }) { + return FinampFeatureChipsConfiguration( + enabled: enabled ?? this.enabled, + features: features ?? this.features, + ); + } +} diff --git a/lib/models/finamp_models.g.dart b/lib/models/finamp_models.g.dart index 4c9612b8..f70cd199 100644 --- a/lib/models/finamp_models.g.dart +++ b/lib/models/finamp_models.g.dart @@ -185,6 +185,19 @@ class FinampSettingsAdapter extends TypeAdapter { : fields[72] as KeepScreenOnOption, keepScreenOnWhilePluggedIn: fields[73] == null ? true : fields[73] as bool, + featureChipsConfiguration: fields[74] == null + ? const FinampFeatureChipsConfiguration(enabled: true, features: [ + FinampFeatureChipType.playCount, + FinampFeatureChipType.additionalPeople, + FinampFeatureChipType.playbackMode, + FinampFeatureChipType.codec, + FinampFeatureChipType.bitRate, + FinampFeatureChipType.bitDepth, + FinampFeatureChipType.sampleRate, + FinampFeatureChipType.size, + FinampFeatureChipType.normalizationGain + ]) + : fields[74] as FinampFeatureChipsConfiguration, ) ..disableGesture = fields[19] == null ? false : fields[19] as bool ..showFastScroller = fields[25] == null ? true : fields[25] as bool @@ -194,7 +207,7 @@ class FinampSettingsAdapter extends TypeAdapter { @override void write(BinaryWriter writer, FinampSettings obj) { writer - ..writeByte(72) + ..writeByte(73) ..writeByte(0) ..write(obj.isOffline) ..writeByte(1) @@ -338,7 +351,9 @@ class FinampSettingsAdapter extends TypeAdapter { ..writeByte(72) ..write(obj.keepScreenOnOption) ..writeByte(73) - ..write(obj.keepScreenOnWhilePluggedIn); + ..write(obj.keepScreenOnWhilePluggedIn) + ..writeByte(74) + ..write(obj.featureChipsConfiguration); } @override @@ -960,6 +975,44 @@ class MediaItemIdAdapter extends TypeAdapter { typeId == other.typeId; } +class FinampFeatureChipsConfigurationAdapter + extends TypeAdapter { + @override + final int typeId = 74; + + @override + FinampFeatureChipsConfiguration read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return FinampFeatureChipsConfiguration( + enabled: fields[0] as bool, + features: (fields[1] as List).cast(), + ); + } + + @override + void write(BinaryWriter writer, FinampFeatureChipsConfiguration obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(obj.enabled) + ..writeByte(1) + ..write(obj.features); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FinampFeatureChipsConfigurationAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + class TabContentTypeAdapter extends TypeAdapter { @override final int typeId = 36; @@ -1878,6 +1931,80 @@ class KeepScreenOnOptionAdapter extends TypeAdapter { typeId == other.typeId; } +class FinampFeatureChipTypeAdapter extends TypeAdapter { + @override + final int typeId = 73; + + @override + FinampFeatureChipType read(BinaryReader reader) { + switch (reader.readByte()) { + case 0: + return FinampFeatureChipType.playCount; + case 1: + return FinampFeatureChipType.additionalPeople; + case 2: + return FinampFeatureChipType.playbackMode; + case 3: + return FinampFeatureChipType.codec; + case 4: + return FinampFeatureChipType.bitRate; + case 5: + return FinampFeatureChipType.bitDepth; + case 6: + return FinampFeatureChipType.size; + case 7: + return FinampFeatureChipType.normalizationGain; + case 8: + return FinampFeatureChipType.sampleRate; + default: + return FinampFeatureChipType.playCount; + } + } + + @override + void write(BinaryWriter writer, FinampFeatureChipType obj) { + switch (obj) { + case FinampFeatureChipType.playCount: + writer.writeByte(0); + break; + case FinampFeatureChipType.additionalPeople: + writer.writeByte(1); + break; + case FinampFeatureChipType.playbackMode: + writer.writeByte(2); + break; + case FinampFeatureChipType.codec: + writer.writeByte(3); + break; + case FinampFeatureChipType.bitRate: + writer.writeByte(4); + break; + case FinampFeatureChipType.bitDepth: + writer.writeByte(5); + break; + case FinampFeatureChipType.size: + writer.writeByte(6); + break; + case FinampFeatureChipType.normalizationGain: + writer.writeByte(7); + break; + case FinampFeatureChipType.sampleRate: + writer.writeByte(8); + break; + } + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is FinampFeatureChipTypeAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} + // ************************************************************************** // IsarCollectionGenerator // ************************************************************************** @@ -6964,3 +7091,33 @@ const _$MediaItemParentTypeEnumMap = { MediaItemParentType.rootCollection: 'rootCollection', MediaItemParentType.instantMix: 'instantMix', }; + +FinampFeatureChipsConfiguration _$FinampFeatureChipsConfigurationFromJson( + Map json) => + FinampFeatureChipsConfiguration( + enabled: json['enabled'] as bool, + features: (json['features'] as List) + .map((e) => $enumDecode(_$FinampFeatureChipTypeEnumMap, e)) + .toList(), + ); + +Map _$FinampFeatureChipsConfigurationToJson( + FinampFeatureChipsConfiguration instance) => + { + 'enabled': instance.enabled, + 'features': instance.features + .map((e) => _$FinampFeatureChipTypeEnumMap[e]!) + .toList(), + }; + +const _$FinampFeatureChipTypeEnumMap = { + FinampFeatureChipType.playCount: 'playCount', + FinampFeatureChipType.additionalPeople: 'additionalPeople', + FinampFeatureChipType.playbackMode: 'playbackMode', + FinampFeatureChipType.codec: 'codec', + FinampFeatureChipType.bitRate: 'bitRate', + FinampFeatureChipType.bitDepth: 'bitDepth', + FinampFeatureChipType.size: 'size', + FinampFeatureChipType.normalizationGain: 'normalizationGain', + FinampFeatureChipType.sampleRate: 'sampleRate', +}; diff --git a/lib/screens/customization_settings_screen.dart b/lib/screens/customization_settings_screen.dart index d8905035..b8797c12 100644 --- a/lib/screens/customization_settings_screen.dart +++ b/lib/screens/customization_settings_screen.dart @@ -41,6 +41,7 @@ class _CustomizationSettingsScreenState const PlaybackSpeedControlVisibilityDropdownListTile(), if (!Platform.isIOS) const ShowStopButtonOnMediaNotificationToggle(), const ShowSeekControlsOnMediaNotificationToggle(), + const ShowFeatureChipsToggle(), ], ), ); @@ -109,3 +110,36 @@ class ShowSeekControlsOnMediaNotificationToggle extends StatelessWidget { ); } } + +class ShowFeatureChipsToggle extends StatelessWidget { + const ShowFeatureChipsToggle({super.key}); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: FinampSettingsHelper.finampSettingsListener, + builder: (context, box, child) { + bool? featureChipsEnabled = + box.get("FinampSettings")?.featureChipsConfiguration.enabled; + + return SwitchListTile.adaptive( + title: + Text(AppLocalizations.of(context)!.showFeatureChipsToggleTitle), + subtitle: Text( + AppLocalizations.of(context)!.showFeatureChipsToggleSubtitle), + value: featureChipsEnabled ?? false, + onChanged: featureChipsEnabled == null + ? null + : (value) { + FinampSettings finampSettingsTemp = + box.get("FinampSettings")!; + finampSettingsTemp.featureChipsConfiguration = + finampSettingsTemp.featureChipsConfiguration + .copyWith(enabled: value); + box.put("FinampSettings", finampSettingsTemp); + }, + ); + }, + ); + } +} diff --git a/lib/screens/player_screen.dart b/lib/screens/player_screen.dart index 1f786163..f700e01d 100644 --- a/lib/screens/player_screen.dart +++ b/lib/screens/player_screen.dart @@ -612,6 +612,10 @@ class PlayerHideableController { _visible.remove(PlayerHideable.controlsPaddingSmall); _visible.remove(PlayerHideable.controlsPaddingBig); } + if (!FinampSettingsHelper + .finampSettings.featureChipsConfiguration.enabled) { + _visible.remove(PlayerHideable.features); + } } /// If we have space for vertical padding, put 33% between control elements and