diff options
author | Scott Murray <scott.murray@konsulko.com> | 2023-12-31 16:24:51 -0500 |
---|---|---|
committer | Scott Murray <scott.murray@konsulko.com> | 2024-01-03 18:23:52 -0500 |
commit | 4742fde5c48726357cc8db06d237e9db6c3df608 (patch) | |
tree | dcca2b3e3c6cb3a4a46b7ae603f64fa9ce5a086c /lib | |
parent | fcd868bd73d35bd79074f3425317152565aeb275 (diff) |
Initial radio implementation
Notable changes:
- Add radio gRPC API protobuf definitation and generated files.
- Reworked existing single gRPC APIs library to split it into
per-API libraries to avoid name collision issues.
- Add radio gRPC client class and associated radio state class
and RiverPod providers.
- Split media controls and play list table classes into media
player and radio specific versions to facilitate customization
and wiring up their appropriate backends in a straightforward
fashion. Some potential rationalization of styling widgets
may be done as a follow up to avoid some duplication.
- Added radio configuration and presets loading. The presets
will be populated with the contents of a radio-presets.yaml
file from the configured location, the default location is
the /etc/xdg/AGL/ics-homescreen directory.
- Implemented FM radio player against the radio gRPC API.
For the sake of expediency, no attempt has been made to make
the player able to handle AM band support.
- Reworked media page navigation state so that active player is
restored when coming back to the page. Logic has been added to
start/stop the radio on navigating to or leaving the FM radio
sub-page. This will potentially be reworked before CES to work
with the pause/stop button present on the other pages.
- Started pruning down global exports.dart a bit to remove files
only used in a specific page/hierarchy, starting with media.
Bug-AGL: SPEC-5029
Change-Id: I1ae0aca4a7a8218e69e4286c863f01509a1cccb7
Signed-off-by: Scott Murray <scott.murray@konsulko.com>
Diffstat (limited to 'lib')
31 files changed, 1232 insertions, 570 deletions
diff --git a/lib/data/data_providers/app_config_provider.dart b/lib/data/data_providers/app_config_provider.dart index 7e0ddc6..a60a462 100644 --- a/lib/data/data_providers/app_config_provider.dart +++ b/lib/data/data_providers/app_config_provider.dart @@ -35,14 +35,40 @@ class KuksaConfig { } } +class RadioConfig { + final String hostname; + final int port; + final String presets; + + static String defaultHostname = 'localhost'; + static int defaultPort = 50053; + static String defaultPresets = + '/etc/xdg/AGL/ics-homescreen/radio-presets.yaml'; + + RadioConfig( + {required this.hostname, required this.port, required this.presets}); + + static RadioConfig defaultConfig() { + return RadioConfig( + hostname: RadioConfig.defaultHostname, + port: RadioConfig.defaultPort, + presets: RadioConfig.defaultPresets); + } +} + class AppConfig { final bool disableBkgAnimation; final bool randomHybridAnimation; final KuksaConfig kuksaConfig; + final RadioConfig radioConfig; static String configFilePath = '/etc/xdg/AGL/ics-homescreen.yaml'; - AppConfig({required this.disableBkgAnimation, required this.randomHybridAnimation, required this.kuksaConfig}); + AppConfig( + {required this.disableBkgAnimation, + required this.randomHybridAnimation, + required this.kuksaConfig, + required this.radioConfig}); static KuksaConfig parseKuksaConfig(YamlMap kuksaMap) { try { @@ -64,7 +90,7 @@ class AppConfig { debugPrint("Reading authorization token $s"); try { token = File(s).readAsStringSync(); - } on Exception catch (_) { + } catch (_) { print("ERROR: Could not read authorization token file $token"); token = ""; } @@ -89,7 +115,7 @@ class AppConfig { } try { ca_cert = File(ca_path).readAsBytesSync(); - } on Exception catch (_) { + } catch (_) { print("ERROR: Could not read CA certificate file $ca_path"); ca_cert = []; } @@ -107,10 +133,33 @@ class AppConfig { use_tls: use_tls, ca_certificate: ca_cert, tls_server_name: tls_server_name); - } on Exception catch (_) { + } catch (_) { return KuksaConfig.defaultConfig(); } } + + static RadioConfig parseRadioConfig(YamlMap radioMap) { + try { + String hostname = RadioConfig.defaultHostname; + if (radioMap.containsKey('hostname')) { + hostname = radioMap['hostname']; + } + + int port = RadioConfig.defaultPort; + if (radioMap.containsKey('port')) { + port = radioMap['port']; + } + + String presets = RadioConfig.defaultPresets; + if (radioMap.containsKey('presets')) { + hostname = radioMap['presets']; + } + + return RadioConfig(hostname: hostname, port: port, presets: presets); + } catch (_) { + return RadioConfig.defaultConfig(); + } + } } final appConfigProvider = Provider((ref) { @@ -133,6 +182,13 @@ final appConfigProvider = Provider((ref) { tls_server_name: ""); } + RadioConfig radioConfig; + if (yamlMap.containsKey('radio')) { + radioConfig = AppConfig.parseRadioConfig(yamlMap['radio']); + } else { + radioConfig = RadioConfig.defaultConfig(); + } + bool disableBkgAnimation = disableBkgAnimationDefault; if (yamlMap.containsKey('disable-bg-animation')) { var value = yamlMap['disable-bg-animation']; @@ -152,11 +208,13 @@ final appConfigProvider = Provider((ref) { return AppConfig( disableBkgAnimation: disableBkgAnimation, randomHybridAnimation: randomHybridAnimation, - kuksaConfig: kuksaConfig); - } on Exception catch (_) { + kuksaConfig: kuksaConfig, + radioConfig: radioConfig); + } catch (_) { return AppConfig( disableBkgAnimation: false, randomHybridAnimation: false, - kuksaConfig: KuksaConfig.defaultConfig()); + kuksaConfig: KuksaConfig.defaultConfig(), + radioConfig: RadioConfig.defaultConfig()); } }); diff --git a/lib/data/data_providers/app_launcher.dart b/lib/data/data_providers/app_launcher.dart index b0199d3..8762643 100644 --- a/lib/data/data_providers/app_launcher.dart +++ b/lib/data/data_providers/app_launcher.dart @@ -1,5 +1,6 @@ import 'package:flutter_ics_homescreen/export.dart'; -import 'package:protos/protos.dart'; +import 'package:protos/applauncher-api.dart'; +import 'package:protos/agl-shell-api.dart'; class AppLauncher { final Ref ref; @@ -9,20 +10,18 @@ class AppLauncher { late ClientChannel appLauncherChannel; late AppLauncherClient appLauncher; - List<String> appStack = [ 'homescreen' ]; + List<String> appStack = ['homescreen']; AppLauncher({required this.ref}) { - aglShellChannel = - ClientChannel('localhost', - port: 14005, - options: ChannelOptions(credentials: ChannelCredentials.insecure())); + aglShellChannel = ClientChannel('localhost', + port: 14005, + options: ChannelOptions(credentials: ChannelCredentials.insecure())); aglShell = AglShellManagerServiceClient(aglShellChannel); - appLauncherChannel = - ClientChannel('localhost', - port: 50052, - options: ChannelOptions(credentials: ChannelCredentials.insecure())); + appLauncherChannel = ClientChannel('localhost', + port: 50052, + options: ChannelOptions(credentials: ChannelCredentials.insecure())); appLauncher = AppLauncherClient(appLauncherChannel); } @@ -58,13 +57,23 @@ class AppLauncher { debugPrint("Got app:"); debugPrint("$info"); // Existing icons are currently not usable, so leave blank for now - apps.add(AppLauncherInfo(id: info.id, name: info.name, icon: "", internal: false)); + apps.add(AppLauncherInfo( + id: info.id, name: info.name, icon: "", internal: false)); } apps.sort((a, b) => a.name.compareTo(b.name)); // Add built-in app widgets - apps.insert(0, AppLauncherInfo(id: "clock", name: "Clock", icon: "clock.svg", internal: true)); - apps.insert(0, AppLauncherInfo(id: "weather", name: "Weather", icon: "weather.svg", internal: true)); + apps.insert( + 0, + AppLauncherInfo( + id: "clock", name: "Clock", icon: "clock.svg", internal: true)); + apps.insert( + 0, + AppLauncherInfo( + id: "weather", + name: "Weather", + icon: "weather.svg", + internal: true)); ref.read(appLauncherListProvider.notifier).update(apps); } catch (e) { @@ -104,5 +113,4 @@ class AppLauncher { } } } - } diff --git a/lib/data/data_providers/app_provider.dart b/lib/data/data_providers/app_provider.dart index 1670eba..ad3dd22 100644 --- a/lib/data/data_providers/app_provider.dart +++ b/lib/data/data_providers/app_provider.dart @@ -4,8 +4,10 @@ import 'package:flutter_ics_homescreen/data/data_providers/time_notifier.dart'; import 'package:flutter_ics_homescreen/data/data_providers/units_notifier.dart'; import 'package:flutter_ics_homescreen/data/data_providers/audio_notifier.dart'; import 'package:flutter_ics_homescreen/data/data_providers/users_notifier.dart'; +import 'package:flutter_ics_homescreen/data/data_providers/radio_notifier.dart'; import 'package:flutter_ics_homescreen/data/data_providers/val_client.dart'; import 'package:flutter_ics_homescreen/data/data_providers/app_launcher.dart'; +import 'package:flutter_ics_homescreen/data/data_providers/radio_client.dart'; import 'package:flutter_ics_homescreen/export.dart'; import '../models/users.dart'; @@ -16,7 +18,7 @@ enum AppState { dashboard, hvac, apps, - mediaPlayer, + media, settings, splash, dateTime, @@ -48,7 +50,14 @@ final appLauncherProvider = Provider((ref) { return AppLauncher(ref: ref); }); -final appLauncherListProvider = NotifierProvider<AppLauncherList, List<AppLauncherInfo>>(AppLauncherList.new); +final appLauncherListProvider = + NotifierProvider<AppLauncherList, List<AppLauncherInfo>>( + AppLauncherList.new); + +final radioClientProvider = Provider((ref) { + RadioConfig config = ref.watch(appConfigProvider).radioConfig; + return RadioClient(config: config, ref: ref); +}); final vehicleProvider = NotifierProvider<VehicleNotifier, Vehicle>(VehicleNotifier.new); @@ -62,7 +71,10 @@ final unitStateProvider = StateNotifierProvider<UnitsNotifier, Units>((ref) { }); final audioStateProvider = - NotifierProvider<AudioNotifier, Audio>(AudioNotifier.new); + NotifierProvider<AudioStateNotifier, AudioState>(AudioStateNotifier.new); + +final radioStateProvider = + NotifierProvider<RadioStateNotifier, RadioState>(RadioStateNotifier.new); final usersProvider = StateNotifierProvider<UsersNotifier, Users>((ref) { return UsersNotifier(Users.initial()); @@ -77,4 +89,3 @@ final currentTimeProvider = StateNotifierProvider<CurrentTimeNotifier, DateTime>((ref) { return CurrentTimeNotifier(); }); - diff --git a/lib/data/data_providers/audio_notifier.dart b/lib/data/data_providers/audio_notifier.dart index 32ab409..a601095 100644 --- a/lib/data/data_providers/audio_notifier.dart +++ b/lib/data/data_providers/audio_notifier.dart @@ -1,10 +1,10 @@ import 'package:flutter_ics_homescreen/export.dart'; -import 'package:protos/protos.dart'; +import 'package:protos/val-api.dart'; -class AudioNotifier extends Notifier<Audio> { +class AudioStateNotifier extends Notifier<AudioState> { @override - Audio build() { - return Audio.initial(); + AudioState build() { + return AudioState.initial(); } void resetToDefaults() { diff --git a/lib/data/data_providers/radio_client.dart b/lib/data/data_providers/radio_client.dart new file mode 100644 index 0000000..2cde65e --- /dev/null +++ b/lib/data/data_providers/radio_client.dart @@ -0,0 +1,161 @@ +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:protos/radio-api.dart' as api; + +class RadioClient { + final RadioConfig config; + final Ref ref; + late api.ClientChannel channel; + late api.RadioClient stub; + + RadioClient({required this.config, required this.ref}) { + debugPrint( + "Connecting to radio service at ${config.hostname}:${config.port}"); + api.ChannelCredentials creds = const api.ChannelCredentials.insecure(); + channel = api.ClientChannel(config.hostname, + port: config.port, options: api.ChannelOptions(credentials: creds)); + stub = api.RadioClient(channel); + } + + void run() async { + getBandParameters(); + + try { + var responseStream = stub.getStatusEvents(api.StatusRequest()); + await for (var event in responseStream) { + handleStatusEvent(event); + } + } catch (e) { + print(e); + } + } + + void getBandParameters() async { + try { + var response = await stub.getBandParameters( + api.GetBandParametersRequest(band: api.Band.BAND_FM)); + ref.read(radioStateProvider.notifier).updateBandParameters( + freqMin: response.min, + freqMax: response.max, + freqStep: response.step); + + // Get initial frequency + var freqResponse = await stub.getFrequency(api.GetFrequencyRequest()); + ref + .read(radioStateProvider.notifier) + .updateFrequency(freqResponse.frequency); + } catch (e) { + print(e); + } + } + + void handleStatusEvent(api.StatusResponse response) { + switch (response.whichStatus()) { + case api.StatusResponse_Status.frequency: + var status = response.frequency; + ref.read(radioStateProvider.notifier).updateFrequency(status.frequency); + break; + case api.StatusResponse_Status.play: + var status = response.play; + ref.read(radioStateProvider.notifier).updatePlaying(status.playing); + break; + case api.StatusResponse_Status.scan: + var status = response.scan; + if (status.stationFound) { + ref.read(radioStateProvider.notifier).updateScanning(false); + } + break; + default: + break; + } + } + + void start() async { + try { + var response = await stub.start(api.StartRequest()); + } catch (e) { + print(e); + } + } + + void stop() async { + try { + var response = await stub.stop(api.StopRequest()); + } catch (e) { + print(e); + } + } + + void setFrequency(int frequency) async { + var radioState = ref.read(radioStateProvider); + if ((frequency < radioState.freqMin) || + (frequency > radioState.freqMax) || + ((frequency - radioState.freqMin) % radioState.freqStep) != 0) { + debugPrint("setFrequency: invalid frequency $frequency!"); + return; + } + try { + var response = await stub + .setFrequency(api.SetFrequencyRequest(frequency: frequency)); + } catch (e) { + print(e); + } + } + + void tuneForward() async { + var radioState = ref.read(radioStateProvider); + if (radioState.freqCurrent < radioState.freqMax) { + int frequency = radioState.freqCurrent + radioState.freqStep; + if (frequency > radioState.freqMax) { + frequency = radioState.freqMax; + } + try { + var response = await stub + .setFrequency(api.SetFrequencyRequest(frequency: frequency)); + } catch (e) { + print(e); + } + } + } + + void tuneBackward() async { + var radioState = ref.read(radioStateProvider); + if (radioState.freqCurrent > radioState.freqMin) { + int frequency = radioState.freqCurrent - radioState.freqStep; + if (frequency < radioState.freqMin) { + frequency = radioState.freqMin; + } + try { + var response = await stub + .setFrequency(api.SetFrequencyRequest(frequency: frequency)); + } catch (e) { + print(e); + } + } + } + + void scanForward() async { + try { + var response = await stub.scanStart(api.ScanStartRequest( + direction: api.ScanDirection.SCAN_DIRECTION_FORWARD)); + } catch (e) { + print(e); + } + } + + void scanBackward() async { + try { + var response = await stub.scanStart(api.ScanStartRequest( + direction: api.ScanDirection.SCAN_DIRECTION_BACKWARD)); + } catch (e) { + print(e); + } + } + + void scanStop() async { + try { + var response = await stub.scanStop(api.ScanStopRequest()); + } catch (e) { + print(e); + } + } +} diff --git a/lib/data/data_providers/radio_notifier.dart b/lib/data/data_providers/radio_notifier.dart new file mode 100644 index 0000000..90e0df5 --- /dev/null +++ b/lib/data/data_providers/radio_notifier.dart @@ -0,0 +1,31 @@ +import 'package:flutter_ics_homescreen/export.dart'; + +class RadioStateNotifier extends Notifier<RadioState> { + @override + RadioState build() { + return RadioState.initial(); + } + + void updateBandParameters( + {required int freqMin, required freqMax, required freqStep}) { + state = + state.copyWith(freqMin: freqMin, freqMax: freqMax, freqStep: freqStep); + } + + void updateFrequency(int frequency) { + state = state.copyWith(freqCurrent: frequency); + } + + void setFrequency(int frequency) { + state = state.copyWith(freqCurrent: frequency); + ref.read(radioClientProvider).setFrequency(frequency); + } + + void updatePlaying(bool playing) { + state = state.copyWith(playing: playing); + } + + void updateScanning(bool scanning) { + state = state.copyWith(scanning: scanning); + } +} diff --git a/lib/data/data_providers/radio_presets_provider.dart b/lib/data/data_providers/radio_presets_provider.dart new file mode 100644 index 0000000..9ee68ac --- /dev/null +++ b/lib/data/data_providers/radio_presets_provider.dart @@ -0,0 +1,49 @@ +import 'dart:io'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:yaml/yaml.dart'; + +class RadioPreset { + final int frequency; + final String name; + + RadioPreset({required this.frequency, required this.name}); +} + +class RadioPresets { + final List<RadioPreset> fmPresets; + + RadioPresets({required this.fmPresets}); +} + +final radioPresetsProvider = Provider((ref) { + final presetsFilename = ref.read(appConfigProvider).radioConfig.presets; + if (presetsFilename.isEmpty) { + return RadioPresets(fmPresets: []); + } + try { + print("Reading radio presets $presetsFilename"); + var presetsFile = File(presetsFilename); + String content = presetsFile.readAsStringSync(); + final dynamic yamlMap = loadYaml(content); + + List<RadioPreset> presets = []; + if (yamlMap.containsKey('fm')) { + dynamic list = yamlMap['fm']; + if (list is YamlList) { + for (var element in list) { + if ((element is YamlMap) && + element.containsKey('frequency') && + element.containsKey('name')) { + presets.add(RadioPreset( + frequency: element['frequency'].toInt(), + name: element['name'].toString())); + } + } + } + } + return RadioPresets(fmPresets: presets); + } catch (_) { + debugPrint("Exception reading presets!"); + return RadioPresets(fmPresets: []); + } +}); diff --git a/lib/data/data_providers/val_client.dart b/lib/data/data_providers/val_client.dart index 28bb480..db962ee 100644 --- a/lib/data/data_providers/val_client.dart +++ b/lib/data/data_providers/val_client.dart @@ -1,5 +1,5 @@ import 'package:flutter_ics_homescreen/export.dart'; -import 'package:protos/protos.dart'; +import 'package:protos/val-api.dart'; class ValClient { final KuksaConfig config; @@ -9,7 +9,7 @@ class ValClient { late String authorization; ValClient({required this.config, required this.ref}) { - debugPrint("Using ${config.hostname}:${config.port}"); + debugPrint("Connecting to KUKSA.val at ${config.hostname}:${config.port}"); ChannelCredentials creds; if (config.use_tls && config.ca_certificate.isNotEmpty) { print("Using TLS"); @@ -25,11 +25,10 @@ class ValClient { } channel = ClientChannel(config.hostname, port: config.port, options: ChannelOptions(credentials: creds)); - debugPrint('Start Listen on port: ${config.port}'); stub = VALClient(channel); } - void startListen() async { + void run() async { List<String> fewSignals = VSSPath().getSignalsList(); var request = SubscribeRequest(); Map<String, String> metadata = {}; diff --git a/lib/data/data_providers/vehicle_notifier.dart b/lib/data/data_providers/vehicle_notifier.dart index 78c5328..6fafb8c 100644 --- a/lib/data/data_providers/vehicle_notifier.dart +++ b/lib/data/data_providers/vehicle_notifier.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter_ics_homescreen/export.dart'; -import 'package:protos/protos.dart'; +import 'package:protos/val-api.dart'; class VehicleNotifier extends Notifier<Vehicle> { @override diff --git a/lib/data/models/audio.dart b/lib/data/models/audio_state.dart index 65490f9..cfa550b 100644 --- a/lib/data/models/audio.dart +++ b/lib/data/models/audio_state.dart @@ -3,13 +3,13 @@ import 'dart:convert'; import 'package:flutter_ics_homescreen/export.dart'; @immutable -class Audio { +class AudioState { final double volume; final double balance; final double fade; final double treble; final double bass; - const Audio({ + const AudioState({ required this.volume, required this.balance, required this.fade, @@ -17,22 +17,21 @@ class Audio { required this.bass, }); - const Audio.initial() + const AudioState.initial() : volume = 5.0, balance = 5.0, fade = 5.0, treble = 5.0, bass = 5.0; - - Audio copyWith({ + AudioState copyWith({ double? volume, double? balance, double? fade, double? treble, double? bass, }) { - return Audio( + return AudioState( volume: volume ?? this.volume, balance: balance ?? this.balance, fade: fade ?? this.fade, @@ -51,8 +50,8 @@ class Audio { }; } - factory Audio.fromMap(Map<String, dynamic> map) { - return Audio( + factory AudioState.fromMap(Map<String, dynamic> map) { + return AudioState( volume: map['volume']?.toDouble() ?? 0.0, balance: map['balance']?.toDouble() ?? 0.0, fade: map['fade']?.toDouble() ?? 0.0, @@ -63,18 +62,19 @@ class Audio { String toJson() => json.encode(toMap()); - factory Audio.fromJson(String source) => Audio.fromMap(json.decode(source)); + factory AudioState.fromJson(String source) => + AudioState.fromMap(json.decode(source)); @override String toString() { - return 'Audio(volume: $volume, balance: $balance, fade: $fade, treble: $treble, bass: $bass)'; + return 'AudioState(volume: $volume, balance: $balance, fade: $fade, treble: $treble, bass: $bass)'; } @override bool operator ==(Object other) { if (identical(this, other)) return true; - - return other is Audio && + + return other is AudioState && other.volume == volume && other.balance == balance && other.fade == fade && diff --git a/lib/data/models/radio_state.dart b/lib/data/models/radio_state.dart new file mode 100644 index 0000000..dd307d9 --- /dev/null +++ b/lib/data/models/radio_state.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; + +import 'package:flutter_ics_homescreen/export.dart'; + +@immutable +class RadioState { + final int freqMin; + final int freqMax; + final int freqStep; + final int freqCurrent; + final bool playing; + final bool scanning; + const RadioState( + {required this.freqMin, + required this.freqMax, + required this.freqStep, + required this.freqCurrent, + required this.playing, + required this.scanning}); + + const RadioState.initial() + : freqMin = 8790000, + freqMax = 1083000, + freqStep = 20000, + freqCurrent = 8790000, + playing = false, + scanning = false; + + RadioState copyWith( + {int? freqMin, + int? freqMax, + int? freqStep, + int? freqCurrent, + bool? playing, + bool? scanning}) { + return RadioState( + freqMin: freqMin ?? this.freqMin, + freqMax: freqMax ?? this.freqMax, + freqStep: freqStep ?? this.freqStep, + freqCurrent: freqCurrent ?? this.freqCurrent, + playing: playing ?? this.playing, + scanning: scanning ?? this.scanning, + ); + } + + Map<String, dynamic> toMap() { + return { + 'freqMin': freqMin, + 'freqMax': freqMax, + 'freqStep': freqStep, + 'freqCurrent': freqCurrent, + 'playing': playing, + 'scanning': scanning, + }; + } + + factory RadioState.fromMap(Map<String, dynamic> map) { + return RadioState( + freqMin: map['freqMin']?.toInt().toUnsigned() ?? 0, + freqMax: map['freqMax']?.toInt().toUnsigned() ?? 0, + freqStep: map['freqStep']?.toInt().toUnsigned() ?? 0, + freqCurrent: map['freqCurrent']?.toInt().toUnsigned() ?? 0, + playing: map['playing']?.toBool() ?? false, + scanning: map['scanning']?.toBool() ?? false, + ); + } + + String toJson() => json.encode(toMap()); + + factory RadioState.fromJson(String source) => + RadioState.fromMap(json.decode(source)); + + @override + String toString() { + return 'RadioState(freqMin: $freqMin, freqMax: $freqMax, freqStep: $freqStep, freqCurrent: $freqCurrent, playing: $playing, scanning: $scanning)'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is RadioState && + other.freqMin == freqMin && + other.freqMax == freqMax && + other.freqStep == freqStep && + other.freqCurrent == freqCurrent && + other.playing == playing && + other.scanning == scanning; + } + + @override + int get hashCode { + return freqMin.hashCode ^ + freqMax.hashCode ^ + freqStep.hashCode ^ + freqCurrent.hashCode ^ + playing.hashCode ^ + scanning.hashCode; + } +} diff --git a/lib/export.dart b/lib/export.dart index 1e07f3f..a5c5626 100644 --- a/lib/export.dart +++ b/lib/export.dart @@ -8,7 +8,8 @@ export 'data/theme/theme.dart'; //Models export 'data/models/vehicle.dart'; export 'data/models/units.dart'; -export 'data/models/audio.dart'; +export 'data/models/audio_state.dart'; +export 'data/models/radio_state.dart'; export 'data/models/connections_signals.dart'; export 'data/models/hybrid.dart'; @@ -25,7 +26,7 @@ export 'presentation/screens/dashboard/widgets/child_lock.dart'; export 'presentation/screens/dashboard/widgets/hybrid_mode.dart'; export 'presentation/common_widget/custom_bottom_bar.dart'; export 'presentation/common_widget/custom_top_bar.dart'; -export 'presentation/screens/media_player/media_player.dart'; +export 'presentation/screens/media/media.dart'; export 'presentation/screens/hvac/hvac.dart'; export 'presentation/screens/settings/settings.dart'; export 'presentation/screens/settings/widgets/settings_list_tile.dart'; @@ -44,13 +45,7 @@ export 'package:flutter_ics_homescreen/presentation/screens/settings/settings_sc export 'presentation/screens/apps/apps.dart'; export 'presentation/screens/splash/splash.dart'; export 'presentation/screens/splash/widget/splash_content.dart'; -//export 'presentation/screens/apps/apps_content.dart'; export 'presentation/screens/hvac/hvac_content.dart'; -export 'presentation/screens/media_player/media_controls.dart'; -export 'presentation/screens/media_player/play_list_table.dart'; -export 'presentation/screens/media_player/player_navigation.dart'; -export 'presentation/screens/media_player/segmented_buttons.dart'; -export 'presentation/screens/media_player/media_content.dart'; export 'presentation/screens/hvac/widgets/climate_controls.dart'; export 'presentation/screens/hvac/widgets/fan_focus.dart'; export 'presentation/screens/hvac/widgets/fan_speed_controls.dart'; diff --git a/lib/presentation/common_widget/custom_bottom_bar.dart b/lib/presentation/common_widget/custom_bottom_bar.dart index 61a7e20..19c56b9 100644 --- a/lib/presentation/common_widget/custom_bottom_bar.dart +++ b/lib/presentation/common_widget/custom_bottom_bar.dart @@ -35,7 +35,7 @@ class CustomBottomBarState extends ConsumerState<CustomBottomBar> { case "HVAC": status = AppState.hvac; case "Media": - status = AppState.mediaPlayer; + status = AppState.media; case "Settings": status = AppState.settings; case "Apps": diff --git a/lib/presentation/common_widget/volume_and_fan_control.dart b/lib/presentation/common_widget/volume_and_fan_control.dart index 051e360..b38e303 100644 --- a/lib/presentation/common_widget/volume_and_fan_control.dart +++ b/lib/presentation/common_widget/volume_and_fan_control.dart @@ -20,7 +20,7 @@ class VolumeFanControl extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Visibility.maintain( - visible: state == AppState.mediaPlayer ? false : true, + visible: state == AppState.media ? false : true, child: const VolumeBar()), SizedBox( height: gapSize, diff --git a/lib/presentation/router/routes/routes.dart b/lib/presentation/router/routes/routes.dart index 57e50d2..45a1a14 100644 --- a/lib/presentation/router/routes/routes.dart +++ b/lib/presentation/router/routes/routes.dart @@ -17,8 +17,8 @@ List<Page<dynamic>> onGenerateAppViewPages( return [HvacPage.page()]; case AppState.apps: return [AppsPage.page()]; - case AppState.mediaPlayer: - return [MediaPlayerPage.page()]; + case AppState.media: + return [MediaPage.page()]; case AppState.settings: return [SettingsPage.page()]; case AppState.splash: diff --git a/lib/presentation/screens/media_player/media_player.dart b/lib/presentation/screens/media/media.dart index 3126ac1..b7ce9e1 100644 --- a/lib/presentation/screens/media_player/media_player.dart +++ b/lib/presentation/screens/media/media.dart @@ -1,13 +1,14 @@ -import 'package:flutter_ics_homescreen/presentation/screens/media_player/fm_player.dart'; - -import '/export.dart'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/presentation/screens/media/media_player.dart'; +import 'package:flutter_ics_homescreen/presentation/screens/media/radio_player.dart'; import 'widgets/media_volume_bar.dart'; +import 'media_nav_notifier.dart'; +import 'player_navigation.dart'; -class MediaPlayerPage extends StatelessWidget { - const MediaPlayerPage({super.key}); +class MediaPage extends StatelessWidget { + const MediaPage({super.key}); - static Page<void> page() => - const MaterialPage<void>(child: MediaPlayerPage()); + static Page<void> page() => const MaterialPage<void>(child: MediaPage()); @override Widget build(BuildContext context) { Size size = MediaQuery.sizeOf(context); @@ -21,7 +22,7 @@ class MediaPlayerPage extends StatelessWidget { // // decoration: // // BoxDecoration(gradient: AGLDemoColors.gradientBackgroundColor), // child: SvgPicture.asset( - // 'assets/MediaPlayerBackground.svg', + // 'assets/Media.svg', // alignment: Alignment.center, // fit: BoxFit.cover, // //width: 200, @@ -42,31 +43,45 @@ class MediaPlayerPage extends StatelessWidget { ), const Padding( padding: EdgeInsets.symmetric(vertical: 50, horizontal: 50), - child: MediaPlayerBackground(), + child: Media(), ) - //const MediaPlayer(), ], ); } } -class MediaPlayerBackground extends StatefulWidget { - const MediaPlayerBackground({super.key}); +class Media extends ConsumerStatefulWidget { + const Media({super.key}); @override - State<MediaPlayerBackground> createState() => _MediaPlayerBackgroundState(); + ConsumerState<Media> createState() => _MediaState(); } -class _MediaPlayerBackgroundState extends State<MediaPlayerBackground> { - String selectedNav = "My Media"; - onPressed(type) { +class _MediaState extends ConsumerState<Media> { + //late MediaNavState selectedNav; + + //@override + //initState() { + // selectedNav = ref.read(mediaNavStateProvider); + // super.initState(); + //} + + onPressed(MediaNavState type) { setState(() { - selectedNav = type; + if (type == MediaNavState.fm) { + ref.read(mediaNavStateProvider.notifier).set(MediaNavState.fm); + ref.read(radioClientProvider).start(); + } else if (type == MediaNavState.media) { + ref.read(mediaNavStateProvider.notifier).set(MediaNavState.media); + ref.read(radioClientProvider).stop(); + } }); } @override Widget build(BuildContext context) { + var navState = ref.watch(mediaNavStateProvider); + return SingleChildScrollView( child: Column( children: [ @@ -81,14 +96,14 @@ class _MediaPlayerBackgroundState extends State<MediaPlayerBackground> { Padding( padding: const EdgeInsets.symmetric(horizontal: 80), child: SingleChildScrollView( - child: selectedNav == "My Media" + child: navState == MediaNavState.media ? const MediaPlayer() - : selectedNav == "FM" - ? const FMPlayer() + : navState == MediaNavState.fm + ? const RadioPlayer() : Container(), ), ), - if (selectedNav == "My Media" || selectedNav == "FM") + if (navState == MediaNavState.media || navState == MediaNavState.fm) const Padding( padding: EdgeInsets.symmetric(horizontal: 144, vertical: 23.5), child: CustomVolumeSlider(), diff --git a/lib/presentation/screens/media/media_nav_notifier.dart b/lib/presentation/screens/media/media_nav_notifier.dart new file mode 100644 index 0000000..6f93850 --- /dev/null +++ b/lib/presentation/screens/media/media_nav_notifier.dart @@ -0,0 +1,18 @@ +import 'package:flutter_ics_homescreen/export.dart'; + +enum MediaNavState { media, fm, am, xm } + +class MediaNavStateNotifier extends Notifier<MediaNavState> { + @override + MediaNavState build() { + return MediaNavState.media; + } + + set(MediaNavState value) { + state = value; + } +} + +final mediaNavStateProvider = + NotifierProvider<MediaNavStateNotifier, MediaNavState>( + MediaNavStateNotifier.new); diff --git a/lib/presentation/screens/media_player/media_content.dart b/lib/presentation/screens/media/media_player.dart index 0625c9c..d7486c7 100644 --- a/lib/presentation/screens/media_player/media_content.dart +++ b/lib/presentation/screens/media/media_player.dart @@ -1,4 +1,7 @@ import 'package:flutter_ics_homescreen/export.dart'; +import 'media_player_controls.dart'; +import 'play_list_table.dart'; +import 'segmented_buttons.dart'; class MediaPlayer extends StatefulWidget { const MediaPlayer({super.key}); @@ -31,7 +34,7 @@ class _MediaPlayerState extends State<MediaPlayer> { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // const PlayerNavigation(), + //const PlayerNavigation(), SegmentedButtons( navItems: navItems, selectedNav: selectedNav, @@ -55,12 +58,10 @@ class _MediaPlayerState extends State<MediaPlayer> { Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - MediaControls( - songName: songName, - songLengthStart: "-1:23", - songLengthStop: "5:03", - type: "media", - ), + MediaPlayerControls( + songName: songName, + songLengthStart: "-1:23", + songLengthStop: "5:03"), const SizedBox( height: 72, ), diff --git a/lib/presentation/screens/media/media_player_controls.dart b/lib/presentation/screens/media/media_player_controls.dart new file mode 100644 index 0000000..518b669 --- /dev/null +++ b/lib/presentation/screens/media/media_player_controls.dart @@ -0,0 +1,235 @@ +import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/presentation/screens/media/widgets/gradient_progress_indicator.dart'; + +class MediaPlayerControls extends StatefulWidget { + const MediaPlayerControls( + {super.key, + required this.songName, + required this.songLengthStart, + required this.songLengthStop}); + + final String songName; + final String songLengthStart; + final String songLengthStop; + + @override + State<MediaPlayerControls> createState() => _MediaPlayerControlsState(); +} + +class _MediaPlayerControlsState extends State<MediaPlayerControls> { + late String songName; + late String songLengthStart; + late String songLengthStop; + final String albumName = "Gorillaz"; + + int songProgress = 20; + + @override + void initState() { + songName = widget.songName; + songLengthStart = widget.songLengthStart; + songLengthStop = widget.songLengthStop; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Text( + songName, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + shadows: [Helpers.dropShadowRegular], + fontSize: 44), + ), + MediaPlayerControlsubDetails( + albumName: albumName, + ), + Column(children: [ + GradientProgressIndicator( + percent: songProgress, + type: "media", + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + AGLDemoColors.jordyBlueColor, + AGLDemoColors.jordyBlueColor.withOpacity(0.8), + ]), + backgroundColor: AGLDemoColors.gradientBackgroundDarkColor, + ), + // const LinearProgressIndicator( + // backgroundColor: AGLDemoColors.gradientBackgroundDarkColor, + // color: Colors.white70, + // minHeight: 8, + // value: 0.7, + // ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + songLengthStart, + style: TextStyle( + color: Colors.white, + fontSize: 26, + shadows: [Helpers.dropShadowRegular]), + ), + Text( + songLengthStop, + style: TextStyle( + color: Colors.white, + fontSize: 26, + shadows: [Helpers.dropShadowRegular]), + ) + ], + ), + ), + ]), + const MediaPlayerActions(), + ]), + ); + } +} + +class MediaPlayerControlsubDetails extends StatefulWidget { + const MediaPlayerControlsubDetails({super.key, required this.albumName}); + final String albumName; + + @override + State<MediaPlayerControlsubDetails> createState() => + _MediaPlayerControlsubDetailsState(); +} + +class _MediaPlayerControlsubDetailsState + extends State<MediaPlayerControlsubDetails> { + bool isShuffleEnabled = false; + bool isRepeatEnabled = false; + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.albumName, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 40, + shadows: [Helpers.dropShadowRegular]), + ), + Row( + children: [ + InkWell( + customBorder: const CircleBorder(), + onTap: () { + setState(() { + isShuffleEnabled = !isShuffleEnabled; + }); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/${isShuffleEnabled ? "ShufflePressed.svg" : "Shuffle.svg"}", + width: 48, + ))), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + setState(() { + isRepeatEnabled = !isRepeatEnabled; + }); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/${isRepeatEnabled ? "RepeatPressed.svg" : "Repeat.svg"}", + width: 48, + ))), + ], + ) + ], + ); + } +} + +class MediaPlayerActions extends StatefulWidget { + const MediaPlayerActions({super.key}); + + @override + State<MediaPlayerActions> createState() => _MediaPlayerActionsState(); +} + +class _MediaPlayerActionsState extends State<MediaPlayerActions> { + bool isPressed = false; + bool isPlaying = true; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + customBorder: const CircleBorder(), + onTap: () {}, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/SkipPrevious.svg", + width: 48, + ), + )), + const SizedBox( + width: 120, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + setState(() { + isPlaying = !isPlaying; + }); + }, + onTapDown: (details) { + setState(() { + isPressed = true; + }); + }, + onTapUp: (details) { + isPressed = false; + }, + child: Container( + width: 64, + height: 64, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + isPressed ? Colors.white : AGLDemoColors.periwinkleColor, + boxShadow: [Helpers.boxDropShadowRegular]), + child: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: AGLDemoColors.resolutionBlueColor, + size: 60, + ), + )), + const SizedBox( + width: 120, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () {}, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/SkipNext.svg", + width: 48, + ), + )), + ], + ); + } +} diff --git a/lib/presentation/screens/media_player/play_list_table.dart b/lib/presentation/screens/media/play_list_table.dart index e5c1292..369bb9c 100644 --- a/lib/presentation/screens/media_player/play_list_table.dart +++ b/lib/presentation/screens/media/play_list_table.dart @@ -23,6 +23,7 @@ class _PlayListTableState extends State<PlayListTable> { late String tableName; late List<PlayListModel> playList; late String selectedPlayListSongName; + @override void initState() { tableName = widget.tableName; diff --git a/lib/presentation/screens/media_player/player_navigation.dart b/lib/presentation/screens/media/player_navigation.dart index 8e09e53..70a9906 100644 --- a/lib/presentation/screens/media_player/player_navigation.dart +++ b/lib/presentation/screens/media/player_navigation.dart @@ -1,19 +1,30 @@ import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; import 'package:flutter_ics_homescreen/export.dart'; +import 'media_nav_notifier.dart'; -class PlayerNavigation extends StatefulWidget { +class PlayerNavigation extends ConsumerStatefulWidget { const PlayerNavigation({super.key, required this.onPressed}); final Function onPressed; @override - State<PlayerNavigation> createState() => _PlayerNavigationState(); + ConsumerState<PlayerNavigation> createState() => _PlayerNavigationState(); } -class _PlayerNavigationState extends State<PlayerNavigation> { +class _PlayerNavigationState extends ConsumerState<PlayerNavigation> { List<String> navItems = ["My Media", "FM", "AM", "XM"]; - String selectedNav = "My Media"; + Map<MediaNavState, String> navStateMap = { + MediaNavState.media: "My Media", + MediaNavState.fm: "FM", + MediaNavState.am: "AM", + MediaNavState.xm: "XM" + }; + //String selectedNav = "My Media"; + @override Widget build(BuildContext context) { + var navState = ref.watch(mediaNavStateProvider); + var selectedNav = navStateMap[navState]; + return Row( children: navItems .map((e) => Expanded( @@ -35,9 +46,16 @@ class _PlayerNavigationState extends State<PlayerNavigation> { child: InkWell( onTap: () { setState(() { - selectedNav = e; + if (e == "My Media" || e == "FM") { + selectedNav = e; + } }); - widget.onPressed(selectedNav); + if (e == "My Media" || e == "FM") { + for (MapEntry<MediaNavState, String> me + in navStateMap.entries) { + if (me.value == e) widget.onPressed(me.key); + } + } }, child: Container( padding: const EdgeInsets.symmetric(vertical: 7), diff --git a/lib/presentation/screens/media_player/fm_player.dart b/lib/presentation/screens/media/radio_player.dart index 31a22ae..4531c7b 100644 --- a/lib/presentation/screens/media_player/fm_player.dart +++ b/lib/presentation/screens/media/radio_player.dart @@ -1,25 +1,37 @@ +import 'package:flutter_ics_homescreen/data/data_providers/radio_presets_provider.dart'; import 'package:flutter_ics_homescreen/export.dart'; +import 'radio_player_controls.dart'; +import 'radio_preset_table.dart'; +import 'segmented_buttons.dart'; -class FMPlayer extends StatefulWidget { - const FMPlayer({super.key}); +class RadioPlayer extends ConsumerStatefulWidget { + const RadioPlayer({super.key}); @override - State<FMPlayer> createState() => _FMPlayerState(); + ConsumerState<RadioPlayer> createState() => _RadioPlayerState(); } -class _FMPlayerState extends State<FMPlayer> { +class _RadioPlayerState extends ConsumerState<RadioPlayer> { String selectedNav = "Standard"; List<String> navItems = [ "Standard", "HD", ]; String tableName = "Presets"; - List<PlayListModel> playList = [ - PlayListModel(songName: "93.1 The Mountain", albumName: "93.1"), - PlayListModel(songName: "Mix 94.1", albumName: "94.1 MHz"), - PlayListModel(songName: "96.3 KKLZ", albumName: "96.3 MHz"), - ]; - String selectedPlayListSongName = "93.1 The Mountain"; + late List<RadioPreset> presets; + late String selectedPreset; + + @override + void initState() { + presets = ref.read(radioPresetsProvider).fmPresets; + if (presets.isNotEmpty) { + selectedPreset = presets.first.name; + } else { + selectedPreset = ""; + } + super.initState(); + } + @override Widget build(BuildContext context) { double fmSignalHeight = 460; @@ -52,21 +64,14 @@ class _FMPlayerState extends State<FMPlayer> { ), Column( children: [ - const MediaControls( - songName: "87.9", - songLengthStart: "87.9 MHz", - songLengthStop: "87.9 MHz", - type: "fm", - ), + const RadioPlayerControls(), const SizedBox( height: 70, ), - PlayListTable( - playList: playList, - selectedPlayListSongName: selectedPlayListSongName, - tableName: tableName, - type: "fm", - ), + RadioPresetTable( + presets: presets, + selectedPreset: selectedPreset, + tableName: tableName), ], ) ], diff --git a/lib/presentation/screens/media/radio_player_controls.dart b/lib/presentation/screens/media/radio_player_controls.dart new file mode 100644 index 0000000..bfa8da6 --- /dev/null +++ b/lib/presentation/screens/media/radio_player_controls.dart @@ -0,0 +1,251 @@ +import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/presentation/screens/settings/settings_screens/audio_settings/widget/slider_widgets.dart'; + +class RadioPlayerControls extends ConsumerWidget { + const RadioPlayerControls({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + var freqCurrent = + ref.watch(radioStateProvider.select((radio) => radio.freqCurrent)); + String currentString = (freqCurrent / 1000000.0).toStringAsFixed(1); + + return Material( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + currentString, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + shadows: [Helpers.dropShadowRegular], + fontSize: 44), + ), + const RadioPlayerControlsSubDetails(), + const RadioPlayerControlsSlider(), + ], + ), + ); + } +} + +class RadioPlayerControlsSubDetails extends ConsumerWidget { + const RadioPlayerControlsSubDetails({super.key}); + + onPressed({required WidgetRef ref, required String type}) { + if (type == "tuneLeft") { + ref.read(radioClientProvider).tuneBackward(); + } else if (type == "tuneRight") { + ref.read(radioClientProvider).tuneForward(); + } else if (type == "scanLeft") { + bool playing = + ref.read(radioStateProvider.select((radio) => radio.playing)); + if (playing) { + ref.read(radioClientProvider).scanBackward(); + } + } else if (type == "scanRight") { + bool playing = + ref.read(radioStateProvider.select((radio) => radio.playing)); + if (playing) { + ref.read(radioClientProvider).scanForward(); + } + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "Tune", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 40, + shadows: [Helpers.dropShadowRegular]), + ), + const SizedBox( + width: 25, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + onPressed(ref: ref, type: "tuneLeft"); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back, + size: 48, + color: AGLDemoColors.periwinkleColor, + ))), + const SizedBox( + width: 25, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + onPressed(ref: ref, type: "tuneRight"); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_forward, + color: AGLDemoColors.periwinkleColor, + size: 48, + ))), + ], + ), + Row( + children: [ + Text( + "Scan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 40, + shadows: [Helpers.dropShadowRegular]), + ), + const SizedBox( + width: 25, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + onPressed(ref: ref, type: "scanLeft"); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back, + color: AGLDemoColors.periwinkleColor, + size: 48, + ))), + const SizedBox( + width: 25, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + onPressed(ref: ref, type: "scanRight"); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_forward, + color: AGLDemoColors.periwinkleColor, + size: 48, + ))), + ], + ) + ], + ), + ); + } +} + +class RadioPlayerControlsSlider extends ConsumerStatefulWidget { + const RadioPlayerControlsSlider({super.key}); + + @override + ConsumerState<RadioPlayerControlsSlider> createState() => + RadioPlayerControlsSliderState(); +} + +class RadioPlayerControlsSliderState + extends ConsumerState<RadioPlayerControlsSlider> { + @override + Widget build(BuildContext context) { + var freqMin = + ref.watch(radioStateProvider.select((radio) => radio.freqMin)); + var freqMax = + ref.watch(radioStateProvider.select((radio) => radio.freqMax)); + var freqStep = + ref.watch(radioStateProvider.select((radio) => radio.freqStep)); + var currentFreq = + ref.watch(radioStateProvider.select((radio) => radio.freqCurrent)) / + 1000000.0; + + String minString = (freqMin / 1000000.0).toStringAsFixed(1); + String maxString = (freqMax / 1000000.0).toStringAsFixed(1); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 64), + child: Container( + decoration: const ShapeDecoration( + color: AGLDemoColors.buttonFillEnabledColor, + shape: StadiumBorder( + side: BorderSide( + color: Color(0xFF5477D4), + width: 0.5, + )), + ), + height: 160, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + minString, + style: TextStyle( + color: Colors.white, + fontSize: 32, + shadows: [Helpers.dropShadowRegular]), + )), + Expanded( + child: SliderTheme( + data: SliderThemeData( + overlayShape: SliderComponentShape.noOverlay, + valueIndicatorShape: SliderComponentShape.noOverlay, + activeTickMarkColor: Colors.transparent, + inactiveTickMarkColor: Colors.transparent, + inactiveTrackColor: AGLDemoColors.backgroundInsetColor, + thumbShape: const PolygonSliderThumb( + sliderValue: 3, thumbRadius: 23), + //trackHeight: 5, + ), + child: Slider( + divisions: (freqMax - freqMin) ~/ freqStep, + min: freqMin / 1000000.0, + max: freqMax / 1000000.0, + value: currentFreq, + onChangeStart: (double value) { + ref.read(radioClientProvider).scanStop(); + }, + onChanged: (double value) { + setState(() { + ref + .read(radioStateProvider.notifier) + .updateFrequency((value * 1000000.0).toInt()); + }); + }, + onChangeEnd: (double value) { + ref + .read(radioStateProvider.notifier) + .setFrequency((value * 1000000.0).toInt()); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + maxString, + style: TextStyle( + color: Colors.white, + fontSize: 32, + shadows: [Helpers.dropShadowRegular]), + )), + ], + ), + )); + } +} diff --git a/lib/presentation/screens/media/radio_preset_table.dart b/lib/presentation/screens/media/radio_preset_table.dart new file mode 100644 index 0000000..816bcb9 --- /dev/null +++ b/lib/presentation/screens/media/radio_preset_table.dart @@ -0,0 +1,151 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/data/data_providers/radio_presets_provider.dart'; + +class RadioPresetTable extends ConsumerStatefulWidget { + const RadioPresetTable( + {super.key, + required this.tableName, + required this.presets, + required this.selectedPreset}); + + final String tableName; + final List<RadioPreset> presets; + final String selectedPreset; + + @override + ConsumerState<RadioPresetTable> createState() => _RadioPresetTableState(); +} + +class _RadioPresetTableState extends ConsumerState<RadioPresetTable> { + bool isAudioSettingsEnabled = false; + late String tableName; + late List<RadioPreset> presets; + late String selectedPreset; + + @override + void initState() { + tableName = widget.tableName; + presets = widget.presets; + selectedPreset = widget.selectedPreset; + super.initState(); + } + + String frequencyToString(int frequency) { + return "${(frequency / 1000000.0).toStringAsFixed(1)} MHz"; + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + tableName, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 40), + ), + ], + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + setState(() { + isAudioSettingsEnabled = !isAudioSettingsEnabled; + }); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/${isAudioSettingsEnabled ? "AudioSettingsPressed.svg" : "AudioSettings.svg"}", + width: 48, + ))) + ], + ), + SizedBox( + height: 325, + child: SingleChildScrollView( + child: Column( + children: presets.map((index) { + return Container( + height: 100, + margin: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + border: Border( + left: selectedPreset == index.name + ? const BorderSide( + color: Colors.white, width: 4) + : BorderSide.none), + gradient: LinearGradient( + colors: selectedPreset == index.name + ? [ + AGLDemoColors.neonBlueColor, + AGLDemoColors.neonBlueColor + .withOpacity(0.15) + ] + : [ + Colors.black, + Colors.black.withOpacity(0.20) + ])), + child: InkWell( + onTap: () { + ref + .read(radioClientProvider) + .setFrequency(index.frequency); + setState(() { + selectedPreset = index.name; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 17, horizontal: 24), + child: Row( + children: [ + Expanded( + flex: 6, + child: AutoSizeText( + index.name, + maxLines: 1, + style: TextStyle( + color: Colors.white, + fontSize: 40, + shadows: [Helpers.dropShadowRegular]), + )), + Expanded( + flex: 4, + child: Text( + frequencyToString(index.frequency), + style: TextStyle( + color: Colors.white, + fontSize: 26, + shadows: [Helpers.dropShadowRegular]), + )) + ], + ), + ), + ), + ); + }).toList()), + ), + ), + ], + )); + } +} + +class PlayListModel { + final String songName; + final String albumName; + + PlayListModel({required this.songName, required this.albumName}); +} diff --git a/lib/presentation/screens/media_player/segmented_buttons.dart b/lib/presentation/screens/media/segmented_buttons.dart index 5cc1d87..5cc1d87 100644 --- a/lib/presentation/screens/media_player/segmented_buttons.dart +++ b/lib/presentation/screens/media/segmented_buttons.dart diff --git a/lib/presentation/screens/media_player/widgets/gradient_progress_indicator.dart b/lib/presentation/screens/media/widgets/gradient_progress_indicator.dart index 24aa244..24aa244 100644 --- a/lib/presentation/screens/media_player/widgets/gradient_progress_indicator.dart +++ b/lib/presentation/screens/media/widgets/gradient_progress_indicator.dart diff --git a/lib/presentation/screens/media_player/widgets/media_volume_bar.dart b/lib/presentation/screens/media/widgets/media_volume_bar.dart index dd59ee0..bd3a4f1 100644 --- a/lib/presentation/screens/media_player/widgets/media_volume_bar.dart +++ b/lib/presentation/screens/media/widgets/media_volume_bar.dart @@ -23,7 +23,7 @@ class CustomVolumeSliderState extends ConsumerState<CustomVolumeSlider> { }); } - void _dercrease() { + void _decrease() { _currentVal -= 10; if (_currentVal < 0) { _currentVal = 0; @@ -34,6 +34,7 @@ class CustomVolumeSliderState extends ConsumerState<CustomVolumeSlider> { } double _currentVal = 50; + @override Widget build(BuildContext context) { final volumeValue = @@ -61,7 +62,7 @@ class CustomVolumeSliderState extends ConsumerState<CustomVolumeSlider> { child: InkWell( customBorder: const CircleBorder(), onTap: () { - _dercrease(); + _decrease(); }, child: const Padding( padding: EdgeInsets.all(8.0), @@ -72,22 +73,6 @@ class CustomVolumeSliderState extends ConsumerState<CustomVolumeSlider> { ))), ), ), - // Padding( - // padding: const EdgeInsets.only(left: 10.0), - // child: SizedBox( - // width: 50, - // child: IconButton( - // padding: EdgeInsets.zero, - // onPressed: () { - // _dercrease(); - // }, - // icon: const Icon( - // CustomIcons.vol_min, - // color: AGLDemoColors.periwinkleColor, - // size: 48, - // )), - // ), - // ), Expanded( child: SliderTheme( data: SliderThemeData( @@ -130,22 +115,6 @@ class CustomVolumeSliderState extends ConsumerState<CustomVolumeSlider> { ))), ), ), - // Padding( - // padding: const EdgeInsets.only(right: 10.0), - // child: SizedBox( - // width: 60, - // child: IconButton( - // padding: EdgeInsets.zero, - // onPressed: () { - // _increase(); - // }, - // icon: const Icon( - // CustomIcons.vol_max, - // color: AGLDemoColors.periwinkleColor, - // size: 48, - // )), - // ), - // ), ], ), ), diff --git a/lib/presentation/screens/media_player/media_controls.dart b/lib/presentation/screens/media_player/media_controls.dart deleted file mode 100644 index 0686187..0000000 --- a/lib/presentation/screens/media_player/media_controls.dart +++ /dev/null @@ -1,413 +0,0 @@ -import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; -import 'package:flutter_ics_homescreen/export.dart'; -import 'package:flutter_ics_homescreen/presentation/screens/media_player/widgets/gradient_progress_indicator.dart'; - -class MediaControls extends StatefulWidget { - const MediaControls( - {super.key, - required this.type, - required this.songName, - required this.songLengthStart, - required this.songLengthStop}); - - final String type; - final String songName; - final String songLengthStart; - final String songLengthStop; - - @override - State<MediaControls> createState() => _MediaControlsState(); -} - -class _MediaControlsState extends State<MediaControls> { - late String songName; - late String songLengthStart; - late String songLengthStop; - final String albumName = "Gorillaz"; - - int songProgress = 20; - - @override - void initState() { - songName = widget.songName; - songLengthStart = widget.songLengthStart; - songLengthStop = widget.songLengthStop; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Material( - color: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - songName, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w400, - shadows: [Helpers.dropShadowRegular], - fontSize: 44), - ), - if (widget.type == "media") - MediaControlSubDetails( - albumName: albumName, - ) - else if (widget.type == "fm") - const FMPlayerSubDetails(), - if (widget.type == "media") - Column(children: [ - GradientProgressIndicator( - percent: songProgress, - type: "media", - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - AGLDemoColors.jordyBlueColor, - AGLDemoColors.jordyBlueColor.withOpacity(0.8), - ]), - backgroundColor: AGLDemoColors.gradientBackgroundDarkColor, - ), - // const LinearProgressIndicator( - // backgroundColor: AGLDemoColors.gradientBackgroundDarkColor, - // color: Colors.white70, - // minHeight: 8, - // value: 0.7, - // ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 5), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - songLengthStart, - style: TextStyle( - color: Colors.white, - fontSize: 26, - shadows: [Helpers.dropShadowRegular]), - ), - Text( - songLengthStop, - style: TextStyle( - color: Colors.white, - fontSize: 26, - shadows: [Helpers.dropShadowRegular]), - ) - ], - ), - ), - ]) - else if (widget.type == "fm") - FMPlayerSlider( - minHertz: songLengthStart, - maxHertz: songLengthStop, - songProgress: songProgress, - ), - if (widget.type == "media") const MediaPlayerActions() - ], - ), - ); - } -} - -class MediaControlSubDetails extends StatefulWidget { - const MediaControlSubDetails({super.key, required this.albumName}); - final String albumName; - - @override - State<MediaControlSubDetails> createState() => _MediaControlSubDetailsState(); -} - -class _MediaControlSubDetailsState extends State<MediaControlSubDetails> { - bool isShuffleEnabled = false; - bool isRepeatEnabled = false; - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.albumName, - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w400, - fontSize: 40, - shadows: [Helpers.dropShadowRegular]), - ), - Row( - children: [ - InkWell( - customBorder: const CircleBorder(), - onTap: () { - setState(() { - isShuffleEnabled = !isShuffleEnabled; - }); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - "assets/${isShuffleEnabled ? "ShufflePressed.svg" : "Shuffle.svg"}", - width: 48, - ))), - InkWell( - customBorder: const CircleBorder(), - onTap: () { - setState(() { - isRepeatEnabled = !isRepeatEnabled; - }); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - "assets/${isRepeatEnabled ? "RepeatPressed.svg" : "Repeat.svg"}", - width: 48, - ))), - ], - ) - ], - ); - } -} - -class FMPlayerSubDetails extends StatefulWidget { - const FMPlayerSubDetails({ - super.key, - }); - - @override - State<FMPlayerSubDetails> createState() => _FMPlayerSubDetailsState(); -} - -class _FMPlayerSubDetailsState extends State<FMPlayerSubDetails> { - onPressed({required String type}) {} - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 5), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - "Tune", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w400, - fontSize: 40, - shadows: [Helpers.dropShadowRegular]), - ), - const SizedBox( - width: 25, - ), - InkWell( - customBorder: const CircleBorder(), - onTap: () { - onPressed(type: "scanLeft"); - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - Icons.arrow_back, - size: 48, - color: AGLDemoColors.periwinkleColor, - ))), - const SizedBox( - width: 25, - ), - InkWell( - customBorder: const CircleBorder(), - onTap: () { - onPressed(type: "scanRight"); - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - Icons.arrow_forward, - color: AGLDemoColors.periwinkleColor, - size: 48, - ))), - ], - ), - Row( - children: [ - Text( - "Scan", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w400, - fontSize: 40, - shadows: [Helpers.dropShadowRegular]), - ), - const SizedBox( - width: 25, - ), - InkWell( - customBorder: const CircleBorder(), - onTap: () { - onPressed(type: "scanLeft"); - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - Icons.arrow_back, - color: AGLDemoColors.periwinkleColor, - size: 48, - ))), - const SizedBox( - width: 25, - ), - InkWell( - customBorder: const CircleBorder(), - onTap: () { - onPressed(type: "scanRight"); - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Icon( - Icons.arrow_forward, - color: AGLDemoColors.periwinkleColor, - size: 48, - ))), - ], - ) - ], - ), - ); - } -} - -class MediaPlayerActions extends StatefulWidget { - const MediaPlayerActions({super.key}); - - @override - State<MediaPlayerActions> createState() => _MediaPlayerActionsState(); -} - -class _MediaPlayerActionsState extends State<MediaPlayerActions> { - bool isPressed = false; - bool isPlaying = true; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - InkWell( - customBorder: const CircleBorder(), - onTap: () {}, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - "assets/SkipPrevious.svg", - width: 48, - ), - )), - const SizedBox( - width: 120, - ), - InkWell( - customBorder: const CircleBorder(), - onTap: () { - setState(() { - isPlaying = !isPlaying; - }); - }, - onTapDown: (details) { - setState(() { - isPressed = true; - }); - }, - onTapUp: (details) { - isPressed = false; - - }, - child: Container( - width: 64, - height: 64, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: - isPressed ? Colors.white : AGLDemoColors.periwinkleColor, - boxShadow: [Helpers.boxDropShadowRegular]), - child: Icon( - isPlaying ? Icons.pause : Icons.play_arrow, - color: AGLDemoColors.resolutionBlueColor, - size: 60, - ), - )), - const SizedBox( - width: 120, - ), - InkWell( - customBorder: const CircleBorder(), - onTap: () {}, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - "assets/SkipNext.svg", - width: 48, - ), - )), - ], - ); - } -} - -class FMPlayerSlider extends StatefulWidget { - const FMPlayerSlider( - {super.key, - required this.minHertz, - required this.maxHertz, - required this.songProgress}); - final String minHertz; - final String maxHertz; - final int songProgress; - - @override - State<FMPlayerSlider> createState() => _FMPlayerSliderState(); -} - -class _FMPlayerSliderState extends State<FMPlayerSlider> { - @override - Widget build(BuildContext context) { - return Row( - children: [ - Text( - widget.minHertz, - style: TextStyle( - color: Colors.white, - fontSize: 26, - shadows: [Helpers.dropShadowRegular]), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: GradientProgressIndicator( - percent: widget.songProgress, - height: 10, - type: "fm", - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - AGLDemoColors.jordyBlueColor, - AGLDemoColors.jordyBlueColor.withOpacity(0.8), - ]), - backgroundColor: AGLDemoColors.gradientBackgroundDarkColor, - ), - ), - ), - Text( - widget.maxHertz, - style: TextStyle( - color: Colors.white, - fontSize: 26, - shadows: [Helpers.dropShadowRegular]), - ) - ], - ); - } -} diff --git a/lib/presentation/screens/media_player/my_media.dart b/lib/presentation/screens/media_player/my_media.dart deleted file mode 100644 index e69de29..0000000 --- a/lib/presentation/screens/media_player/my_media.dart +++ /dev/null diff --git a/lib/presentation/screens/settings/settings_screens/audio_settings/widget/slider_widgets.dart b/lib/presentation/screens/settings/settings_screens/audio_settings/widget/slider_widgets.dart index fefd9ed..6988caa 100644 --- a/lib/presentation/screens/settings/settings_screens/audio_settings/widget/slider_widgets.dart +++ b/lib/presentation/screens/settings/settings_screens/audio_settings/widget/slider_widgets.dart @@ -74,7 +74,7 @@ class CustomBalanceState extends ConsumerState<CustomBalanceSlider> { onTap: () { _decrease(); }, - child: Text( + child: const Text( 'LEFT', style: TextStyle( fontSize: 18, @@ -127,7 +127,7 @@ class CustomBalanceState extends ConsumerState<CustomBalanceSlider> { onTap: () { _increase(); }, - child: Text( + child: const Text( 'RIGHT', style: TextStyle( fontSize: 18, @@ -217,14 +217,14 @@ class CustomFaderState extends ConsumerState<CustomFaderSlider> { onTap: () { _decrease(); }, - child: Text( + child: const Text( 'REAR', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: AGLDemoColors.periwinkleColor, ), - )), + )), ), SizedBox( width: 584, @@ -246,9 +246,7 @@ class CustomFaderState extends ConsumerState<CustomFaderSlider> { max: 10, value: faderValue, onChanged: (newValue) { - ref - .read(audioStateProvider.notifier) - .setFade(newValue); + ref.read(audioStateProvider.notifier).setFade(newValue); _currentVal = newValue; }, onChangeEnd: (value) { @@ -270,7 +268,7 @@ class CustomFaderState extends ConsumerState<CustomFaderSlider> { onTap: () { _increase(); }, - child: Text( + child: const Text( 'FRONT', style: TextStyle( fontSize: 18, diff --git a/lib/presentation/screens/splash/widget/splash_content.dart b/lib/presentation/screens/splash/widget/splash_content.dart index d93be4f..29d8d6c 100644 --- a/lib/presentation/screens/splash/widget/splash_content.dart +++ b/lib/presentation/screens/splash/widget/splash_content.dart @@ -66,7 +66,8 @@ class SplashContentState extends ConsumerState<SplashContent> @override void didChangeDependencies() { - ref.read(valClientProvider).startListen(); + ref.read(valClientProvider).run(); + ref.read(radioClientProvider).run(); super.didChangeDependencies(); } |