Clean up legacy control classes and code

This commit is contained in:
2021-03-28 15:37:35 -05:00
parent 2bca5c8a13
commit ce64f24b4b
39 changed files with 47 additions and 2392 deletions
-2
View File
@@ -1,7 +1,6 @@
using Acr.UserDialogs;
using Xamarin.Forms;
using Xamarin.Essentials;
using qController.Communication;
using qController.Pages;
using System;
using QControlKit;
@@ -19,7 +18,6 @@ namespace qController
public static double Width;
public static double HeightUnit;
public static double WidthUnit;
public static QController qControllerToResume;
public static bool MenuIsPresented
{
get
@@ -1,72 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
using qController.QItems;
namespace qController.Communication
{
public class CueEventArgs : EventArgs
{
public QCue Cue
{
get;
set;
}
}
public class WorkspaceEventArgs : EventArgs
{
public QOldWorkspace UpdatedWorkspace
{
get;
set;
}
}
public class PlaybackPositionArgs : EventArgs
{
public string PlaybackPosition
{
get;
set;
}
}
public class ConnectEventArgs : EventArgs
{
public string Status
{
get;
set;
}
public string WorkspaceId
{
get;
set;
}
}
public class WorkspaceInfoArgs : EventArgs
{
public List<QControlKit.QWorkspaceInfo> WorkspaceInfo
{
get;
set;
}
}
public class ChildrenEventArgs : EventArgs
{
public string cue_id
{
get;
set;
}
public List<QCue> children
{
get;
set;
}
}
}
@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace qController.Communication
{
public delegate void SelectedCueUpdatedHandler(object source, CueEventArgs args);
public delegate void WorkspaceUpdatedHandler(object source, WorkspaceEventArgs args);
public delegate void CueInfoUpdatedHandler(object source, CueEventArgs args);
public delegate void PlaybackPositionUpdatedHandler(object source, PlaybackPositionArgs args);
public delegate void ConnectionStatusHandler(object source, ConnectEventArgs args);
public delegate void ChildrenUpdateHandler(object source, ChildrenEventArgs args);
public delegate void WorkspaceDisconnectHandler(object source, EventArgs args);
public delegate void WorkspaceInfoHandler(object source, WorkspaceInfoArgs args);
public delegate void WorkspaceLoadErrorHandler(object source, WorkspaceEventArgs args);
}
@@ -1,109 +0,0 @@
//Class used to facilitate communication with a QInstance
//Listens for incoming messages via qReceiver (UDP) and tcpClient (TCP)
//TODO: WEIRD MESSAGE PARSING RULES NEED FIXED
using System;
using SharpOSC;
using Serilog;
namespace qController.Communication
{
public class QClient
{
private TCPClient tcpClient;
public QParser qParser;
string Address;
int Port;
public bool connected;
public QClient(string address, int port)
{
Address = address;
Port = port;
qParser = new QParser();
tcpClient = new TCPClient(Address, Port);
connected = tcpClient.Connect();
tcpClient.MessageReceived += OnMessageReceived;
}
//one method for messages received whether from TCP or UDP (SAME MSG FORMAT)
private void OnMessageReceived(object source, MessageEventArgs args)
{
Log.Debug("QCLIENT - Message Received: " + args.Message.Address);
//TODO: find a better filtering process
if (!args.Message.Address.Contains("update"))
qParser.ParseMessage(args.Message);
else if (args.Message.Address.Contains("playbackPosition") || args.Message.Address.Contains("cueList") || args.Message.Address.Contains("dashboard"))
qParser.ParseMessage(args.Message);
else if (args.Message.Address.Contains("cue_id"))
{
var parts = args.Message.Address.Split('/');
UpdateSpecificCue(parts[3], parts[5]);
}
else
ProcessUpdate(args.Message);
}
public void sendTCP(string address)
{
//Log.Debug($"QCLIENT - TCP Sent with address: {address}");
try
{
tcpClient.Send(new OscMessage(address));
}
catch (Exception ex)
{
Log.Debug("QCLIENT - Send and Receive Exception: " + ex.Message);
}
}
public void sendTCP(string address, params object[] args)
{
//Log.Debug($"QCLIENT - TCP Sent with address: {address}");
try
{
tcpClient.Send(new OscMessage(address, args));
}
catch (Exception ex)
{
Log.Debug("QCLIENT - Send and Receive w/Args Exception: " + ex.Message);
}
}
public void ProcessUpdate(OscMessage msg)
{
Log.Debug("QCLIENT - Process Update: " + msg.Address);
if (msg.Address.Contains("workspace"))
{
UpdateWorkspace("not yet implemented");
}
}
public void UpdateSpecificCue(string workspace_id,string cue_id)
{
string valuesForKeys = "[\"number\",\"uniqueID\",\"flagged\",\"listName\",\"type\",\"colorName\",\"name\",\"armed\",\"displayName\",\"isBroken\",\"isLoaded\",\"isPaused\",\"isRunning\",\"preWait\",\"duration\",\"postWait\",\"translationX\",\"translationY\",\"opacity\",\"scaleX\",\"scaleY\",\"notes\",\"levels\"]";
string address = "/workspace/" + workspace_id + "/cue_id/" + cue_id + "/valuesForKeys";
sendTCP(address, valuesForKeys);
}
public void UpdateSelectedCue(string workspace_id)
{
string valuesForKeys = "[\"number\",\"uniqueID\",\"flagged\",\"listName\",\"type\",\"colorName\",\"name\",\"armed\",\"displayName\",\"isBroken\",\"isLoaded\",\"isPaused\",\"isRunning\",\"preWait\",\"duration\",\"postWait\",\"translationX\",\"translationY\",\"opacity\",\"scaleX\",\"scaleY\",\"notes\",\"levels\"]";
string address = "/workspace/" + workspace_id + "/cue/selected/valuesForKeys";
sendTCP(address, valuesForKeys);
}
public void UpdateWorkspace(string ws_id)
{
Log.Debug("QCLIENT - Workspace needs to be updated: " + ws_id);
}
public void Close()
{
tcpClient.Close();
}
}
}
@@ -1,81 +0,0 @@
//Class used for overall Workspace control (ONLY REPRESENTS ONE WORKSPACE)
//Contains all necessary items to facilitate communication (sending, receiving) to a QLab workspace
//Also contains the local QWorkspace object which stores all the information that is used in displaying
//TODO: STILL NEED TO WORK ON PASSWORD PROTECTED WORKSPACES
using Serilog;
using qController.QItems;
namespace qController.Communication
{
public class QController
{
public QUpdater qUpdater;
public QClient qClient;
public QOldWorkspace qWorkspace;
public string playbackPosition;
private string ipAddress;
private int port;
public QController(string address, int port)
{
this.port = port;
this.ipAddress = address;
qClient = new QClient(ipAddress, port);
qUpdater = new QUpdater(this);
}
public void Connect(string workspace_id)
{
Log.Debug($"QCONTROLLER - Connect Called: {workspace_id}");
qWorkspace = new QOldWorkspace(workspace_id);
qClient.sendTCP("/workspace/"+workspace_id+"/connect");
}
public void Connect(string workspace_id, string passcode)
{
Log.Debug($"QCONTROLLER - Connect with Passcode Called: {workspace_id}:{passcode}");
qWorkspace = new QOldWorkspace(workspace_id);
qClient.sendTCP("/workspace/" + workspace_id + "/connect", passcode);
}
public void Connect(QOldWorkspace workspace)
{
if(workspace.passcode != null)
Connect(workspace.workspace_id, workspace.passcode);
else
Connect(workspace.workspace_id);
}
public void KickOff()
{
Log.Debug($"QCONTROLLER - Searching for workspaces on: {ipAddress}");
qClient.sendTCP("/workspaces");
}
public void Disconnect()
{
Log.Debug($"QCONTROLLER - Disconnecting from: {qWorkspace.workspace_id}");
qClient.sendTCP("/workspace/"+qWorkspace.workspace_id+"/disconnect");
}
public void Resume()
{
//qClient = new QClient(ipAddress, port);
//qUpdater = new QUpdater(this);
if(qWorkspace != null)
{
Log.Debug($"QCONTROLLER - Resuming: {qWorkspace.workspace_id}");
}
}
public void Kill(){
Log.Debug("QCONTROLLER - Killing");
if (qClient != null)
qClient.Close();
}
}
}
@@ -1,23 +0,0 @@
//Currently unused would like to reimplement eventually
//Currently being done in qConnectionPage.xaml.cs Scan() method
//TODO: Implement this along with a way of getting active Instances and # of workspaces for an instance
using Serilog;
using System.Collections.Generic;
using Zeroconf;
namespace qController.Communication
{
public class QFinder
{
public QFinder()
{
}
public async void SearchForWorkspaces()
{
Log.Debug("QFINDER - Scanning Started");
IReadOnlyList<IZeroconfHost> results = await ZeroconfResolver.ResolveAsync("_qlab._udp.local.");
Log.Debug("QFINDER - Scanning Done");
}
}
}
@@ -1,184 +0,0 @@
using System;
using SharpOSC;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using System.Collections.Generic;
using qController.QItems;
using Serilog;
namespace qController.Communication
{
public class QParser
{
public QParser()
{
}
public event WorkspaceLoadErrorHandler WorkspaceLoadError;
public event WorkspaceInfoHandler WorkspaceInfoReceived;
public event WorkspaceDisconnectHandler WorkspaceDisconnect;
public event ChildrenUpdateHandler ChildrenUpdated;
public event ConnectionStatusHandler ConnectionStatusChanged;
public event SelectedCueUpdatedHandler SelectedCueUpdated;
public event WorkspaceUpdatedHandler WorkspaceUpdated;
public event CueInfoUpdatedHandler CueInfoUpdated;
public event PlaybackPositionUpdatedHandler PlaybackPositionUpdated;
public void ParseMessage(OscMessage msg){
if (!msg.Address.Contains("null"))
{
if (msg.Address.Contains("valuesForKeys"))
{
ParseCueUpdateInfo(msg);
}
else if (msg.Address.Contains("cueLists"))
ParseWorkspaceInfo(msg);
else if (msg.Address.Contains("playbackPosition"))
ParsePositionUpdateInfo(msg);
else if (msg.Address.Contains("thump"))
Log.Debug("QPARSER - Heartbeat Received");
else if (msg.Address.Contains("disconnect"))
OnWorkspaceDisconnect();
else if (msg.Address.Contains("connect"))
ParseConnectInfo(msg);
else if (msg.Address.Contains("children"))
ParseChildrenInfo(msg);
else if (msg.Address.Contains("workspaces"))
ParseQInfo(msg);
else
{
Log.Debug("QPARSER - Unkown message type: " + msg.Address);
foreach (var item in msg.Arguments)
{
Log.Debug(item.ToString());
}
}
}
}
public void ParseConnectInfo(OscMessage msg)
{
if (msg.Arguments.Count > 0)
{
JToken connectStatus = OSC2JSON(msg);
;
OnConnectionStatusChanged(connectStatus.ToString(), msg.Address.Split('/')[3]);
}
}
public void ParsePositionUpdateInfo(OscMessage msg)
{
if(msg.Arguments.Count > 0)
{
OnPlaybackPositionUpdated(msg.Arguments[0].ToString());
}
}
public void ParseCueUpdateInfo(OscMessage msg)
{
JToken cueUpdate = OSC2JSON(msg);
QCue cue = JsonConvert.DeserializeObject<QCue>(cueUpdate.ToString());
OnCueInfoUpdated(cue);
}
public void ParseSelectedCueInfo(OscMessage msg){
JToken selectedCue = OSC2JSON(msg);
QCue cue = JsonConvert.DeserializeObject<QCue>(selectedCue.ToString());
OnSelectedCueUpdated(cue);
}
public void ParseQInfo(OscMessage msg)
{
JToken qInfo = OSC2JSON(msg);
List<QControlKit.QWorkspaceInfo> qWorkspaceInfo = JsonConvert.DeserializeObject<List<QControlKit.QWorkspaceInfo>>(qInfo.ToString());
OnWorkspaceInfoReceived(qWorkspaceInfo);
}
public void ParseWorkspaceInfo(OscMessage msg)
{
if (msg.Arguments.Count > 0)
{
var parts = msg.Address.Split('/');
string id = parts[3];
if (msg.Arguments[0].ToString() != "")
{
try
{
QOldWorkspace workspace = JsonConvert.DeserializeObject<QOldWorkspace>(msg.Arguments[0].ToString());
OnWorkspaceUpdated(workspace);
}
catch (Exception ex)
{
Log.Debug($"QPARSER - Workspace Load Error: {ex.ToString()}");
OnWorkspaceLoadError(id);
}
}
else
{
OnWorkspaceLoadError(id);
}
}
}
public void ParseChildrenInfo(OscMessage msg)
{
if (msg.Arguments.Count > 0)
{
string cue_id = msg.Address.Split('/')[3];
JToken children_obj = OSC2JSON(msg);
List<QCue> children = JsonConvert.DeserializeObject<List<QCue>>(children_obj.ToString());
OnChildrenUpdated(cue_id, children);
}
}
public JToken OSC2JSON(OscMessage Msg){
JObject json = JObject.Parse(Msg.Arguments.ToArray()[0].ToString());
return json.GetValue("data");
}
protected virtual void OnWorkspaceUpdated(QOldWorkspace workspace)
{
WorkspaceUpdated?.Invoke(this, new WorkspaceEventArgs() { UpdatedWorkspace = workspace });
}
protected virtual void OnSelectedCueUpdated(QCue cue)
{
SelectedCueUpdated?.Invoke(this, new CueEventArgs() { Cue = cue });
}
protected virtual void OnCueInfoUpdated(QCue cue)
{
CueInfoUpdated?.Invoke(this, new CueEventArgs() { Cue = cue });
}
protected virtual void OnPlaybackPositionUpdated(string id)
{
PlaybackPositionUpdated?.Invoke(this, new PlaybackPositionArgs() { PlaybackPosition = id });
}
protected virtual void OnConnectionStatusChanged(string status, string workspace_id)
{
ConnectionStatusChanged?.Invoke(this, new ConnectEventArgs() { Status = status, WorkspaceId = workspace_id });
}
protected virtual void OnChildrenUpdated(string id, List<QCue> cues)
{
ChildrenUpdated?.Invoke(this, new ChildrenEventArgs() { cue_id = id, children = cues });
}
protected virtual void OnWorkspaceDisconnect()
{
WorkspaceDisconnect?.Invoke(this, new EventArgs());
}
protected virtual void OnWorkspaceInfoReceived(List<QControlKit.QWorkspaceInfo> workspaces)
{
WorkspaceInfoReceived?.Invoke(this, new WorkspaceInfoArgs() { WorkspaceInfo = workspaces });
}
protected virtual void OnWorkspaceLoadError(string id)
{
WorkspaceLoadError?.Invoke(this, new WorkspaceEventArgs { UpdatedWorkspace = new QOldWorkspace(id) });
}
}
}
@@ -1,32 +0,0 @@
using System;
using System.Net;
using SharpOSC;
namespace qController
{
public class QSender
{
TCPSender tcpSender;
public QSender(string address, int port)
{
tcpSender = new TCPSender(address, port);
tcpSender.MessageReceived += OnMessageReceived;
}
void OnMessageReceived(object source, MessageEventArgs args)
{
Console.WriteLine("New Message Received");
}
public void sendString(string address) {
tcpSender.Send(new OscMessage(address));
}
public void sendArgs(string address, object args){
tcpSender.Send(new OscMessage(address, args));
}
}
}
@@ -1,69 +0,0 @@
//Class used for updating local "copy" of workspace info, cue-lists, cues, etc.
using Serilog;
namespace qController.Communication
{
public class QUpdater
{
private QController qController;
public QUpdater(QController controller)
{
qController = controller;
qController.qClient.qParser.CueInfoUpdated += OnCueUpdateReceived;
qController.qClient.qParser.WorkspaceUpdated += OnWorkspaceUpdated;
qController.qClient.qParser.PlaybackPositionUpdated += OnPlaybackPositionUpdated;
qController.qClient.qParser.WorkspaceLoadError += OnWorkspaceLoadError;
qController.qClient.qParser.ConnectionStatusChanged += OnConnectionStatusChanged;
}
private void OnConnectionStatusChanged(object source, ConnectEventArgs args)
{
Log.Debug("QUPDATER - Connection Status Changed: " + args.Status);
if (args.Status == "ok")
{
qController.qClient.sendTCP("/workspace/" + qController.qWorkspace.workspace_id + "/updates", 1);
qController.qClient.sendTCP("/workspace/" + qController.qWorkspace.workspace_id + "/cueLists");
}
}
private void OnWorkspaceLoadError(object source, WorkspaceEventArgs args)
{
Log.Debug("QUPDATER - Loading cuelists has failed for some reason retrying");
}
public void OnCueUpdateReceived(object source, CueEventArgs args)
{
qController.qWorkspace.UpdateCue(args.Cue);
if (args.Cue.type == "Group")
{
//Log.Debug("QUpdater/Updated cue was group cue, sending children request");
qController.qClient.sendTCP("/workspace/"+qController.qWorkspace.workspace_id+"/cue_id/" + args.Cue.uniqueID + "/children");
}
}
public void OnWorkspaceUpdated(object source, WorkspaceEventArgs args)
{
qController.qWorkspace = args.UpdatedWorkspace;
qController.playbackPosition = null;
qController.qWorkspace.CheckPopulated();
if (qController.qWorkspace.IsPopulated)
{
Log.Debug("QUPDATER - Workspace group cues are already populated");
App.showToast("Workspace cues have been loaded....");
//get selected cue
qController.qClient.UpdateSelectedCue(qController.qWorkspace.workspace_id);
return;
}
}
public void OnPlaybackPositionUpdated(object source, PlaybackPositionArgs args)
{
qController.playbackPosition = args.PlaybackPosition;
Log.Debug("QUPDATER - Update Specific Cue Called because of Playback Position Updated");
qController.qClient.UpdateSpecificCue(qController.qWorkspace.workspace_id,args.PlaybackPosition);
}
}
}
@@ -2,7 +2,7 @@
//IMPLEMENT A ISREACHABLE METHOD TO BE ABLE TO DISPLAY ONLINE QINSTANCES
using System.Net;
namespace qController.Communication
namespace qController.Helpers
{
public class IPHelper
{
@@ -1,51 +0,0 @@
using System.Collections.Generic;
namespace qController.QItems
{
public class QCue
{
//info loaded from "/cueLists"
public string number { get; set; }
public string uniqueID { get; set; }
public bool flagged { get; set; }
public string listName { get; set; }
public string type { get; set; }
public string colorName { get; set; }
public string name { get; set; }
public bool armed { get; set; }
//info loaded from "/valuesForKeys
public decimal translationX { get; set; }
public bool isRunning { get; set; }
public decimal scaleX { get; set; }
public bool isPaused { get; set; }
public decimal translationY { get; set; }
public decimal preWait { get; set; }
public decimal opacity { get; set; }
public decimal scaleY { get; set; }
public decimal duration { get; set; }
public decimal postWait { get; set; }
public bool isBroken { get; set; }
public string displayName { get; set; }
public bool isLoaded { get; set; }
public string notes { get; set; }
public List<List<double>> levels { get; set; }
//info loaded from /children
public List<QCue> cues { get; set; }
//get string for icon font from cue type
public string getIconString()
{
return QIcon.GetIconFromType(type);
}
public string IconText
{
get
{
return QIcon.GetIconFromType(type);
}
}
}
}
@@ -1,26 +0,0 @@
using System.Collections.Generic;
using qController.Helpers;
namespace qController.QItems
{
public class QCueList
{
public string number { get; set; }
public string uniqueID { get; set; }
public List<QCue> cues { get; set; }
public bool flagged { get; set; }
public string listName { get; set; }
public string type { get; set; }
public string colorName { get; set; }
public string name { get; set; }
public bool armed { get; set; }
public string IconText
{
get
{
return IconConstants.FormatListBulleted;
}
}
}
}
@@ -1,10 +0,0 @@
using System;
namespace qController.Classes.Cues
{
public class QDefinitions
{
public QDefinitions()
{
}
}
}
@@ -1,120 +0,0 @@
//Class pertains to the local copy of a workspace, with methods for fetching/updating information inside
using System.Collections.Generic;
using Serilog;
namespace qController.QItems
{
public class QOldWorkspace
{
public string status { get; set; }
public List<QCueList> data { get; set; }
public string workspace_id { get; set; }
public string address { get; set; }
public bool IsPopulated { get; set; }
public string passcode { get; set; }
//ONLY FOR PASSING A WORKSPACE LOAD ERROR
public QOldWorkspace(string id)
{
workspace_id = id;
}
public QOldWorkspace(string id, string passcode)
{
workspace_id = id;
this.passcode = passcode;
}
public QOldWorkspace()
{
}
public QCue GetCue(string cue_id)
{
foreach (var cueList in data)
{
foreach (var cue in cueList.cues)
{
if(cue.uniqueID == cue_id)
{
return cue;
}
}
}
return null;
}
public QCueList GetCueList(string id)
{
foreach (var cueList in data)
{
if(cueList.uniqueID == id)
{
return cueList;
}
}
return null;
}
public void UpdateCue(QCue cue)
{
for (int i = 0; i < data.Count; i++)
{
for (int j = 0; j < data[i].cues.Count; j++)
{
if(data[i].cues[j].uniqueID == cue.uniqueID)
{
//Log.Debug("Cue found and updated in workspace: " + cue.uniqueID);
data[i].cues[j] = cue;
return;
}
}
}
}
public void UpdateChildren(string cue_id, List<QCue> children)
{
for (int i = 0; i < data.Count; i++)
{
for (int j = 0; j < data[i].cues.Count; j++)
{
if (data[i].cues[j].uniqueID == cue_id)
{
if(data[i].cues[j].type == "Group")
{
data[i].cues[j].cues = children;
return;
}
}
}
}
}
public bool CheckPopulated()
{
for (int i = 0; i < data.Count; i++)
{
for (int j = 0; j < data[i].cues.Count; j++)
{
if (data[i].cues[j].type == "Group" && data[i].cues[j].cues == null)
{
IsPopulated = false;
return false;
}
}
}
IsPopulated = true;
return true;
}
public void PrintStats()
{
foreach (var cueList in data)
{
Log.Debug(cueList.listName + "("+ cueList.cues.Count + " cues)");
}
}
}
}
@@ -1,15 +0,0 @@
using qController.QItems;
namespace qController
{
public class NoQCueSelected : QCue
{
public NoQCueSelected()
{
listName = "No Cue Selected";
type = "";
notes = "Workspace has loaded but no cue is selected";
number = "!";
}
}
}
@@ -1,11 +0,0 @@
namespace qController
{
public class OSCListItem
{
public string Text { get; set; }
public string Icon { get; set; }
public string Command { get; set; }
}
}
@@ -1,17 +0,0 @@
namespace qController
{
public class QCommand
{
public string osc;
public string text;
public string type;
public QCommand(string display, string cmd, string command_type)
{
text = display;
osc = cmd;
type = command_type;
}
}
}
@@ -1,17 +0,0 @@
namespace qController
{
public static class QCommands
{
public static QCommand GO = new QCommand("GO", "/go","WORKSPACE");
public static QCommand PANIC = new QCommand("Panic", "/panic","WORKSPACE");
public static QCommand PAUSE = new QCommand("Pause", "/pause","WORKSPACE");
public static QCommand PREVIEW = new QCommand("Preview", "/preview","WORKSPACE");
public static QCommand RESUME = new QCommand("Resume", "/resume","WORKSPACE");
public static QCommand PREVIOUS = new QCommand("Previous","/select/previous","WORKSPACE");
public static QCommand NEXT = new QCommand("Next", "/select/next","WORKSPACE");
public static QCommand RESET = new QCommand("Reset","/reset","WORKSPACE");
public static QCommand STOP = new QCommand("Stop","/stop","WORKSPACE");
public static QCommand HARDSTOP = new QCommand("Hard Stop", "/hardStop","WORKSPACE");
}
}
@@ -1,5 +1,4 @@
using Xamarin.Forms;
//Legacy but needed incase there are still instances stored in this way
namespace qController.QItems
{
public class QInstance
@@ -17,7 +16,7 @@ namespace qController.QItems
}
public bool IsReachable()
{
return qController.Communication.IPHelper.IsReachable(address);
return qController.Helpers.IPHelper.IsReachable(address);
}
}
}
@@ -9,18 +9,43 @@ namespace qController
public class QStorage
{
public static ObservableCollection<QInstance> qInstances;
public static ObservableCollection<QServerInfo> qStoredServers;
public static QRecentWorkspaceInfo recentWorkspaceInfo;
static QStorage(){
qInstances = (ObservableCollection<QInstance>)CrossSettings.Current.GetValue(new ObservableCollection<QInstance>().GetType(), "qInstances", null);
qStoredServers = (ObservableCollection<QServerInfo>)CrossSettings.Current.GetValue(new ObservableCollection<QInstance>().GetType(), "qStoredServers", null);
recentWorkspaceInfo = (QRecentWorkspaceInfo)CrossSettings.Current.GetValue(typeof (QRecentWorkspaceInfo),"recentWorkspaceInfo",null);
if (qInstances == null)
{
Log.Debug("QSTORAGE - No qInstance exists creating one");
Log.Debug("[QStorage] No qInstances exists creating one");
qInstances = new ObservableCollection<QInstance>();
UpdateStorage();
}
if (qStoredServers == null)
{
Log.Debug("[QStorage] No qStoredServers exists creating one");
qStoredServers = new ObservableCollection<QServerInfo>();
UpdateStorage();
}
if (qInstances != null)
{
Log.Debug("[QStorage] qInstances exists though so load in those instances as servers");
foreach (QInstance instance in qInstances)
{
QServerInfo serverInfo = new QServerInfo();
serverInfo.host = instance.address.ToString();
//TODO: port?
qStoredServers.Add(serverInfo);
}
UpdateStorage();
}
}
public static void UpdateRecentWorkspace(QRecentWorkspaceInfo packagedInfo)
@@ -59,6 +84,7 @@ namespace qController
private static void UpdateStorage(){
CrossSettings.Current.SetValue("qInstances", qInstances);
CrossSettings.Current.SetValue("qStoredServers", qStoredServers);
}
public static bool Contains(string name, string address){
@@ -1,45 +0,0 @@
using Xamarin.Forms;
namespace qController.UI.Buttons
{
public class QButton : Button
{
public QCommand qCommand
{
get;
set;
}
public QButton(QCommand command)
{
qCommand = command;
Text = qCommand.text;
TextColor = Color.Black;
FontSize = App.HeightUnit * 2.5;
if (qCommand.osc.Contains("go"))
{
BackgroundColor = Color.SeaGreen;
switch (Device.RuntimePlatform)
{
case Device.iOS:
FontSize = App.HeightUnit * 4;
FontAttributes = FontAttributes.Bold;
break;
case Device.Android:
FontSize = App.HeightUnit * 4;
FontAttributes = FontAttributes.Bold;
break;
}
}
else if (qCommand.osc.Contains("panic"))
{
BackgroundColor = Color.IndianRed;
}
else
{
BackgroundColor = Color.FromHex("D8D8D8");
}
}
}
}
@@ -1,29 +0,0 @@
using Xamarin.Forms;
namespace qController.UI.Buttons
{
public class QLevelsButton : ShadowButton
{
public Button button;
public QLevelsButton()
{
button = new Button
{
Text = QIcon.SLIDERS,
TextColor = Color.Black,
FontFamily = (OnPlatform<string>)Application.Current.Resources["QFontFamily"],
FontSize = App.HeightUnit * 4,
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
HeightRequest = App.HeightUnit * 8,
WidthRequest = App.HeightUnit * 8,
CornerRadius = (int)(App.HeightUnit * 4),
BackgroundColor = Color.LightBlue
};
HeightRequest = button.Height;
WidthRequest = button.Width;
CornerRadius = button.CornerRadius;
Content = button;
}
}
}
@@ -1,128 +0,0 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
using qController.UI.Buttons;
namespace qController.UI.Cells
{
public class QControlsBlock : Frame
{
Grid mainG;
EventHandler callback;
public QControlsBlock(EventHandler callback)
{
this.callback = callback;
Margin = new Thickness(10);
Padding = new Thickness(0);
IsVisible = true;
BackgroundColor = Color.Transparent;
//highlight Button Grid
//BackgroundColor = Color.FromHex("FF0000");
List<QCommand> commands = new List<QCommand>();
commands.Add(QCommands.PREVIOUS);
commands.Add(QCommands.PAUSE);
commands.Add(QCommands.NEXT);
commands.Add(QCommands.PREVIEW);
commands.Add(QCommands.PANIC);
commands.Add(QCommands.RESUME);
//setCustomButtons(commands);
setDefaultButtons();
}
void setDefaultButtons()
{
List<QButton> buttons = new List<QButton>();
buttons.Add(new QButton(QCommands.PREVIOUS));
buttons.Add(new QButton(QCommands.PANIC));
buttons.Add(new QButton(QCommands.NEXT));
buttons.Add(new QButton(QCommands.PREVIEW));
buttons.Add(new QButton(QCommands.PAUSE));
buttons.Add(new QButton(QCommands.RESUME));
mainG = new Grid
{
Padding = new Thickness(0),
RowDefinitions = {
new RowDefinition{Height = GridLength.Star},
new RowDefinition{Height = GridLength.Star},
new RowDefinition{Height = GridLength.Star}
},
ColumnDefinitions = {
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)}
},
Margin = new Thickness(0)
};
int row = 0;
int column = 0;
for (int i = 0; i < buttons.Count; i++)
{
QButton b = buttons[i];
b.Clicked += callback;
mainG.Children.Add(b, column, row);
row++;
if (row == 3)
{
row = 0;
column = 2;
}
}
QButton goButton = new QButton(QCommands.GO);
goButton.Clicked += callback;
mainG.Children.Add(goButton, 1, 0);
Grid.SetRowSpan(goButton, 3);
Content = mainG;
}
public void setCustomButtons(List<QCommand> commands)
{
mainG = new Grid
{
Padding = new Thickness(0),
RowDefinitions = {
new RowDefinition{Height = GridLength.Auto},
new RowDefinition{Height = GridLength.Auto},
new RowDefinition{Height = GridLength.Auto}
},
ColumnDefinitions = {
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)}
},
Margin = new Thickness(0)
};
int row = 0;
int column = 0;
for (int i = 0; i < commands.Count; i++)
{
QButton b = new QButton(commands[i]);
b.Clicked += callback;
mainG.Children.Add(b, column, row);
row++;
if (row == 3)
{
row = 0;
column = 2;
}
}
QButton goButton = new QButton(QCommands.GO);
goButton.Clicked += callback;
mainG.Children.Add(goButton, 1, 0);
Grid.SetRowSpan(goButton, 3);
Content = mainG;
}
}
}
@@ -1,122 +0,0 @@
using System.Collections.ObjectModel;
using Xamarin.Forms;
using qController.QItems;
namespace qController.UI.Cells
{
public class QCueListCell : Frame
{
public ListView cueListView;
public Button closeButton;
public ObservableCollection<OSCListItem> items;
public QCueListCell(QCueList qCueList)
{
Margin = new Thickness(10);
Padding = new Thickness(10);
items = new ObservableCollection<OSCListItem>();
cueListView = new ListView
{
ItemsSource = items,
RowHeight = (int)(App.HeightUnit * 6),
ItemTemplate = new DataTemplate(() =>
{
var grid = new Grid { Padding = new Thickness(5, 10) };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(30) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
var icon = new Label();
icon.FontFamily = (OnPlatform<string>)Application.Current.Resources["QFontFamily"];
icon.SetBinding(Label.TextProperty, "Icon");
icon.HorizontalTextAlignment = TextAlignment.Center;
icon.VerticalTextAlignment = TextAlignment.Center;
icon.FontSize = App.HeightUnit * 3;
var label = new Label { VerticalOptions = LayoutOptions.FillAndExpand };
label.SetBinding(Label.TextProperty, "Text");
if (label.Text == "Disconnect")
{
label.TextColor = Color.DarkRed;
}
switch (Device.RuntimePlatform)
{
case Device.iOS:
label.FontSize = App.HeightUnit * 3;
break;
case Device.Android:
label.FontSize = App.HeightUnit * 2.2;
break;
}
grid.Children.Add(icon);
grid.Children.Add(label, 1, 0);
return new ViewCell { View = grid };
})
};
StackLayout layout = new StackLayout();
closeButton = new Button
{
BackgroundColor = Color.Gray,
Text = "Close",
TextColor = Color.White
};
layout.Children.Add(closeButton);
layout.Children.Add(cueListView);
Content = layout;
for(int j=0; j < qCueList.cues.Count; j++)
{
var cue = qCueList.cues[j];
AddSubCues(cue, 0);
}
}
public void AddSubCues(QCue cue, int level)
{
var cueIcon = cue.getIconString();
var cueTitle = "";
for (int i = 0; i < level; i++)
{
cueTitle += " ";
}
if (cue.number != "")
{
cueTitle += cue.number + " - " + cue.listName;
}
else
{
cueTitle += cue.listName;
}
if (cue.cues != null)
{
items.Add(new OSCListItem
{
Text = cueTitle,
Icon = cueIcon,
Command = "/select_id/" + cue.uniqueID
});
//uncomment to load nested group cues
for (int i = 0; i < cue.cues.Count; i++)
{
var sub_cue = cue.cues[i];
AddSubCues(sub_cue, level + 1);
}
}
else
{
items.Add(new OSCListItem
{
Text = cueTitle,
Icon = cueIcon,
Command = "/select_id/" + cue.uniqueID
});
}
}
}
}
@@ -1,121 +0,0 @@
using System;
using Xamarin.Forms;
using Serilog;
using qController.Helpers;
namespace qController.UI.Cells
{
public class QInstanceCell : ViewCell
{
public QInstanceCell()
{
Label nameLabel = new Label();
Label addressLabel = new Label();
Label connectLabel = new Label();
Label deleteLabel = new Label();
var connectTapGesture = new TapGestureRecognizer();
var deleteTapGesture = new TapGestureRecognizer();
connectTapGesture.Tapped += Connect;
connectLabel.GestureRecognizers.Add(connectTapGesture);
deleteTapGesture.Tapped += Delete;
deleteLabel.GestureRecognizers.Add(deleteTapGesture);
InitItems();
//SET BINDINGS
nameLabel.SetBinding(Label.TextProperty, new Binding("name"));
addressLabel.SetBinding(Label.TextProperty,new Binding("address"));
Grid mainG = new Grid
{
//Padding = new Thickness(10),
RowDefinitions = {
new RowDefinition{Height = GridLength.Auto},
new RowDefinition{Height = GridLength.Auto}
},
ColumnDefinitions = {
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(4,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)}
}
};
mainG.Children.Add(nameLabel, 1, 0);
mainG.Children.Add(addressLabel,1,1);
mainG.Children.Add(connectLabel,2,0);
Grid.SetRowSpan(connectLabel,2);
mainG.Children.Add(deleteLabel, 0, 0);
Grid.SetRowSpan(deleteLabel,2);
Frame f = new Frame
{
Content = mainG,
BorderColor = Color.Black,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
Padding = 0,
Margin = new Thickness(10,10,10,10),
CornerRadius=20
};
f.SetDynamicResource(Frame.BackgroundColorProperty, "WorkspaceCellBackgroundColor");
View = f;
void InitItems()
{
nameLabel.HorizontalTextAlignment = TextAlignment.Center;
nameLabel.VerticalTextAlignment = TextAlignment.End;
nameLabel.FontAttributes = FontAttributes.Bold;
nameLabel.FontSize = App.HeightUnit * 3;
nameLabel.Margin = new Thickness(0, 20, 0, 0);
nameLabel.SetDynamicResource(Label.TextColorProperty, "PrimaryTextColor");
addressLabel.HorizontalTextAlignment = TextAlignment.Center;
addressLabel.VerticalTextAlignment = TextAlignment.Start;
addressLabel.FontSize = App.HeightUnit * 2.5;
addressLabel.Margin = new Thickness(0, 0, 0, 20);
addressLabel.SetDynamicResource(Label.TextColorProperty, "PrimaryTextColor");
connectLabel.HorizontalOptions = LayoutOptions.StartAndExpand;
connectLabel.VerticalOptions = LayoutOptions.CenterAndExpand;
connectLabel.Text = IconConstants.Wifi;
connectLabel.FontSize = App.HeightUnit * 5;
connectLabel.TextColor = Color.LimeGreen;
deleteLabel.HorizontalOptions = LayoutOptions.CenterAndExpand;
deleteLabel.VerticalOptions = LayoutOptions.CenterAndExpand;
deleteLabel.Text = IconConstants.Delete;
deleteLabel.FontSize = App.HeightUnit * 5;
deleteLabel.TextColor = Color.Red;
connectLabel.FontFamily = (OnPlatform<string>)Application.Current.Resources["MaterialFontFamily"];
deleteLabel.FontFamily = (OnPlatform<string>)Application.Current.Resources["MaterialFontFamily"];
}
void Delete(object sender, EventArgs e)
{
Log.Debug("QINSTANCECELL - Delete " + nameLabel.Text + "," + addressLabel.Text + " Pressed");
QStorage.RemoveInstance(nameLabel.Text,addressLabel.Text);
}
void Connect(object sender, EventArgs e)
{
App.NavigationPage.Navigation.PushAsync(new ControlPage(nameLabel.Text,addressLabel.Text));
Log.Debug("QINSTANCECELL - Connect To " + nameLabel.Text + " Pressed");
}
}
}
}
@@ -1,222 +0,0 @@
//NEEDS migrated to QSelectedCueGrid
using System;
using Acr.UserDialogs;
using Xamarin.Forms;
using qController.QItems;
using qController.Events;
namespace qController.UI.Cells
{
public class QSelectedCueCell : Frame
{
public event SelectedCueEditedHandler SelectedCueEdited;
Label name;
Label number;
Label type;
public Label notes;
public QCue activeCue;
public QSelectedCueCell()
{
Grid mainG = new Grid
{
Padding = new Thickness(0),
RowDefinitions =
{
new RowDefinition{Height = GridLength.Star},
new RowDefinition{Height = new GridLength(2, GridUnitType.Star)}
},
ColumnDefinitions =
{
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)}
}
};
Grid topGrid = new Grid
{
Padding = new Thickness(0),
RowDefinitions =
{
new RowDefinition{Height = GridLength.Auto}
},
ColumnDefinitions =
{
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)}
}
};
Grid bottomGrid = new Grid
{
Padding = new Thickness(0),
RowDefinitions =
{
new RowDefinition{Height = new GridLength(1,GridUnitType.Star)},
new RowDefinition{Height = new GridLength(2,GridUnitType.Star)}
},
ColumnDefinitions =
{
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)}
}
};
number = new Label {
Text = "",
FontAttributes = FontAttributes.Bold,
FontSize = App.HeightUnit * 5
};
name = new Label {
Text = "Loading Workspace....",
FontAttributes = FontAttributes.Bold,
HorizontalTextAlignment = TextAlignment.Center,
FontSize = App.HeightUnit * 3.5,
Margin = new Thickness(0)
};
type = new Label {
Text = QIcon.SPIN3,
FontFamily = (OnPlatform<string>)Application.Current.Resources["QFontFamily"],
VerticalTextAlignment = TextAlignment.Center,
HorizontalTextAlignment = TextAlignment.End,
FontSize = App.HeightUnit * 5
};
notes = new Label {
Text = "Loading Cue Lists and Playhead Position",
HorizontalTextAlignment = TextAlignment.Center,
Margin = new Thickness(0,0,0,10),
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand
};
//BACKGROUND COLORS FOR TESTING ONLY
//notes.BackgroundColor = Color.Red;
//number.BackgroundColor = Color.Red;
//name.BackgroundColor = Color.Red;
//type.BackgroundColor = Color.Red;
//topGrid.BackgroundColor = Color.Green;
//bottomGrid.BackgroundColor = Color.Green;
topGrid.Children.Add(number, 0, 0);
topGrid.Children.Add(type, 4, 0);
Grid.SetColumnSpan(number, 3);
Grid.SetColumnSpan(type, 1);
bottomGrid.Children.Add(name, 0, 0);
bottomGrid.Children.Add(notes, 0, 1);
Grid.SetColumnSpan(name, 5);
Grid.SetColumnSpan(notes,5);
mainG.Children.Add(topGrid, 0, 0);
Grid.SetColumnSpan(topGrid, 5);
mainG.Children.Add(bottomGrid, 0, 1);
Grid.SetColumnSpan(bottomGrid, 5);
CornerRadius = 20;
BackgroundColor = Color.FromHex("D8D8D8");
HeightRequest = App.HeightUnit * 25;
Margin = new Thickness(10);
Content = mainG;
SetupDoubleTapEdit();
}
void SetupDoubleTapEdit()
{
var notesDoubleTap = new TapGestureRecognizer();
var nameDoubleTap = new TapGestureRecognizer();
var numberDoubleTap = new TapGestureRecognizer();
notesDoubleTap.NumberOfTapsRequired = 2;
nameDoubleTap.NumberOfTapsRequired = 2;
numberDoubleTap.NumberOfTapsRequired = 2;
notesDoubleTap.Tapped += (s, e) =>
{
UserDialogs.Instance.Prompt(new PromptConfig
{
Title = "Update Notes",
Message = "Changes notes to update",
OkText = "Update",
Text = notes.Text,
OnAction = (qNotes) =>
{
if (!qNotes.Ok)
return;
OnSelectedCueEdited("notes", qNotes.Text);
}
});
};
nameDoubleTap.Tapped += (s, e) =>
{
UserDialogs.Instance.Prompt(new PromptConfig
{
Title = "Update Name",
Message = "Change name to update",
OkText = "Update",
Text = name.Text,
OnAction = (qName) =>
{
if (!qName.Ok)
return;
OnSelectedCueEdited("name", qName.Text);
}
});
};
numberDoubleTap.Tapped += (s, e) =>
{
UserDialogs.Instance.Prompt(new PromptConfig
{
Title = "Update Number",
Message = "Change number to update",
OkText = "Update",
Text = number.Text,
OnAction = (qNumber) =>
{
if (!qNumber.Ok)
return;
OnSelectedCueEdited("number", qNumber.Text);
}
});
};
notes.GestureRecognizers.Add(notesDoubleTap);
name.GestureRecognizers.Add(nameDoubleTap);
number.GestureRecognizers.Add(numberDoubleTap);
}
public void UpdateSelectedCue(QCue cue)
{
activeCue = cue;
name.Text = cue.listName;
number.Text = cue.number;
type.Text = cue.getIconString();
notes.Text = cue.notes;
}
protected virtual void OnSelectedCueEdited(string prop, string value)
{
if (SelectedCueEdited != null)
SelectedCueEdited(this, new CueEditArgs() { CueID = activeCue.uniqueID, Property = prop, NewValue = value }) ;
}
}
}
@@ -1,5 +1,5 @@
using Acr.UserDialogs;
using qController.Communication;
using qController.Helpers;
namespace qController.UI.Dialogs
{
@@ -1,15 +0,0 @@
using System;
using Acr.UserDialogs;
namespace qController.UI.Dialogs
{
public class NoWorkspacesConfig : ConfirmConfig
{
public NoWorkspacesConfig(Action<bool> action)
{
Message = "QLab doesn't have any workspaces open?";
OkText = "Disconnect";
OnAction = action;
}
}
}
@@ -1,88 +0,0 @@
using System;
using System.Collections.Generic;
using Acr.UserDialogs;
using qController.QItems;
using Serilog;
using QControlKit;
namespace qController.UI.Dialogs
{
public class WorkspacePromptArgs : EventArgs
{
public QOldWorkspace SelectedWorkspace
{
get;
set;
}
}
public class WorkspacePrompt
{
public delegate void WorkspaceSelectedHandler(object source, WorkspacePromptArgs args);
public event WorkspaceSelectedHandler WorkspaceSelected;
public WorkspacePrompt()
{
}
public ActionSheetConfig getActionSheetConfigForWorkspaces(List<QWorkspaceInfo> workspaces)
{
ActionSheetConfig actionSheetConfig = new ActionSheetConfig();
actionSheetConfig.SetTitle("Select Workspace");
for (int i = 0; i < workspaces.Count; i++)
{
QWorkspaceInfo workspace = workspaces[i];
actionSheetConfig.Add(workspace.displayName, new Action(() => {
Log.Debug("WorkspacePrompt - Workspace Selected " + workspace.displayName);
if (!workspace.hasPasscode)
{
OnWorkspaceSelected(new QOldWorkspace(workspace.uniqueID));
}
else
{
promptWorkspacePasscode(workspace.uniqueID);
}
}));
}
return actionSheetConfig;
}
public void promptWorkspacePasscode(string workspace_id)
{
UserDialogs.Instance.Prompt(new PromptConfig
{
InputType = InputType.Number,
MaxLength = 4,
Title = "Enter Workspace Passcode",
OkText = "Connect",
IsCancellable = true,
OnAction = (resp) =>
{
if (resp.Ok)
{
OnWorkspaceSelected(new QOldWorkspace(workspace_id,resp.Value));
}
else
{
OnWorkspaceSelected(null);
}
}
});
}
protected virtual void OnWorkspaceSelected(QOldWorkspace workspace)
{
if (WorkspaceSelected != null)
WorkspaceSelected(this, new WorkspacePromptArgs() { SelectedWorkspace = workspace });
}
}
}
@@ -1,7 +1,5 @@
using Acr.UserDialogs;
using qController.Helpers;
using qController.ViewModels;
using Serilog;
using Xamarin.Forms;
namespace qController.UI
@@ -1,140 +0,0 @@
using Acr.UserDialogs;
using qController.ViewModels;
using Xamarin.Forms;
namespace qController.UI
{
public class QSelectedCueGrid : Grid
{
public QSelectedCueGrid()
{
RowSpacing = 0;
ColumnSpacing = 0;
BackgroundColor = Color.FromHex("#D8D8D8");
RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
RowDefinitions.Add(new RowDefinition { Height = GridLength.Star });
ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Star });
Frame border = new Frame
{
BorderColor = Color.Black,
BackgroundColor = Color.Transparent
};
Children.Add(border);
SetRowSpan(border, 3);
SetColumnSpan(border, 5);
Label number = new Label
{
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
Margin = 0,
Padding = 0,
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Label))
};
number.SetBinding(Label.TextProperty, "number", BindingMode.OneWay);
Children.Add(number,0,0);
Label type = new Label
{
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.CenterAndExpand,
VerticalTextAlignment = TextAlignment.Center,
HorizontalTextAlignment = TextAlignment.Center,
Margin = 0,
Padding = 0,
FontSize = App.HeightUnit * 4,
FontFamily = (OnPlatform<string>)Application.Current.Resources["QFontFamily"]
};
type.SetBinding(Label.TextProperty, "type", BindingMode.OneWay);
Children.Add(type, 4, 0);
Label name = new Label
{
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
HorizontalTextAlignment = TextAlignment.Center,
VerticalTextAlignment = TextAlignment.Center,
BackgroundColor = Color.Transparent,
FontSize = Device.GetNamedSize(NamedSize.Large, typeof(Label))
};
name.SetBinding(Label.TextProperty, "name", BindingMode.TwoWay);
Children.Add(name, 0, 1);
SetColumnSpan(name, 5);
Editor notes = new Editor
{
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
Margin = 5,
BackgroundColor = Color.Transparent,
FontSize = Device.GetNamedSize(NamedSize.Medium, typeof(Editor))
};
notes.SetBinding(Editor.TextProperty, "notes", BindingMode.TwoWay);
Children.Add(notes, 0, 2);
SetColumnSpan(notes, 5);
//Double tap to edit name/number
var nameDoubleTap = new TapGestureRecognizer();
nameDoubleTap.NumberOfTapsRequired = 2;
nameDoubleTap.Tapped += (s, e) =>
{
UserDialogs.Instance.Prompt(new PromptConfig
{
Title = "Update Name",
Message = "Change name to update",
OkText = "Update",
Text = name.Text,
OnAction = (qName) =>
{
if (!qName.Ok)
return;
name.Text = qName.Text;
}
});
};
name.GestureRecognizers.Add(nameDoubleTap);
var numberDoubleTap = new TapGestureRecognizer();
numberDoubleTap.NumberOfTapsRequired = 2;
numberDoubleTap.Tapped += (s, e) =>
{
UserDialogs.Instance.Prompt(new PromptConfig
{
Title = "Update Number",
Message = "Change number to update",
OkText = "Update",
Text = number.Text,
OnAction = (qNumber) =>
{
if (!qNumber.Ok)
return;
//This bypasses the label .Text property because the number might not be valid (no duplicates)
((QCueViewModel)BindingContext).number = qNumber.Text;
}
});
};
number.GestureRecognizers.Add(numberDoubleTap);
}
}
}
@@ -80,20 +80,6 @@ namespace qController
}
public void ChangeToWorkspace(QOldWorkspace workspace)
{
for(int i = 0; i < workspace.data.Count; i++)
{
var cueList = workspace.data[i];
items.Add(new MenuPageItem
{
Title = cueList.listName,
Icon = cueList.IconText,
Command = "cueList " + cueList.uniqueID
});
}
}
public void ChangeToHome()
{
items.Clear();
@@ -1,37 +0,0 @@
<?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="qController.ControlPage"
BackgroundColor="#4A4A4A">
<ContentPage.Content>
<AbsoluteLayout x:Name="sLayout">
<Grid x:Name="topBar"
HorizontalOptions="FillAndExpand"
AbsoluteLayout.LayoutBounds="0,0,1,.09"
AbsoluteLayout.LayoutFlags="All"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="4*" />
</Grid.ColumnDefinitions>
<Label x:Name="menuButton"
StyleClass="QFont"
HorizontalOptions="Start"
VerticalOptions="End"
Margin="20,10,20,10"
Grid.Row="0" Grid.Column="0"
FontSize="25"
Text="&#xF0C9;"/>
<Label x:Name="instanceName"
HorizontalOptions = "FillAndExpand"
VerticalOptions="End"
HorizontalTextAlignment="End"
Margin="20,10,20,10"
Grid.Row="0" Grid.Column="1"/>
</Grid>
</AbsoluteLayout>
</ContentPage.Content>
</ContentPage>
@@ -1,407 +0,0 @@
using System;
using System.Collections.Generic;
using Xamarin.Forms;
using Serilog;
using Acr.UserDialogs;
using qController.UI.Dialogs;
using qController.QItems;
using qController.UI.Cells;
using qController.UI.Buttons;
using qController.Communication;
using qController.Events;
namespace qController
{
public partial class ControlPage : ContentPage
{
QController qController;
QSelectedCueCell qCell;
QLevelsCell qLevelsCell;
QCueListCell qCueListCell;
QLevelsButton showLevelsButton;
QControlsBlock qControlsBlock;
WorkspacePrompt workspacePrompt = new WorkspacePrompt();
public ControlPage(string name, string address)
{
InitializeComponent();
qController = new QController(address, 53000);
qController.qClient.qParser.WorkspaceInfoReceived += WorkspaceInfoReceived;
qController.qClient.qParser.WorkspaceUpdated += WorkspaceUpdated;
qController.qClient.qParser.WorkspaceDisconnect += WorkspaceDisconnected;
qController.qClient.qParser.PlaybackPositionUpdated += PlaybackPositionUpdated;
qController.qClient.qParser.ConnectionStatusChanged += OnConnectionStatusChanged;
qController.qClient.qParser.CueInfoUpdated += OnCueUpdateReceived;
qController.qClient.qParser.ChildrenUpdated += OnChildrenUpdated;
workspacePrompt.WorkspaceSelected += OnWorkspaceSelected;
App.rootPage.MenuItemSelected += OnMenuItemSelected;
instanceName.Text = name;
InitGUI();
if (!qController.qClient.connected)
{
App.showToast("Error connecting...make sure QLab is running");
Back();
}
else
{
qController.KickOff();
}
}
private void OnWorkspaceSelected(object source, WorkspacePromptArgs args)
{
if (args.SelectedWorkspace != null)
{
qController.Connect(args.SelectedWorkspace);
}
else
{
Log.Debug("CONTROLPAGE - No Workspace selected backing out");
Back();
}
}
private void OnConnectionStatusChanged(object source, ConnectEventArgs args)
{
Log.Debug($"CONTROLPAGE - Connection Status Changed: {args.WorkspaceId} : {args.Status}");
if (args.Status.Equals("ok"))
{
Device.BeginInvokeOnMainThread(() => {
FinishUI();
});
}
else if (args.Status.Equals("badpass"))
{
workspacePrompt.promptWorkspacePasscode(args.WorkspaceId);
}
}
private void WorkspaceInfoReceived(object source, WorkspaceInfoArgs args)
{
if (args.WorkspaceInfo.Count > 1)
{
Log.Debug("CONTROLPAGE - MULTIPLE WORKSPACES ON SELECTED COMPUTER");
PromptForWorkspace(args.WorkspaceInfo);
}
else if (args.WorkspaceInfo.Count == 1)
{
Log.Debug("CONTROLPAGE - ONLY ONE WORKSPACE ON SELECTED COMPUTER");
if (!args.WorkspaceInfo[0].hasPasscode)
{
qController.Connect(args.WorkspaceInfo[0].uniqueID);
}
else
{
workspacePrompt.promptWorkspacePasscode(args.WorkspaceInfo[0].uniqueID);
}
}
else
{
NoWorkspacesConfig noWorkspacesConfig = new NoWorkspacesConfig(NoWorkspaceDetected);
UserDialogs.Instance.Confirm(noWorkspacesConfig);
}
}
private void PromptForWorkspace(List<QControlKit.QWorkspaceInfo> workspaces)
{
UserDialogs.Instance.ActionSheet(workspacePrompt.getActionSheetConfigForWorkspaces(workspaces));
}
private void InitGUI()
{
App.rootPage.MenuPage.ChangeToControl();
NavigationPage.SetHasNavigationBar(this, false);
qCell = new QSelectedCueCell();
qCell.SelectedCueEdited += OnSelectedCueEdited;
AbsoluteLayout.SetLayoutFlags(qCell, AbsoluteLayoutFlags.All);
instanceName.FontSize = App.HeightUnit * 3;
switch (Device.RuntimePlatform)
{
case Device.iOS:
AbsoluteLayout.SetLayoutBounds(qCell, new Rectangle(0, 0.13, 1, 0.30));
topBar.HeightRequest = App.Height * .09;
menuButton.FontSize = App.Height * .04;
break;
case Device.Android:
AbsoluteLayout.SetLayoutBounds(qCell, new Rectangle(0, 0.13, 1, 0.35));
topBar.HeightRequest = App.Height * .06;
menuButton.FontSize = App.Height * .05;
break;
}
//Menu Button Setup
var menuButtonGesture = new TapGestureRecognizer();
menuButtonGesture.Tapped += ShowMenu;
menuButton.GestureRecognizers.Add(menuButtonGesture);
menuButton.Margin = new Thickness(App.WidthUnit * 2, 0, 0, App.WidthUnit * 2);
sLayout.Children.Add(qCell);
}
private void SendOSCFromButton(object sender, EventArgs args)
{
if (((QButton)sender).qCommand.type == "WORKSPACE")
{
string workspace_prefix = "/workspace/" + qController.qWorkspace.workspace_id;
string command = workspace_prefix + ((QButton)sender).qCommand.osc;
qController.qClient.sendTCP(command);
}
}
private void OnSelectedCueEdited(object source, CueEditArgs args)
{
string address = "/workspace/" + qController.qWorkspace.workspace_id + "/cue_id/" + args.CueID + "/" + args.Property;
qController.qClient.sendTCP(address, args.NewValue);
}
void FinishUI()
{
string workspace_prefix = "/workspace/" + qController.qWorkspace.workspace_id;
qLevelsCell = new QLevelsCell();
qLevelsCell.mainSlider.ValueChanged += (sender, args) =>
{
qController.qClient.sendTCP(workspace_prefix + "/cue_id/" + qLevelsCell.activeCue + "/sliderLevel/0", (float)args.NewValue);
};
for(int i = 0; i < qLevelsCell.sliders.Count; i++)
{
var channel = i + 1;
Log.Debug("Setting value changed for channel: " + channel);
qLevelsCell.sliders[i].ValueChanged += (sender, args) =>
{
qLevelsCell.sliderLabels[channel-1].Text = $"{args.NewValue}";
qController.qClient.sendTCP(workspace_prefix + "/cue_id/" + qLevelsCell.activeCue + "/sliderLevel/" + channel, (float)args.NewValue);
};
}
//qLevelsCell.leftSlider.ValueChanged += (sender, args) =>
//{
// qController.qClient.sendTCP(workspace_prefix + "/cue_id/" + qLevelsCell.activeCue + "/sliderLevel/1", (float)args.NewValue);
//};
//qLevelsCell.rightSlider.ValueChanged += (sender, args) =>
//{
// qController.qClient.sendTCP(workspace_prefix + "/cue_id/" + qLevelsCell.activeCue + "/sliderLevel/2", (float)args.NewValue);
//};
showLevelsButton = new QLevelsButton();
showLevelsButton.button.Clicked += ToggeQLevelsCellVisiblity;
qControlsBlock = new QControlsBlock(SendOSCFromButton);
AbsoluteLayout.SetLayoutFlags(qControlsBlock, AbsoluteLayoutFlags.All);
AbsoluteLayout.SetLayoutFlags(qLevelsCell, AbsoluteLayoutFlags.All);
AbsoluteLayout.SetLayoutFlags(showLevelsButton, AbsoluteLayoutFlags.PositionProportional);
switch (Device.RuntimePlatform)
{
case Device.iOS:
AbsoluteLayout.SetLayoutBounds(qControlsBlock, new Rectangle(0, 0.53, 1, 0.25));
AbsoluteLayout.SetLayoutBounds(qLevelsCell, new Rectangle(0, 0.53, 1, 0.25));
AbsoluteLayout.SetLayoutBounds(showLevelsButton, new Rectangle(0.02, 0.34, App.HeightUnit * 8, App.HeightUnit * 8));
break;
case Device.Android:
AbsoluteLayout.SetLayoutBounds(qControlsBlock, new Rectangle(0, 0.58, 1, 0.25));
AbsoluteLayout.SetLayoutBounds(qLevelsCell, new Rectangle(0, 0.58, 1, 0.25));
AbsoluteLayout.SetLayoutBounds(showLevelsButton, new Rectangle(0.02, 0.39, App.HeightUnit * 8, App.HeightUnit * 8));
break;
}
sLayout.Children.Add(qControlsBlock);
sLayout.Children.Add(qLevelsCell);
sLayout.Children.Add(showLevelsButton);
}
private void ToggeQLevelsCellVisiblity(object sender, EventArgs e)
{
qLevelsCell.IsVisible = !qLevelsCell.IsVisible;
}
void ShowMenu(object sender, EventArgs e)
{
App.MenuIsPresented = true;
}
void Back()
{
if(qController.qClient.connected)
qController.Kill();
App.rootPage.MenuItemSelected -= OnMenuItemSelected;
Device.BeginInvokeOnMainThread(() =>
{
App.rootPage.MenuPage.ChangeToHome();
App.NavigationPage.Navigation.PopAsync();
});
}
public void NoWorkspaceDetected(bool resp)
{
if (resp)
Back();
}
public void WorkspaceDisconnected(object sender, EventArgs e)
{
Back();
}
public void WorkspaceUpdated(object sender, WorkspaceEventArgs e)
{
if(e.UpdatedWorkspace.data.Count > 0)
{
qController.qWorkspace = e.UpdatedWorkspace;
qController.qWorkspace.CheckPopulated();
if (qController.qWorkspace.IsPopulated)
{
Device.BeginInvokeOnMainThread(() =>
{
App.rootPage.MenuPage.ChangeToWorkspace(e.UpdatedWorkspace);
});
if (qCell.activeCue == null)
{
Device.BeginInvokeOnMainThread(() => {
qCell.UpdateSelectedCue(new NoQCueSelected());
});
Log.Debug("CONTROLPAGE - Update Selected Cue Called because of Inital Workspace Load");
qController.qClient.UpdateSelectedCue(qController.qWorkspace.workspace_id);
}
Log.Debug("CONTROLPAGE - Workspace Updated " + qController.qWorkspace.workspace_id);
}
}
}
public void PlaybackPositionUpdated(object sender, PlaybackPositionArgs e)
{
qController.playbackPosition = e.PlaybackPosition;
Log.Debug("CONTROLPAGE - Playback Position Updated " + qController.playbackPosition);
QCue cue = qController.qWorkspace.GetCue(qController.playbackPosition);
if(cue != null)
{
Device.BeginInvokeOnMainThread(() => {
if (cue.levels != null)
showLevelsButton.IsVisible = true;
else
{
showLevelsButton.IsVisible = false;
qLevelsCell.IsVisible = false;
}
qCell.UpdateSelectedCue(cue);
});
}
}
public void OnCueUpdateReceived(object sender, CueEventArgs args)
{
if(qController != null)
{
if(qController.qWorkspace != null)
{
qController.qWorkspace.UpdateCue(args.Cue);
if (qController.playbackPosition == null)
{
qController.playbackPosition = args.Cue.uniqueID;
}
if (args.Cue.uniqueID == qController.playbackPosition)
{
Device.BeginInvokeOnMainThread(() =>
{
Log.Debug("CONTROLPAGE - Refreshing Currently Displayed Cue");
if (args.Cue.levels != null)
showLevelsButton.IsVisible = true;
else
{
showLevelsButton.IsVisible = false;
qLevelsCell.IsVisible = false;
}
qCell.UpdateSelectedCue(args.Cue);
if(qLevelsCell != null)
{
qLevelsCell.activeCue = args.Cue.uniqueID;
if (args.Cue.levels != null)
{
qLevelsCell.UpdateLevels(args.Cue.levels[0]);
}
}
});
}
}
}
}
private void OnChildrenUpdated(object source, ChildrenEventArgs args)
{
if(qController != null)
{
if(qController.qWorkspace != null)
{
qController.qWorkspace.UpdateChildren(args.cue_id, args.children);
}
}
}
private void OnMenuItemSelected(object source, MenuEventArgs args)
{
if (args.Command.Contains("/"))
{
qController.qClient.sendTCP(args.Command);
}
else if (args.Command == "disconnect")
{
Back();
}else if (args.Command.Contains("cueList"))
{
var parts = args.Command.Split(' ');
if(parts.Length > 1)
{
Device.BeginInvokeOnMainThread(() =>
{
qCueListCell = new QCueListCell(qController.qWorkspace.GetCueList(parts[1]));
qCueListCell.closeButton.Clicked += CloseCueList;
qCueListCell.cueListView.ItemSelected += OnCueListItemSelected;
AbsoluteLayout.SetLayoutFlags(qCueListCell, AbsoluteLayoutFlags.All);
AbsoluteLayout.SetLayoutBounds(qCueListCell, new Rectangle(0, 0.2, 1, 0.9));
sLayout.Children.Add(qCueListCell);
});
}
}
}
private void CloseCueList(object sender, EventArgs e)
{
Device.BeginInvokeOnMainThread(() =>
{
sLayout.Children.Remove(qCueListCell);
});
}
private void OnCueListItemSelected(object sender, SelectedItemChangedEventArgs e)
{
OSCListItem cue = (OSCListItem)e.SelectedItem;
string selectCueOSC = "/workspace/" + qController.qWorkspace.workspace_id + cue.Command;
qController.qClient.sendTCP(selectCueOSC);
CloseCueList(sender,e);
}
}
}
@@ -1,32 +0,0 @@
<?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="qController.QConnectionPage">
<StackLayout x:Name="sLayout">
<Grid x:Name="topBar"
HorizontalOptions="FillAndExpand"
BackgroundColor="{DynamicResource NavigationBarColor}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label x:Name = "menuButton"
StyleClass="QFont"
HorizontalOptions = "Start"
VerticalOptions="End"
Margin="20,10,20,10"
Grid.Row="0" Grid.Column="0"
Text="&#xF0C9;"/>
</Grid>
<ListView x:Name="listView"
HasUnevenRows="true"
BackgroundColor="#4A4A4A"
SeparatorVisibility="None">
</ListView>
</StackLayout>
</ContentPage>
@@ -1,114 +0,0 @@
using Xamarin.Forms;
using System;
using Acr.UserDialogs;
using Zeroconf;
using System.Collections.Generic;
using Serilog;
using qController.QItems;
using qController.UI.Dialogs;
using qController.UI.Cells;
using Xamarin.Essentials;
namespace qController
{
public partial class QConnectionPage : ContentPage
{
public QConnectionPage()
{
InitializeComponent();
InitGUI();
App.rootPage.MenuItemSelected += OnMenuItemSelected;
}
private void OnMenuItemSelected(object source, MenuEventArgs args)
{
if(args.Command == "scan")
{
Scan();
}
else if(args.Command == "add")
{
AddWorkspace();
}
else if (args.Command == "feedback")
{
Launcher.OpenAsync(new Uri("mailto:feedback@jwetzell.com?subject=qController%20feedback"));
}
else if(args.Command == "support")
{
UserDialogs.Instance.Confirm(new DonatePrompt());
}
}
private void InitGUI()
{
NavigationPage.SetHasNavigationBar(this, false);
//ListView Setup
listView.ItemsSource = QStorage.qInstances;
listView.ItemTemplate = new DataTemplate(typeof(QInstanceCell));
listView.ItemTapped += (object sender, ItemTappedEventArgs e) =>
{
// don't do anything if we just de-selected the row
if (e.Item == null) return;
// do something with e.SelectedItem
((ListView)sender).SelectedItem = null; // de-select the row
};
//Platform Specific Setup
switch (Device.RuntimePlatform)
{
case Device.iOS:
topBar.HeightRequest = App.Height * .09;
menuButton.FontSize = App.Height * .04;
menuButton.Margin = new Thickness(App.WidthUnit * 2, 0, 0, App.WidthUnit * 2);
break;
case Device.Android:
topBar.HeightRequest = App.Height * .09;
menuButton.FontSize = App.Height * .05;
menuButton.Margin = new Thickness(App.WidthUnit * 2, 0, 0, App.WidthUnit * 2);
break;
}
BackgroundColor = Color.FromHex("4A4A4A");
//MenuButton Setup
var menuButtonGesture = new TapGestureRecognizer();
menuButtonGesture.Tapped += App.ShowMenu;
menuButton.GestureRecognizers.Add(menuButtonGesture);
}
void AddWorkspace(){
UserDialogs.Instance.Prompt(new AddInstancePrompt());
}
async void Scan(){
bool workspacesFound = false;
App.showToast("Scanning for Instances...");
Log.Debug("QCONNECTIONPAGE - Begin Scanning");
IReadOnlyList<IZeroconfHost> results = await ZeroconfResolver.ResolveAsync("_qlab._udp.local.",TimeSpan.FromSeconds(3));
if(results != null){
foreach (var result in results)
{
if (result != null)
{
QInstance instance = new QInstance(result.DisplayName, result.IPAddress);
if(QStorage.AddInstance(instance)){
Log.Debug($"QCONNECTIONPAGE - {result.DisplayName} @ {result.IPAddress} added");
workspacesFound = true;
}
}
}
}
if(workspacesFound){
App.showToast("Instance Found and Added!");
}else{
App.showToast("No New Instances Found!");
}
}
}
}
+5 -4
View File
@@ -16,18 +16,19 @@
<!--Ordering is based on the order last is on top seems pretty backwards to me-->
<!--Old "Instance" listview -->
<ListView x:Name="storageListView"
<!--<ListView x:Name="storageListView"
HasUnevenRows="true"
BackgroundColor="{DynamicResource PageBackgroundColor}"
SeparatorVisibility="None">
</ListView>
</ListView>-->
<!--Simple Divider-->
<BoxView HeightRequest="10"
BackgroundColor="DarkGray"/>
<!--<BoxView HeightRequest="10"
BackgroundColor="DarkGray"/>-->
<!--Beginning of new style of "server" view-->
<Frame BackgroundColor="{DynamicResource PageBackgroundColor}"
CornerRadius="0"
Margin="0"
@@ -23,15 +23,17 @@ namespace qController.Pages
serverListView.BindingContext = qBrowserViewModel;
serverListView.ItemSelected += QWorkspaceSelected;
storageListView.ItemsSource = QStorage.qInstances;
storageListView.ItemTemplate = new DataTemplate(typeof(QInstanceCell));
storageListView.ItemTapped += (object sender, ItemTappedEventArgs e) =>
{
// don't do anything if we just de-selected the row
if (e.Item == null) return;
// do something with e.SelectedItem
((ListView)sender).SelectedItem = null; // de-select the row
};
//Old way of displaying "QInstances"
//storageListView.ItemsSource = QStorage.qInstances;
//storageListView.ItemTemplate = new DataTemplate(typeof(QInstanceCell));
//storageListView.ItemTapped += (object sender, ItemTappedEventArgs e) =>
//{
// // don't do anything if we just de-selected the row
// if (e.Item == null) return;
// // do something with e.SelectedItem
// ((ListView)sender).SelectedItem = null; // de-select the row
//};
}
-2
View File
@@ -86,14 +86,12 @@
<EmbeddedResource Include="App.xaml" />
</ItemGroup>-->
<ItemGroup>
<Folder Include="Classes\QItems\" />
<Folder Include="Classes\Settings\" />
<Folder Include="Classes\UI\" />
<Folder Include="Classes\Events\" />
<Folder Include="Classes\UI\Buttons\" />
<Folder Include="Classes\UI\Cells\" />
<Folder Include="Classes\Helpers\" />
<Folder Include="Views\Old\" />
<Folder Include="Classes\UI\Dialogs\" />
</ItemGroup>
<ItemGroup>