diff options
25 files changed, 1229 insertions, 441 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; diff --git a/lib/data/models/audio_state.dart b/lib/data/models/audio_state.dart index cfa550b..60720a8 100644 --- a/lib/data/models/audio_state.dart +++ b/lib/data/models/audio_state.dart @@ -40,36 +40,6 @@ class AudioState { ); } - Map<String, dynamic> toMap() { - return { - 'volume': volume, - 'balance': balance, - 'fade': fade, - 'treble': treble, - 'bass': bass, - }; - } - - 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, - treble: map['treble']?.toDouble() ?? 0.0, - bass: map['bass']?.toDouble() ?? 0.0, - ); - } - - String toJson() => json.encode(toMap()); - - factory AudioState.fromJson(String source) => - AudioState.fromMap(json.decode(source)); - - @override - String toString() { - return 'AudioState(volume: $volume, balance: $balance, fade: $fade, treble: $treble, bass: $bass)'; - } - @override bool operator ==(Object other) { if (identical(this, other)) return true; diff --git a/lib/data/models/mediaplayer_state.dart b/lib/data/models/mediaplayer_state.dart new file mode 100644 index 0000000..f880a1e --- /dev/null +++ b/lib/data/models/mediaplayer_state.dart @@ -0,0 +1,49 @@ +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/data/data_providers/playlist_notifier.dart'; + +enum PlayState { stopped, playing, paused } + +@immutable +class MediaPlayerState { + final int playlistPosition; + final PlayState playState; + final PlaylistEntry? song; + + const MediaPlayerState( + {required this.playlistPosition, + required this.playState, + required this.song}); + + const MediaPlayerState.initial() + : playlistPosition = -1, + playState = PlayState.stopped, + song = null; + + MediaPlayerState copyWith( + {int? playlistPosition, + PlayState? playState, + PlaylistEntry? song, + Duration? songPosition, + Duration? songLength}) { + return MediaPlayerState( + playlistPosition: playlistPosition ?? this.playlistPosition, + playState: playState ?? this.playState, + song: song ?? this.song, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is MediaPlayerState && + other.playlistPosition == playlistPosition && + other.playState == playState && + other.song == song; + } + + @override + int get hashCode { + return playlistPosition.hashCode ^ playState.hashCode ^ song.hashCode; + } +} diff --git a/lib/data/models/radio_state.dart b/lib/data/models/radio_state.dart index dd307d9..da972fd 100644 --- a/lib/data/models/radio_state.dart +++ b/lib/data/models/radio_state.dart @@ -43,38 +43,6 @@ class RadioState { ); } - 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; diff --git a/lib/export.dart b/lib/export.dart index a5c5626..e97fd90 100644 --- a/lib/export.dart +++ b/lib/export.dart @@ -10,6 +10,7 @@ export 'data/models/vehicle.dart'; export 'data/models/units.dart'; export 'data/models/audio_state.dart'; export 'data/models/radio_state.dart'; +export 'data/models/mediaplayer_state.dart'; export 'data/models/connections_signals.dart'; export 'data/models/hybrid.dart'; diff --git a/lib/presentation/common_widget/volume_and_fan_control.dart b/lib/presentation/common_widget/volume_and_fan_control.dart index b38e303..765193b 100644 --- a/lib/presentation/common_widget/volume_and_fan_control.dart +++ b/lib/presentation/common_widget/volume_and_fan_control.dart @@ -19,9 +19,7 @@ class VolumeFanControl extends ConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Visibility.maintain( - visible: state == AppState.media ? false : true, - child: const VolumeBar()), + VolumeBar(), SizedBox( height: gapSize, ), diff --git a/lib/presentation/common_widget/volume_bar.dart b/lib/presentation/common_widget/volume_bar.dart index b54283e..b966fc9 100644 --- a/lib/presentation/common_widget/volume_bar.dart +++ b/lib/presentation/common_widget/volume_bar.dart @@ -43,13 +43,21 @@ class VolumeBarState extends ConsumerState<VolumeBar> { }); } - void pause() {} + void play() { + ref.read(playControllerProvider).play(); + } + + void pause() { + ref.read(playControllerProvider).pause(); + } @override Widget build(BuildContext context) { final volumeValue = ref.watch(audioStateProvider.select((audio) => audio.volume)); val = volumeValue.toDouble(); + final isPlaying = ref.watch(playStateProvider); + return Column( // mainAxisAlignment: MainAxisAlignment.center, // crossAxisAlignment: CrossAxisAlignment.center, @@ -166,12 +174,15 @@ class VolumeBarState extends ConsumerState<VolumeBar> { padding: EdgeInsets.zero, color: AGLDemoColors.periwinkleColor, onPressed: () { - pause(); + if (isPlaying) { + pause(); + } else { + play(); + } }, - icon: const Icon( - Icons.pause, - size: 30, - )), + icon: isPlaying + ? const Icon(Icons.pause, size: 40) + : const Icon(Icons.play_arrow, size: 40)), ), ], ); diff --git a/lib/presentation/screens/media/media.dart b/lib/presentation/screens/media/media.dart index b7ce9e1..e2ba927 100644 --- a/lib/presentation/screens/media/media.dart +++ b/lib/presentation/screens/media/media.dart @@ -1,7 +1,7 @@ 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 'package:flutter_ics_homescreen/data/data_providers/play_controller.dart'; import 'media_nav_notifier.dart'; import 'player_navigation.dart'; @@ -50,6 +50,21 @@ class MediaPage extends StatelessWidget { } } +class MediaPlayingStateNotifier extends Notifier<bool> { + @override + bool build() { + return false; + } + + set(bool value) { + state = value; + } +} + +final mediaPlayingStateProvider = + NotifierProvider<MediaPlayingStateNotifier, bool>( + MediaPlayingStateNotifier.new); + class Media extends ConsumerStatefulWidget { const Media({super.key}); @@ -58,22 +73,44 @@ class Media extends ConsumerStatefulWidget { } class _MediaState extends ConsumerState<Media> { - //late MediaNavState selectedNav; - - //@override - //initState() { - // selectedNav = ref.read(mediaNavStateProvider); - // super.initState(); - //} + @override + void initState() { + // Set initial source so external control (like the volume bar button) + // will work from the start. + var navState = ref.read(mediaNavStateProvider); + switch (navState) { + case MediaNavState.fm: + ref.read(playControllerProvider).setSource(PlaySource.radio); + break; + case MediaNavState.media: + default: + ref.read(playControllerProvider).setSource(PlaySource.media); + break; + } + super.initState(); + } onPressed(MediaNavState type) { setState(() { if (type == MediaNavState.fm) { ref.read(mediaNavStateProvider.notifier).set(MediaNavState.fm); + ref.read(playControllerProvider).setSource(PlaySource.radio); + + bool mediaPlaying = false; + if (ref.read(mediaPlayerStateProvider).playState == PlayState.playing) { + ref.read(mpdClientProvider).pause(); + mediaPlaying = true; + } + ref.read(mediaPlayingStateProvider.notifier).set(mediaPlaying); ref.read(radioClientProvider).start(); } else if (type == MediaNavState.media) { ref.read(mediaNavStateProvider.notifier).set(MediaNavState.media); + ref.read(playControllerProvider).setSource(PlaySource.media); + ref.read(radioClientProvider).stop(); + if (ref.read(mediaPlayingStateProvider)) { + ref.read(mpdClientProvider).play(); + } } }); } @@ -103,11 +140,13 @@ class _MediaState extends ConsumerState<Media> { : Container(), ), ), + /* 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_player.dart b/lib/presentation/screens/media/media_player.dart index d7486c7..0dab346 100644 --- a/lib/presentation/screens/media/media_player.dart +++ b/lib/presentation/screens/media/media_player.dart @@ -3,38 +3,31 @@ import 'media_player_controls.dart'; import 'play_list_table.dart'; import 'segmented_buttons.dart'; -class MediaPlayer extends StatefulWidget { +class MediaPlayer extends ConsumerStatefulWidget { const MediaPlayer({super.key}); @override - State<MediaPlayer> createState() => _MediaPlayerState(); + ConsumerState<MediaPlayer> createState() => _MediaPlayerState(); } -class _MediaPlayerState extends State<MediaPlayer> { - String selectedNav = "Bluetooth"; - List<String> navItems = ["Bluetooth", "SD", "USB"]; - - late String songName = "Feel Good Inc."; - - String tableName = "2000’s Dance Hits"; - List<PlayListModel> playList = [ - PlayListModel(songName: "Feel Good Inc.", albumName: "Gorillaz"), - PlayListModel( - songName: "Hips Don’t Lie", albumName: "Shakira, Wyclef Jean"), - PlayListModel(songName: "AG1", albumName: "Paid Advertisement"), - PlayListModel(songName: "Hey Ya!", albumName: "Outkast"), - PlayListModel(songName: "One, Two, Step", albumName: "Ciara, Missy Elliot"), - PlayListModel(songName: "Don’t Trust Me", albumName: "3OH!3"), - ]; - String selectedPlayListSongName = "Feel Good Inc."; +class _MediaPlayerState extends ConsumerState<MediaPlayer> { + String selectedNav = "USB"; + List<String> navItems = ["USB", "SD", "Bluetooth"]; @override Widget build(BuildContext context) { - double albumArtSize = 460; + double albumArtSize = 400; + final playlistPosition = ref.watch(mediaPlayerStateProvider + .select((mediaplayer) => mediaplayer.playlistPosition)); + final playlistArt = ref.watch(playlistArtProvider); + Uint8List art = Uint8List(0); + if (playlistArt.containsKey(playlistPosition)) { + art = playlistArt[playlistPosition]!; + } + return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - //const PlayerNavigation(), SegmentedButtons( navItems: navItems, selectedNav: selectedNav, @@ -45,11 +38,20 @@ class _MediaPlayerState extends State<MediaPlayer> { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Image.asset( - "assets/AlbumArtMedia.png", - width: albumArtSize, - height: albumArtSize, - ) + art.isNotEmpty + ? Image.memory(art, + width: albumArtSize, + height: albumArtSize, + fit: BoxFit.contain) + : Container( + width: albumArtSize, + height: albumArtSize, + color: AGLDemoColors.jordyBlueColor.withOpacity(0.2), + child: Icon( + Icons.music_note, + size: albumArtSize, + color: AGLDemoColors.jordyBlueColor, + )) ], ), const SizedBox( @@ -58,19 +60,11 @@ class _MediaPlayerState extends State<MediaPlayer> { Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - MediaPlayerControls( - songName: songName, - songLengthStart: "-1:23", - songLengthStop: "5:03"), + MediaPlayerControls(), const SizedBox( - height: 72, - ), - PlayListTable( - playList: playList, - selectedPlayListSongName: selectedPlayListSongName, - tableName: tableName, - type: "media", + height: 12, ), + PlayListTable(), ], ) ], diff --git a/lib/presentation/screens/media/media_player_controls.dart b/lib/presentation/screens/media/media_player_controls.dart index 518b669..26cdfce 100644 --- a/lib/presentation/screens/media/media_player_controls.dart +++ b/lib/presentation/screens/media/media_player_controls.dart @@ -1,40 +1,49 @@ 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'; +import 'package:flutter_ics_homescreen/presentation/screens/settings/settings_screens/audio_settings/widget/slider_widgets.dart'; -class MediaPlayerControls extends StatefulWidget { - const MediaPlayerControls( - {super.key, - required this.songName, - required this.songLengthStart, - required this.songLengthStop}); +// Time to string helper, returns HH:MM:SS or MM:SS as appropriate +String timeToString(Duration time) { + String result = ""; + if (time > const Duration(minutes: 59, seconds: 59)) { + result = time.toString().split('.').first.padLeft(8, "0"); + } else { + result = time.toString().substring(2, 7); + } + return result; +} - final String songName; - final String songLengthStart; - final String songLengthStop; +class MediaPlayerControls extends ConsumerStatefulWidget { + const MediaPlayerControls({super.key}); @override - State<MediaPlayerControls> createState() => _MediaPlayerControlsState(); + ConsumerState<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(); - } +class _MediaPlayerControlsState extends ConsumerState<MediaPlayerControls> { + //@override + //void initState() { + // super.initState(); + //} @override Widget build(BuildContext context) { + var currentSong = ref.watch( + mediaPlayerStateProvider.select((mediaplayer) => mediaplayer.song)); + var songPosition = ref.watch(mediaPlayerPositionProvider); + + String songName = ""; + String songDetail = ""; + String songPositionString = "00:00"; + String songLengthString = "00:00"; + if (currentSong != null) { + songName = currentSong.title; + songDetail = currentSong.artist; + songLengthString = timeToString(currentSong.duration); + } + songPositionString = timeToString(songPosition); + return Material( color: Colors.transparent, child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -46,42 +55,25 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> { shadows: [Helpers.dropShadowRegular], fontSize: 44), ), - MediaPlayerControlsubDetails( - albumName: albumName, + MediaPlayerControlsDetails( + songDetail: songDetail, ), 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, - // ), + const MediaPlayerControlsSlider(), Padding( padding: const EdgeInsets.symmetric(vertical: 5), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - songLengthStart, + songPositionString, style: TextStyle( color: Colors.white, fontSize: 26, shadows: [Helpers.dropShadowRegular]), ), Text( - songLengthStop, + songLengthString, style: TextStyle( color: Colors.white, fontSize: 26, @@ -91,23 +83,23 @@ class _MediaPlayerControlsState extends State<MediaPlayerControls> { ), ), ]), - const MediaPlayerActions(), + const MediaPlayerControlsActions(), ]), ); } } -class MediaPlayerControlsubDetails extends StatefulWidget { - const MediaPlayerControlsubDetails({super.key, required this.albumName}); - final String albumName; +class MediaPlayerControlsDetails extends StatefulWidget { + const MediaPlayerControlsDetails({super.key, required this.songDetail}); + final String songDetail; @override - State<MediaPlayerControlsubDetails> createState() => - _MediaPlayerControlsubDetailsState(); + State<MediaPlayerControlsDetails> createState() => + _MediaPlayerControlsDetailsState(); } -class _MediaPlayerControlsubDetailsState - extends State<MediaPlayerControlsubDetails> { +class _MediaPlayerControlsDetailsState + extends State<MediaPlayerControlsDetails> { bool isShuffleEnabled = false; bool isRepeatEnabled = false; @override @@ -116,7 +108,7 @@ class _MediaPlayerControlsubDetailsState mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - widget.albumName, + widget.songDetail, style: TextStyle( color: Colors.white, fontWeight: FontWeight.w400, @@ -158,25 +150,98 @@ class _MediaPlayerControlsubDetailsState } } -class MediaPlayerActions extends StatefulWidget { - const MediaPlayerActions({super.key}); +class MediaPlayerControlsSlider extends ConsumerStatefulWidget { + const MediaPlayerControlsSlider({super.key}); + + @override + ConsumerState<MediaPlayerControlsSlider> createState() => + MediaPlayerControlsSliderState(); +} + +class MediaPlayerControlsSliderState + extends ConsumerState<MediaPlayerControlsSlider> { + //late Duration songPosition; + + //@override + //void initState() { + // songPosition = ref.read(mediaPlayerPositionProvider); + // super.initState(); + //} + + @override + Widget build(BuildContext context) { + var currentSong = ref.watch( + mediaPlayerStateProvider.select((mediaplayer) => mediaplayer.song)); + var songPosition = ref.watch(mediaPlayerPositionProvider); + + Duration songLength = Duration.zero; + if (currentSong != null) { + songLength = currentSong.duration; + } + + return Container( + height: 80, + child: SliderTheme( + data: SliderThemeData( + overlayShape: SliderComponentShape.noOverlay, + valueIndicatorShape: SliderComponentShape.noOverlay, + activeTickMarkColor: Colors.transparent, + inactiveTickMarkColor: Colors.transparent, + inactiveTrackColor: AGLDemoColors.periwinkleColor, + thumbShape: const PolygonSliderThumb(sliderValue: 3, thumbRadius: 23), + //trackHeight: 5, + ), + child: Slider( + max: songLength.inMilliseconds.toDouble(), + value: songPosition.inMilliseconds.toDouble(), + onChangeStart: (double value) { + // Disable timer so position will not change while control is + // being dragged. It will be re-enabled via the playback state + // update from MPD. + ref.read(mediaPlayerPositionProvider.notifier).pause(); + }, + onChanged: (double newValue) { + setState(() { + ref + .read(mediaPlayerPositionProvider.notifier) + .set(Duration(milliseconds: newValue.toInt())); + }); + }, + onChangeEnd: (double newValue) { + ref.read(mpdClientProvider).seek(newValue.toInt()); + }, + ), + ), + ); + } +} + +class MediaPlayerControlsActions extends ConsumerStatefulWidget { + const MediaPlayerControlsActions({super.key}); @override - State<MediaPlayerActions> createState() => _MediaPlayerActionsState(); + ConsumerState<MediaPlayerControlsActions> createState() => + _MediaPlayerControlsActionsState(); } -class _MediaPlayerActionsState extends State<MediaPlayerActions> { +class _MediaPlayerControlsActionsState + extends ConsumerState<MediaPlayerControlsActions> { bool isPressed = false; - bool isPlaying = true; @override Widget build(BuildContext context) { + bool isPlaying = ref.watch(mediaPlayerStateProvider + .select((mediaplayer) => mediaplayer.playState)) == + PlayState.playing; + return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ InkWell( customBorder: const CircleBorder(), - onTap: () {}, + onTap: () { + ref.read(mpdClientProvider).previous(); + }, child: Padding( padding: const EdgeInsets.all(8.0), child: SvgPicture.asset( @@ -191,7 +256,11 @@ class _MediaPlayerActionsState extends State<MediaPlayerActions> { customBorder: const CircleBorder(), onTap: () { setState(() { - isPlaying = !isPlaying; + if (isPlaying) { + ref.read(mpdClientProvider).pause(); + } else { + ref.read(mpdClientProvider).play(); + } }); }, onTapDown: (details) { @@ -221,7 +290,9 @@ class _MediaPlayerActionsState extends State<MediaPlayerActions> { ), InkWell( customBorder: const CircleBorder(), - onTap: () {}, + onTap: () { + ref.read(mpdClientProvider).next(); + }, child: Padding( padding: const EdgeInsets.all(8.0), child: SvgPicture.asset( diff --git a/lib/presentation/screens/media/play_list_table.dart b/lib/presentation/screens/media/play_list_table.dart index 369bb9c..71d2fc9 100644 --- a/lib/presentation/screens/media/play_list_table.dart +++ b/lib/presentation/screens/media/play_list_table.dart @@ -2,38 +2,29 @@ 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'; -class PlayListTable extends StatefulWidget { - const PlayListTable( - {super.key, - required this.type, - required this.tableName, - required this.playList, - required this.selectedPlayListSongName}); - final String type; - final String tableName; - final List<PlayListModel> playList; - final String selectedPlayListSongName; +class PlayListTable extends ConsumerStatefulWidget { + PlayListTable({super.key}); @override - State<PlayListTable> createState() => _PlayListTableState(); + ConsumerState<PlayListTable> createState() => _PlayListTableState(); } -class _PlayListTableState extends State<PlayListTable> { +class _PlayListTableState extends ConsumerState<PlayListTable> { bool isAudioSettingsEnabled = false; - late String tableName; - late List<PlayListModel> playList; - late String selectedPlayListSongName; - @override - void initState() { - tableName = widget.tableName; - playList = widget.playList; - selectedPlayListSongName = widget.selectedPlayListSongName; - super.initState(); - } + //@override + //void initState() { + // super.initState(); + //} @override Widget build(BuildContext context) { + final controller = ScrollController(); + var playlist = ref.watch(playlistProvider); + var selectedPosition = ref.watch(mediaPlayerStateProvider + .select((mediaplayer) => mediaplayer.playlistPosition)); + late String tableName = "USB"; + return Material( color: Colors.transparent, child: Column( @@ -51,19 +42,18 @@ class _PlayListTableState extends State<PlayListTable> { fontWeight: FontWeight.w400, fontSize: 40), ), - if (widget.type == "media") - InkWell( - customBorder: const CircleBorder(), - onTap: () {}, - child: Opacity( - opacity: 0.5, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SvgPicture.asset( - "assets/AppleMusic.svg", - width: 32, - )), - )), + InkWell( + customBorder: const CircleBorder(), + onTap: () {}, + child: Opacity( + opacity: 0.5, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/AppleMusic.svg", + width: 32, + )), + )), ], ), InkWell( @@ -81,77 +71,119 @@ class _PlayListTableState extends State<PlayListTable> { ))) ], ), - SizedBox( - height: 325, - child: SingleChildScrollView( - child: Column( - children: playList.map((index) { - return Container( - height: 100, - margin: const EdgeInsets.symmetric(vertical: 4), - decoration: BoxDecoration( - border: Border( - left: selectedPlayListSongName == index.songName - ? const BorderSide( - color: Colors.white, width: 4) - : BorderSide.none), - gradient: LinearGradient( - colors: selectedPlayListSongName == index.songName - ? [ - AGLDemoColors.neonBlueColor, - AGLDemoColors.neonBlueColor - .withOpacity(0.15) - ] - : [ - Colors.black, - Colors.black.withOpacity(0.20) - ])), - child: InkWell( - onTap: () { - setState(() { - selectedPlayListSongName = index.songName; - }); - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 17, horizontal: 24), - child: Row( - children: [ - Expanded( - flex: 6, - child: AutoSizeText( - index.songName, - maxLines: 1, - style: TextStyle( - color: Colors.white, - fontSize: 40, - shadows: [Helpers.dropShadowRegular]), - )), - Expanded( - flex: 4, - child: Text( - index.albumName, - style: TextStyle( - color: Colors.white, - fontSize: 26, - shadows: [Helpers.dropShadowRegular]), - )) - ], - ), - ), - ), - ); - }).toList()), - ), - ), + Padding( + padding: const EdgeInsets.only(right: 12), + child: SizedBox( + height: 500, + child: RawScrollbar( + controller: controller, + thickness: 32, + thumbVisibility: true, + radius: const Radius.circular(10), + thumbColor: AGLDemoColors.periwinkleColor, + minThumbLength: 60, + interactive: true, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + scrollbars: false, + overscroll: false, + ), + child: CustomScrollView( + controller: controller, + physics: const ClampingScrollPhysics(), + slivers: <Widget>[ + SliverList.separated( + itemCount: playlist.length, + itemBuilder: (_, int index) { + return Container( + height: 92, + margin: + const EdgeInsets.only(right: 44), + decoration: BoxDecoration( + border: Border( + left: selectedPosition == + playlist[index].position + ? const BorderSide( + color: Colors.white, + width: 4) + : BorderSide.none), + gradient: LinearGradient( + colors: selectedPosition == + playlist[index].position + ? [ + AGLDemoColors + .neonBlueColor, + AGLDemoColors + .neonBlueColor + .withOpacity(0.15) + ] + : [ + Colors.black, + Colors.black + .withOpacity(0.20) + ])), + child: InkWell( + onTap: () { + setState(() { + selectedPosition = + playlist[index].position; + ref + .read(mpdClientProvider) + .pickTrack( + playlist[index].position); + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 17, horizontal: 24), + child: Column( + children: [ + Expanded( + flex: 6, + child: Align( + alignment: Alignment + .centerLeft, + child: AutoSizeText( + playlist[index].title, + maxLines: 1, + style: TextStyle( + color: + Colors.white, + fontSize: 40, + shadows: [ + Helpers + .dropShadowRegular + ]), + ))), + Expanded( + flex: 4, + child: Align( + alignment: Alignment + .centerLeft, + child: Text( + playlist[index] + .artist, + style: TextStyle( + color: + Colors.white, + fontSize: 22, + shadows: [ + Helpers + .dropShadowRegular + ]), + ))) + ], + ), + ), + ), + ); + }, + separatorBuilder: (_, __) { + return SizedBox(height: 8); + }, + ), + ]))))), ], )); } } - -class PlayListModel { - final String songName; - final String albumName; - - PlayListModel({required this.songName, required this.albumName}); -} diff --git a/lib/presentation/screens/media/radio_player.dart b/lib/presentation/screens/media/radio_player.dart index 4531c7b..f6695f1 100644 --- a/lib/presentation/screens/media/radio_player.dart +++ b/lib/presentation/screens/media/radio_player.dart @@ -34,8 +34,8 @@ class _RadioPlayerState extends ConsumerState<RadioPlayer> { @override Widget build(BuildContext context) { - double fmSignalHeight = 460; - double fmSignalWidth = 460; + double fmSignalHeight = 400; + double fmSignalWidth = 400; return Container( padding: const EdgeInsets.only(left: 7, right: 7), diff --git a/lib/presentation/screens/media/radio_player_controls.dart b/lib/presentation/screens/media/radio_player_controls.dart index bfa8da6..acc8291 100644 --- a/lib/presentation/screens/media/radio_player_controls.dart +++ b/lib/presentation/screens/media/radio_player_controls.dart @@ -10,6 +10,12 @@ class RadioPlayerControls extends ConsumerWidget { var freqCurrent = ref.watch(radioStateProvider.select((radio) => radio.freqCurrent)); String currentString = (freqCurrent / 1000000.0).toStringAsFixed(1); + var freqMin = + ref.watch(radioStateProvider.select((radio) => radio.freqMin)); + String freqMinString = (freqMin / 1000000.0).toStringAsFixed(1); + var freqMax = + ref.watch(radioStateProvider.select((radio) => radio.freqMax)); + String freqMaxString = (freqMax / 1000000.0).toStringAsFixed(1); return Material( color: Colors.transparent, @@ -24,16 +30,40 @@ class RadioPlayerControls extends ConsumerWidget { shadows: [Helpers.dropShadowRegular], fontSize: 44), ), - const RadioPlayerControlsSubDetails(), - const RadioPlayerControlsSlider(), + const RadioPlayerControlsActions(), + Column(children: [ + const RadioPlayerControlsSlider(), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + freqMinString, + style: TextStyle( + color: Colors.white, + fontSize: 26, + shadows: [Helpers.dropShadowRegular]), + ), + Text( + freqMaxString, + style: TextStyle( + color: Colors.white, + fontSize: 26, + shadows: [Helpers.dropShadowRegular]), + ) + ], + ), + ), + ]), ], ), ); } } -class RadioPlayerControlsSubDetails extends ConsumerWidget { - const RadioPlayerControlsSubDetails({super.key}); +class RadioPlayerControlsActions extends ConsumerWidget { + const RadioPlayerControlsActions({super.key}); onPressed({required WidgetRef ref, required String type}) { if (type == "tuneLeft") { @@ -174,78 +204,40 @@ class RadioPlayerControlsSliderState 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]), - )), - ], - ), - )); + return Container( + height: 80, + child: SliderTheme( + data: SliderThemeData( + overlayShape: SliderComponentShape.noOverlay, + valueIndicatorShape: SliderComponentShape.noOverlay, + activeTickMarkColor: Colors.transparent, + inactiveTickMarkColor: Colors.transparent, + inactiveTrackColor: AGLDemoColors.periwinkleColor, + 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()); + }, + ), + ), + ); } } diff --git a/lib/presentation/screens/media/radio_preset_table.dart b/lib/presentation/screens/media/radio_preset_table.dart index 816bcb9..97affb8 100644 --- a/lib/presentation/screens/media/radio_preset_table.dart +++ b/lib/presentation/screens/media/radio_preset_table.dart @@ -38,6 +38,8 @@ class _RadioPresetTableState extends ConsumerState<RadioPresetTable> { @override Widget build(BuildContext context) { + final controller = ScrollController(); + return Material( color: Colors.transparent, child: Column( @@ -72,72 +74,112 @@ class _RadioPresetTableState extends ConsumerState<RadioPresetTable> { ))) ], ), - 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]), - )) - ], + Padding( + padding: const EdgeInsets.only(right: 12), + child: SizedBox( + height: 500, + child: RawScrollbar( + controller: controller, + thickness: 32, + thumbVisibility: true, + radius: const Radius.circular(10), + thumbColor: AGLDemoColors.periwinkleColor, + minThumbLength: 60, + interactive: true, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + scrollbars: false, + overscroll: false, ), - ), - ), - ); - }).toList()), - ), - ), + child: CustomScrollView( + controller: controller, + physics: const ClampingScrollPhysics(), + slivers: <Widget>[ + SliverList.separated( + itemCount: presets.length, + itemBuilder: (_, int index) { + return Container( + height: 92, + margin: const EdgeInsets.only(right: 44), + decoration: BoxDecoration( + border: Border( + left: selectedPreset == + presets[index].name + ? const BorderSide( + color: Colors.white, + width: 4) + : BorderSide.none), + gradient: LinearGradient( + colors: selectedPreset == + presets[index].name + ? [ + AGLDemoColors.neonBlueColor, + AGLDemoColors.neonBlueColor + .withOpacity(0.15) + ] + : [ + Colors.black, + Colors.black + .withOpacity(0.20) + ])), + child: InkWell( + onTap: () { + ref + .read(radioClientProvider) + .setFrequency( + presets[index].frequency); + setState(() { + selectedPreset = presets[index].name; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 17, horizontal: 24), + child: Row( + children: [ + Expanded( + flex: 6, + child: AutoSizeText( + presets[index].name, + maxLines: 1, + style: TextStyle( + color: Colors.white, + fontSize: 32, + shadows: [ + Helpers + .dropShadowRegular + ]), + )), + Expanded( + flex: 4, + child: Align( + alignment: + Alignment.centerRight, + child: Text( + frequencyToString( + presets[index] + .frequency), + style: TextStyle( + color: Colors.white, + fontSize: 24, + shadows: [ + Helpers + .dropShadowRegular + ]), + ))) + ], + ), + ), + ), + ); + }, + separatorBuilder: (_, __) { + return SizedBox(height: 8); + }, + ), + ])), + ), + )), ], )); } diff --git a/lib/presentation/screens/splash/widget/splash_content.dart b/lib/presentation/screens/splash/widget/splash_content.dart index 29d8d6c..51ee71f 100644 --- a/lib/presentation/screens/splash/widget/splash_content.dart +++ b/lib/presentation/screens/splash/widget/splash_content.dart @@ -68,6 +68,7 @@ class SplashContentState extends ConsumerState<SplashContent> void didChangeDependencies() { ref.read(valClientProvider).run(); ref.read(radioClientProvider).run(); + ref.read(mpdClientProvider).run(); super.didChangeDependencies(); } diff --git a/pubspec.lock b/pubspec.lock index f189947..f44a74e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" + dart_mpd: + dependency: "direct main" + description: + name: dart_mpd + sha256: dbdaef05e7cb1881911bf3a92563586e1661ce74c3c9e570645fdd75a1b8c2bc + url: "https://pub.dev" + source: hosted + version: "0.3.2" dbus: dependency: transitive description: @@ -133,10 +141,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "6.1.4" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8768c94..411e082 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,6 +55,7 @@ dependencies: auto_size_text: ^3.0.0 get_ip_address: ^0.0.6 flutter_calendar_carousel: ^2.4.2 + dart_mpd: ^0.3.2 dev_dependencies: flutter_test: |