Reorganization

This commit is contained in:
watsonb8
2019-07-05 14:17:09 -04:00
parent a01d399a1f
commit ec6a7586c7
76 changed files with 475 additions and 296 deletions

View File

@ -0,0 +1,42 @@
using System;
using Xamarin.Forms;
namespace Aurora.Design.Behaviors
{
public class BehaviorBase<T> : Behavior<T> where T : BindableObject
{
public T AssociatedObject { get; private set; }
protected override void OnAttachedTo(T bindable)
{
base.OnAttachedTo(bindable);
AssociatedObject = bindable;
if (bindable.BindingContext != null)
{
BindingContext = bindable.BindingContext;
}
bindable.BindingContextChanged += OnBindingContextChanged;
}
protected override void OnDetachingFrom(T bindable)
{
base.OnDetachingFrom(bindable);
bindable.BindingContextChanged -= OnBindingContextChanged;
AssociatedObject = null;
}
void OnBindingContextChanged(object sender, EventArgs e)
{
OnBindingContextChanged();
}
protected override void OnBindingContextChanged()
{
base.OnBindingContextChanged();
BindingContext = AssociatedObject.BindingContext;
}
}
}

View File

@ -0,0 +1,132 @@
using System;
using System.Reflection;
using System.Windows.Input;
using Xamarin.Forms;
namespace Aurora.Design.Behaviors
{
public class EventToCommandBehavior : BehaviorBase<View>
{
Delegate eventHandler;
public static readonly BindableProperty EventNameProperty = BindableProperty.Create("EventName", typeof(string), typeof(EventToCommandBehavior), null, propertyChanged: OnEventNameChanged);
public static readonly BindableProperty CommandProperty = BindableProperty.Create("Command", typeof(ICommand), typeof(EventToCommandBehavior), null);
public static readonly BindableProperty CommandParameterProperty = BindableProperty.Create("CommandParameter", typeof(object), typeof(EventToCommandBehavior), null);
public static readonly BindableProperty InputConverterProperty = BindableProperty.Create("Converter", typeof(IValueConverter), typeof(EventToCommandBehavior), null);
public string EventName
{
get { return (string)GetValue(EventNameProperty); }
set { SetValue(EventNameProperty, value); }
}
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
public IValueConverter Converter
{
get { return (IValueConverter)GetValue(InputConverterProperty); }
set { SetValue(InputConverterProperty, value); }
}
protected override void OnAttachedTo(View bindable)
{
base.OnAttachedTo(bindable);
RegisterEvent(EventName);
}
protected override void OnDetachingFrom(View bindable)
{
DeregisterEvent(EventName);
base.OnDetachingFrom(bindable);
}
void RegisterEvent(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return;
}
EventInfo eventInfo = AssociatedObject.GetType().GetRuntimeEvent(name);
if (eventInfo == null)
{
throw new ArgumentException(string.Format("EventToCommandBehavior: Can't register the '{0}' event.", EventName));
}
MethodInfo methodInfo = typeof(EventToCommandBehavior).GetTypeInfo().GetDeclaredMethod("OnEvent");
eventHandler = methodInfo.CreateDelegate(eventInfo.EventHandlerType, this);
eventInfo.AddEventHandler(AssociatedObject, eventHandler);
}
void DeregisterEvent(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
return;
}
if (eventHandler == null)
{
return;
}
EventInfo eventInfo = AssociatedObject.GetType().GetRuntimeEvent(name);
if (eventInfo == null)
{
throw new ArgumentException(string.Format("EventToCommandBehavior: Can't de-register the '{0}' event.", EventName));
}
eventInfo.RemoveEventHandler(AssociatedObject, eventHandler);
eventHandler = null;
}
void OnEvent(object sender, object eventArgs)
{
if (Command == null)
{
return;
}
object resolvedParameter;
if (CommandParameter != null)
{
resolvedParameter = CommandParameter;
}
else if (Converter != null)
{
resolvedParameter = Converter.Convert(eventArgs, typeof(object), null, null);
}
else
{
resolvedParameter = eventArgs;
}
if (Command.CanExecute(resolvedParameter))
{
Command.Execute(resolvedParameter);
}
}
static void OnEventNameChanged(BindableObject bindable, object oldValue, object newValue)
{
var behavior = (EventToCommandBehavior)bindable;
if (behavior.AssociatedObject == null)
{
return;
}
string oldEventName = (string)oldValue;
string newEventName = (string)newValue;
behavior.DeregisterEvent(oldEventName);
behavior.RegisterEvent(newEventName);
}
}
}

View File

