summaryrefslogtreecommitdiffstats
path: root/lib/data/data_providers
diff options
context:
space:
mode:
Diffstat (limited to 'lib/data/data_providers')
-rw-r--r--lib/data/data_providers/app_config_provider.dart50
-rw-r--r--lib/data/data_providers/app_provider.dart46
-rw-r--r--lib/data/data_providers/mediaplayer_notifier.dart25
-rw-r--r--lib/data/data_providers/mediaplayer_position_notifier.dart29
-rw-r--r--lib/data/data_providers/mpd_client.dart378
-rw-r--r--lib/data/data_providers/play_controller.dart40
-rw-r--r--lib/data/data_providers/playlist_art_notifier.dart20
-rw-r--r--lib/data/data_providers/playlist_notifier.dart37
-rw-r--r--lib/data/data_providers/radio_presets_provider.dart2
9 files changed, 619 insertions, 8 deletions
diff --git a/lib/data/data_providers/app_config_provider.dart b/lib/data/data_providers/app_config_provider.dart
index a60a462..54d0e07 100644
--- a/lib/data/data_providers/app_config_provider.dart
+++ b/lib/data/data_providers/app_config_provider.dart
@@ -56,11 +56,27 @@ class RadioConfig {
}
}
+class MpdConfig {
+ final String hostname;
+ final int port;
+
+ static String defaultHostname = 'localhost';
+ static int defaultPort = 6600;
+
+ MpdConfig({required this.hostname, required this.port});
+
+ static MpdConfig defaultConfig() {
+ return MpdConfig(
+ hostname: MpdConfig.defaultHostname, port: MpdConfig.defaultPort);
+ }
+}
+
class AppConfig {
final bool disableBkgAnimation;
final bool randomHybridAnimation;
final KuksaConfig kuksaConfig;
final RadioConfig radioConfig;
+ final MpdConfig mpdConfig;
static String configFilePath = '/etc/xdg/AGL/ics-homescreen.yaml';
@@ -68,7 +84,8 @@ class AppConfig {
{required this.disableBkgAnimation,
required this.randomHybridAnimation,
required this.kuksaConfig,
- required this.radioConfig});
+ required this.radioConfig,
+ required this.mpdConfig});
static KuksaConfig parseKuksaConfig(YamlMap kuksaMap) {
try {
@@ -160,6 +177,24 @@ class AppConfig {
return RadioConfig.defaultConfig();
}
}
+
+ static MpdConfig parseMpdConfig(YamlMap mpdMap) {
+ try {
+ String hostname = MpdConfig.defaultHostname;
+ if (mpdMap.containsKey('hostname')) {
+ hostname = mpdMap['hostname'];
+ }
+
+ int port = MpdConfig.defaultPort;
+ if (mpdMap.containsKey('port')) {
+ port = mpdMap['port'];
+ }
+
+ return MpdConfig(hostname: hostname, port: port);
+ } catch (_) {
+ return MpdConfig.defaultConfig();
+ }
+ }
}
final appConfigProvider = Provider((ref) {
@@ -189,6 +224,13 @@ final appConfigProvider = Provider((ref) {
radioConfig = RadioConfig.defaultConfig();
}
+ MpdConfig mpdConfig;
+ if (yamlMap.containsKey('mpd')) {
+ mpdConfig = AppConfig.parseMpdConfig(yamlMap['mpd']);
+ } else {
+ mpdConfig = MpdConfig.defaultConfig();
+ }
+
bool disableBkgAnimation = disableBkgAnimationDefault;
if (yamlMap.containsKey('disable-bg-animation')) {
var value = yamlMap['disable-bg-animation'];
@@ -209,12 +251,14 @@ final appConfigProvider = Provider((ref) {
disableBkgAnimation: disableBkgAnimation,
randomHybridAnimation: randomHybridAnimation,
kuksaConfig: kuksaConfig,
- radioConfig: radioConfig);
+ radioConfig: radioConfig,
+ mpdConfig: mpdConfig);
} catch (_) {
return AppConfig(
disableBkgAnimation: false,
randomHybridAnimation: false,
kuksaConfig: KuksaConfig.defaultConfig(),
- radioConfig: RadioConfig.defaultConfig());
+ radioConfig: RadioConfig.defaultConfig(),
+ mpdConfig: MpdConfig.defaultConfig());
}
});
diff --git a/lib/data/data_providers/app_provider.dart b/lib/data/data_providers/app_provider.dart
index ad3dd22..3d00d4c 100644
--- a/lib/data/data_providers/app_provider.dart
+++ b/lib/data/data_providers/app_provider.dart
@@ -2,16 +2,23 @@ import 'package:flutter_ics_homescreen/data/data_providers/hybrid_notifier.dart'
import 'package:flutter_ics_homescreen/data/data_providers/signal_notifier.dart';
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/vehicle_notifier.dart';
+import 'package:flutter_ics_homescreen/data/data_providers/audio_notifier.dart';
import 'package:flutter_ics_homescreen/data/data_providers/radio_notifier.dart';
+import 'package:flutter_ics_homescreen/data/data_providers/mediaplayer_notifier.dart';
+import 'package:flutter_ics_homescreen/data/data_providers/mediaplayer_position_notifier.dart';
+import 'package:flutter_ics_homescreen/data/data_providers/playlist_notifier.dart';
+import 'package:flutter_ics_homescreen/data/data_providers/playlist_art_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/data/data_providers/mpd_client.dart';
+import 'package:flutter_ics_homescreen/data/data_providers/play_controller.dart';
import 'package:flutter_ics_homescreen/export.dart';
-import '../models/users.dart';
-import 'vehicle_notifier.dart';
+import 'package:flutter_ics_homescreen/data/models/users.dart';
+import 'package:flutter_ics_homescreen/data/models/mediaplayer_state.dart';
enum AppState {
home,
@@ -59,6 +66,11 @@ final radioClientProvider = Provider((ref) {
return RadioClient(config: config, ref: ref);
});
+final mpdClientProvider = Provider((ref) {
+ MpdConfig config = ref.watch(appConfigProvider).mpdConfig;
+ return MpdClient(config: config, ref: ref);
+});
+
final vehicleProvider =
NotifierProvider<VehicleNotifier, Vehicle>(VehicleNotifier.new);
@@ -76,6 +88,34 @@ final audioStateProvider =
final radioStateProvider =
NotifierProvider<RadioStateNotifier, RadioState>(RadioStateNotifier.new);
+final mediaPlayerStateProvider =
+ NotifierProvider<MediaPlayerStateNotifier, MediaPlayerState>(
+ MediaPlayerStateNotifier.new);
+
+final mediaPlayerPositionProvider =
+ NotifierProvider<MediaPlayerPositionNotifier, Duration>(
+ MediaPlayerPositionNotifier.new);
+
+final playlistProvider =
+ NotifierProvider<PlaylistNotifier, List<PlaylistEntry>>(
+ PlaylistNotifier.new);
+
+final playlistArtProvider =
+ NotifierProvider<PlaylistArtNotifier, Map<int, Uint8List>>(
+ PlaylistArtNotifier.new);
+
+final playStateProvider = StateProvider<bool>((ref) {
+ final mediaPlayState = ref.watch(
+ mediaPlayerStateProvider.select((mediaplayer) => mediaplayer.playState));
+ final radioPlaying =
+ ref.watch(radioStateProvider.select((radio) => radio.playing));
+ return (mediaPlayState == PlayState.playing || radioPlaying);
+});
+
+final playControllerProvider = Provider((ref) {
+ return PlayController(ref: ref);
+});
+
final usersProvider = StateNotifierProvider<UsersNotifier, Users>((ref) {
return UsersNotifier(Users.initial());
});
diff --git a/lib/data/data_providers/mediaplayer_notifier.dart b/lib/data/data_providers/mediaplayer_notifier.dart
new file mode 100644
index 0000000..2d02e75
--- /dev/null
+++ b/lib/data/data_providers/mediaplayer_notifier.dart
@@ -0,0 +1,25 @@
+import 'package:flutter_ics_homescreen/export.dart';
+import 'playlist_notifier.dart';
+
+class MediaPlayerStateNotifier extends Notifier<MediaPlayerState> {
+ @override
+ MediaPlayerState build() {
+ return MediaPlayerState.initial();
+ }
+
+ void updatePlayState(PlayState newState) {
+ state = state.copyWith(playState: newState);
+ }
+
+ void updatePlaylistPosition(int position) {
+ state = state.copyWith(playlistPosition: position);
+ }
+
+ void updateCurrent(PlaylistEntry song) {
+ state = state.copyWith(song: song, playlistPosition: song.position);
+ }
+
+ void reset() {
+ state = MediaPlayerState.initial();
+ }
+}
diff --git a/lib/data/data_providers/mediaplayer_position_notifier.dart b/lib/data/data_providers/mediaplayer_position_notifier.dart
new file mode 100644
index 0000000..be896da
--- /dev/null
+++ b/lib/data/data_providers/mediaplayer_position_notifier.dart
@@ -0,0 +1,29 @@
+import 'dart:async';
+import 'package:flutter_ics_homescreen/export.dart';
+
+class MediaPlayerPositionNotifier extends Notifier<Duration> {
+ Timer? positionTimer;
+
+ @override
+ Duration build() {
+ return Duration.zero;
+ }
+
+ void set(Duration position) {
+ state = position;
+ }
+
+ void play() {
+ positionTimer ??=
+ Timer.periodic(const Duration(milliseconds: 250), (timer) {
+ state = state + const Duration(milliseconds: 250);
+ });
+ }
+
+ void pause() {
+ if (positionTimer != null) {
+ positionTimer!.cancel();
+ positionTimer = null;
+ }
+ }
+}
diff --git a/lib/data/data_providers/mpd_client.dart b/lib/data/data_providers/mpd_client.dart
new file mode 100644
index 0000000..98d8918
--- /dev/null
+++ b/lib/data/data_providers/mpd_client.dart
@@ -0,0 +1,378 @@
+import 'package:dart_mpd/dart_mpd.dart' as api;
+import 'package:flutter_ics_homescreen/export.dart';
+import 'playlist_notifier.dart';
+
+class ArtStateEntry {
+ bool reading = false;
+ bool read = false;
+
+ ArtStateEntry({required this.reading, required this.read});
+}
+
+class MpdClient {
+ final MpdConfig config;
+ final Ref ref;
+ late api.MpdClient eventClient;
+ late api.MpdClient client;
+ Map<int, ArtStateEntry> artState = {};
+
+ MpdClient({required this.config, required this.ref}) {
+ debugPrint("Connecting to MPD at ${config.hostname}:${config.port}");
+ client = api.MpdClient(
+ connectionDetails: api.MpdConnectionDetails(
+ host: config.hostname,
+ port: config.port,
+ timeout: const Duration(minutes: 2)),
+ onConnect: () => handleConnect(),
+ onDone: () => handleDone(),
+ onError: (e, st) => handleError(e, st));
+
+ // Second client instance to keep running in idle state to receive
+ // events
+ eventClient = api.MpdClient(
+ connectionDetails: api.MpdConnectionDetails(
+ host: config.hostname,
+ port: config.port,
+ timeout: const Duration(minutes: 2)),
+ onConnect: () => handleConnect(),
+ onDone: () => handleDone(),
+ onError: (e, st) => handleError(e, st));
+ }
+
+ void run() async {
+ var idleEvents = <api.MpdSubsystem>{
+ api.MpdSubsystem.database,
+ api.MpdSubsystem.playlist,
+ api.MpdSubsystem.player
+ };
+ bool done = false;
+ while (!done) {
+ //debugPrint("Calling MPD idle!");
+ var events = await eventClient.idle(idleEvents);
+ for (var event in events) {
+ switch (event) {
+ case api.MpdSubsystem.database:
+ //debugPrint("Got MPD database event");
+ await handleDatabaseEvent();
+ break;
+ case api.MpdSubsystem.playlist:
+ //debugPrint("Got MPD queue event");
+ await handleQueueEvent();
+ break;
+ case api.MpdSubsystem.player:
+ //debugPrint("Got MPD player event");
+ await handlePlayerEvent();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ }
+
+ void handleConnect() {
+ debugPrint("Connected to MPD!");
+ }
+
+ void handleResponse(api.MpdResponse response) {
+ debugPrint('Got MPD response $response');
+ }
+
+ void handleDone() {
+ debugPrint('Got MPD done!');
+ }
+
+ void handleError(Object e, StackTrace st) {
+ debugPrint('ERROR:\n$st');
+ }
+
+ // Idle state event handlers
+
+ Future handleDatabaseEvent() async {
+ eventClient.clear();
+ eventClient.add("/");
+ }
+
+ Future handleQueueEvent() async {
+ ref.read(playlistProvider.notifier).clear();
+ artState.clear();
+ ref.read(playlistArtProvider.notifier).clear();
+ ref.read(mediaPlayerStateProvider.notifier).reset();
+
+ var songs = await eventClient.playlistinfo();
+ for (var song in songs) {
+ //debugPrint("Got song ${song.title} - ${song.artist} at pos ${song.pos}");
+ int position = 0;
+ if (song.pos != null) {
+ position = song.pos!;
+ } else {
+ //debugPrint("WARNING: song has no position in queue, ignoring");
+ continue;
+ }
+ String title = "";
+ if (song.title != null) {
+ title = song.title!.join(" ");
+ } else {
+ // Just use filename
+ title = song.file;
+ }
+ String album = "";
+ if (song.album != null) {
+ album = song.album!.join(" ");
+ }
+ String artist = "";
+ if (song.artist != null) {
+ artist = song.artist!.join(" ");
+ }
+ Duration duration = Duration.zero;
+ if (song.duration != null) {
+ duration = song.duration!;
+ }
+ //debugPrint(
+ // "Got playlist entry \"$title\" - \"$album\" - \"$artist\" at $position");
+
+ if (position == 0) {
+ ref.read(mediaPlayerStateProvider.notifier).updateCurrent(PlaylistEntry(
+ title: title,
+ album: album,
+ artist: artist,
+ file: song.file,
+ duration: duration,
+ position: position));
+
+ if (song.file.isNotEmpty) {
+ readSongArt(position, song.file);
+ }
+ }
+ ref.read(playlistProvider.notifier).add(PlaylistEntry(
+ title: title,
+ album: album,
+ artist: artist,
+ file: song.file,
+ duration: duration,
+ position: position));
+ }
+ }
+
+ Future handlePlayerEvent() async {
+ String songFile = "";
+ int songPosition = -1;
+ var song = await eventClient.currentsong();
+ if (song != null) {
+ //debugPrint(
+ // "Player event: song ${song.title} - ${song.artist} at pos ${song.pos}");
+ if (song.pos != null) {
+ songPosition = song.pos!;
+ }
+ if (songPosition < 0) {
+ debugPrint("WARNING: song has no position in queue, ignoring");
+ return;
+ }
+ String title = "";
+ if (song.title != null) {
+ title = song.title!.first;
+ } else {
+ // Just use filename
+ title = song.file;
+ }
+ String album = "";
+ if (song.album != null) {
+ album = song.album!.first;
+ }
+ String artist = "";
+ if (song.artist != null) {
+ artist = song.artist!.first;
+ }
+ Duration duration = Duration.zero;
+ if (song.duration != null) {
+ duration = song.duration!;
+ }
+ songFile = song.file;
+ //debugPrint(
+ // "Got song \"$title\" - \"$album\" - \"$artist\" at $songPosition, file \"$songFile\"");
+ ref.read(mediaPlayerStateProvider.notifier).updateCurrent(PlaylistEntry(
+ title: title,
+ album: album,
+ artist: artist,
+ file: songFile,
+ duration: duration,
+ position: songPosition));
+ }
+
+ var status = await eventClient.status();
+ if (status.elapsed != null) {
+ //debugPrint("Using elapsed time ${status.elapsed} s");
+ ref
+ .read(mediaPlayerPositionProvider.notifier)
+ .set(Duration(milliseconds: (status.elapsed! * 1000.0).toInt()));
+ }
+ PlayState playState = PlayState.stopped;
+ if (status.state != null) {
+ switch (status.state!) {
+ case api.MpdState.stop:
+ //debugPrint("status.state = stop");
+ playState = PlayState.stopped;
+ ref.read(mediaPlayerPositionProvider.notifier).pause();
+ break;
+ case api.MpdState.play:
+ //debugPrint("status.state = play");
+ playState = PlayState.playing;
+ ref.read(mediaPlayerPositionProvider.notifier).play();
+ break;
+ case api.MpdState.pause:
+ //debugPrint("status.state = paused");
+ playState = PlayState.paused;
+ ref.read(mediaPlayerPositionProvider.notifier).pause();
+ break;
+ default:
+ break;
+ }
+ ref.read(mediaPlayerStateProvider.notifier).updatePlayState(playState);
+ }
+
+ if (playState != PlayState.playing) {
+ // No need to attempt to load art, exit
+ return;
+ }
+
+ if (!artState.containsKey(songPosition) ||
+ !(artState[songPosition]!.read || artState[songPosition]!.reading)) {
+ if (songFile.isNotEmpty) {
+ readSongArt(songPosition, songFile);
+ } else {
+ // Do not attempt any fallback for providing the art for now
+ debugPrint("No file for position $songPosition, no art");
+ artState[songPosition] = ArtStateEntry(reading: false, read: true);
+ ref
+ .read(playlistArtProvider.notifier)
+ .update(songPosition, Uint8List(0));
+ }
+ }
+ }
+
+ void readSongArt(int position, String file) async {
+ int offset = 0;
+ int size = 0;
+ List<int> bytes = [];
+
+ if (artState.containsKey(position)) {
+ if (artState[position]!.reading) {
+ return;
+ } else {
+ //debugPrint("Reading art for position $position");
+ artState[position]!.reading = true;
+ }
+ } else {
+ artState[position] = ArtStateEntry(reading: true, read: false);
+ }
+ debugPrint("Reading art for \"$file\"");
+ // Work around dart_mpd not escaping spaced strings itself
+ String escapedFile = file.replaceAll(RegExp(r" "), "\\ ");
+ escapedFile = "\"$file\"";
+
+ bool first = true;
+ do {
+ //debugPrint("Reading, offset = $offset, size = $size");
+ var chunk = await client.readpicture(escapedFile, offset);
+ if (chunk != null) {
+ if (chunk.size != null) {
+ if (chunk.size == 0) {
+ if (first) {
+ // No art, exit
+ break;
+ }
+ }
+ size = chunk.size!;
+ }
+ // else unexpected error
+
+ if (chunk.bytes.isNotEmpty) {
+ //debugPrint("Got ${chunk.bytes.length} bytes of album art");
+ bytes = bytes + chunk.bytes;
+ offset += chunk.bytes.length;
+ } else {
+ break;
+ }
+ }
+ } while (offset < size);
+ //debugPrint("Done, offset = $offset, size = $size");
+
+ if (offset == size) {
+ debugPrint("Read $size bytes of album art for $file");
+ } else {
+ // else error, leave art empty
+ bytes.clear();
+ }
+
+ artState[position]!.read = true;
+ artState[position]!.reading = false;
+ ref
+ .read(playlistArtProvider.notifier)
+ .update(position, Uint8List.fromList(bytes));
+ }
+
+ // Player commands
+
+ void play() async {
+ var playState = ref.read(mediaPlayerStateProvider
+ .select((mediaplayer) => mediaplayer.playState));
+ if (playState == PlayState.stopped) {
+ int position = ref.read(mediaPlayerStateProvider
+ .select((mediaplayer) => mediaplayer.playlistPosition));
+ //debugPrint("Calling MPD play, position = $position");
+ if (position >= 0) {
+ client.play(position);
+ }
+ } else if (playState == PlayState.paused) {
+ client.pause(false);
+ }
+ }
+
+ void pause() async {
+ if (ref.read(mediaPlayerStateProvider
+ .select((mediaplayer) => mediaplayer.playState)) ==
+ PlayState.playing) {
+ client.pause(true);
+ }
+ }
+
+ void next() async {
+ if (ref.read(mediaPlayerStateProvider
+ .select((mediaplayer) => mediaplayer.playState)) ==
+ PlayState.playing) {
+ client.next();
+ }
+ }
+
+ void previous() async {
+ if (ref.read(mediaPlayerStateProvider
+ .select((mediaplayer) => mediaplayer.playState)) ==
+ PlayState.playing) {
+ client.previous();
+ }
+ }
+
+ void seek(int milliseconds) async {
+ client.seekcur((milliseconds / 1000.0).toString());
+ }
+
+ void fastForward(int milliseconds) async {
+ if (milliseconds > 0) {
+ client.seekcur("+${(milliseconds / 1000.0).toString()}");
+ }
+ }
+
+ void rewind(int milliseconds) async {
+ if (milliseconds > 0) {
+ client.seekcur("-${(milliseconds / 1000.0).toString()}");
+ }
+ }
+
+ void pickTrack(int position) async {
+ if (position >= 0) {
+ client.play(position);
+ }
+ }
+
+ void loopPlaylist(bool loop) async {}
+}
diff --git a/lib/data/data_providers/play_controller.dart b/lib/data/data_providers/play_controller.dart
new file mode 100644
index 0000000..38ef309
--- /dev/null
+++ b/lib/data/data_providers/play_controller.dart
@@ -0,0 +1,40 @@
+import 'package:flutter_ics_homescreen/export.dart';
+
+enum PlaySource { none, media, radio }
+
+class PlayController {
+ final Ref ref;
+ PlaySource source = PlaySource.none;
+
+ PlayController({required this.ref});
+
+ void setSource(PlaySource newSource) {
+ source = newSource;
+ }
+
+ void play() async {
+ switch (source) {
+ case PlaySource.media:
+ ref.read(mpdClientProvider).play();
+ break;
+ case PlaySource.radio:
+ ref.read(radioClientProvider).start();
+ break;
+ default:
+ break;
+ }
+ }
+
+ void pause() async {
+ switch (source) {
+ case PlaySource.media:
+ ref.read(mpdClientProvider).pause();
+ break;
+ case PlaySource.radio:
+ ref.read(radioClientProvider).stop();
+ break;
+ default:
+ break;
+ }
+ }
+}
diff --git a/lib/data/data_providers/playlist_art_notifier.dart b/lib/data/data_providers/playlist_art_notifier.dart
new file mode 100644
index 0000000..417aa9a
--- /dev/null
+++ b/lib/data/data_providers/playlist_art_notifier.dart
@@ -0,0 +1,20 @@
+import 'package:flutter_ics_homescreen/export.dart';
+
+class PlaylistArtNotifier extends Notifier<Map<int, Uint8List>> {
+ @override
+ Map<int, Uint8List> build() {
+ return {};
+ }
+
+ void update(int position, Uint8List art) {
+ // Having to copy the Map to trigger notification seems not particularly
+ // efficient, it may make sense to notify against the last position
+ // updated or similar...
+ state[position] = art;
+ state = Map.of(state);
+ }
+
+ void clear() {
+ state = {};
+ }
+}
diff --git a/lib/data/data_providers/playlist_notifier.dart b/lib/data/data_providers/playlist_notifier.dart
new file mode 100644
index 0000000..2936591
--- /dev/null
+++ b/lib/data/data_providers/playlist_notifier.dart
@@ -0,0 +1,37 @@
+import 'package:flutter_ics_homescreen/export.dart';
+
+class PlaylistEntry {
+ final String title;
+ final String album;
+ final String artist;
+ final String file;
+ final Duration duration;
+ final int position;
+
+ const PlaylistEntry(
+ {required this.title,
+ required this.album,
+ required this.artist,
+ required this.file,
+ required this.duration,
+ required this.position});
+}
+
+class PlaylistNotifier extends Notifier<List<PlaylistEntry>> {
+ @override
+ List<PlaylistEntry> build() {
+ return [];
+ }
+
+ void update({required List<PlaylistEntry> newPlaylist}) {
+ state = newPlaylist;
+ }
+
+ void add(PlaylistEntry entry) {
+ state.add(entry);
+ }
+
+ void clear() {
+ state = [];
+ }
+}
diff --git a/lib/data/data_providers/radio_presets_provider.dart b/lib/data/data_providers/radio_presets_provider.dart
index 9ee68ac..af14997 100644
--- a/lib/data/data_providers/radio_presets_provider.dart
+++ b/lib/data/data_providers/radio_presets_provider.dart
@@ -1,6 +1,4 @@
-import 'dart:io';
import 'package:flutter_ics_homescreen/export.dart';
-import 'package:yaml/yaml.dart';
class RadioPreset {
final int frequency;