diff --git a/Radio.sln b/Radio.sln
new file mode 100644
index 0000000..0f231ab
--- /dev/null
+++ b/Radio.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35327.3
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Radio", "Radio\Radio.csproj", "{9546A907-D0F0-4D80-88E1-B1EE743C0A2B}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {9546A907-D0F0-4D80-88E1-B1EE743C0A2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9546A907-D0F0-4D80-88E1-B1EE743C0A2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9546A907-D0F0-4D80-88E1-B1EE743C0A2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9546A907-D0F0-4D80-88E1-B1EE743C0A2B}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {EB6325A6-44A3-4FC1-A484-432BEFEABE13}
+ EndGlobalSection
+EndGlobal
diff --git a/Radio/FodyWeavers.xml b/Radio/FodyWeavers.xml
new file mode 100644
index 0000000..8b8d956
--- /dev/null
+++ b/Radio/FodyWeavers.xml
@@ -0,0 +1,7 @@
+
+
+
+ LibVLCSharp
+
+
+
\ No newline at end of file
diff --git a/Radio/IconPaths.cs b/Radio/IconPaths.cs
new file mode 100644
index 0000000..8d4abbd
--- /dev/null
+++ b/Radio/IconPaths.cs
@@ -0,0 +1,29 @@
+using System.Windows.Media;
+
+namespace Radio
+{
+ public static class IconPaths
+ {
+ private static readonly string PlayPath = "M 6.1057828,63.999624 A 6.1512566,6.1512566 0 0 1 3.0623268,63.187481 C 0.97539178,62.004946 -0.32200322,59.70923 -0.32200322,57.217092 V 6.7826921 c 0,-2.499092 1.297432,-4.787778 3.38433002,-5.97038902 a 6.1095183,6.1095183 0 0 1 6.22082,0.07861 L 52.387174,26.692465 a 6.2608214,6.2608214 0 0 1 0,10.608612 L 9.2761928,63.109548 a 6.1738653,6.1738653 0 0 1 -3.17041,0.890454 z";
+ private static readonly string PausePath = "M 7.6906505,2.9120095 C 7.1110983,4.0497216 7.1110983,5.5431249 7.1110983,8.5333333 V 55.466629 c 0,2.986655 0,4.48002 0.5795522,5.621323 a 5.3333268,5.3333268 0 0 0 2.3324575,2.328905 C 11.160858,64 12.654223,64 15.644432,64 h 0.711117 c 2.986655,0 4.479983,0 5.621324,-0.57959 a 5.3333268,5.3333268 0 0 0 2.332419,-2.33242 c 0.57959,-1.137712 0.57959,-2.631116 0.57959,-5.621324 V 8.533371 c 0,-2.9866552 0,-4.4800206 -0.57959,-5.6213237 A 5.3333268,5.3333268 0 0 0 21.976873,0.57958976 C 20.839085,-1.875e-7 19.345757,-1.875e-7 16.355549,-1.875e-7 h -0.711117 c -2.986656,0 -4.480021,0 -5.621324,0.5795899475 A 5.3333268,5.3333268 0 0 0 7.6906505,2.9120095 Z m 31.9999825,0 c -0.57959,1.1377121 -0.57959,2.6311154 -0.57959,5.6213238 V 55.466629 c 0,2.986655 0,4.48002 0.583142,5.621323 a 5.3333268,5.3333268 0 0 0 2.328905,2.328905 C 43.160802,64 44.654168,64 47.644414,64 h 0.711155 c 2.986655,0 4.479983,0 5.621323,-0.57959 a 5.3333268,5.3333268 0 0 0 2.328867,-2.33242 c 0.583143,-1.137712 0.583143,-2.631116 0.583143,-5.621324 V 8.533371 c 0,-2.9866552 0,-4.4800206 -0.57959,-5.6213237 A 5.3333268,5.3333268 0 0 0 53.976892,0.57958976 C 52.839029,3.7607738e-5 51.345777,3.7607738e-5 48.355569,3.7607738e-5 h -0.711155 c -2.986656,0 -4.479983,0 -5.621324,0.579552152262 A 5.3333268,5.3333268 0 0 0 39.694185,2.9120473 Z";
+ private static readonly string StopPath = "M -4.6743125e-7,55.322732 C -4.6743125e-7,60.780371 3.2982428,64 8.8344619,64 H 55.165575 c 5.536215,0 8.834458,-3.219629 8.834458,-8.677268 V 8.6773065 C 64.000033,3.2196673 60.70179,4.6743145e-7 55.165575,4.6743145e-7 H 8.8344619 C 3.2982466,4.6743145e-7 -4.6743125e-7,3.2197051 -4.6743125e-7,8.6773065 Z";
+ private static readonly string ForwardPath = "M 56.00125,-3.1715222e-5 A 2.6668035,2.6668035 0 0 0 53.33443,2.6667883 V 25.406273 L 14.537441,2.1851013 A 5.8553003,5.8553003 0 0 0 8.575459,2.1121513 C 6.575344,3.2450853 5.331967,5.4384953 5.331967,7.8336293 V 56.169463 c 0,2.395096 1.243339,4.588544 3.243492,5.721931 a 5.8569672,5.8569672 0 0 0 5.961982,-0.07559 L 53.33443,38.59701 v 22.739484 a 2.6668035,2.6668035 0 0 0 5.333603,0 V 2.6667883 A 2.6668035,2.6668035 0 0 0 56.00125,-3.1715222e-5 Z";
+ private static readonly string BackwardPath = "M 7.9987735,-1.374354e-4 A 2.666805,2.666805 0 0 1 10.665558,2.6666466 V 25.406183 L 49.462569,2.1849586 a 5.8553038,5.8553038 0 0 1 5.961985,-0.07295 c 2.000116,1.132935 3.243494,3.333074 3.243494,5.721557 V 56.169391 c 0,2.395135 -1.24334,4.588585 -3.243494,5.721973 a 5.8569707,5.8569707 0 0 1 -5.961985,-0.0756 L 10.665558,38.59689 v 22.739498 a 2.666805,2.666805 0 0 1 -5.3336065,0 V 2.6666846 A 2.666805,2.666805 0 0 1 7.9987735,-9.9435399e-5 Z";
+ private static readonly string SoundHighPath = "M 54.24702,51.514546 49.249451,47.516368 C 60.389048,35.34333 60.389048,16.734448 49.249301,4.5614426 l 4.997527,-3.9979844 c 13.0043,14.5207958 13.004108,36.4302908 1.5e-4,50.9510878 z m -12.670155,-40.814983 -5.00343,4.00266 c 5.975084,6.391292 5.974892,16.282234 1.5e-4,22.673525 l 5.00343,4.00266 c 7.838187,-8.74158 7.838187,-21.937456 -1.5e-4,-30.678845 z M 28.79015,0.22367581 12.146035,13.32064 H 0 V 38.945255 H 12.164805 L 28.79015,51.776332 Z M 6.4061536,19.726793 h 7.9607674 l 8.017077,-6.309195 V 38.738786 L 14.348151,32.539102 H 6.4061536 Z";
+ private static readonly string SoundLowPath = "M 41.937839,41.511882 36.890992,37.474487 C 42.917536,31.027736 42.91773,21.050968 36.89084,14.604216 l 5.046805,-4.037426 c 7.906395,8.817243 7.906395,22.127624 1.51e-4,30.945092 z M 29.039974,0 12.251432,13.210612 H 0 v 25.84697 H 12.270364 L 29.039974,52 Z M 6.4617424,19.672354 h 8.0298466 l 8.086645,-6.363943 v 25.54091 L 14.472655,32.59584 H 6.4617424 Z";
+ private static readonly string SoundMutePath = "M 54.926931,26.181819 64,35.254888 l -4.593629,4.593629 -9.073069,-9.073069 -9.073069,9.073069 -4.593627,-4.593629 9.073069,-9.073069 -9.073069,-9.073069 4.593627,-4.593627 9.073069,9.073069 9.073069,-9.073069 L 64,17.10875 Z M 29.198769,0 12.318424,13.28285 H 0 V 39.271153 H 12.33746 L 29.198769,52.284342 Z M 6.4970761,19.779925 H 14.57083 l 8.130864,-6.398742 v 25.68057 L 14.551793,32.774078 H 6.4970761 Z";
+ private static readonly string NotaSinglePath = "M 101.33169,47.896905 v 26.66441 18.81441 A 13.333162,13.333162 0 0 0 93.334706,90.668559 13.333162,13.333162 0 0 0 80.001525,104.00174 13.333162,13.333162 0 0 0 93.334706,117.33492 13.333162,13.333162 0 0 0 106.66789,104.00174 V 71.893703 L 128,61.229599 Z";
+ private static readonly string NotaDualPath = "M 80.001037,0 21.331624,10.652384 V 26.77916 h -5e-4 V 61.334097 A 13.333162,13.333162 0 0 0 13.333181,58.667459 13.333162,13.333162 0 0 0 0,72.000641 13.333162,13.333162 0 0 0 13.333181,85.333822 13.333162,13.333162 0 0 0 26.666362,72.000641 a 13.333162,13.333162 0 0 0 -0.0015,-0.05957 V 31.172933 L 74.6648,22.457657 v 33.53926 c -2.228098,-1.673233 -4.9969,-2.66517 -7.997954,-2.66517 -7.363365,0 -13.334646,5.972744 -13.334646,13.336109 0,7.363365 5.971281,13.332206 13.334646,13.332206 7.365365,0 13.334156,-5.968841 13.334156,-13.332206 V 21.488859 Z";
+
+ public static Geometry Play() { return Geometry.Parse(PlayPath); }
+ public static Geometry Pause() { return Geometry.Parse(PausePath); }
+ public static Geometry Stop() { return Geometry.Parse(StopPath); }
+ public static Geometry Forward() { return Geometry.Parse(ForwardPath); }
+ public static Geometry Backward() { return Geometry.Parse(BackwardPath); }
+ public static Geometry SoundHigh() { return Geometry.Parse(SoundHighPath); }
+ public static Geometry SoundLow() { return Geometry.Parse(SoundLowPath); }
+ public static Geometry SoundMute() { return Geometry.Parse(SoundMutePath); }
+ public static Geometry NotaSingle() { return Geometry.Parse(NotaSinglePath); }
+ public static Geometry NotaDual() { return Geometry.Parse(NotaDualPath); }
+ }
+}
diff --git a/Radio/MainWindow.xaml b/Radio/MainWindow.xaml
new file mode 100644
index 0000000..e6419d4
--- /dev/null
+++ b/Radio/MainWindow.xaml
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Radio/MainWindow.xaml.cs b/Radio/MainWindow.xaml.cs
new file mode 100644
index 0000000..2a3354c
--- /dev/null
+++ b/Radio/MainWindow.xaml.cs
@@ -0,0 +1,399 @@
+using System.Collections.ObjectModel;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using System.Windows.Threading;
+using LibVLCSharp.Shared;
+using Newtonsoft.Json;
+using Widgets.Common;
+
+namespace Radio
+{
+ ///
+ /// Interaction logic for MainWindow.xaml
+ ///
+ public partial class MainWindow : Window, IWidgetWindow
+ {
+ public readonly static string WidgetName = "Radio";
+ public readonly static string SettingsFile = "settings.radio.json";
+ private readonly Config config = new(SettingsFile);
+ private RadioViewModel.SettingsStruct Settings = RadioViewModel.Default;
+ public RadioViewModel Radio { get; set; }
+ private DispatcherTimer? _timer;
+ private DoubleAnimation? MarqueeAnimation;
+
+ public MainWindow()
+ {
+ LoadSettings();
+
+ Radio = new RadioViewModel(Settings);
+ InitializeComponent();
+ DataContext = Radio;
+ }
+
+ // Window to WdigetWindow
+ public WidgetWindow WidgetWindow()
+ {
+ return new WidgetWindow(this, WidgetDefaultStruct());
+ }
+
+ // WidgetWindow default settings
+ public static WidgetDefaultStruct WidgetDefaultStruct()
+ {
+ return new()
+ {
+ Width = 400,
+ MinHeight = 200,
+ MaxHeight = 600,
+ SizeToContent = SizeToContent.Height
+ };
+ }
+
+ // config file load
+ private void LoadSettings()
+ {
+ try
+ {
+ Settings.FontSize = PropertyParser.ToFloat(config.GetValue("font_size"), Settings.FontSize);
+ Settings.PrimaryColor = PropertyParser.ToColorBrush(config.GetValue("primary_color"), Settings.PrimaryColor.ToString());
+ Settings.PrimaryColorLight = PropertyParser.ToColorBrush(config.GetValue("primary_color_light"), Settings.PrimaryColorLight.ToString());
+ Settings.SecondaryColor = PropertyParser.ToColorBrush(config.GetValue("secondary_color"), Settings.SecondaryColor.ToString());
+ Settings.SecondaryColorLight = PropertyParser.ToColorBrush(config.GetValue("secondary_color_light"), Settings.SecondaryColorLight.ToString());
+ Settings.TextColor = PropertyParser.ToColorBrush(config.GetValue("text_color"), Settings.TextColor.ToString());
+ Settings.Volume = PropertyParser.ToInt(config.GetValue("radio_volume"), Settings.Volume);
+ Settings.RadioIndex = PropertyParser.ToInt(config.GetValue("radio_index"), Settings.RadioIndex);
+ var stringJson = PropertyParser.ToString(config.GetValue("radio_list"));
+ Settings.RadioList = JsonConvert.DeserializeObject>(stringJson) ?? Settings.RadioList;
+ }
+ catch (Exception)
+ {
+ config.Add("font_size", Settings.FontSize);
+ config.Add("primary_color", Settings.PrimaryColor);
+ config.Add("primary_color_light", Settings.PrimaryColorLight);
+ config.Add("secondary_color", Settings.SecondaryColor);
+ config.Add("secondary_color_light", Settings.SecondaryColorLight);
+ config.Add("text_color", Settings.TextColor);
+ config.Add("radio_volume", Settings.Volume);
+ config.Add("radio_index", Settings.RadioIndex);
+ config.Add("radio_list", Settings.RadioList ?? []);
+
+ config.Save();
+ }
+ }
+
+
+ private void OrderedRadioListAndUpdateSettings()
+ {
+ var orderedRadioList = Radio.RadioList.OrderBy(item => item.Name).ToList();
+ var selectedRadio = Radio.SelectedRadio;
+ Radio.RadioList.Clear();
+
+ foreach (var radio in orderedRadioList)
+ {
+ Radio.RadioList.Add(radio);
+ }
+
+ Radio.SelectedRadio = selectedRadio;
+ RadioListBox.SelectedItem = Radio.SelectedRadio;
+ Settings.RadioList = Radio.RadioList;
+ Settings.RadioIndex = RadioListBox.SelectedIndex;
+
+ RadioListBox.ScrollIntoView(RadioListBox.SelectedItem);
+
+ config.Add("font_size", Settings.FontSize);
+ config.Add("primary_color", Settings.PrimaryColor);
+ config.Add("primary_color_light", Settings.PrimaryColorLight);
+ config.Add("secondary_color", Settings.SecondaryColor);
+ config.Add("secondary_color_light", Settings.SecondaryColorLight);
+ config.Add("text_color", Settings.TextColor);
+ config.Add("radio_volume", Settings.Volume);
+ config.Add("radio_index", Settings.RadioIndex);
+ config.Add("radio_list", Settings.RadioList ?? []);
+
+ config.Save();
+ }
+
+
+ // After content rendering
+ protected override void OnContentRendered(EventArgs e)
+ {
+ var mainWindowsResource = this.Resources.MergedDictionaries;
+ var styleResource = mainWindowsResource.FirstOrDefault(d => d.Source.OriginalString == "Style.xaml");
+
+ if (styleResource != null)
+ {
+ styleResource["PrimaryColor"] = Settings.PrimaryColor.Color;
+ styleResource["PrimaryColorLight"] = Settings.PrimaryColorLight.Color;
+ styleResource["SecondaryColor"] = Settings.SecondaryColor.Color;
+ styleResource["SecondaryColorLight"] = Settings.SecondaryColorLight.Color;
+ styleResource["TextColor"] = Settings.TextColor.Color;
+ styleResource["AnimateBrush_1"] = Settings.PrimaryColor;
+ styleResource["AnimateBrush_2"] = Settings.PrimaryColorLight;
+ }
+
+ Lazy_SelectionChanged();
+ RadioListBox.ScrollIntoView(RadioListBox.SelectedItem);
+ base.OnContentRendered(e);
+ }
+
+ // Add new radio with popup window
+ private void AddRadioWindow_Click(object sender, RoutedEventArgs e)
+ {
+ var popup = new PopupWindow
+ {
+ Owner = this,
+ Width = 300,
+ SizeToContent = SizeToContent.Height
+ };
+
+ if (popup.ShowDialog() == true && popup.NewRadio != null)
+ {
+ Radio.RadioList.Add(popup.NewRadio);
+ Radio.SelectedRadio = popup.NewRadio;
+ OrderedRadioListAndUpdateSettings();
+ }
+ }
+
+ // Delete radio with delete key
+ private void DeleteRadio(Radio? radioList = null)
+ {
+ radioList ??= Radio.SelectedRadio;
+
+ if (radioList != null)
+ {
+ MessageBoxResult messageBoxResult = MessageBox.Show(
+ $"Are you sure delete {radioList.Name} radio?",
+ "Delete Confirmation", MessageBoxButton.YesNo
+ );
+
+ if (messageBoxResult == MessageBoxResult.Yes)
+ {
+ var willdeleteIndex = RadioListBox.Items.IndexOf(radioList);
+ Radio.RadioList.Remove(radioList);
+ if(willdeleteIndex > 0)
+ {
+ RadioListBox.SelectedItem = RadioListBox.Items[willdeleteIndex-1];
+ Radio.SelectedRadio = (Radio)RadioListBox.SelectedItem;
+ }
+
+ OrderedRadioListAndUpdateSettings();
+ }
+ }
+ }
+
+ // RadioListbox item context menu
+ // Delete Radio
+ // Edit Radio
+ public void ListBox_PreviewMouseRightButtonUp(object sender, MouseButtonEventArgs e)
+ {
+ if (sender is ListBox listBox && e.OriginalSource is DependencyObject source)
+ {
+ var listBoxItem = ItemsControl.ContainerFromElement(listBox, source) as ListBoxItem;
+
+ if (listBoxItem?.DataContext is Radio radio)
+ {
+ ContextMenu contextMenu = new();
+ MenuItem option1 = new() { Header = "Edit", Icon = "🖉" };
+ option1.Click += (sender, e) =>
+ {
+ var popup = new PopupWindow(radio)
+ {
+ Owner = this,
+ Width = 300,
+ SizeToContent = SizeToContent.Height,
+ };
+
+ if (popup.ShowDialog() == true && popup.NewRadio != null)
+ {
+ if (Radio.RadioList.Contains(popup.NewRadio))
+ {
+ OrderedRadioListAndUpdateSettings();
+ }
+ }
+ };
+ MenuItem option2 = new() { Header = "Delete", Icon = "✗" };
+ option2.Click += (sender, e) =>
+ {
+ DeleteRadio(radio);
+ };
+ contextMenu.Items.Add(option1);
+ contextMenu.Items.Add(option2);
+
+ contextMenu.IsOpen = true;
+ }
+ }
+
+ e.Handled = true;
+ }
+
+ // When window closed
+ protected override void OnClosed(EventArgs e)
+ {
+ base.OnClosed(e);
+
+ Radio.Stop();
+ Radio.Dispose();
+
+ config.Add("radio_index", RadioListBox.SelectedIndex);
+ config.Add("radio_list", Settings.RadioList);
+ config.Save();
+
+ Logger.Info($"{WidgetName} is closed");
+ }
+
+ // Sortcut keys
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ base.OnKeyDown(e);
+
+ if (e.Key == Key.MediaPlayPause || e.Key == Key.Space || e.Key == Key.Enter)
+ {
+ PlayButton_Click(this, e);
+ }
+ else if (e.Key == Key.MediaNextTrack || e.Key == Key.Right || e.Key == Key.Down)
+ {
+ ForwardButton_Click(this, e);
+ }
+ else if (e.Key == Key.MediaPreviousTrack || e.Key == Key.Left || e.Key == Key.Up)
+ {
+ BackwardButton_Click(this, e);
+ }
+ else if (e.Key == Key.Delete)
+ {
+ DeleteRadio();
+ }
+
+ e.Handled = true;
+ }
+
+ // Animated text update for content length
+ private void ScrollingTextChanging(object? sender, SizeChangedEventArgs e)
+ {
+ if (Radio.PlayerState() == VLCState.Playing)
+ {
+ double windowWidth = ActualWidth;
+ ScrollingText.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
+ MarqueeAnimation = new DoubleAnimation
+ {
+ From = windowWidth,
+ To = ScrollingText.DesiredSize.Width * -1,
+ Duration = new Duration(TimeSpan.FromSeconds(10)),
+ RepeatBehavior = RepeatBehavior.Forever
+ };
+ TextTransform.BeginAnimation(TranslateTransform.XProperty, MarqueeAnimation);
+ }
+ }
+
+ // Play & Stop Button
+ private void PlayButton_Click(object sender, RoutedEventArgs e)
+ {
+ var storyboard = (Storyboard)FindResource("AnimateNotes");
+
+ if (Radio.PlayerState() != VLCState.Playing)
+ {
+ Radio.Play();
+ TextTransform.BeginAnimation(TranslateTransform.XProperty, MarqueeAnimation);
+ storyboard.Begin();
+ return;
+ }
+
+ if (Radio.PlayerState() != VLCState.Stopped)
+ {
+ Radio.Stop();
+ TextTransform.BeginAnimation(TranslateTransform.XProperty, null);
+ storyboard.Stop();
+ return;
+ }
+ }
+
+ // Backward Button
+ private void BackwardButton_Click(object sender, RoutedEventArgs e)
+ {
+ RadioListBox.SelectedIndex = Math.Max(RadioListBox.SelectedIndex - 1, 0);
+ RadioListBox.ScrollIntoView(RadioListBox.SelectedItem);
+ }
+
+ // Forward Button
+ private void ForwardButton_Click(object sender, RoutedEventArgs e)
+ {
+ RadioListBox.SelectedIndex = Math.Min(RadioListBox.SelectedIndex + 1, RadioListBox.Items.Count);
+ RadioListBox.ScrollIntoView(RadioListBox.SelectedItem);
+ }
+
+ // Delay selected radio changing
+ private void Lazy_SelectionChanged()
+ {
+ _timer = new DispatcherTimer
+ {
+ Interval = TimeSpan.FromSeconds(1)
+ };
+ _timer.Tick += Selection_Timer_Tick;
+ }
+
+ // Delay timer tick
+ private async void Selection_Timer_Tick(object? sender, EventArgs e)
+ {
+ _timer?.Stop();
+
+ if (Radio.PlayerState() == VLCState.Playing)
+ {
+ Radio.Stop();
+ await Task.Delay(500);
+ Radio.Play();
+ }
+ }
+
+ // Radio Listbox selected chaned
+ private void RadioListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ _timer?.Stop();
+ ListBoxItem selectedItem = (ListBoxItem)RadioListBox.ItemContainerGenerator.ContainerFromItem(RadioListBox.SelectedItem);
+
+ if (selectedItem == null) return;
+
+ BackwardButton.IsEnabled = !(RadioListBox.SelectedIndex <= 0);
+ ForwardButton.IsEnabled = !(RadioListBox.SelectedIndex >= RadioListBox.Items.Count - 1);
+
+ _timer?.Start();
+ }
+
+ // When Mohusewhell event only 1 item change
+ private void RadioListScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
+ {
+ var listBox = sender as ListBox;
+ var scrollViewer = FindScrollViewer(listBox);
+
+ if (listBox != null && scrollViewer != null)
+ {
+ var scrollAmount = 1;
+
+ if (e.Delta > 0)
+ {
+ scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - Math.Abs(scrollAmount));
+ }
+ else
+ {
+ scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + Math.Abs(scrollAmount));
+ }
+ e.Handled = true;
+ }
+ }
+
+ // Helper function for scrollviewer
+ private static ScrollViewer? FindScrollViewer(DependencyObject? element)
+ {
+ if (element is ScrollViewer scrollViewer) return scrollViewer;
+
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
+ {
+ var child = VisualTreeHelper.GetChild(element, i);
+ var result = FindScrollViewer(child);
+ if (result != null) return result;
+ }
+
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Radio/PopupWindow.xaml b/Radio/PopupWindow.xaml
new file mode 100644
index 0000000..1959743
--- /dev/null
+++ b/Radio/PopupWindow.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Radio/PopupWindow.xaml.cs b/Radio/PopupWindow.xaml.cs
new file mode 100644
index 0000000..9a19615
--- /dev/null
+++ b/Radio/PopupWindow.xaml.cs
@@ -0,0 +1,63 @@
+using System.Windows;
+
+namespace Radio
+{
+ public partial class PopupWindow : Window
+ {
+ public Radio? NewRadio;
+
+ public PopupWindow(Radio? radio = null)
+ {
+ InitializeComponent();
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+
+ if (radio != null)
+ {
+ NewRadio = radio;
+ Url.Text = radio.Url;
+ RadioName.Text = radio.Name;
+ Description.Text = radio.Description;
+ }
+ }
+
+ private void OkButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!Uri.TryCreate(Url.Text, UriKind.Absolute, out Uri? url))
+ {
+ MessageBox.Show("Invalid Stream Url", "Error");
+ return;
+ }
+
+ if (RadioName.Text.Length == 0)
+ {
+ MessageBox.Show("Radio Name is Required", "Error");
+ return;
+ }
+
+ if (NewRadio != null)
+ {
+ NewRadio.Url = url.ToString();
+ NewRadio.Name = RadioName.Text;
+ NewRadio.Description = Description.Text;
+ }
+ else
+ {
+ NewRadio = new Radio
+ {
+ Url = url.ToString(),
+ Name = RadioName.Text,
+ Description = Description.Text,
+ };
+ }
+
+ DialogResult = true;
+ Close();
+ }
+
+ private void CloseButton_Click(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+ }
+
+}
diff --git a/Radio/Radio.cs b/Radio/Radio.cs
new file mode 100644
index 0000000..e6d8996
--- /dev/null
+++ b/Radio/Radio.cs
@@ -0,0 +1,55 @@
+using System.ComponentModel;
+
+namespace Radio
+{
+ public class Radio : INotifyPropertyChanged
+ {
+ private string _name = "";
+ public required string Name
+ {
+ get { return _name; }
+ set
+ {
+ if (_name != value)
+ {
+ _name = value;
+ OnPropertyChanged(nameof(Name));
+ }
+ }
+ }
+
+ private string _url = "";
+ public required string Url
+ {
+ get { return _url; }
+ set
+ {
+ if (value != _url)
+ {
+ _url = value;
+ OnPropertyChanged(nameof(Url));
+ }
+ }
+ }
+
+ private string? _description;
+ public string? Description
+ {
+ get { return _description; }
+ set
+ {
+ if (value != _description)
+ {
+ _description = value;
+ OnPropertyChanged(nameof(Description));
+ }
+ }
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected virtual void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/Radio/Radio.csproj b/Radio/Radio.csproj
new file mode 100644
index 0000000..a92f049
--- /dev/null
+++ b/Radio/Radio.csproj
@@ -0,0 +1,69 @@
+
+
+
+ net8.0-windows
+ enable
+ true
+ enable
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ True
+
+
+
+
+
+ ..\..\Widgets.Common\Widgets.Common\bin\Debug\net8.0-windows\Widgets.Common.dll
+
+
+
+
+ $(AssemblyName)\libvlc\win-x64
+ $(AssemblyName)\libvlc\win-x86
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Radio/RadioPlugin.cs b/Radio/RadioPlugin.cs
new file mode 100644
index 0000000..e569d4d
--- /dev/null
+++ b/Radio/RadioPlugin.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.Composition;
+using Widgets.Common;
+
+namespace Radio
+{
+ [Export(typeof(IPlugin))]
+ internal class RadioPlugin : IPlugin
+ {
+ public string Name => MainWindow.WidgetName;
+ public string? ConfigFile => MainWindow.SettingsFile;
+ public WidgetDefaultStruct WidgetDefaultStruct()
+ {
+ return MainWindow.WidgetDefaultStruct();
+ }
+ public WidgetWindow WidgetWindow()
+ {
+ return new MainWindow().WidgetWindow();
+ }
+ }
+}
diff --git a/Radio/RadioViewModel.cs b/Radio/RadioViewModel.cs
new file mode 100644
index 0000000..e3063d6
--- /dev/null
+++ b/Radio/RadioViewModel.cs
@@ -0,0 +1,349 @@
+using LibVLCSharp.Shared;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.IO;
+using System.Reflection;
+using System.Windows.Media;
+using Widgets.Common;
+using MediaPlayer = LibVLCSharp.Shared.MediaPlayer;
+
+namespace Radio
+{
+ public class RadioViewModel : INotifyPropertyChanged, IDisposable
+ {
+ private readonly LibVLC _libVLC;
+ public readonly MediaPlayer _mediaPlayer;
+ public ObservableCollection RadioList { get; set; }
+ private readonly System.Timers.Timer _metaDataTimer = new(2000);
+ private int _reconnectionAttempt = 0;
+ private readonly int _reconnectionAttemptMax = 10;
+
+ public struct SettingsStruct
+ {
+ public ObservableCollection RadioList { get; set; }
+ public int RadioIndex { get; set; }
+ public int Volume { get; set; }
+ public float FontSize { get; set; }
+ public SolidColorBrush PrimaryColor { get; set; }
+ public SolidColorBrush PrimaryColorLight { get; set; }
+ public SolidColorBrush SecondaryColor { get; set; }
+ public SolidColorBrush SecondaryColorLight { get; set; }
+ public SolidColorBrush TextColor { get; set; }
+ }
+
+ public static SettingsStruct Default => new()
+ {
+ RadioList = [
+ new Radio{
+ Name = "Soho Radio",
+ Url = "https://sohoradiomusic.doughunt.co.uk:8010/320mp3",
+ Description = "Live",
+ }
+ ],
+ Volume = 100,
+ RadioIndex = 0,
+ FontSize = 14,
+ PrimaryColor = new SolidColorBrush(Colors.Brown),
+ PrimaryColorLight = new SolidColorBrush(Colors.RosyBrown),
+ SecondaryColor = new SolidColorBrush(Color.FromRgb(33, 33, 33)),
+ SecondaryColorLight = new SolidColorBrush(Colors.LightGray),
+ TextColor = new SolidColorBrush(Colors.White),
+ };
+
+ private SettingsStruct _settings = Default;
+ public SettingsStruct Settings
+ {
+ get => _settings;
+ set
+ {
+ _settings = value;
+ OnPropertyChanged(nameof(Settings));
+ }
+ }
+
+ private Geometry _volumeIcon = IconPaths.SoundLow();
+ public Geometry VolumeIcon
+ {
+ get { return _volumeIcon; }
+ set
+ {
+ if (_volumeIcon != value)
+ {
+ _volumeIcon = value;
+ OnPropertyChanged(nameof(VolumeIcon));
+ }
+ }
+ }
+
+ private Geometry _playStopIcon = IconPaths.Play();
+ public Geometry PlayStopIcon
+ {
+ get { return _playStopIcon; }
+ set
+ {
+ if (_playStopIcon != value)
+ {
+ _playStopIcon = value;
+ OnPropertyChanged(nameof(PlayStopIcon));
+ }
+ }
+ }
+
+ public Geometry ForwardIcon { get; } = IconPaths.Forward();
+ public Geometry BackwardIcon { get; } = IconPaths.Backward();
+ public Geometry NotaSingleIcon { get; } = IconPaths.NotaSingle();
+ public Geometry NotaDualIcon { get; } = IconPaths.NotaDual();
+
+ private int _volume;
+ public int Volume
+ {
+ get { return _volume; }
+ set
+ {
+ if (_volume != value)
+ {
+ _volume = value;
+ _mediaPlayer.Volume = value;
+ Mute = _volume < 0;
+ SetVolumeIcon(_volume);
+ OnPropertyChanged(nameof(Volume));
+ }
+ }
+ }
+
+ private bool _mute;
+ public bool Mute
+ {
+ get { return _mute; }
+ set
+ {
+ if (_mute != value)
+ {
+ _mute = value;
+ _mediaPlayer.Mute = value;
+ var volume = _mute ? 0 : Volume;
+ SetVolumeIcon(volume);
+ OnPropertyChanged(nameof(Mute));
+ }
+ }
+ }
+
+ private Radio? _selectedRadio;
+ public Radio? SelectedRadio
+ {
+ get => _selectedRadio;
+ set
+ {
+ _selectedRadio = value;
+ OnPropertyChanged(nameof(SelectedRadio));
+ }
+ }
+
+
+ private string? _mediaTitle;
+ public string? MediaTitle
+ {
+ get { return _mediaTitle; }
+ set
+ {
+ if (_mediaTitle != value)
+ {
+ _mediaTitle = string.IsNullOrEmpty(value) ? value : " 🎵 " + value + " 🎵 ";
+ OnPropertyChanged($"{nameof(MediaTitle)}");
+ }
+ }
+ }
+
+ private string? _mediaNowPlaying;
+ public string? MediaNowPlaying
+ {
+ get { return _mediaNowPlaying; }
+ set
+ {
+ if (_mediaNowPlaying != value)
+ {
+ _mediaNowPlaying = string.IsNullOrEmpty(value) ? value : value + " 🎵 ";
+ OnPropertyChanged($"{nameof(MediaNowPlaying)}");
+ }
+ }
+ }
+
+ private string? _mediaGenre;
+ public string? MediaGenre
+ {
+ get { return _mediaGenre; }
+ set
+ {
+ if (_mediaGenre != value)
+ {
+ _mediaGenre = string.IsNullOrEmpty(value) ? value : value + " 🎵 ";
+ OnPropertyChanged($"{nameof(MediaGenre)}");
+ }
+ }
+ }
+
+ public RadioViewModel(SettingsStruct settings)
+ {
+ string? dllDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
+ Core.Initialize(dllDirectory + "\\libvlc\\win-x64");
+
+ //var options = new[] { "--network-caching=2000", "--demux=avformat", "--codec=avcodec" };
+ var options = new[] { "--network-caching=2000" };
+
+ _libVLC = new LibVLC(options);
+ _mediaPlayer = new MediaPlayer(_libVLC);
+
+ _libVLC.Log += (sender, e) =>
+ {
+ //Debug.WriteLine(e.Module + " " + e.SourceFile + " " + e.Message);
+ if (e.Level == LibVLCSharp.Shared.LogLevel.Error)
+ {
+ Logger.Info($"{LibVLCSharp.Shared.LogLevel.Error}: {e.Message}");
+ MediaTitle = e.Message;
+ MediaNowPlaying = "";
+ MediaGenre = "";
+ }
+ //Debug.WriteLine($"[LibVLC] {e.Level}: {e.Message}");
+ };
+
+ _mediaPlayer.Playing += (sender, e) =>
+ {
+ _metaDataTimer.Start();
+ PlayStopIcon = IconPaths.Pause();
+ };
+
+ _mediaPlayer.Stopped += (sender, e) =>
+ {
+ _metaDataTimer.Stop();
+ PlayStopIcon = IconPaths.Play();
+ };
+
+ _mediaPlayer.Opening += (sender, e) =>
+ {
+ _metaDataTimer.Stop();
+ PlayStopIcon = IconPaths.Stop();
+ MediaTitle = "Radio is opening...";
+ MediaNowPlaying = "";
+ MediaGenre = "";
+ };
+
+ _mediaPlayer.Buffering += (sender, e) =>
+ {
+ if (e.Cache == 100)
+ {
+ _reconnectionAttempt = 0;
+ }
+ };
+
+ _mediaPlayer.EndReached += async (sender, e) =>
+ {
+ _metaDataTimer.Stop();
+ PlayStopIcon = IconPaths.Stop();
+ MediaNowPlaying = "";
+ MediaGenre = "";
+
+ await Task.Delay(5000);
+
+ if (_reconnectionAttempt < _reconnectionAttemptMax)
+ {
+ MediaTitle = "Reconnecting...";
+ _reconnectionAttempt++;
+ Play();
+ }
+ else
+ {
+ MediaTitle = "Stream Ended...";
+ _reconnectionAttempt = 0;
+ }
+ };
+
+
+ Settings = settings;
+ RadioList = Settings.RadioList;
+ if (Settings.RadioIndex >= 0 && Settings.RadioIndex < RadioList.Count)
+ {
+ SelectedRadio = RadioList[Settings.RadioIndex];
+ }
+ Volume = _mediaPlayer.Volume;
+ Mute = _mediaPlayer.Mute;
+ Media_MetaUpdater();
+ }
+
+ public void Play()
+ {
+ if (SelectedRadio == null) return;
+
+ if (PlayerState() != VLCState.Opening ||
+ PlayerState() != VLCState.Buffering)
+ {
+ var streamUrl = SelectedRadio.Url;
+ var media = new Media(_libVLC, streamUrl, FromType.FromLocation);
+ media.Parse(MediaParseOptions.ParseNetwork, -1);
+ _mediaPlayer.Play(media);
+ }
+ }
+
+ public void Stop()
+ {
+ _mediaPlayer.Stop();
+ }
+
+ public VLCState PlayerState()
+ {
+ return _mediaPlayer.State;
+ }
+
+ private void SetVolumeIcon(int volume)
+ {
+ if (volume > 50)
+ {
+ VolumeIcon = IconPaths.SoundHigh();
+ }
+ else if (volume > 0)
+ {
+ VolumeIcon = IconPaths.SoundLow();
+ }
+ else
+ {
+ VolumeIcon = IconPaths.SoundMute();
+ }
+ }
+
+ //private void Media_MetaChanged(object? sender, MediaMetaChangedEventArgs e)
+ //{
+ // if (_mediaPlayer.Media != null)
+ // {
+ // MediaTitle = _mediaPlayer.Media.Meta(MetadataType.Title);
+ // MediaNowPlaying = _mediaPlayer.Media.Meta(MetadataType.NowPlaying);
+ // MediaGenre = _mediaPlayer.Media.Meta(MetadataType.Genre);
+ // }
+ //}
+
+ private void Media_MetaUpdater()
+ {
+ _metaDataTimer.Elapsed += (sender, args) =>
+ {
+ if (_mediaPlayer.Media != null)
+ {
+ MediaTitle = _mediaPlayer.Media.Meta(MetadataType.Title);
+ MediaNowPlaying = _mediaPlayer.Media.Meta(MetadataType.NowPlaying);
+ MediaGenre = _mediaPlayer.Media.Meta(MetadataType.Genre);
+ }
+ };
+ _metaDataTimer.Start();
+ }
+
+ public void Dispose()
+ {
+ _mediaPlayer?.Dispose();
+ _metaDataTimer?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ protected virtual void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/Radio/Style.xaml b/Radio/Style.xaml
new file mode 100644
index 0000000..cfd8186
--- /dev/null
+++ b/Radio/Style.xaml
@@ -0,0 +1,146 @@
+
+
+ Brown
+ RosyBrown
+ #212121
+ LightGray
+ White
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file