@ -0,0 +1,168 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows.Input;
using Xamarin.Forms;
namespace Aurora.Design.Components.HorizontalList
{
public class HorizontalList : Grid
{
private ICommand _innerSelectedCommand;
private readonly ScrollView _scrollView;
private readonly StackLayout _itemsStackLayout;
public event EventHandler SelectedItemChanged;
public StackOrientation ListOrientation { get; set; }
public double Spacing { get; set; }
public static readonly BindableProperty SelectedCommandProperty =
BindableProperty.Create("SelectedCommand", typeof(ICommand), typeof(HorizontalList), null);
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create("ItemsSource", typeof(IEnumerable), typeof(HorizontalList), default(IEnumerable<object>), BindingMode.TwoWay, propertyChanged: ItemsSourceChanged);
public static readonly BindableProperty SelectedItemProperty =
BindableProperty.Create("SelectedItem", typeof(object), typeof(HorizontalList), null, BindingMode.TwoWay, propertyChanged: OnSelectedItemChanged);
public static readonly BindableProperty ItemTemplateProperty =
BindableProperty.Create("ItemTemplate", typeof(DataTemplate), typeof(HorizontalList), default(DataTemplate));
public ICommand SelectedCommand
{
get { return (ICommand)GetValue(SelectedCommandProperty); }
set { SetValue(SelectedCommandProperty, value); }
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public object SelectedItem
{
get { return (object)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public DataTemplate ItemTemplate
{
get { return (DataTemplate)GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
private static void ItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var itemsLayout = (HorizontalList)bindable;
itemsLayout.SetItems();
}
public HorizontalList()
{
// BackgroundColor = Color.FromHex("#1E2634");
Spacing = 6;
_scrollView = new ScrollView();
_itemsStackLayout = new StackLayout
{
BackgroundColor = BackgroundColor,
Padding = Padding,
Spacing = Spacing,
HorizontalOptions = LayoutOptions.FillAndExpand
};
_scrollView.BackgroundColor = BackgroundColor;
_scrollView.Content = _itemsStackLayout;
Children.Add(_scrollView);
}
protected virtual void SetItems()
{
_itemsStackLayout.Children.Clear();
_itemsStackLayout.Spacing = Spacing;
_innerSelectedCommand = new Command<View>(view =>
{
SelectedItem = view.BindingContext;
SelectedItem = null; // Allowing item second time selection
});
_itemsStackLayout.Orientation = ListOrientation;
_scrollView.Orientation = ListOrientation == StackOrientation.Horizontal
? ScrollOrientation.Horizontal
: ScrollOrientation.Vertical;
if (ItemsSource == null)
{
return;
}
foreach (var item in ItemsSource)
{
_itemsStackLayout.Children.Add(GetItemView(item));
}
_itemsStackLayout.BackgroundColor = BackgroundColor;
SelectedItem = null;
}
protected virtual View GetItemView(object item)
{
var content = ItemTemplate.CreateContent();
var view = content as View;
if (view == null)
{
return null;
}
view.BindingContext = item;
var gesture = new TapGestureRecognizer
{
Command = _innerSelectedCommand,
CommandParameter = view
};
AddGesture(view, gesture);
return view;
}
private void AddGesture(View view, TapGestureRecognizer gesture)
{
view.GestureRecognizers.Add(gesture);
var layout = view as Layout<View>;
if (layout == null)
{
return;
}
foreach (var child in layout.Children)
{
AddGesture(child, gesture);
}
}
private static void OnSelectedItemChanged(BindableObject bindable, object oldValue, object newValue)
{
var itemsView = (HorizontalList)bindable;
if (newValue == oldValue && newValue != null)
{
return;
}
itemsView.SelectedItemChanged?.Invoke(itemsView, EventArgs.Empty);
if (itemsView.SelectedCommand?.CanExecute(newValue) ?? false)
{
itemsView.SelectedCommand?.Execute(newValue);
}
}
}
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Aurora.Design.Components.HostSelector.HostSelector">
<ContentView.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="*"/>
</Grid.RowDefinitions>
<StackLayout
Grid.Row="0"
x:Name="CredentialEditorLayout"
Orientation="Horizontal"
HorizontalOptions="Center"
VerticalOptions="Start">
<Label
Text="Hostname"
VerticalOptions="Center"/>
<Entry
x:Name="HostnameEntry"/>
<Label
Text="Port"
VerticalOptions="Center"/>
<Entry
x:Name="PortEntry"/>
<Button
HorizontalOptions="Center"
x:Name="buttonHost"
Text="Host"/>
<Button
HorizontalOptions="Center"
x:Name="buttonClient"
Text="Join"/>
</StackLayout>
</Grid>
</ContentView.Content>
</ContentView>

View File

@ -0,0 +1,152 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Aurora.Design.Components.HostSelector
{
public enum ConnectionType
{
Host,
Client,
}
public enum SelectorState
{
SelectingHost,
EnteringCredentials,
}
public partial class HostSelector : ContentView
{
public HostSelector()
{
InitializeComponent();
//Set initial conditions
CredentialEditorLayout.IsVisible = true;
buttonHost.Clicked += OnButtonHostClicked;
buttonClient.Clicked += OnButtonClientClicked;
HostnameEntry.TextChanged += (sender, e) =>
{
Hostname = e.NewTextValue;
};
PortEntry.TextChanged += (sender, e) =>
{
Port = e.NewTextValue;
};
}
/// <summary>
/// On the host button clicked.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">E.</param>
void OnButtonHostClicked(object sender, EventArgs e)
{
if (HostCommand.CanExecute(null))
{
HostCommand.Execute(null);
}
}
/// <summary>
/// On the client button clicked.
/// </summary>
/// <param name="sender">Sender.</param>
/// <param name="e">E.</param>
void OnButtonClientClicked(object sender, EventArgs e)
{
if (JoinCommand.CanExecute(null))
{
JoinCommand.Execute(null);
}
}
#region Host Selected Command
public static readonly BindableProperty HostCommandProperty =
BindableProperty.Create(propertyName: "HostSelectedCommand",
returnType: typeof(Command),
declaringType: typeof(HostSelector));
public Command HostCommand
{
get { return (Command)GetValue(HostCommandProperty); }
set { SetValue(HostCommandProperty, value); }
}
#endregion Host Selected Command
#region Client Selected Command
public static readonly BindableProperty JoinCommandProperty =
BindableProperty.Create(propertyName: "JoinSelectedCommand",
returnType: typeof(Command),
declaringType: typeof(HostSelector));
public Command JoinCommand
{
get { return (Command)GetValue(JoinCommandProperty); }
set { SetValue(JoinCommandProperty, value); }
}
#endregion Client Selected Command
#region Hostname property
public static readonly BindableProperty HostnameProperty =
BindableProperty.Create(propertyName: "Hostname",
returnType: typeof(string),
declaringType: typeof(HostSelector),
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: OnHostNameChanged);
public string Hostname
{
get { return (string)GetValue(HostnameProperty); }
set { SetValue(HostnameProperty, value); }
}
private static void OnHostNameChanged(BindableObject bindable, object oldValue, object newValue)
{
string newVal = newValue as string;
HostSelector instance = bindable as HostSelector;
if (instance.HostnameEntry.Text != newVal)
{
instance.HostnameEntry.Text = newVal;
}
}
#endregion Hostname property
#region Port property
public static readonly BindableProperty PortProperty =
BindableProperty.Create(propertyName: "Port",
returnType: typeof(string),
declaringType: typeof(HostSelector),
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: OnPortChanged);
public string Port
{
get { return (string)GetValue(PortProperty); }
set { SetValue(PortProperty, value); }
}
private static void OnPortChanged(BindableObject bindable, object oldValue, object newValue)
{
string newVal = newValue as string;
HostSelector instance = bindable as HostSelector;
if (instance.PortEntry.Text != newVal)
{
instance.PortEntry.Text = newVal;
}
}
#endregion Port property
}
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Aurora.Design.Components.MediaPlayer.Player">
<ContentView.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="100"/>
<ColumnDefinition
Width="*"/>
</Grid.ColumnDefinitions>
<StackLayout
Grid.Column="0">
<Label
Text="{Binding SongTitle}"/>
<Label
Text="{Binding ArtistName}"/>
</StackLayout>
<StackLayout
Grid.Column="1"
Orientation="Horizontal">
<Button
Text="Previous"
Command="{Binding PreviousCommand}"
WidthRequest="100"
HeightRequest="50"/>
<Button
Text="{Binding PlayButtonText}"
Command="{Binding PlayCommand}"
WidthRequest="100"
HeightRequest="50"/>
<Button
Text="Next"
Command="{Binding NextCommand}"
WidthRequest="100"
HeightRequest="50"/>
</StackLayout>
</Grid>
</ContentView.Content>
</ContentView>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Aurora.Design.Components.MediaPlayer
{
public partial class Player : ContentView
{
public Player()
{
BindingContext = new PlayerViewModel();
InitializeComponent();
}
}
}

View File

@ -0,0 +1,163 @@
using System;
using Xamarin.Forms;
using Aurora.Design.Views;
using Aurora.Services.PlayerService;
using Aurora.Models.Media;
namespace Aurora.Design.Components.MediaPlayer
{
public class PlayerViewModel : BaseViewModel
{
PlayerService _playerService;
BaseMetadata _metadata;
public PlayerViewModel()
{
_playerService = PlayerService.Instance;
_playerService.PlaybackStateChanged += OnPlaybackStateChanged;
_playerService.MediaChanged += OnMediaChanged;
PlayCommand = new Command(OnPlayExecute, CanPlayExecute);
PreviousCommand = new Command(OnPreviousExecute, CanPreviousExecute);
NextCommand = new Command(OnNextExecute, CanNextExecute);
}
~PlayerViewModel()
{
_playerService.PlaybackStateChanged -= OnPlaybackStateChanged;
}
#region Public Properties
public Command PlayCommand { get; private set; }
public Command NextCommand { get; private set; }
public Command PreviousCommand { get; private set; }
public string PlayButtonText
{
get { return _playerService.PlaybackState == PlaybackState.Buffering ? "Play" : "Pause"; }
}
/// <summary>
/// TODO keep player view generic between audio and video.
/// </summary>
/// <value></value>
public string ArtistName
{
get
{
if (_metadata == null)
{
return "";
}
AudioMetadata metadata = _metadata as AudioMetadata;
return metadata.Artist;
}
}
/// <summary>
/// TODO keep player view generic between audio and video.
/// </summary>
/// <value></value>
public string SongTitle
{
get
{
if (_metadata == null)
{
return "";
}
AudioMetadata metadata = _metadata as AudioMetadata;
return metadata.Title;
}
}
#endregion Public Properties
#region Public Methods
public bool CanPreviousExecute()
{
return true;
}
public void OnPreviousExecute()
{
}
public bool CanPlayExecute()
{
switch (_playerService.PlaybackState)
{
case PlaybackState.Buffering:
{
return true;
}
case PlaybackState.Playing:
{
return true;
}
case PlaybackState.Stopped:
{
return false;
}
}
return false;
}
public void OnPlayExecute()
{
switch (_playerService.PlaybackState)
{
case PlaybackState.Buffering:
{
_playerService.Play();
break;
}
case PlaybackState.Playing:
{
_playerService.Pause();
break;
}
}
}
public bool CanNextExecute()
{
return true;
}
public void OnNextExecute()
{
}
#endregion public Methods
#region EventHandlers
/// <summary>
/// PlayerService playback state changed event handler.
/// </summary>
/// <param name="sender">The sending object.</param>
/// <param name="args">Event arguments.</param>
public void OnPlaybackStateChanged(object sender, PlaybackStateChangedEventArgs args)
{
OnPropertyChanged("PlayButtonText");
PlayCommand.ChangeCanExecute();
NextCommand.ChangeCanExecute();
PreviousCommand.ChangeCanExecute();
}
/// <summary>
/// PlayerService media changed event handler.
/// </summary>
/// <param name="sender">The sending object.</param>
/// <param name="args">Event arguments.</param>
public void OnMediaChanged(object sender, MediaChangedEventArgs args)
{
_metadata = args.NewMetadata;
OnPropertyChanged("ArtistName");
OnPropertyChanged("SongTitle");
}
#endregion EventHandlers
}
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:hl="clr-namespace:Aurora.Design.Components.HorizontalList"
x:Class="Aurora.Design.Components.MemberList.MemberList">
<ContentView.Content>
<StackLayout>
<hl:HorizontalList
x:Name="MembersHorizontalList"
ListOrientation="Horizontal"
VerticalOptions="Start">
<hl:HorizontalList.ItemTemplate>
<DataTemplate>
<Frame>
<Label
Text="{Binding .}"/>
</Frame>
</DataTemplate>
</hl:HorizontalList.ItemTemplate>
</hl:HorizontalList>
</StackLayout>
</ContentView.Content>
</ContentView>

View File

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
using Aurora.Design.Components.HorizontalList;
namespace Aurora.Design.Components.MemberList
{
public partial class MemberList : ContentView
{
public MemberList()
{
InitializeComponent();
}
/// <summary>
/// Bindable property for members list.
/// </summary>
/// <param name=""Members""></param>
/// <param name="typeof(IEnumerable<string>"></param>
/// <returns></returns>
public static readonly BindableProperty MembersProperty =
BindableProperty.Create(propertyName: "Members",
returnType: typeof(IEnumerable<string>),
declaringType: typeof(MemberList),
defaultBindingMode: BindingMode.Default,
propertyChanged: OnMembersChanged);
/// <summary>
/// Backing property for MembersProperty
/// </summary>
/// <value></value>
public IEnumerable<string> Members
{
get
{
return (IEnumerable<string>)GetValue(MembersProperty);
}
set
{
SetValue(MembersProperty, value);
}
}
/// <summary>
/// Memberes changed event handler. Assign member list source.
/// </summary>
/// <param name="bindable"></param>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
private static void OnMembersChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (MemberList)bindable;
var membersList = control.FindByName("MembersHorizontalList") as HorizontalList.HorizontalList;
if (membersList != null)
{
membersList.ItemsSource = newValue as IEnumerable<string>;
}
}
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Aurora.Design.Components.NavigationMenu
{
public class NavigationGroupItem : List<NavigationItem>
{
public NavigationGroupItem()
{
}
public NavigationGroupItem(string heading)
{
GroupHeading = heading;
}
public List<NavigationItem> Items => this;
public string GroupHeading { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Aurora.Design.Views.Main;
namespace Aurora.Design.Components.NavigationMenu
{
public class NavigationItem
{
public NavigationItem()
{
}
public int Id { get; set; }
public string Title { get; set; }
public string Group { get; set; }
public Type TargetType { get; set; }
}
}

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Aurora.Design.Components.NavigationMenu.NavigationMenu"
Title="Navigation">
<ContentView.Content>
<StackLayout>
<ListView
x:Name="MenuItemsListView"
SeparatorVisibility="None"
HasUnevenRows="true"
BackgroundColor="{StaticResource MenuBackgroundColor}"
IsGroupingEnabled="true"
CachingStrategy="RecycleElement">
<ListView.Header>
<Grid
BackgroundColor="#03A9F4">
<Grid.ColumnDefinitions>
<ColumnDefinition
Width="10"/>
<ColumnDefinition
Width="*"/>
<ColumnDefinition
Width="10"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition
Height="30"/>
<RowDefinition
Height="80"/>
<RowDefinition
Height="Auto"/>
<RowDefinition
Height="10"/>
</Grid.RowDefinitions>
<Label
Grid.Column="1"
Grid.Row="2"
Text="Aurora"
Style="{DynamicResource SubtitleStyle}"/>
</Grid>
</ListView.Header>
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Label
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Start"
Text="{Binding GroupHeading}"
FontSize="18"
TextColor="White"/>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout
Padding="15,10"
HorizontalOptions="FillAndExpand">
<Label
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
Text="{Binding Title}"
FontSize="24"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentView.Content>
</ContentPage>

View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Xamarin.Forms;
namespace Aurora.Design.Components.NavigationMenu
{
public partial class NavigationMenu : ContentPage
{
public NavigationMenu()
{
InitializeComponent();
ListView = MenuItemsListView;
}
public ListView ListView;
public static readonly BindableProperty ItemsProperty =
BindableProperty.Create(propertyName: nameof(Items),
returnType: typeof(ObservableCollection<NavigationItem>),
declaringType: typeof(NavigationMenu),
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: OnItemsChanged);
public ObservableCollection<NavigationItem> Items
{
get
{
return (ObservableCollection<NavigationItem>)GetValue(ItemsProperty);
}
set
{
SetValue(ItemsProperty, value);
}
}
/// <summary>
/// Items changed event handler. Organizes items in groups for display.
/// </summary>
/// <param name="bindable">The changed Item.</param>
/// <param name="oldValue">The previous value.</param>
/// <param name="newValue">The new value.</param>
private static void OnItemsChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (NavigationMenu)bindable;
ObservableCollection<NavigationItem> items = (ObservableCollection<NavigationItem>)newValue;
Dictionary<string, NavigationGroupItem> groupDictioanry = new Dictionary<string, NavigationGroupItem>();
//Populate dictionary where group heading is the key
foreach (NavigationItem item in items)
{
if (groupDictioanry.ContainsKey(item.Group))
{
groupDictioanry.TryGetValue(item.Group, out var groupItem);
groupItem.Items.Add(item);
}
else
{
NavigationGroupItem groupItem = new NavigationGroupItem(item.Group);
groupItem.Add(item);
groupDictioanry.Add(item.Group, groupItem);
}
}
ObservableCollection<NavigationGroupItem> groups = new ObservableCollection<NavigationGroupItem>();
foreach (string groupHeading in groupDictioanry.Keys)
{
groupDictioanry.TryGetValue(groupHeading, out var groupItem);
groups.Add(groupItem);
}
control.MenuItemsListView.ItemsSource = groups;
}
}
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:dg="clr-namespace:Xamarin.Forms.DataGrid;assembly=Xamarin.Forms.DataGrid"
x:Class="Aurora.Design.Components.Queue.Queue">
<ContentView.Content>
<dg:DataGrid
x:Name="QueueDataGrid"
SelectionEnabled="True"
RowHeight="25"
HeaderHeight="40"
BorderColor="#CCCCCC"
HeaderBackground="#E0E6F8">
<dg:DataGrid.HeaderFontSize>
<OnIdiom
x:TypeArguments="x:Double">
<OnIdiom.Tablet>15</OnIdiom.Tablet>
<OnIdiom.Phone>13</OnIdiom.Phone>
<OnIdiom.Desktop>20</OnIdiom.Desktop>
</OnIdiom>
</dg:DataGrid.HeaderFontSize>
<dg:DataGrid.Columns>
<dg:DataGridColumn
Title="Title"
PropertyName="Metadata.Title"
Width="2*"/>
<dg:DataGridColumn
Title="Album"
PropertyName="Metadata.Album"
Width="0.95*"/>
<dg:DataGridColumn
Title="Artist"
PropertyName="Metadata.Artist"
Width="1*"/>
<dg:DataGridColumn
Title="Duration"
PropertyName="Metadata.Duration"/>
</dg:DataGrid.Columns>
<dg:DataGrid.RowsBackgroundColorPalette>
<dg:PaletteCollection>
<Color>#F2F2F2</Color>
<Color>#FFFFFF</Color>
</dg:PaletteCollection>
</dg:DataGrid.RowsBackgroundColorPalette>
</dg:DataGrid>
</ContentView.Content>
</ContentView>

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;
using Xamarin.Forms.DataGrid;
using Aurora.Models.Media;
namespace Aurora.Design.Components.Queue
{
public partial class Queue : ContentView
{
public Queue()
{
InitializeComponent();
}
#region ItemsSource Property
/// <summary>
/// Bindable Property for the ItemsSource of the datagrid.
/// </summary>
/// <param name=""ItemsSource""></param>
/// <param name="typeof(IEnumerable<object>"></param>
/// <returns></returns>
public static readonly BindableProperty ItemsSourceProperty =
BindableProperty.Create(propertyName: "ItemsSource",
returnType: typeof(IEnumerable<object>),
declaringType: typeof(Queue),
defaultBindingMode: BindingMode.Default,
propertyChanged: OnItemsSourceChanged);
/// <summary>
/// Backing property for the ItemsSource property.
/// </summary>
/// <value></value>
public IEnumerable<object> ItemsSource
{
get
{
return (IEnumerable<object>)GetValue(ItemsSourceProperty);
}
set
{
SetValue(ItemsSourceProperty, value);
}
}
/// <summary>
/// ItemsSource Changed event handler
/// </summary>
/// <param name="bindable"></param>
/// <param name="oldValue"></param>
/// <param name="newValue"></param>
private static void OnItemsSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
Queue control = bindable as Queue;
var queueDataGrid = control.FindByName("QueueDataGrid") as DataGrid;
queueDataGrid.ItemsSource = newValue as IEnumerable<object>;
}
#endregion ItemsSource Property
/// <summary>
/// Bindable property for the selected item field on the datagrid.
/// </summary>
/// <param name=""SelectedItem""></param>
/// <param name="typeof(BaseMetadata"></param>
/// <returns></returns>
public static readonly BindableProperty SelectedItemProperty =
BindableProperty.Create(propertyName: "SelectedItem",
returnType: typeof(object),
declaringType: typeof(Queue),
defaultBindingMode: BindingMode.TwoWay,
propertyChanged: OnSelectedItemChanged);
/// <summary>
/// Backing property for the SelectedItem property.
/// </summary>
/// <value></value>
public object SelectedItem
{
get
{
return ((object)GetValue(SelectedItemProperty));
}
set
{
SetValue(SelectedItemProperty, value);
}
}
/// <summary>
/// Handles selection change events.
/// </summary>
/// <param name="bindable">The bindable object.</param>
/// <param name="newValue"></param>
/// <param name="oldValue"></param>
private static void OnSelectedItemChanged(BindableObject bindable, object newValue, object oldValue)
{
Queue control = bindable as Queue;
var queueDataGrid = control.FindByName("QueueDataGrid") as DataGrid;
IEnumerable<object> source = (IEnumerable<object>)queueDataGrid.ItemsSource;
if (source.Contains(newValue))
{
queueDataGrid.SelectedItem = newValue;
}
}
}
}

View File

@ -0,0 +1,24 @@
using System;
using System.Globalization;
using Xamarin.Forms;
namespace Aurora.Design.Converters
{
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is bool))
{
throw new InvalidOperationException("The target must be a boolean");
}
return !(bool)value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Globalization;
using Xamarin.Forms;
namespace Aurora.Design.Converters
{
public class ToUpperConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value.ToString().ToUpper();
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Aurora.Design.Views.Albums.AlbumsView">
<ContentPage.Content>
<Grid></Grid>
</ContentPage.Content>
</ContentView>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Aurora.Design.Views.Albums
{
public partial class AlbumsView : ContentView
{
public AlbumsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Aurora.Design.Views.Albums
{
public class AlbumsViewModel
{
public AlbumsViewModel()
{
}
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Aurora.Design.Views.Artists.ArtistsView">
<ContentPage.Content></ContentPage.Content>
</ContentView>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Aurora.Design.Views.Artists
{
public partial class ArtistsView : ContentView
{
public ArtistsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Aurora.Design.Views.Artists
{
public class ArtistsViewModel
{
public ArtistsViewModel()
{
}
}
}

View File

@ -0,0 +1,34 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace Aurora.Design.Views
{
public class BaseViewModel : INotifyPropertyChanged
{
public BaseViewModel()
{
}
#region INotifyPropertyChanged Implementation
public bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged == null)
return;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<MasterDetailPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:Aurora.Design.Views.MainView"
xmlns:navigation="clr-namespace:Aurora.Design.Components.NavigationMenu"
x:Class="Aurora.Design.Views.Main.MainView"
MasterBehavior="Split">
<MasterDetailPage.Master>
<navigation:NavigationMenu
x:Name="MasterPage"
Items="{Binding Pages}"/>
</MasterDetailPage.Master>
<MasterDetailPage.Detail>
<NavigationPage>
<x:Arguments>
<views:PageContainer
x:Name="ContentPage"/>
</x:Arguments>
</NavigationPage>
</MasterDetailPage.Detail>
</MasterDetailPage>

View File

@ -0,0 +1,59 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using Aurora.Design.Components.NavigationMenu;
using Aurora.Design.Views.MainView;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
namespace Aurora.Design.Views.Main
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainView : MasterDetailPage
{
public MainView()
{
InitializeComponent();
BindingContext = new MainViewModel();
MasterPage.ListView.ItemSelected += ListView_ItemSelected;
Appearing += OnAppearing;
}
~MainView()
{
Appearing -= OnAppearing;
}
private void ListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as NavigationItem;
if (item == null)
return;
var view = (View)Activator.CreateInstance(item.TargetType);
ContentPresenter viewContent = (ContentPresenter)ContentPage.Content.FindByName("ViewContent");
viewContent.Content = view;
MasterPage.ListView.SelectedItem = null;
}
/// <summary>
/// Event handler for page appearing.
/// </summary>
/// <param name="sender">The object that fired the event.</param>
/// <param name="args">The event arguments</param>
private void OnAppearing(object sender, EventArgs args)
{
//Set initial view from first item in list
ObservableCollection<NavigationGroupItem> screenList = (ObservableCollection<NavigationGroupItem>)MasterPage.ListView.ItemsSource;
var view = (View)Activator.CreateInstance(screenList.FirstOrDefault().FirstOrDefault().TargetType);
ContentPresenter viewContent = (ContentPresenter)ContentPage.Content.FindByName("ViewContent");
viewContent.Content = view;
MasterPage.ListView.SelectedItem = screenList.FirstOrDefault();
}
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Aurora.Design.Components.NavigationMenu;
using Aurora.Design.Views.Albums;
using Aurora.Design.Views.Artists;
using Aurora.Design.Views.Songs;
using Aurora.Design.Views.Stations;
using Aurora.Design.Views.Party;
using Aurora.Design.Views.Profile;
namespace Aurora.Design.Views.MainView
{
public class MainViewModel : BaseViewModel
{
private ObservableCollection<NavigationItem> _pages;
public ObservableCollection<NavigationItem> Pages
{
get { return _pages; }
set
{
if (value != _pages)
{
_pages = value;
OnPropertyChanged("Pages");
}
}
}
public MainViewModel()
{
_pages = new ObservableCollection<NavigationItem>(new[]
{
new NavigationItem { Id = 4, Title = "Party", Group="Social", TargetType = typeof(PartyView)},
new NavigationItem { Id = 5, Title = "Profile", Group="Social", TargetType = typeof(ProfileView)},
new NavigationItem { Id = 0, Title = "Songs", Group="Library", TargetType = typeof(SongsView) },
new NavigationItem { Id = 1, Title = "Artists", Group="Library", TargetType = typeof(ArtistsView)},
new NavigationItem { Id = 2, Title = "Albums", Group="Library", TargetType = typeof(AlbumsView)},
new NavigationItem { Id = 3, Title = "Stations", Group="Library", TargetType = typeof(StationsView)},
});
}
}
}

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:components="clr-namespace:Aurora.Design.Components"
xmlns:mp="clr-namespace:Aurora.Design.Components.MediaPlayer"
x:Class="Aurora.Design.Views.MainView.PageContainer">
<ContentPage.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="*"/>
<RowDefinition
Height="50"/>
</Grid.RowDefinitions>
<ContentPresenter
Grid.Row="0"
x:Name="ViewContent"/>
<mp:Player
Grid.Row="1"
HorizontalOptions="CenterAndExpand"
VerticalOptions="End"
HeightRequest="200"/>
</Grid>
</ContentPage.Content>
</ContentPage>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Aurora.Design.Views.MainView
{
public partial class PageContainer : ContentPage
{
public PageContainer()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:hs="clr-namespace:Aurora.Design.Components.HostSelector"
xmlns:ml="clr-namespace:Aurora.Design.Components.MemberList"
xmlns:qu="clr-namespace:Aurora.Design.Components.Queue"
x:Class="Aurora.Design.Views.Party.PartyView">
<ContentView.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition
Height="*"/>
</Grid.RowDefinitions>
<StackLayout
Grid.Row="0"
IsVisible="{Binding IsNotSelectingHost}">
<Label
Text="Party Members"/>
<ml:MemberList
VerticalOptions="FillAndExpand"
Members="{Binding Members}"/>
<Label
Text="Queue"/>
<qu:Queue/>
</StackLayout>
<hs:HostSelector
Grid.Row="0"
Hostname="{Binding Hostname}"
Port="{Binding Port}"
HostCommand="{Binding HostCommand}"
JoinCommand="{Binding JoinCommand}"
IsVisible="{Binding IsSelectingHost}"/>
</Grid>
</ContentView.Content>
</ContentView>

View File

@ -0,0 +1,14 @@
using System;
using Xamarin.Forms;
namespace Aurora.Design.Views.Party
{
public partial class PartyView : ContentView
{
public PartyView()
{
InitializeComponent();
BindingContext = new PartyViewModel();
}
}
}

View File

@ -0,0 +1,133 @@
using System;
using System.Collections.ObjectModel;
using Aurora.Executors;
using Aurora.Design.Components.HostSelector;
using Aurora.Services;
using Xamarin.Forms;
namespace Aurora.Design.Views.Party
{
enum PartyState
{
SelectingHost,
InParty,
Connecting,
}
public class PartyViewModel : BaseViewModel
{
private ObservableCollection<string> _members;
private PartyState _state;
private BaseExecutor _executor;
private string _hostname;
private string _port;
public PartyViewModel()
{
_members = new ObservableCollection<string>()
{
"Kevin",
"Brandon",
"Sheila",
"Dale",
"Austin",
"Tori",
"Ashley",
"Spencer",
};
OnPropertyChanged("Members");
this.JoinCommand = new Command(OnJoinExecute, CanJoinExecute);
this.HostCommand = new Command(OnHostExecute, CanHostExecute);
State(PartyState.SelectingHost);
}
#region Properties
public ObservableCollection<string> Members
{
get { return _members; }
set { SetProperty(ref _members, value); }
}
public bool IsSelectingHost
{
get { return _state == PartyState.SelectingHost; }
}
public bool IsNotSelectingHost
{
get { return _state != PartyState.SelectingHost; }
}
public Command JoinCommand { get; set; }
public Command HostCommand { get; set; }
public string Hostname
{
get { return _hostname; }
set { SetProperty(ref _hostname, value); }
}
public string Port
{
get { return _port; }
set { SetProperty(ref _port, value); }
}
#endregion Properties
private void State(PartyState state)
{
_state = state;
OnPropertyChanged("IsSelectingHost");
}
#region Commands
private void OnJoinExecute()
{
_executor = BaseExecutor.CreateExecutor<ClientExecutor>();
Int32.TryParse(this.Port, out int intPort);
_executor.Initialize();
State(PartyState.Connecting);
}
private bool CanJoinExecute()
{
return true;
}
private void OnHostExecute()
{
Int32.TryParse(this.Port, out int intPort);
//Init gRPC server
ServerService.Instance.Initialize(this.Hostname, intPort);
//Instantiate and initialize all executors
_executor = BaseExecutor.CreateExecutor<HostExecutor>();
_executor.Initialize();
//start gRPC server
ServerService.Instance.Start();
//Change state
State(PartyState.Connecting);
}
private bool CanHostExecute()
{
return true;
}
#endregion Commands
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Aurora.Design.Views.Profile.ProfileView">
<ContentView.Content>
<StackLayout
Orientation="Vertical">
<StackLayout
Orientation="Horizontal">
<Label
Text="Username"/>
<Entry
Text="{Binding Username}"/>
</StackLayout>
</StackLayout>
</ContentView.Content>
</ContentView>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Aurora.Design.Views.Profile
{
public partial class ProfileView : ContentView
{
public ProfileView()
{
InitializeComponent();
BindingContext = new ProfileViewModel();
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using Aurora.Services;
namespace Aurora.Design.Views.Profile
{
public class ProfileViewModel : BaseViewModel
{
public ProfileViewModel()
{
}
public string Username
{
get { return SettingsService.Instance.Username; }
set
{
SettingsService.Instance.Username = value;
OnPropertyChanged("Username");
}
}
}
}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:songs="clr-namespace:Aurora.Design.Views.Songs"
xmlns:dg="clr-namespace:Xamarin.Forms.DataGrid;assembly=Xamarin.Forms.DataGrid"
x:Class="Aurora.Design.Views.Songs.SongsView">
<ContentPage.BindingContext>
<songs:SongsViewModel
x:Name="songsViewModel"/>
</ContentPage.BindingContext>
<ContentPage.Content>
<dg:DataGrid
ItemsSource="{Binding SongsList}"
SelectionEnabled="True"
SelectedItem="{Binding SelectedSong}"
RowHeight="30"
HeaderHeight="50"
BorderColor="#CCCCCC"
HeaderBackground="#E0E6F8">
<dg:DataGrid.GestureRecognizers>
<TapGestureRecognizer
Command="{Binding PlayCommand}"
NumberOfTapsRequired="2"/>
</dg:DataGrid.GestureRecognizers><!-- Header -->
<dg:DataGrid.HeaderFontSize>
<OnIdiom
x:TypeArguments="x:Double">
<OnIdiom.Tablet>15</OnIdiom.Tablet>
<OnIdiom.Phone>13</OnIdiom.Phone>
<OnIdiom.Desktop>20</OnIdiom.Desktop>
</OnIdiom>
</dg:DataGrid.HeaderFontSize><!-- Columns -->
<dg:DataGrid.Columns>
<dg:DataGridColumn
Title="Title"
PropertyName="Metadata.Title"
Width="2*"/>
<dg:DataGridColumn
Title="Album"
PropertyName="Metadata.Album"
Width="0.95*"/>
<dg:DataGridColumn
Title="Artist"
PropertyName="Metadata.Artist"
Width="1*"/>
<dg:DataGridColumn
Title="Duration"
PropertyName="Metadata.Duration"/>
</dg:DataGrid.Columns><!-- Row Colors -->
<dg:DataGrid.RowsBackgroundColorPalette>
<dg:PaletteCollection>
<Color>#F2F2F2</Color>
<Color>#FFFFFF</Color>
</dg:PaletteCollection>
</dg:DataGrid.RowsBackgroundColorPalette>
</dg:DataGrid>
</ContentPage.Content>
</ContentView>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Aurora.Design.Views.Songs
{
public partial class SongsView : ContentView
{
public SongsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,60 @@
using System.Collections.ObjectModel;
using Aurora.Models.Media;
using Aurora.Services;
using Aurora.Services.PlayerService;
using Xamarin.Forms;
namespace Aurora.Design.Views.Songs
{
public class SongsViewModel : BaseViewModel
{
#region Fields
private ObservableCollection<BaseMedia> _songsList;
private BaseMedia _selectedSong;
#endregion Fields
#region Constructor
public SongsViewModel()
{
_songsList = new ObservableCollection<BaseMedia>();
PlayCommand = new Command(PlayExecute);
Initialize();
}
#endregion Constructor
#region Properties
public ObservableCollection<BaseMedia> SongsList
{
get { return _songsList; }
set { SetProperty(ref _songsList, value); }
}
public BaseMedia SelectedSong
{
get { return _selectedSong; }
set { SetProperty(ref _selectedSong, value); }
}
public Command PlayCommand { get; private set; }
#endregion Properties
#region Methods
public void Initialize()
{
SongsList = LibraryService.Instance.GetLibrary();
}
public void PlayExecute()
{
PlayerService.Instance.LoadMedia(_selectedSong);
PlayerService.Instance.Play();
}
#endregion Methods
}
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentView
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Aurora.Design.Views.Stations.StationsView">
<ContentPage.Content></ContentPage.Content>
</ContentView>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
namespace Aurora.Design.Views.Stations
{
public partial class StationsView : ContentView
{
public StationsView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,10 @@
using System;
namespace Aurora.Design.Views.Stations
{
public class StationsViewModel
{
public StationsViewModel()
{
}
}
}