diff options
author | Scott Murray <scott.murray@konsulko.com> | 2023-12-31 16:24:51 -0500 |
---|---|---|
committer | Scott Murray <scott.murray@konsulko.com> | 2024-01-03 18:23:52 -0500 |
commit | 4742fde5c48726357cc8db06d237e9db6c3df608 (patch) | |
tree | dcca2b3e3c6cb3a4a46b7ae603f64fa9ce5a086c /lib/presentation/screens/media | |
parent | fcd868bd73d35bd79074f3425317152565aeb275 (diff) |
Initial radio implementation
Notable changes:
- Add radio gRPC API protobuf definitation and generated files.
- Reworked existing single gRPC APIs library to split it into
per-API libraries to avoid name collision issues.
- Add radio gRPC client class and associated radio state class
and RiverPod providers.
- Split media controls and play list table classes into media
player and radio specific versions to facilitate customization
and wiring up their appropriate backends in a straightforward
fashion. Some potential rationalization of styling widgets
may be done as a follow up to avoid some duplication.
- Added radio configuration and presets loading. The presets
will be populated with the contents of a radio-presets.yaml
file from the configured location, the default location is
the /etc/xdg/AGL/ics-homescreen directory.
- Implemented FM radio player against the radio gRPC API.
For the sake of expediency, no attempt has been made to make
the player able to handle AM band support.
- Reworked media page navigation state so that active player is
restored when coming back to the page. Logic has been added to
start/stop the radio on navigating to or leaving the FM radio
sub-page. This will potentially be reworked before CES to work
with the pause/stop button present on the other pages.
- Started pruning down global exports.dart a bit to remove files
only used in a specific page/hierarchy, starting with media.
Bug-AGL: SPEC-5029
Change-Id: I1ae0aca4a7a8218e69e4286c863f01509a1cccb7
Signed-off-by: Scott Murray <scott.murray@konsulko.com>
Diffstat (limited to 'lib/presentation/screens/media')
12 files changed, 1484 insertions, 0 deletions
diff --git a/lib/presentation/screens/media/media.dart b/lib/presentation/screens/media/media.dart new file mode 100644 index 0000000..b7ce9e1 --- /dev/null +++ b/lib/presentation/screens/media/media.dart @@ -0,0 +1,115 @@ +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/presentation/screens/media/media_player.dart'; +import 'package:flutter_ics_homescreen/presentation/screens/media/radio_player.dart'; +import 'widgets/media_volume_bar.dart'; +import 'media_nav_notifier.dart'; +import 'player_navigation.dart'; + +class MediaPage extends StatelessWidget { + const MediaPage({super.key}); + + static Page<void> page() => const MaterialPage<void>(child: MediaPage()); + @override + Widget build(BuildContext context) { + Size size = MediaQuery.sizeOf(context); + + return Stack( + children: [ + // SizedBox( + // width: size.width, + // height: size.height, + // //color: Colors.black, + // // decoration: + // // BoxDecoration(gradient: AGLDemoColors.gradientBackgroundColor), + // child: SvgPicture.asset( + // 'assets/Media.svg', + // alignment: Alignment.center, + // fit: BoxFit.cover, + // //width: 200, + // //height: 200, + // ), + // ), + SizedBox( + width: size.width, + height: size.height, + // color: Colors.black, + child: SvgPicture.asset( + 'assets/MediaPlayerBackgroundTextures.svg', + // alignment: Alignment.center, + fit: BoxFit.cover, + //width: 200, + //height: 200, + ), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 50, horizontal: 50), + child: Media(), + ) + ], + ); + } +} + +class Media extends ConsumerStatefulWidget { + const Media({super.key}); + + @override + ConsumerState<Media> createState() => _MediaState(); +} + +class _MediaState extends ConsumerState<Media> { + //late MediaNavState selectedNav; + + //@override + //initState() { + // selectedNav = ref.read(mediaNavStateProvider); + // super.initState(); + //} + + onPressed(MediaNavState type) { + setState(() { + if (type == MediaNavState.fm) { + ref.read(mediaNavStateProvider.notifier).set(MediaNavState.fm); + ref.read(radioClientProvider).start(); + } else if (type == MediaNavState.media) { + ref.read(mediaNavStateProvider.notifier).set(MediaNavState.media); + ref.read(radioClientProvider).stop(); + } + }); + } + + @override + Widget build(BuildContext context) { + var navState = ref.watch(mediaNavStateProvider); + + return SingleChildScrollView( + child: Column( + children: [ + const SizedBox( + height: 55, + ), + PlayerNavigation( + onPressed: (val) { + onPressed(val); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 80), + child: SingleChildScrollView( + child: navState == MediaNavState.media + ? const MediaPlayer() + : navState == MediaNavState.fm + ? const RadioPlayer() + : 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_nav_notifier.dart b/lib/presentation/screens/media/media_nav_notifier.dart new file mode 100644 index 0000000..6f93850 --- /dev/null +++ b/lib/presentation/screens/media/media_nav_notifier.dart @@ -0,0 +1,18 @@ +import 'package:flutter_ics_homescreen/export.dart'; + +enum MediaNavState { media, fm, am, xm } + +class MediaNavStateNotifier extends Notifier<MediaNavState> { + @override + MediaNavState build() { + return MediaNavState.media; + } + + set(MediaNavState value) { + state = value; + } +} + +final mediaNavStateProvider = + NotifierProvider<MediaNavStateNotifier, MediaNavState>( + MediaNavStateNotifier.new); diff --git a/lib/presentation/screens/media/media_player.dart b/lib/presentation/screens/media/media_player.dart new file mode 100644 index 0000000..d7486c7 --- /dev/null +++ b/lib/presentation/screens/media/media_player.dart @@ -0,0 +1,79 @@ +import 'package:flutter_ics_homescreen/export.dart'; +import 'media_player_controls.dart'; +import 'play_list_table.dart'; +import 'segmented_buttons.dart'; + +class MediaPlayer extends StatefulWidget { + const MediaPlayer({super.key}); + + @override + State<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."; + + @override + Widget build(BuildContext context) { + double albumArtSize = 460; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + //const PlayerNavigation(), + SegmentedButtons( + navItems: navItems, + selectedNav: selectedNav, + ), + const SizedBox( + height: 32, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + "assets/AlbumArtMedia.png", + width: albumArtSize, + height: albumArtSize, + ) + ], + ), + const SizedBox( + height: 40, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + MediaPlayerControls( + songName: songName, + songLengthStart: "-1:23", + songLengthStop: "5:03"), + const SizedBox( + height: 72, + ), + PlayListTable( + playList: playList, + selectedPlayListSongName: selectedPlayListSongName, + tableName: tableName, + type: "media", + ), + ], + ) + ], + ); + } +} diff --git a/lib/presentation/screens/media/media_player_controls.dart b/lib/presentation/screens/media/media_player_controls.dart new file mode 100644 index 0000000..518b669 --- /dev/null +++ b/lib/presentation/screens/media/media_player_controls.dart @@ -0,0 +1,235 @@ +import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/presentation/screens/media/widgets/gradient_progress_indicator.dart'; + +class MediaPlayerControls extends StatefulWidget { + const MediaPlayerControls( + {super.key, + required this.songName, + required this.songLengthStart, + required this.songLengthStop}); + + final String songName; + final String songLengthStart; + final String songLengthStop; + + @override + State<MediaPlayerControls> createState() => _MediaPlayerControlsState(); +} + +class _MediaPlayerControlsState extends State<MediaPlayerControls> { + late String songName; + late String songLengthStart; + late String songLengthStop; + final String albumName = "Gorillaz"; + + int songProgress = 20; + + @override + void initState() { + songName = widget.songName; + songLengthStart = widget.songLengthStart; + songLengthStop = widget.songLengthStop; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + Text( + songName, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + shadows: [Helpers.dropShadowRegular], + fontSize: 44), + ), + MediaPlayerControlsubDetails( + albumName: albumName, + ), + Column(children: [ + GradientProgressIndicator( + percent: songProgress, + type: "media", + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + AGLDemoColors.jordyBlueColor, + AGLDemoColors.jordyBlueColor.withOpacity(0.8), + ]), + backgroundColor: AGLDemoColors.gradientBackgroundDarkColor, + ), + // const LinearProgressIndicator( + // backgroundColor: AGLDemoColors.gradientBackgroundDarkColor, + // color: Colors.white70, + // minHeight: 8, + // value: 0.7, + // ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + songLengthStart, + style: TextStyle( + color: Colors.white, + fontSize: 26, + shadows: [Helpers.dropShadowRegular]), + ), + Text( + songLengthStop, + style: TextStyle( + color: Colors.white, + fontSize: 26, + shadows: [Helpers.dropShadowRegular]), + ) + ], + ), + ), + ]), + const MediaPlayerActions(), + ]), + ); + } +} + +class MediaPlayerControlsubDetails extends StatefulWidget { + const MediaPlayerControlsubDetails({super.key, required this.albumName}); + final String albumName; + + @override + State<MediaPlayerControlsubDetails> createState() => + _MediaPlayerControlsubDetailsState(); +} + +class _MediaPlayerControlsubDetailsState + extends State<MediaPlayerControlsubDetails> { + bool isShuffleEnabled = false; + bool isRepeatEnabled = false; + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.albumName, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 40, + shadows: [Helpers.dropShadowRegular]), + ), + Row( + children: [ + InkWell( + customBorder: const CircleBorder(), + onTap: () { + setState(() { + isShuffleEnabled = !isShuffleEnabled; + }); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/${isShuffleEnabled ? "ShufflePressed.svg" : "Shuffle.svg"}", + width: 48, + ))), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + setState(() { + isRepeatEnabled = !isRepeatEnabled; + }); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/${isRepeatEnabled ? "RepeatPressed.svg" : "Repeat.svg"}", + width: 48, + ))), + ], + ) + ], + ); + } +} + +class MediaPlayerActions extends StatefulWidget { + const MediaPlayerActions({super.key}); + + @override + State<MediaPlayerActions> createState() => _MediaPlayerActionsState(); +} + +class _MediaPlayerActionsState extends State<MediaPlayerActions> { + bool isPressed = false; + bool isPlaying = true; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + InkWell( + customBorder: const CircleBorder(), + onTap: () {}, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/SkipPrevious.svg", + width: 48, + ), + )), + const SizedBox( + width: 120, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + setState(() { + isPlaying = !isPlaying; + }); + }, + onTapDown: (details) { + setState(() { + isPressed = true; + }); + }, + onTapUp: (details) { + isPressed = false; + }, + child: Container( + width: 64, + height: 64, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + isPressed ? Colors.white : AGLDemoColors.periwinkleColor, + boxShadow: [Helpers.boxDropShadowRegular]), + child: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + color: AGLDemoColors.resolutionBlueColor, + size: 60, + ), + )), + const SizedBox( + width: 120, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () {}, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/SkipNext.svg", + width: 48, + ), + )), + ], + ); + } +} diff --git a/lib/presentation/screens/media/play_list_table.dart b/lib/presentation/screens/media/play_list_table.dart new file mode 100644 index 0000000..369bb9c --- /dev/null +++ b/lib/presentation/screens/media/play_list_table.dart @@ -0,0 +1,157 @@ +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; + + @override + State<PlayListTable> createState() => _PlayListTableState(); +} + +class _PlayListTableState extends State<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 + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + tableName, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 40), + ), + 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: () { + setState(() { + isAudioSettingsEnabled = !isAudioSettingsEnabled; + }); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/${isAudioSettingsEnabled ? "AudioSettingsPressed.svg" : "AudioSettings.svg"}", + width: 48, + ))) + ], + ), + SizedBox( + height: 325, + child: SingleChildScrollView( + child: Column( + children: 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()), + ), + ), + ], + )); + } +} + +class PlayListModel { + final String songName; + final String albumName; + + PlayListModel({required this.songName, required this.albumName}); +} diff --git a/lib/presentation/screens/media/player_navigation.dart b/lib/presentation/screens/media/player_navigation.dart new file mode 100644 index 0000000..70a9906 --- /dev/null +++ b/lib/presentation/screens/media/player_navigation.dart @@ -0,0 +1,98 @@ +import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'media_nav_notifier.dart'; + +class PlayerNavigation extends ConsumerStatefulWidget { + const PlayerNavigation({super.key, required this.onPressed}); + final Function onPressed; + + @override + ConsumerState<PlayerNavigation> createState() => _PlayerNavigationState(); +} + +class _PlayerNavigationState extends ConsumerState<PlayerNavigation> { + List<String> navItems = ["My Media", "FM", "AM", "XM"]; + Map<MediaNavState, String> navStateMap = { + MediaNavState.media: "My Media", + MediaNavState.fm: "FM", + MediaNavState.am: "AM", + MediaNavState.xm: "XM" + }; + //String selectedNav = "My Media"; + + @override + Widget build(BuildContext context) { + var navState = ref.watch(mediaNavStateProvider); + var selectedNav = navStateMap[navState]; + + return Row( + children: navItems + .map((e) => Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + gradient: LinearGradient(colors: [ + selectedNav == e + ? AGLDemoColors.neonBlueColor + : AGLDemoColors.buttonFillEnabledColor, + AGLDemoColors.gradientBackgroundDarkColor + ], begin: Alignment.topCenter, end: Alignment.bottomCenter), + // color: selectedNav == e + // ? AGLDemoColors.neonBlueColor + // : AGLDemoColors.buttonFillEnabledColor, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setState(() { + if (e == "My Media" || e == "FM") { + selectedNav = e; + } + }); + if (e == "My Media" || e == "FM") { + for (MapEntry<MediaNavState, String> me + in navStateMap.entries) { + if (me.value == e) widget.onPressed(me.key); + } + } + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 7), + decoration: BoxDecoration( + border: Border( + left: selectedNav == e + ? const BorderSide(color: Colors.white12) + : BorderSide.none, + right: selectedNav == e + ? const BorderSide(color: Colors.white12) + : BorderSide.none, + top: BorderSide( + color: selectedNav == e + ? Colors.white + : Colors.white24, + width: selectedNav == e ? 2 : 1))), + child: Text( + e, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 26, + shadows: [ + selectedNav == e + ? Helpers.dropShadowRegular + : Helpers.dropShadowBig + ], + color: selectedNav == e + ? Colors.white + : AGLDemoColors.periwinkleColor, + fontWeight: selectedNav == e + ? FontWeight.w700 + : FontWeight.w500), + ), + ), + ), + ), + ))) + .toList()); + } +} diff --git a/lib/presentation/screens/media/radio_player.dart b/lib/presentation/screens/media/radio_player.dart new file mode 100644 index 0000000..4531c7b --- /dev/null +++ b/lib/presentation/screens/media/radio_player.dart @@ -0,0 +1,81 @@ +import 'package:flutter_ics_homescreen/data/data_providers/radio_presets_provider.dart'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'radio_player_controls.dart'; +import 'radio_preset_table.dart'; +import 'segmented_buttons.dart'; + +class RadioPlayer extends ConsumerStatefulWidget { + const RadioPlayer({super.key}); + + @override + ConsumerState<RadioPlayer> createState() => _RadioPlayerState(); +} + +class _RadioPlayerState extends ConsumerState<RadioPlayer> { + String selectedNav = "Standard"; + List<String> navItems = [ + "Standard", + "HD", + ]; + String tableName = "Presets"; + late List<RadioPreset> presets; + late String selectedPreset; + + @override + void initState() { + presets = ref.read(radioPresetsProvider).fmPresets; + if (presets.isNotEmpty) { + selectedPreset = presets.first.name; + } else { + selectedPreset = ""; + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + double fmSignalHeight = 460; + double fmSignalWidth = 460; + + return Container( + padding: const EdgeInsets.only(left: 7, right: 7), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SegmentedButtons( + navItems: navItems, + selectedNav: selectedNav, + ), + const SizedBox( + height: 32, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + "assets/AlbumArtFM.png", + width: fmSignalWidth, + height: fmSignalHeight, + ) + ], + ), + const SizedBox( + height: 40, + ), + Column( + children: [ + const RadioPlayerControls(), + const SizedBox( + height: 70, + ), + RadioPresetTable( + presets: presets, + selectedPreset: selectedPreset, + tableName: tableName), + ], + ) + ], + ), + ); + } +} diff --git a/lib/presentation/screens/media/radio_player_controls.dart b/lib/presentation/screens/media/radio_player_controls.dart new file mode 100644 index 0000000..bfa8da6 --- /dev/null +++ b/lib/presentation/screens/media/radio_player_controls.dart @@ -0,0 +1,251 @@ +import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/presentation/screens/settings/settings_screens/audio_settings/widget/slider_widgets.dart'; + +class RadioPlayerControls extends ConsumerWidget { + const RadioPlayerControls({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + var freqCurrent = + ref.watch(radioStateProvider.select((radio) => radio.freqCurrent)); + String currentString = (freqCurrent / 1000000.0).toStringAsFixed(1); + + return Material( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + currentString, + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + shadows: [Helpers.dropShadowRegular], + fontSize: 44), + ), + const RadioPlayerControlsSubDetails(), + const RadioPlayerControlsSlider(), + ], + ), + ); + } +} + +class RadioPlayerControlsSubDetails extends ConsumerWidget { + const RadioPlayerControlsSubDetails({super.key}); + + onPressed({required WidgetRef ref, required String type}) { + if (type == "tuneLeft") { + ref.read(radioClientProvider).tuneBackward(); + } else if (type == "tuneRight") { + ref.read(radioClientProvider).tuneForward(); + } else if (type == "scanLeft") { + bool playing = + ref.read(radioStateProvider.select((radio) => radio.playing)); + if (playing) { + ref.read(radioClientProvider).scanBackward(); + } + } else if (type == "scanRight") { + bool playing = + ref.read(radioStateProvider.select((radio) => radio.playing)); + if (playing) { + ref.read(radioClientProvider).scanForward(); + } + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + "Tune", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 40, + shadows: [Helpers.dropShadowRegular]), + ), + const SizedBox( + width: 25, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + onPressed(ref: ref, type: "tuneLeft"); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back, + size: 48, + color: AGLDemoColors.periwinkleColor, + ))), + const SizedBox( + width: 25, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + onPressed(ref: ref, type: "tuneRight"); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_forward, + color: AGLDemoColors.periwinkleColor, + size: 48, + ))), + ], + ), + Row( + children: [ + Text( + "Scan", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 40, + shadows: [Helpers.dropShadowRegular]), + ), + const SizedBox( + width: 25, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + onPressed(ref: ref, type: "scanLeft"); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_back, + color: AGLDemoColors.periwinkleColor, + size: 48, + ))), + const SizedBox( + width: 25, + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + onPressed(ref: ref, type: "scanRight"); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + Icons.arrow_forward, + color: AGLDemoColors.periwinkleColor, + size: 48, + ))), + ], + ) + ], + ), + ); + } +} + +class RadioPlayerControlsSlider extends ConsumerStatefulWidget { + const RadioPlayerControlsSlider({super.key}); + + @override + ConsumerState<RadioPlayerControlsSlider> createState() => + RadioPlayerControlsSliderState(); +} + +class RadioPlayerControlsSliderState + extends ConsumerState<RadioPlayerControlsSlider> { + @override + Widget build(BuildContext context) { + var freqMin = + ref.watch(radioStateProvider.select((radio) => radio.freqMin)); + var freqMax = + ref.watch(radioStateProvider.select((radio) => radio.freqMax)); + var freqStep = + ref.watch(radioStateProvider.select((radio) => radio.freqStep)); + var currentFreq = + ref.watch(radioStateProvider.select((radio) => radio.freqCurrent)) / + 1000000.0; + + String minString = (freqMin / 1000000.0).toStringAsFixed(1); + String maxString = (freqMax / 1000000.0).toStringAsFixed(1); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 64), + child: Container( + decoration: const ShapeDecoration( + color: AGLDemoColors.buttonFillEnabledColor, + shape: StadiumBorder( + side: BorderSide( + color: Color(0xFF5477D4), + width: 0.5, + )), + ), + height: 160, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + minString, + style: TextStyle( + color: Colors.white, + fontSize: 32, + shadows: [Helpers.dropShadowRegular]), + )), + Expanded( + child: SliderTheme( + data: SliderThemeData( + overlayShape: SliderComponentShape.noOverlay, + valueIndicatorShape: SliderComponentShape.noOverlay, + activeTickMarkColor: Colors.transparent, + inactiveTickMarkColor: Colors.transparent, + inactiveTrackColor: AGLDemoColors.backgroundInsetColor, + thumbShape: const PolygonSliderThumb( + sliderValue: 3, thumbRadius: 23), + //trackHeight: 5, + ), + child: Slider( + divisions: (freqMax - freqMin) ~/ freqStep, + min: freqMin / 1000000.0, + max: freqMax / 1000000.0, + value: currentFreq, + onChangeStart: (double value) { + ref.read(radioClientProvider).scanStop(); + }, + onChanged: (double value) { + setState(() { + ref + .read(radioStateProvider.notifier) + .updateFrequency((value * 1000000.0).toInt()); + }); + }, + onChangeEnd: (double value) { + ref + .read(radioStateProvider.notifier) + .setFrequency((value * 1000000.0).toInt()); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + maxString, + style: TextStyle( + color: Colors.white, + fontSize: 32, + shadows: [Helpers.dropShadowRegular]), + )), + ], + ), + )); + } +} diff --git a/lib/presentation/screens/media/radio_preset_table.dart b/lib/presentation/screens/media/radio_preset_table.dart new file mode 100644 index 0000000..816bcb9 --- /dev/null +++ b/lib/presentation/screens/media/radio_preset_table.dart @@ -0,0 +1,151 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; +import 'package:flutter_ics_homescreen/export.dart'; +import 'package:flutter_ics_homescreen/data/data_providers/radio_presets_provider.dart'; + +class RadioPresetTable extends ConsumerStatefulWidget { + const RadioPresetTable( + {super.key, + required this.tableName, + required this.presets, + required this.selectedPreset}); + + final String tableName; + final List<RadioPreset> presets; + final String selectedPreset; + + @override + ConsumerState<RadioPresetTable> createState() => _RadioPresetTableState(); +} + +class _RadioPresetTableState extends ConsumerState<RadioPresetTable> { + bool isAudioSettingsEnabled = false; + late String tableName; + late List<RadioPreset> presets; + late String selectedPreset; + + @override + void initState() { + tableName = widget.tableName; + presets = widget.presets; + selectedPreset = widget.selectedPreset; + super.initState(); + } + + String frequencyToString(int frequency) { + return "${(frequency / 1000000.0).toStringAsFixed(1)} MHz"; + } + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + tableName, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w400, + fontSize: 40), + ), + ], + ), + InkWell( + customBorder: const CircleBorder(), + onTap: () { + setState(() { + isAudioSettingsEnabled = !isAudioSettingsEnabled; + }); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset( + "assets/${isAudioSettingsEnabled ? "AudioSettingsPressed.svg" : "AudioSettings.svg"}", + width: 48, + ))) + ], + ), + SizedBox( + height: 325, + child: SingleChildScrollView( + child: Column( + children: presets.map((index) { + return Container( + height: 100, + margin: const EdgeInsets.symmetric(vertical: 4), + decoration: BoxDecoration( + border: Border( + left: selectedPreset == index.name + ? const BorderSide( + color: Colors.white, width: 4) + : BorderSide.none), + gradient: LinearGradient( + colors: selectedPreset == index.name + ? [ + AGLDemoColors.neonBlueColor, + AGLDemoColors.neonBlueColor + .withOpacity(0.15) + ] + : [ + Colors.black, + Colors.black.withOpacity(0.20) + ])), + child: InkWell( + onTap: () { + ref + .read(radioClientProvider) + .setFrequency(index.frequency); + setState(() { + selectedPreset = index.name; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 17, horizontal: 24), + child: Row( + children: [ + Expanded( + flex: 6, + child: AutoSizeText( + index.name, + maxLines: 1, + style: TextStyle( + color: Colors.white, + fontSize: 40, + shadows: [Helpers.dropShadowRegular]), + )), + Expanded( + flex: 4, + child: Text( + frequencyToString(index.frequency), + style: TextStyle( + color: Colors.white, + fontSize: 26, + shadows: [Helpers.dropShadowRegular]), + )) + ], + ), + ), + ), + ); + }).toList()), + ), + ), + ], + )); + } +} + +class PlayListModel { + final String songName; + final String albumName; + + PlayListModel({required this.songName, required this.albumName}); +} diff --git a/lib/presentation/screens/media/segmented_buttons.dart b/lib/presentation/screens/media/segmented_buttons.dart new file mode 100644 index 0000000..5cc1d87 --- /dev/null +++ b/lib/presentation/screens/media/segmented_buttons.dart @@ -0,0 +1,87 @@ +import 'package:flutter_ics_homescreen/export.dart'; + +class SegmentedButtons extends StatefulWidget { + const SegmentedButtons( + {super.key, required this.navItems, required this.selectedNav}); + + final List<String> navItems; + final String selectedNav; + @override + State<SegmentedButtons> createState() => _SegmentedButtonsState(); +} + +class _SegmentedButtonsState extends State<SegmentedButtons> { + late List<String> navItems; + late String selectedNav; + + @override + void initState() { + navItems = widget.navItems; + selectedNav = widget.selectedNav; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: const EdgeInsets.only(top: 40), + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + offset: const Offset(0, 4), + blurRadius: 4, + color: Colors.black.withOpacity(0.25)) + ], + borderRadius: BorderRadius.circular(40), + color: AGLDemoColors.buttonFillEnabledColor, + border: Border.all(color: Colors.white12), + ), + child: Row( + children: navItems + .map((e) => Container( + decoration: BoxDecoration( + borderRadius: selectedNav == e + ? BorderRadius.circular(40) + : BorderRadius.zero, + color: selectedNav == e + ? AGLDemoColors.backgroundInsetColor + : null, + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(40), + onTap: () { + setState(() { + selectedNav = e; + }); + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 24, horizontal: 32), + child: Text( + e, + style: TextStyle( + color: selectedNav == e + ? Colors.white + : AGLDemoColors.periwinkleColor, + fontSize: 26, + fontWeight: selectedNav == e + ? FontWeight.w700 + : FontWeight.w500), + ), + ), + ), + ), + )) + .toList(), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/screens/media/widgets/gradient_progress_indicator.dart b/lib/presentation/screens/media/widgets/gradient_progress_indicator.dart new file mode 100644 index 0000000..24aa244 --- /dev/null +++ b/lib/presentation/screens/media/widgets/gradient_progress_indicator.dart @@ -0,0 +1,88 @@ +import 'package:flutter_ics_homescreen/core/utils/helpers.dart'; +import 'package:flutter_ics_homescreen/export.dart'; + +class GradientProgressIndicator extends StatelessWidget { + ///it can be anything between 0 to 100 + final int percent; + final Gradient gradient; + final Color backgroundColor; + final double height; + final String type; + + const GradientProgressIndicator( + {required this.percent, + required this.gradient, + required this.backgroundColor, + Key? key, + this.height = 16, + required this.type}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Flexible( + flex: percent, + fit: FlexFit.tight, + child: Container( + height: height, + margin: const EdgeInsets.all(1), + decoration: BoxDecoration( + border: Border.all( + color: AGLDemoColors.neonBlueColor.withOpacity(0.5), + width: 1), + gradient: gradient, + borderRadius: + BorderRadius.all(Radius.circular(type == "fm" ? 16 : 2)), + ), + alignment: Alignment.centerRight, + ), + ), + type == "media" + ? Container( + height: height, + width: 2, + color: Colors.white, + ) + : Container( + height: 64, + width: 64, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + Helpers.boxDropShadowRegular, + ], + color: AGLDemoColors.periwinkleColor), + child: Container( + height: 32, + width: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + boxShadow: [ + Helpers.boxDropShadowRegular, + ], + border: Border.all( + color: AGLDemoColors.neonBlueColor, width: 2), + color: AGLDemoColors.periwinkleColor), + ), + ), + Flexible( + fit: FlexFit.tight, + flex: 100 - percent, + child: Container( + decoration: BoxDecoration( + color: backgroundColor, + border: Border.all( + color: AGLDemoColors.neonBlueColor.withOpacity(0.5), + width: 1), + borderRadius: const BorderRadius.all(Radius.circular(2)), + ), + child: SizedBox(height: height), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/screens/media/widgets/media_volume_bar.dart b/lib/presentation/screens/media/widgets/media_volume_bar.dart new file mode 100644 index 0000000..bd3a4f1 --- /dev/null +++ b/lib/presentation/screens/media/widgets/media_volume_bar.dart @@ -0,0 +1,124 @@ +import 'package:flutter_ics_homescreen/presentation/custom_icons/custom_icons.dart'; + +import '../../../../export.dart'; +import '../../settings/settings_screens/audio_settings/widget/slider_widgets.dart'; + +class CustomVolumeSlider extends ConsumerStatefulWidget { + const CustomVolumeSlider({ + super.key, + }); + + @override + CustomVolumeSliderState createState() => CustomVolumeSliderState(); +} + +class CustomVolumeSliderState extends ConsumerState<CustomVolumeSlider> { + void _increase() { + _currentVal += 10; + if (_currentVal > 100) { + _currentVal = 100; + } + setState(() { + ref.read(audioStateProvider.notifier).setVolume(_currentVal); + }); + } + + void _decrease() { + _currentVal -= 10; + if (_currentVal < 0) { + _currentVal = 0; + } + setState(() { + ref.read(audioStateProvider.notifier).setVolume(_currentVal); + }); + } + + double _currentVal = 50; + + @override + Widget build(BuildContext context) { + final volumeValue = + ref.watch(audioStateProvider.select((audio) => audio.volume)); + + return Column( + //crossAxisAlignment: CrossAxisAlignment.center, + children: [ + 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.only(left: 20), + child: Material( + color: Colors.transparent, + child: InkWell( + customBorder: const CircleBorder(), + onTap: () { + _decrease(); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + CustomIcons.vol_min, + color: AGLDemoColors.periwinkleColor, + size: 60, + ))), + ), + ), + 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: 10, + min: 0, + max: 100, + value: volumeValue.toDouble(), + onChanged: (newValue) { + ref.read(audioStateProvider.notifier).setVolume(newValue); + _currentVal = newValue; + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(right: 20), + child: Material( + color: Colors.transparent, + child: InkWell( + customBorder: const CircleBorder(), + onTap: () { + _increase(); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon( + CustomIcons.vol_max, + color: AGLDemoColors.periwinkleColor, + size: 60, + ))), + ), + ), + ], + ), + ), + ], + ); + } +} |