aboutsummaryrefslogtreecommitdiffstats
path: root/lib/data
diff options
context:
space:
mode:
authorScott Murray <scott.murray@konsulko.com>2023-12-31 16:24:51 -0500
committerScott Murray <scott.murray@konsulko.com>2024-01-03 18:23:52 -0500
commit4742fde5c48726357cc8db06d237e9db6c3df608 (patch)
treedcca2b3e3c6cb3a4a46b7ae603f64fa9ce5a086c /lib/data
parentfcd868bd73d35bd79074f3425317152565aeb275 (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/data')
-rw-r--r--lib/data/data_providers/app_config_provider.dart72
-rw-r--r--lib/data/data_providers/app_launcher.dart36
-rw-r--r--lib/data/data_providers/app_provider.dart19
-rw-r--r--lib/data/data_providers/audio_notifier.dart8
-rw-r--r--lib/data/data_providers/radio_client.dart161
-rw-r--r--lib/data/data_providers/radio_notifier.dart31
-rw-r--r--lib/data/data_providers/radio_presets_provider.dart49
-rw-r--r--lib/data/data_providers/val_client.dart7
-rw-r--r--lib/data/data_providers/vehicle_notifier.dart2
-rw-r--r--lib/data/models/audio_state.dart (renamed from lib/data/models/audio.dart)24
-rw-r--r--lib/data/models/radio_state.dart100
11 files changed, 463 insertions, 46 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;
+ }
+}