summaryrefslogtreecommitdiffstats
path: root/lib/presentation/screens/media
diff options
context:
space:
mode:
authorScott Murray <scott.murray@konsulko.com>2023-12-31 16:24:51 -0500
committerScott Murray <scott.murray@konsulko.com>2024-01-03 18:23:52 -0500
commit4742fde5c48726357cc8db06d237e9db6c3df608 (patch)
treedcca2b3e3c6cb3a4a46b7ae603f64fa9ce5a086c /lib/presentation/screens/media
parentfcd868bd73d35bd79074f3425317152565aeb275 (diff)
Initial radio implementation
Notable changes: - Add radio gRPC API protobuf definitation and generated files. - Reworked existing single gRPC APIs library to split it into per-API libraries to avoid name collision issues. - Add radio gRPC client class and associated radio state class and RiverPod providers. - Split media controls and play list table classes into media player and radio specific versions to facilitate customization and wiring up their appropriate backends in a straightforward fashion. Some potential rationalization of styling widgets may be done as a follow up to avoid some duplication. - Added radio configuration and presets loading. The presets will be populated with the contents of a radio-presets.yaml file from the configured location, the default location is the /etc/xdg/AGL/ics-homescreen directory. - Implemented FM radio player against the radio gRPC API. For the sake of expediency, no attempt has been made to make the player able to handle AM band support. - Reworked media page navigation state so that active player is restored when coming back to the page. Logic has been added to start/stop the radio on navigating to or leaving the FM radio sub-page. This will potentially be reworked before CES to work with the pause/stop button present on the other pages. - Started pruning down global exports.dart a bit to remove files only used in a specific page/hierarchy, starting with media. Bug-AGL: SPEC-5029 Change-Id: I1ae0aca4a7a8218e69e4286c863f01509a1cccb7 Signed-off-by: Scott Murray <scott.murray@konsulko.com>
Diffstat (limited to 'lib/presentation/screens/media')
-rw-r--r--lib/presentation/screens/media/media.dart115
-rw-r--r--lib/presentation/screens/media/media_nav_notifier.dart18
-rw-r--r--lib/presentation/screens/media/media_player.dart79
-rw-r--r--lib/presentation/screens/media/media_player_controls.dart235
-rw-r--r--lib/presentation/screens/media/play_list_table.dart157
-rw-r--r--lib/presentation/screens/media/player_navigation.dart98
-rw-r--r--lib/presentation/screens/media/radio_player.dart81
-rw-r--r--lib/presentation/screens/media/radio_player_controls.dart251
-rw-r--r--lib/presentation/screens/media/radio_preset_table.dart151
-rw-r--r--lib/presentation/screens/media/segmented_buttons.dart87
-rw-r--r--lib/presentation/screens/media/widgets/gradient_progress_indicator.dart88
-rw-r--r--lib/presentation/screens/media/widgets/media_volume_bar.dart124
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,
+ ))),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}