* App.xaml:

* App.xaml.cs:
* MainPage.xaml:
* Info.plist:
* MainPage.xaml.cs:
* AppDelegate.cs:
* CueListPage.xaml.cs:
* QControlKitXamDemo.csproj:
* QCueViewModel.cs:
* QServerViewModel.cs:
* QBrowserViewModel.cs:
* QWorkspaceViewModel.cs:
* QControlKitXamDemo.iOS.csproj:
* QControlKitXamDemo.Android.csproj:
* materialdesignicons-webfont.ttf:
* materialdesignicons-webfont.ttf: Update demo to look a bit closer to
  real

* QClient.cs:
* QBrowser.cs: Change log levels

* QControlKit.csproj: Update Package Version

* QCue.cs: decent fix for bool property fetching

* QMessage.cs: add ToString()

* QServer.cs: Add Logging

* TCPClient.cs: Big Restructure using SuperSimpleTcp and SlipDecode
  GAH!

* QWorkspace.cs: Add ability to address active cues
This commit is contained in:
2022-02-17 17:27:05 -06:00
parent 6057105f44
commit 19794f40a7
24 changed files with 751 additions and 298 deletions
@@ -101,6 +101,9 @@
<Name>QControlKitXamDemo</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<AndroidAsset Include="Assets\materialdesignicons-webfont.ttf" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<ProjectExtensions>
<VisualStudio>
@@ -23,7 +23,7 @@ namespace QControlKitXamDemo.iOS
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init();
Log.Logger = new LoggerConfiguration().WriteTo.NSLog().MinimumLevel.Debug().CreateLogger();
Log.Logger = new LoggerConfiguration().WriteTo.NSLog().MinimumLevel.Information().CreateLogger();
LoadApplication(new App());
return base.FinishedLaunching(app, options);
@@ -40,5 +40,9 @@
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Scan network for QLab instances</string>
<key>UIAppFonts</key>
<array>
<string>materialdesignicons-webfont.ttf</string>
</array>
</dict>
</plist>
@@ -133,6 +133,9 @@
</PackageReference>
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2337" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.1" />
<PackageReference Include="SuperSimpleTcp">
<Version>2.6.0</Version>
</PackageReference>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
<ItemGroup>
@@ -141,4 +144,7 @@
<Name>QControlKitXamDemo</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<BundleResource Include="Resources\materialdesignicons-webfont.ttf" />
</ItemGroup>
</Project>
@@ -6,6 +6,52 @@
mc:Ignorable="d"
x:Class="QControlKitXamDemo.App">
<Application.Resources>
<OnPlatform x:Key="MaterialFontFamily" x:TypeArguments="x:String">
<On Platform="iOS">Material Design Icons</On>
<On Platform="Android">materialdesignicons-webfont.ttf#Material Design Icons</On>
</OnPlatform>
<Style TargetType="Label"
Class="PrimaryText"
ApplyToDerivedTypes="true">
<Setter Property="TextColor"
Value="{DynamicResource PrimaryTextColor}" />
</Style>
<Style TargetType="Button"
Class="PrimaryButton"
ApplyToDerivedTypes="true">
<Setter Property="TextColor"
Value="{DynamicResource PrimaryTextColor}" />
</Style>
<x:Double x:Key="SmallPadding">4</x:Double>
<x:Double x:Key="MediumPadding">8</x:Double>
<x:Double x:Key="StandardPadding">16</x:Double>
<Thickness x:Key="SmallTopThickness" Top="{StaticResource SmallPadding}" />
<Thickness x:Key="MediumTopThickness" Top="{StaticResource MediumPadding}" />
<Thickness x:Key="StandardTopThickness" Top="{StaticResource StandardPadding}" />
<Thickness x:Key="SmallThickness"
Bottom="{StaticResource SmallPadding}"
Left="{StaticResource SmallPadding}"
Right="{StaticResource SmallPadding}"
Top="{StaticResource SmallPadding}" />
<Thickness x:Key="MediumThickness"
Bottom="{StaticResource MediumPadding}"
Left="{StaticResource MediumPadding}"
Right="{StaticResource MediumPadding}"
Top="{StaticResource MediumPadding}" />
<Thickness x:Key="StandardThickness"
Bottom="{StaticResource StandardPadding}"
Left="{StaticResource StandardPadding}"
Right="{StaticResource StandardPadding}"
Top="{StaticResource StandardPadding}" />
</Application.Resources>
</Application>
@@ -9,6 +9,8 @@ namespace QControlKitXamDemo
InitializeComponent();
MainPage = new NavigationPage(new MainPage());
Current.RequestedThemeChanged += Current_RequestedThemeChanged;
SetAppResources();
}
protected override void OnStart()
@@ -22,5 +24,80 @@ namespace QControlKitXamDemo
protected override void OnResume()
{
}
private void Current_RequestedThemeChanged(object sender, AppThemeChangedEventArgs e)
{
SetAppResources();
}
private void SetAppResources()
{
switch (Current.RequestedTheme)
{
case OSAppTheme.Light:
SetLightResources();
break;
case OSAppTheme.Dark:
SetDarkResources();
break;
case OSAppTheme.Unspecified:
//Default to Light
SetLightResources();
break;
default:
SetLightResources();
break;
}
}
private void SetDarkResources()
{
Resources["NavigationBarColor"] = Color.FromHex("#3f6dab");
Resources["PageBackgroundColor"] = Color.FromHex("#525252");
Resources["PrimaryColor"] = Color.WhiteSmoke;
Resources["SecondaryColor"] = Color.White;
Resources["PrimaryTextColor"] = Color.LightGray;
Resources["SecondaryTextColor"] = Color.White;
Resources["TertiaryTextColor"] = Color.Gray;
Resources["TransparentColor"] = Color.Transparent;
Resources["IconTextColor"] = Color.WhiteSmoke;
Resources["ListViewBackgroundColor"] = Color.FromHex("#363636");
Resources["ServerCellBackgroundColor"] = Color.FromHex("#363636");
Resources["WorkspaceCellBackgroundColor"] = Color.FromHex("#737373");
Resources["SelectedCueCellBackgroundColor"] = Color.FromHex("#737373");
}
private void SetLightResources()
{
Resources["NavigationBarColor"] = Color.FromHex("#3089ff");
Resources["PageBackgroundColor"] = Color.LightGray;
Resources["PrimaryColor"] = Color.WhiteSmoke;
Resources["SecondaryColor"] = Color.White;
Resources["PrimaryTextColor"] = Color.Black;
Resources["SecondaryTextColor"] = Color.White;
Resources["TertiaryTextColor"] = Color.Gray;
Resources["TransparentColor"] = Color.Transparent;
Resources["IconTextColor"] = Color.Black;
Resources["ListViewBackgroundColor"] = Color.White;
Resources["ServerCellBackgroundColor"] = Color.White;
Resources["WorkspaceCellBackgroundColor"] = Color.FromHex("#D8D8D8");
Resources["SelectedCueCellBackgroundColor"] = Color.FromHex("#D8D8D8");
}
}
}
@@ -95,7 +95,7 @@ namespace QControlKitXamDemo
HorizontalOptions = LayoutOptions.StartAndExpand,
VerticalTextAlignment = TextAlignment.Center,
};
cueLabel.SetBinding(Label.TextProperty, "name", BindingMode.OneWay);
cueLabel.SetBinding(Label.TextProperty, "status", BindingMode.OneWay);
var cueBackground = new Frame
{
BindingContext = qCueViewModel,
@@ -3,14 +3,121 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:d="http://xamarin.com/schemas/2014/forms/design"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Class="QControlKitXamDemo.MainPage">
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
mc:Ignorable="d"
x:Class="QControlKitXamDemo.MainPage"
ios:Page.UseSafeArea="true"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<ContentPage.Content>
<ListView x:Name="serverListView" GroupDisplayBinding="{Binding name}" IsGroupingEnabled="True">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ContentPage>
<StackLayout>
<Frame BackgroundColor="{DynamicResource PageBackgroundColor}"
CornerRadius="0"
Margin="0"
Padding="0"
HasShadow="False">
<StackLayout Spacing="0" BackgroundColor="Transparent">
<Label Margin="0,10,0,10"
Padding="0"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
HorizontalTextAlignment="Center"
FontSize="Title"
Text="Discovered QLab Instances" />
<!--Need to try and remove the group spacing caused by ios:GroupHeaderStyle-->
<ListView x:Name="serverListView"
ios:ListView.GroupHeaderStyle="Grouped"
GroupDisplayBinding="{Binding name}"
IsGroupingEnabled="True"
d:IsGroupingEnabled="False"
BackgroundColor="Transparent"
SeparatorVisibility="None"
ItemsSource="{Binding ServersGrouped}"
Margin="0">
<ListView.GroupHeaderTemplate>
<DataTemplate>
<ViewCell>
<Frame BackgroundColor="{DynamicResource ServerCellBackgroundColor}"
Margin="0,0,0,0"
Padding="10,0,10,0"
CornerRadius="0">
<Grid BackgroundColor="Transparent" VerticalOptions="FillAndExpand"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Text="{Binding GroupName}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
HorizontalTextAlignment="Start"
StyleClass="PrimaryText"/>
<Label Grid.Column="1"
Text="{Binding version}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
HorizontalTextAlignment="End"
StyleClass="PrimaryText"/>
</Grid>
</Frame>
</ViewCell>
</DataTemplate>
</ListView.GroupHeaderTemplate>
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Frame BackgroundColor="{DynamicResource WorkspaceCellBackgroundColor}"
BorderColor="Black"
Margin="5,0,5,0"
Padding="10,0,10,0"
CornerRadius="0"
HasShadow="False">
<Grid Margin="0"
BackgroundColor="Transparent"
RowSpacing="0"
ColumnSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" Text="{Binding Name}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
HorizontalTextAlignment="Start"
StyleClass="PrimaryText"/>
<Label Grid.Column="1" Text="&#xf1184;"
FontFamily="{StaticResource MaterialFontFamily}"
FontSize="Large"
IsVisible="{Binding HasPasscode}"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"
VerticalTextAlignment="Center"
HorizontalTextAlignment="End"
StyleClass="PrimaryText"/>
</Grid>
</Frame>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</Frame>
</StackLayout>
</ContentPage.Content>
</ContentPage>
@@ -2,150 +2,71 @@
using System.ComponentModel;
using Xamarin.Forms;
using QControlKit;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;
using QControlKit.Events;
using System.Threading.Tasks;
using Serilog;
using QControlKitXamDemo.ViewModels;
using Acr.UserDialogs;
namespace QControlKitXamDemo
{
// Learn more about making custom code visible in the Xamarin.Forms previewer
// by visiting https://aka.ms/xamarinforms-previewer
[DesignTimeVisible(false)]
public partial class MainPage : ContentPage
{
public ObservableCollection<ServerGroup> servers = new ObservableCollection<ServerGroup>();
private QBrowserViewModel qBrowserViewModel;
public MainPage()
{
InitializeComponent();
serverListView.ItemsSource = servers;
QBrowser browser = new QBrowser();
browser.ServerLost += Browser_ServerLost;
browser.ServerFound += Browser_ServerFound;
serverListView.ItemSelected += ServerListView_ItemSelected;
Device.StartTimer(TimeSpan.FromSeconds(5), () =>
{
Task.Run(async () =>
{
Device.BeginInvokeOnMainThread(() =>
{
browser.ProbeForQLabInstances();
});
});
return true;
});
qBrowserViewModel = new QBrowserViewModel(new QBrowser());
serverListView.BindingContext = qBrowserViewModel;
serverListView.ItemSelected += QWorkspaceSelected;
}
private void Browser_ServerFound(object source, QServerFoundArgs args)
async void QWorkspaceSelected(object sender, SelectedItemChangedEventArgs e)
{
List<ServerGroup> serverGroups = servers.ToList();
bool serverFound = false;
foreach (var serverGroup in serverGroups)
if (e.SelectedItem == null)
{
if (serverGroup.host == args.server.host)
{
serverFound = true;
}
return;
}
if (!serverFound)
{
Device.BeginInvokeOnMainThread(() =>
{
ServerGroup serverGroup = new ServerGroup(args.server.name);
args.server.WorkspaceAdded += Server_WorkspaceAdded;
args.server.WorkspaceRemoved += Server_WorkspaceRemoved;
serverGroup.host = args.server.host;
servers.Add(serverGroup);
});
}
}
qBrowserViewModel.autoUpdate = false;
QWorkspace selectedWorkspace = (e.SelectedItem as QWorkspaceViewModel).workspace;
Log.Debug($"[demo] workspace: {selectedWorkspace.nameWithoutPathExtension} has been selected");
((ListView)sender).SelectedItem = null;
private void Server_WorkspaceRemoved(object source, QServerWorkspaceChangedArgs args)
{
foreach (var server in servers)
if (selectedWorkspace.hasPasscode)
{
if (server.host == args.server.host)
UserDialogs.Instance.Prompt(new PromptConfig
{
Device.BeginInvokeOnMainThread(() =>
InputType = InputType.Number,
MaxLength = 4,
Title = "Workspace Requires Passcode",
OkText = "Connect",
IsCancellable = true,
OnTextChanged = args =>
{
server.Remove(args.workspace);
});
}
}
}
private void Server_WorkspaceAdded(object source, QServerWorkspaceChangedArgs args)
{
foreach(var server in servers)
{
if(server.host == args.server.host)
{
Device.BeginInvokeOnMainThread(() =>
args.IsValid = args.Value != null && !args.Value.Equals("") && args.Value.Length == 4;
},
OnAction = async (resp) =>
{
server.Add(args.workspace);
});
}
}
}
private void Browser_ServerLost(object source, QServerLostArgs args)
{
List<ServerGroup> serverGroups = servers.ToList();
foreach(var serverGroup in serverGroups)
{
if(serverGroup.host == args.server.host)
{
Device.BeginInvokeOnMainThread(() =>
{
servers.Remove(serverGroup);
});
}
}
}
async void ServerListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
if(e.SelectedItem != null)
{
QWorkspace selectedWorkspace = e.SelectedItem as QWorkspace;
Log.Debug($"[demo] Workspace <{selectedWorkspace.nameWithoutPathExtension}> has been selected");
if (selectedWorkspace.hasPasscode)
{
string passcode = await DisplayPromptAsync("Workspace has passcode", "Enter Passcode", maxLength: 4, keyboard: Keyboard.Numeric);
if (passcode != null)
{
await Navigation.PushAsync(new CueListPage(selectedWorkspace, passcode));
if (resp.Ok)
{
await Navigation.PushAsync(new CueListPage(selectedWorkspace, resp.Value));
}
}
}
else
{
await Navigation.PushAsync(new CueListPage(selectedWorkspace));
}
serverListView.SelectedItem = null;
});
}
else
{
await Navigation.PushAsync(new CueListPage(selectedWorkspace));
}
}
}
public class ServerGroup : ObservableCollection<QWorkspace>{
public string name { get; set; }
public string host { get; set; }
public ServerGroup(string name)
protected override void OnAppearing()
{
this.name = name;
base.OnAppearing();
qBrowserViewModel.autoUpdate = true;
}
}
}
}
@@ -14,6 +14,7 @@
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2337" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.1" />
<PackageReference Include="Acr.UserDialogs" Version="7.2.0.562" />
</ItemGroup>
<ItemGroup>
@@ -28,4 +29,7 @@
<ItemGroup>
<ProjectReference Include="..\..\QControlKit\QControlKit.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Acr.UserDialogs" />
</ItemGroup>
</Project>
@@ -0,0 +1,93 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using QControlKit;
using QControlKit.Events;
using System;
using System.Threading.Tasks;
using Serilog;
namespace QControlKitXamDemo.ViewModels
{
public class QBrowserViewModel : INotifyPropertyChanged
{
QBrowser browser;
public event PropertyChangedEventHandler PropertyChanged;
public bool autoUpdate = false;
public ObservableCollection<QServerViewModel> ServersGrouped { get; set; }
public QBrowserViewModel(QBrowser browser)
{
this.browser = browser;
ServersGrouped = new ObservableCollection<QServerViewModel>();
this.browser.ServerFound += OnServerFound;
this.browser.ServerLost += OnServerLost;
Device.StartTimer(TimeSpan.FromSeconds(4), () =>
{
Task.Run(() =>
{
if (autoUpdate)
{
Device.BeginInvokeOnMainThread(() =>
{
Log.Debug("[QBrowserViewModel] QBrowser probe triggered");
this.browser.ProbeForQLabInstances();
});
}
});
return true;
});
}
private void OnServerFound(object source, QServerFoundArgs args)
{
Device.BeginInvokeOnMainThread(() =>
{
Log.Debug($"[QBrowserViewModel] adding server: {args.server.description}");
ServersGrouped.Add(new QServerViewModel(args.server));
});
}
private void OnServerLost(object source, QServerLostArgs args)
{
QServerViewModel serverToRemove = null;
foreach (QServerViewModel serverGroup in ServersGrouped)
{
if (serverGroup.host.Equals(args.server.host))
{
serverToRemove = serverGroup;
break;
}
}
if (serverToRemove != null)
{
Device.BeginInvokeOnMainThread(() =>
{
Log.Debug($"[QBrowserViewModel] removing server: {args.server.description}");
ServersGrouped.Remove(serverToRemove);
});
}
}
void OnPropertyChanged([CallerMemberName] string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public void InitiateScan()
{
if (!autoUpdate)
{
Log.Debug("[QBrowserViewModel] Manual Scan Initiated");
browser.ProbeForQLabInstances();
}
}
}
}
@@ -5,6 +5,7 @@ using Xamarin.Forms;
using Serilog;
using QControlKit.Events;
using QControlKit.Constants;
using System;
namespace QControlKitXamDemo.ViewModels
{
@@ -26,13 +27,19 @@ namespace QControlKitXamDemo.ViewModels
{
foreach(var property in args.properties)
{
Log.Debug($"[cueviewmodel] property <{property}> has been updated.");
Log.Debug($"[cueviewmodel] property <{property}> has been updated for cue {name}.");
if (property.Equals(QOSCKey.Name) || property.Equals(QOSCKey.ListName))
{
OnPropertyChanged("name");
}else if (property.Equals(QOSCKey.ColorName))
{
OnPropertyChanged("color");
}else if (property.Equals(QOSCKey.ActionElapsed))
{
OnPropertyChanged("actionElapsed");
}else if (property.Equals(QOSCKey.IsBroken) || property.Equals(QOSCKey.IsRunning))
{
OnPropertyChanged("status");
}
}
}
@@ -84,6 +91,34 @@ namespace QControlKitXamDemo.ViewModels
}
}
public string actionElapsed
{
get
{
TimeSpan time = TimeSpan.FromSeconds(cue.actionElapsed);
return time.ToString(@"mm\:ss\:ff");
}
}
public string status
{
get
{
if (cue.IsRunning)
{
return "running";
}else if (cue.IsBroken)
{
return "broken";
}
else
{
return " ";
}
}
}
public bool IsSelected {
get
{
@@ -0,0 +1,87 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using QControlKit;
using QControlKit.Events;
namespace QControlKitXamDemo.ViewModels
{
public class QServerViewModel : ObservableCollection<QWorkspaceViewModel>, INotifyPropertyChanged
{
QServer server;
public new event PropertyChangedEventHandler PropertyChanged;
public string name
{
get => server.name;
set => server.name = value;
}
public string host => server.host;
public string version => server.version == null ? "" : $"v{server.version}";
public string GroupName => $"{name} ({host})";
public QServerViewModel(QServer server)
{
this.server = server;
name = this.server.name;
foreach (QWorkspace workspace in this.server.workspaces)
{
Device.BeginInvokeOnMainThread(() =>
{
Add(new QWorkspaceViewModel(workspace));
});
}
this.server.WorkspaceAdded += Server_WorkspaceAdded;
this.server.WorkspaceRemoved += Server_WorkspaceRemoved;
this.server.ServerUpdated += OnServerUpdated;
}
private void Server_WorkspaceRemoved(object source, QServerWorkspaceChangedArgs args)
{
QWorkspaceViewModel workspaceToRemove = null;
foreach (QWorkspaceViewModel workspaceViewModel in this)
{
if (workspaceViewModel.workspace.uniqueID == args.workspace.uniqueID)
{
workspaceToRemove = workspaceViewModel;
break;
}
}
if (workspaceToRemove != null)
{
Device.BeginInvokeOnMainThread(() =>
{
Remove(workspaceToRemove);
});
}
}
private void Server_WorkspaceAdded(object source, QServerWorkspaceChangedArgs args)
{
QWorkspaceViewModel workspaceViewModel = new QWorkspaceViewModel(args.workspace);
Device.BeginInvokeOnMainThread(() =>
{
Add(workspaceViewModel);
});
}
private void OnServerUpdated(object source, QServerUpdatedArgs args)
{
OnPropertyChanged("GroupName");
OnPropertyChanged("version");
}
void OnPropertyChanged([CallerMemberName] string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
@@ -0,0 +1,17 @@
using QControlKit;
namespace QControlKitXamDemo.ViewModels
{
public class QWorkspaceViewModel
{
public QWorkspace workspace;
public QWorkspaceViewModel(QWorkspace workspace)
{
this.workspace = workspace;
}
public string Name => workspace.name;
public bool HasPasscode => workspace.hasPasscode;
}
}