initial working maui migration with android only

This commit is contained in:
2025-01-26 17:45:09 -06:00
parent e851c815fc
commit e0c300e60d
189 changed files with 57617 additions and 45110 deletions
+45107 -45108
View File
File diff suppressed because it is too large Load Diff
+14
View File
@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:qControlMAUI"
x:Class="qControlMAUI.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+12
View File
@@ -0,0 +1,12 @@
namespace qControlMAUI
{
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
}
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="qControlMAUI.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:qControlMAUI"
Shell.FlyoutBehavior="Disabled"
Title="qControlMAUI">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Shell>
+10
View File
@@ -0,0 +1,10 @@
namespace qControlMAUI
{
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
}
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="qControlMAUI.MainPage">
<ScrollView>
<VerticalStackLayout
Padding="30,0"
Spacing="25">
<Image
Source="dotnet_bot.png"
HeightRequest="185"
Aspect="AspectFit"
SemanticProperties.Description="dot net bot in a race car number eight" />
<Label
Text="Hello, World!"
Style="{StaticResource Headline}"
SemanticProperties.HeadingLevel="Level1" />
<Label
Text="Welcome to &#10;.NET Multi-platform App UI"
Style="{StaticResource SubHeadline}"
SemanticProperties.HeadingLevel="Level2"
SemanticProperties.Description="Welcome to dot net Multi platform App U I" />
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Fill" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>
+25
View File
@@ -0,0 +1,25 @@
namespace qControlMAUI
{
public partial class MainPage : ContentPage
{
int count = 0;
public MainPage()
{
InitializeComponent();
}
private void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
SemanticScreenReader.Announce(CounterBtn.Text);
}
}
}
+25
View File
@@ -0,0 +1,25 @@
using Microsoft.Extensions.Logging;
namespace qControlMAUI
{
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>
@@ -0,0 +1,11 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
namespace qControlMAUI
{
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}
}
@@ -0,0 +1,16 @@
using Android.App;
using Android.Runtime;
namespace qControlMAUI
{
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>
@@ -0,0 +1,10 @@
using Foundation;
namespace qControlMAUI
{
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
<dict>
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>
@@ -0,0 +1,16 @@
using ObjCRuntime;
using UIKit;
namespace qControlMAUI
{
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
+17
View File
@@ -0,0 +1,17 @@
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using System;
namespace qControlMAUI
{
internal class Program : MauiApplication
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
static void Main(string[] args)
{
var app = new Program();
app.Run(args);
}
}
}
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="qControlMAUI.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
</manifest>
+8
View File
@@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="qControlMAUI.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:qControlMAUI.WinUI">
</maui:MauiWinUIApplication>
@@ -0,0 +1,25 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace qControlMAUI.WinUI
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
<mp:PhoneIdentity PhoneProductId="B9BD1003-B46F-4997-B80F-501D62A4D047" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="qControlMAUI.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>
+10
View File
@@ -0,0 +1,10 @@
using Foundation;
namespace qControlMAUI
{
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
}
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>
+16
View File
@@ -0,0 +1,16 @@
using ObjCRuntime;
using UIKit;
namespace qControlMAUI
{
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps.
The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK.
You are responsible for adding extra entries as needed for your application.
More information: https://aka.ms/maui-privacy-manifest
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<!--
The entry below is only needed when you're using the Preferences API in your app.
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict> -->
</array>
</dict>
</plist>
@@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "MsixPackage",
"nativeDebugging": false
}
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

After

Width:  |  Height:  |  Size: 231 B

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

@@ -0,0 +1,15 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories). Deployment of the asset to your application
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
These files will be deployed with your package and will be accessible using Essentials:
async Task LoadMauiAsset()
{
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
using var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
}
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+45
View File
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
<Color x:Key="Primary">#512BD4</Color>
<Color x:Key="PrimaryDark">#ac99ea</Color>
<Color x:Key="PrimaryDarkText">#242424</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="SecondaryDarkText">#9880e5</Color>
<Color x:Key="Tertiary">#2B0B98</Color>
<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Magenta">#D600AA</Color>
<Color x:Key="MidnightBlue">#190649</Color>
<Color x:Key="OffBlack">#1f1f1f</Color>
<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
</ResourceDictionary>
+427
View File
@@ -0,0 +1,427 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
</Style>
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
</Style>
<Style TargetType="BoxView">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Frame">
<Setter Property="HasShadow" Value="False" />
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value="0.5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Span">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="Label" x:Key="Headline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="32" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Label" x:Key="SubHeadline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="ListView">
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Shadow">
<Setter Property="Radius" Value="15" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Offset" Value="10,10" />
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
</Style>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
</ResourceDictionary>
+65
View File
@@ -0,0 +1,65 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<RootNamespace>qControlMAUI</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Display name -->
<ApplicationTitle>qControlMAUI</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.qcontrolmaui</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.1" />
</ItemGroup>
</Project>
+29 -2
View File
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29424.173
# Visual Studio Version 17
VisualStudioVersion = 17.12.35707.178
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "qController.iOS", "iOS\qController.iOS.csproj", "{A30F10F2-4CBC-47E6-841D-8DCAD05BAA0D}"
EndProject
@@ -8,6 +8,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "qController.Droid", "Droid\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "qController", "qController.Standard\qController.csproj", "{86160AAC-2150-41B0-BB06-4442CA054A78}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Xamarin.Forms", "Xamarin.Forms", "{862BB23A-99C3-4550-B2DD-AFB554060BB0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "qController", "qController\qController.csproj", "{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -56,10 +60,33 @@ Global
{86160AAC-2150-41B0-BB06-4442CA054A78}.Release|iPhone.Build.0 = Release|Any CPU
{86160AAC-2150-41B0-BB06-4442CA054A78}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{86160AAC-2150-41B0-BB06-4442CA054A78}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Debug|Any CPU.Build.0 = Debug|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Debug|iPhone.ActiveCfg = Debug|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Debug|iPhone.Build.0 = Debug|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Debug|iPhone.Deploy.0 = Debug|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Release|Any CPU.ActiveCfg = Release|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Release|Any CPU.Build.0 = Release|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Release|Any CPU.Deploy.0 = Release|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Release|iPhone.ActiveCfg = Release|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Release|iPhone.Build.0 = Release|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Release|iPhone.Deploy.0 = Release|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
{63B39EEA-36D4-4B2C-8FFD-FB0E12FD8B14}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A30F10F2-4CBC-47E6-841D-8DCAD05BAA0D} = {862BB23A-99C3-4550-B2DD-AFB554060BB0}
{D960ED89-25D2-46E8-B24C-1FFFCD19C0D9} = {862BB23A-99C3-4550-B2DD-AFB554060BB0}
{86160AAC-2150-41B0-BB06-4442CA054A78} = {862BB23A-99C3-4550-B2DD-AFB554060BB0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3DBBFC12-E39D-4331-B179-FF5D0493949C}
EndGlobalSection
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="qController.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:qController"
Shell.FlyoutBehavior="Disabled"
Title="qController">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:RootPage}"
Route="MainPage" />
</Shell>
+10
View File
@@ -0,0 +1,10 @@
namespace qController
{
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
}
+45
View File
@@ -0,0 +1,45 @@
using Microsoft.Maui.Graphics;
namespace qController
{
public class QButton : Button
{
public QCommand qCommand
{
get;
set;
}
public QButton(QCommand command)
{
qCommand = command;
Text = qCommand.text;
TextColor = Colors.Black;
FontSize = App.WidthUnit * 5;
if (qCommand.osc.Contains("go"))
{
BackgroundColor = Colors.SeaGreen;
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
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 = Colors.IndianRed;
}
else
{
BackgroundColor = Color.FromArgb("D8D8D8");
}
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.Maui.Graphics;
namespace qController
{
public class QLevelsButton : ShadowButton
{
public Button button;
public QLevelsButton()
{
button = new Button
{
Text = QIcon.SLIDERS,
TextColor = Colors.Black,
FontFamily = App.QFont,
FontSize = App.HeightUnit * 4,
VerticalOptions = LayoutOptions.FillAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
HeightRequest = App.HeightUnit * 8,
WidthRequest = App.HeightUnit * 8,
CornerRadius = (int)(App.HeightUnit * 4),
BackgroundColor = Colors.LightBlue
};
HeightRequest = button.Height;
WidthRequest = button.Width;
CornerRadius = button.CornerRadius;
Content = button;
}
}
}
@@ -0,0 +1,27 @@
using Microsoft.Maui.Graphics;
namespace qController
{
public class ShadowButton : Frame
{
public ShadowButton(Button b)
{
//Content = b;
HeightRequest = b.Height;
WidthRequest = b.Width;
CornerRadius = b.CornerRadius;
Padding = new Thickness(0);
Content = b;
HasShadow = true;
BorderColor = Colors.Black;
IsVisible = false;
}
public ShadowButton()
{
HasShadow = true;
BorderColor = Colors.Black;
IsVisible = false;
Padding = new Thickness(0);
}
}
}
+124
View File
@@ -0,0 +1,124 @@
namespace qController.Cell
{
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 = Colors.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.Add(b, column, row);
row++;
if (row == 3)
{
row = 0;
column = 2;
}
}
QButton goButton = new QButton(QCommands.GO);
goButton.Clicked += callback;
mainG.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.Add(b, column, row);
row++;
if (row == 3)
{
row = 0;
column = 2;
}
}
QButton goButton = new QButton(QCommands.GO);
goButton.Clicked += callback;
mainG.Add(goButton, 1, 0);
Grid.SetRowSpan(goButton, 3);
Content = mainG;
}
}
}
+131
View File
@@ -0,0 +1,131 @@
using System.Collections.ObjectModel;
using qController.QItems;
namespace qController.Cell
{
public class QCueListCell : Frame
{
public ListView cueListView;
public Button closeButton;
public ObservableCollection<OSCListItem> items;
public QCueListCell(QCueList qCueList)
{
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
switch (Device.RuntimePlatform)
{
case Device.iOS:
Margin = new Thickness(10, 30, 10, 10);
break;
case Device.Android:
Margin = new Thickness(10, 10, 10, 10);
break;
}
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 = App.QFont;
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 = Colors.DarkRed;
}
// TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
switch (Device.RuntimePlatform)
{
case Device.iOS:
label.FontSize = App.HeightUnit * 3;
break;
case Device.Android:
label.FontSize = App.HeightUnit * 2.2;
break;
}
grid.Add(icon);
grid.Add(label, 1, 0);
return new ViewCell { View = grid };
})
};
StackLayout layout = new StackLayout();
closeButton = new Button
{
BackgroundColor = Colors.Gray,
Text = "Close",
TextColor = Colors.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
});
}
}
}
}
+116
View File
@@ -0,0 +1,116 @@
using Serilog;
namespace qController.Cell
{
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.Add(nameLabel, 1, 0);
mainG.Add(addressLabel,1,1);
mainG.Add(connectLabel,2,0);
Grid.SetRowSpan(connectLabel,2);
mainG.Add(deleteLabel, 0, 0);
Grid.SetRowSpan(deleteLabel,2);
Frame f = new Frame
{
Content = mainG,
BorderColor = Colors.Black,
VerticalOptions = LayoutOptions.CenterAndExpand,
HorizontalOptions = LayoutOptions.FillAndExpand,
Padding = 0,
Margin = new Thickness(10,10,10,10),
CornerRadius=20,
BackgroundColor = Color.FromArgb("D8D8D8")
};
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);
addressLabel.HorizontalTextAlignment = TextAlignment.Center;
addressLabel.VerticalTextAlignment = TextAlignment.Start;
addressLabel.FontSize = App.HeightUnit * 2.5;
addressLabel.Margin = new Thickness(0, 0, 0, 20);
connectLabel.HorizontalOptions = LayoutOptions.StartAndExpand;
connectLabel.VerticalOptions = LayoutOptions.CenterAndExpand;
connectLabel.Text = QIcon.WIFI;
connectLabel.FontSize = App.HeightUnit * 5;
connectLabel.TextColor = Colors.LimeGreen;
deleteLabel.HorizontalOptions = LayoutOptions.CenterAndExpand;
deleteLabel.VerticalOptions = LayoutOptions.CenterAndExpand;
deleteLabel.Text = QIcon.TRASH_EMPTY;
deleteLabel.FontSize = App.HeightUnit * 5;
deleteLabel.TextColor = Colors.Red;
connectLabel.FontFamily = App.QFont;
deleteLabel.FontFamily = App.QFont;
}
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.rootPage.Detail.Navigation.PushAsync(new ControlPage(nameLabel.Text, addressLabel.Text));
Log.Debug("QINSTANCECELL - Connect To " + nameLabel.Text + " Pressed");
}
}
}
}
+118
View File
@@ -0,0 +1,118 @@
namespace qController.Cell
{
public class QLevelsCell : Frame
{
public List<double> levels;
public string activeCue;
public Slider mainSlider;
public Slider leftSlider;
public Slider rightSlider;
public Label mainLabel;
public Label rightLabel;
public Label leftLabel;
public QLevelsCell()
{
Padding = new Thickness(5);
CornerRadius = 20;
BorderColor = Colors.Black;
HasShadow = true;
BackgroundColor = Color.FromArgb("D8D8D8");
IsVisible = false;
//highlight for testing
//BackgroundColor = Color.Red;
Grid 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)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)},
new ColumnDefinition{Width = new GridLength(1,GridUnitType.Star)}
}
};
mainSlider = new Slider
{
Minimum = -60,
Maximum = 12
};
leftSlider = new Slider
{
Minimum = -60,
Maximum = 12
};
rightSlider = new Slider
{
Minimum = -60,
Maximum = 12
};
mainLabel = new Label
{
Text = "MAIN:",
VerticalTextAlignment = TextAlignment.Center,
HorizontalTextAlignment = TextAlignment.Center
};
leftLabel = new Label
{
Text = "1:",
VerticalTextAlignment = TextAlignment.Center,
HorizontalTextAlignment = TextAlignment.Center
};
rightLabel = new Label
{
Text = "2:",
VerticalTextAlignment = TextAlignment.Center,
HorizontalTextAlignment = TextAlignment.Center
};
mainG.Add(mainSlider, 1, 0);
mainG.Add(leftSlider, 1, 1);
mainG.Add(rightSlider, 1, 2);
Grid.SetColumnSpan(mainSlider, 4);
Grid.SetColumnSpan(leftSlider, 4);
Grid.SetColumnSpan(rightSlider, 4);
mainG.Add(mainLabel, 0, 0);
mainG.Add(leftLabel, 0, 1);
mainG.Add(rightLabel, 0, 2);
Content = mainG;
Margin = new Thickness(10);
}
public void UpdateLevels(List<double> lin_levels)
{
levels = lin_levels;
if (mainSlider != null)
{
mainSlider.Value = levels[0];
}
if (leftSlider != null)
{
leftSlider.Value = levels[1];
}
if (rightSlider != null)
{
rightSlider.Value = levels[2];
}
}
}
}
@@ -0,0 +1,233 @@
using Acr.UserDialogs;
using qController.QItems;
namespace qController.Cell
{
public class CueEditArgs : EventArgs
{
public string CueID
{
get;
set;
}
public string Property
{
get;
set;
}
public string NewValue
{
get;
set;
}
}
public class QSelectedCueCell : Frame
{
public delegate void SelectedCueEditedHandler(object source, CueEditArgs args);
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 = App.QFont,
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.Add(number, 0, 0);
topGrid.Add(type, 4, 0);
Grid.SetColumnSpan(number, 3);
Grid.SetColumnSpan(type, 1);
bottomGrid.Add(name, 0, 0);
bottomGrid.Add(notes, 0, 1);
Grid.SetColumnSpan(name, 5);
Grid.SetColumnSpan(notes,5);
mainG.Add(topGrid, 0, 0);
Grid.SetColumnSpan(topGrid, 5);
mainG.Add(bottomGrid, 0, 1);
Grid.SetColumnSpan(bottomGrid, 5);
CornerRadius = 20;
BackgroundColor = Color.FromArgb("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)
{
SelectedCueEdited?.Invoke(this, new CueEditArgs() { CueID = activeCue.uniqueID, Property = prop, NewValue = value });
}
}
}
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using qController.QItems;
namespace qController.Communication
{
public class CueEventArgs : EventArgs
{
public QCue Cue
{
get;
set;
}
}
public class WorkspaceEventArgs : EventArgs
{
public QWorkspace 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<QWorkspaceInfo> WorkspaceInfo
{
get;
set;
}
}
public class ChildrenEventArgs : EventArgs
{
public string cue_id
{
get;
set;
}
public List<QCue> children
{
get;
set;
}
}
}
@@ -0,0 +1,16 @@
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);
}
@@ -0,0 +1,19 @@
//IP Address verification
//IMPLEMENT A ISREACHABLE METHOD TO BE ABLE TO DISPLAY ONLINE QINSTANCES
using System.Net;
namespace qController.Communication
{
public class IPHelper
{
public IPHelper()
{
}
public static bool IsValidAddress(string ipString){
if ((ipString.Split('.').Length - 1) != 3) return false;
IPAddress address;
return IPAddress.TryParse(ipString, out address);
}
}
}
@@ -0,0 +1,109 @@
//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();
}
}
}
@@ -0,0 +1,81 @@
//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 QWorkspace 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 QWorkspace(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 QWorkspace(workspace_id);
qClient.sendTCP("/workspace/" + workspace_id + "/connect", passcode);
}
public void Connect(QWorkspace 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();
}
}
}
@@ -0,0 +1,23 @@
//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._tcp.local.");
Log.Debug("QFINDER - Scanning Done");
}
}
}
@@ -0,0 +1,184 @@
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<QWorkspaceInfo> qWorkspaceInfo = JsonConvert.DeserializeObject<List<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
{
QWorkspace workspace = JsonConvert.DeserializeObject<QWorkspace>(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(QWorkspace 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<QWorkspaceInfo> workspaces)
{
WorkspaceInfoReceived?.Invoke(this, new WorkspaceInfoArgs() { WorkspaceInfo = workspaces });
}
protected virtual void OnWorkspaceLoadError(string id)
{
WorkspaceLoadError?.Invoke(this, new WorkspaceEventArgs { UpdatedWorkspace = new QWorkspace(id) });
}
}
}
@@ -0,0 +1,76 @@
//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.Contains("ok"))
{
//new sort of connect format for QLab 5 ok:view|control|edit is connection good
if(args.Status.Contains(":") && !args.Status.Contains("view"))
{
return;
}
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);
}
}
}
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SharpOSC
{
internal static class Extensions
{
public static int FirstIndexAfter<T>(this IEnumerable<T> items, int start, Func<T, bool> predicate)
{
if (items == null) throw new ArgumentNullException("items");
if (predicate == null) throw new ArgumentNullException("predicate");
if (start >= items.Count()) throw new ArgumentOutOfRangeException("start");
int retVal = 0;
foreach (var item in items)
{
if (retVal >= start && predicate(item)) return retVal;
retVal++;
}
return -1;
}
public static List<List<T>> Split<T>(this IEnumerable<T> data, Func<T, bool> predicate)
{
var output = new List<List<T>>();
var curr = new List<T>();
output.Add(curr);
foreach (var x in data)
{
if (predicate(x))
{
curr = new List<T>();
output.Add(curr);
}
else
curr.Add(x);
}
return output;
}
public static T[] SubArray<T>(this T[] data, int index, int length)
{
T[] result = new T[length];
Array.Copy(data, index, result, 0, length);
return result;
}
}
}
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SharpOSC
{
public struct Midi
{
public byte Port;
public byte Status;
public byte Data1;
public byte Data2;
public Midi(byte port, byte status, byte data1, byte data2)
{
this.Port = port;
this.Status = status;
this.Data1 = data1;
this.Data2 = data2;
}
public override bool Equals(System.Object obj)
{
if (obj.GetType() == typeof(Midi))
{
if (this.Port == ((Midi)obj).Port && this.Status == ((Midi)obj).Status && this.Data1 == ((Midi)obj).Data1 && this.Data2 == ((Midi)obj).Data2)
return true;
else
return false;
}
else if (obj.GetType() == typeof(byte[]))
{
if (this.Port == ((byte[])obj)[0] && this.Status == ((byte[])obj)[1] && this.Data1 == ((byte[])obj)[2] && this.Data2 == ((byte[])obj)[3])
return true;
else
return false;
}
else
return false;
}
public static bool operator ==(Midi a, Midi b)
{
if (a.Equals(b))
return true;
else
return false;
}
public static bool operator !=(Midi a, Midi b)
{
if (!a.Equals(b))
return true;
else
return false;
}
public override int GetHashCode()
{
return (Port << 24) + (Status << 16) + (Data1 << 8) + (Data2);
}
}
}
@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SharpOSC
{
public class OscBundle : OscPacket
{
Timetag _timetag;
public UInt64 Timetag
{
get { return _timetag.Tag; }
set { _timetag.Tag = value; }
}
public DateTime Timestamp
{
get { return _timetag.Timestamp; }
set { _timetag.Timestamp = value; }
}
public List<OscMessage> Messages;
public OscBundle(UInt64 timetag, params OscMessage[] args)
{
_timetag = new Timetag(timetag);
Messages = new List<OscMessage>();
Messages.AddRange(args);
}
public override byte[] GetBytes()
{
string bundle = "#bundle";
int bundleTagLen = Utils.AlignedStringLength(bundle);
byte[] tag = setULong(_timetag.Tag);
List<byte[]> outMessages = new List<byte[]>();
foreach (OscMessage msg in Messages)
{
outMessages.Add(msg.GetBytes());
}
int len = bundleTagLen + tag.Length + outMessages.Sum(x => x.Length + 4);
int i = 0;
byte[] output = new byte[len];
Encoding.UTF8.GetBytes(bundle).CopyTo(output, i);
i += bundleTagLen;
tag.CopyTo(output, i);
i += tag.Length;
foreach (byte[] msg in outMessages)
{
var size = setInt(msg.Length);
size.CopyTo(output, i);
i += size.Length;
msg.CopyTo(output, i);
i += msg.Length; // msg size is always a multiple of 4
}
return output;
}
}
}
@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace SharpOSC
{
public class OscMessage : OscPacket
{
public string Address;
public List<object> Arguments;
public OscMessage(string address, params object[] args)
{
this.Address = address;
Arguments = new List<object>();
Arguments.AddRange(args);
}
public override byte[] GetBytes()
{
List<byte[]> parts = new List<byte[]>();
List<object> currentList = Arguments;
int ArgumentsIndex = 0;
string typeString = ",";
int i = 0;
while (i < currentList.Count)
{
var arg = currentList[i];
string type = (arg != null) ? arg.GetType().ToString() : "null";
switch (type)
{
case "System.Int32":
typeString += "i";
parts.Add(setInt((int)arg));
break;
case "System.Single":
if (float.IsPositiveInfinity((float)arg))
{
typeString += "I";
}
else
{
typeString += "f";
parts.Add(setFloat((float)arg));
}
break;
case "System.String":
typeString += "s";
parts.Add(setString((string)arg));
break;
case "System.Byte[]":
typeString += "b";
parts.Add(setBlob((byte[])arg));
break;
case "System.Int64":
typeString += "h";
parts.Add(setLong((Int64)arg));
break;
case "System.UInt64":
typeString += "t";
parts.Add(setULong((UInt64)arg));
break;
case "SharpOSC.Timetag":
typeString += "t";
parts.Add(setULong(((Timetag)arg).Tag));
break;
case "System.Double":
if (Double.IsPositiveInfinity((double)arg))
{
typeString += "I";
}
else
{
typeString += "d";
parts.Add(setDouble((double)arg));
}
break;
case "SharpOSC.Symbol":
typeString += "S";
parts.Add(setString(((Symbol)arg).Value));
break;
case "System.Char":
typeString += "c";
parts.Add(setChar((char)arg));
break;
case "SharpOSC.RGBA":
typeString += "r";
parts.Add(setRGBA((RGBA)arg));
break;
case "SharpOSC.Midi":
typeString += "m";
parts.Add(setMidi((Midi)arg));
break;
case "System.Boolean":
typeString += ((bool)arg) ? "T" : "F";
break;
case "null":
typeString += "N";
break;
// This part handles arrays. It points currentList to the array and resets i
// The array is processed like normal and when it is finished we replace
// currentList back with Arguments and continue from where we left off
case "System.Object[]":
case "System.Collections.Generic.List`1[System.Object]":
if(arg.GetType() == typeof(object[]))
arg = ((object[])arg).ToList();
if (Arguments != currentList)
throw new Exception("Nested Arrays are not supported");
typeString += "[";
currentList = (List<object>)arg;
ArgumentsIndex = i;
i = 0;
continue;
default:
throw new Exception("Unable to transmit values of type " + type);
}
i++;
if (currentList != Arguments && i == currentList.Count)
{
// End of array, go back to main Argument list
typeString += "]";
currentList = Arguments;
i = ArgumentsIndex+1;
}
}
int addressLen = (Address.Length == 0 || Address == null ) ? 0 : Utils.AlignedStringLength(Address);
int typeLen = Utils.AlignedStringLength(typeString);
int total = addressLen + typeLen + parts.Sum(x => x.Length);
byte[] output = new byte[total];
i = 0;
Encoding.UTF8.GetBytes(Address).CopyTo(output, i);
i += addressLen;
Encoding.UTF8.GetBytes(typeString).CopyTo(output, i);
i += typeLen;
foreach (byte[] part in parts)
{
part.CopyTo(output, i);
i += part.Length;
}
return output;
}
}
}
@@ -0,0 +1,494 @@
using System;
using System.Collections.Generic;
using System.Text;
using Serilog;
namespace SharpOSC
{
public abstract class OscPacket
{
public static OscPacket GetPacket(byte[] OscData)
{
if (OscData[0] == '#')
return parseBundle(OscData);
else
return parseMessage(OscData);
}
public abstract byte[] GetBytes();
#region Parse OSC packages
/// <summary>
/// Takes in an OSC bundle package in byte form and parses it into a more usable OscBundle object
/// </summary>
/// <param name="msg"></param>
/// <returns>Message containing various arguments and an address</returns>
private static OscMessage parseMessage(byte[] msg)
{
int index = 0;
//
//("Raw ASCII DATA: " + System.Text.Encoding.ASCII.GetString(msg));
//Log.Debug("Raw UTF-8 DATA: " + System.Text.Encoding.UTF8.GetString(msg));
string address = null;
char[] types = new char[0];
List<object> arguments = new List<object>();
List<object> mainArray = arguments; // used as a reference when we are parsing arrays to get the main array back
// Get address
address = getAddress(msg, index);
index += msg.FirstIndexAfter(address.Length, x => x == ',');
if (index % 4 != 0)
throw new Exception("Misaligned OSC Packet data. Address string is not padded correctly and does not align to 4 byte interval");
// Get type tags
types = getTypes(msg, index);
index += types.Length;
while (index % 4 != 0)
index++;
bool commaParsed = false;
foreach (char type in types)
{
// skip leading comma
if (type == ',' && !commaParsed)
{
commaParsed = true;
continue;
}
switch(type)
{
case ('\0'):
break;
case('i'):
int intVal = getInt(msg, index);
arguments.Add(intVal);
index += 4;
break;
case('f'):
float floatVal = getFloat(msg, index);
arguments.Add(floatVal);
index += 4;
break;
case('s'):
string stringVal = getString(msg, index);
arguments.Add(stringVal);
index += stringVal.Length;
break;
case('b'):
byte[] blob = getBlob(msg, index);
arguments.Add(blob);
index += 4 + blob.Length;
break;
case ('h'):
Int64 hval = getLong(msg, index);
arguments.Add(hval);
index += 8;
break;
case ('t'):
UInt64 sval = getULong(msg, index);
arguments.Add(new Timetag(sval));
index += 8;
break;
case ('d'):
double dval = getDouble(msg, index);
arguments.Add(dval);
index += 8;
break;
case ('S'):
string SymbolVal = getString(msg, index);
arguments.Add(new Symbol(SymbolVal));
index += SymbolVal.Length;
break;
case ('c'):
char cval = getChar(msg, index);
arguments.Add(cval);
index += 4;
break;
case ('r'):
RGBA rgbaval = getRGBA(msg, index);
arguments.Add(rgbaval);
index += 4;
break;
case ('m'):
Midi midival = getMidi(msg, index);
arguments.Add(midival);
index += 4;
break;
case ('T'):
arguments.Add(true);
break;
case ('F'):
arguments.Add(false);
break;
case ('N'):
arguments.Add(null);
break;
case ('I'):
arguments.Add(double.PositiveInfinity);
break;
case ('['):
if (arguments != mainArray)
throw new Exception("SharpOSC does not support nested arrays");
arguments = new List<object>(); // make arguments point to a new object array
break;
case (']'):
mainArray.Add(arguments); // add the array to the main array
arguments = mainArray; // make arguments point back to the main array
break;
default:
throw new Exception("OSC type tag '" + type + "' is unknown.");
}
while (index % 4 != 0)
index++;
}
return new OscMessage(address, arguments.ToArray());
}
/// <summary>
/// Takes in an OSC bundle package in byte form and parses it into a more usable OscBundle object
/// </summary>
/// <param name="bundle"></param>
/// <returns>Bundle containing elements and a timetag</returns>
private static OscBundle parseBundle(byte[] bundle)
{
UInt64 timetag;
List<OscMessage> messages = new List<OscMessage>();
int index = 0;
var bundleTag = Encoding.UTF8.GetString(bundle.SubArray(0, 8));
index += 8;
timetag = getULong(bundle, index);
index += 8;
if (bundleTag != "#bundle\0")
throw new Exception("Not a bundle");
while (index < bundle.Length)
{
int size = getInt(bundle, index);
index += 4;
byte[] messageBytes = bundle.SubArray(index, size);
var message = parseMessage(messageBytes);
messages.Add(message);
index += size;
while (index % 4 != 0)
index++;
}
OscBundle output = new OscBundle(timetag, messages.ToArray());
return output;
}
#endregion
#region Get arguments from byte array
private static string getAddress(byte[] msg, int index)
{
int i = index;
string address = "";
for (; i < msg.Length; i += 4)
{
if (msg[i] == ',')
{
if (i == 0)
return "";
address = Encoding.UTF8.GetString(msg.SubArray(index, i - 1));
break;
}
}
if (i >= msg.Length && address == null)
throw new Exception("no comma found");
return address.Replace("\0", "");
}
private static char[] getTypes(byte[] msg, int index)
{
int i = index + 4;
char[] types = null;
for (; i < msg.Length; i += 4)
{
if (msg[i - 1] == 0)
{
types = Encoding.UTF8.GetChars(msg.SubArray(index, i - index));
break;
}
}
if (types == null)
{
byte[] term = { 0 };
types = Encoding.UTF8.GetChars(term);
}
if (i >= msg.Length && types == null)
throw new Exception("No null terminator after type string");
return types;
}
private static int getInt(byte[] msg, int index)
{
int val = (msg[index] << 24) + (msg[index + 1] << 16) + (msg[index + 2] << 8) + (msg[index + 3] << 0);
return val;
}
private static float getFloat(byte[] msg, int index)
{
byte[] reversed = new byte[4];
reversed[3] = msg[index];
reversed[2] = msg[index+1];
reversed[1] = msg[index+2];
reversed[0] = msg[index + 3];
float val = System.BitConverter.ToSingle(reversed, 0);
return val;
}
private static string getString(byte[] msg, int index)
{
string output = null;
int i = index + 4;
for (; (i-1) < msg.Length; i += 4)
{
if (msg[i - 1] == 0)
{
output = Encoding.UTF8.GetString(msg.SubArray(index, i - index));
break;
}
}
if (output == null)
{
byte[] term = { 0 };
output = Encoding.UTF8.GetString(term);
}
if (i >= msg.Length && output == null)
throw new Exception("No null terminator after type string");
return output.Replace("\0", "");
}
private static byte[] getBlob(byte[] msg, int index)
{
int size = getInt(msg, index);
return msg.SubArray(index + 4, size);
}
private static UInt64 getULong(byte[] msg, int index)
{
UInt64 val = ((UInt64)msg[index] << 56) + ((UInt64)msg[index + 1] << 48) + ((UInt64)msg[index + 2] << 40) + ((UInt64)msg[index + 3] << 32)
+ ((UInt64)msg[index + 4] << 24) + ((UInt64)msg[index + 5] << 16) + ((UInt64)msg[index + 6] << 8) + ((UInt64)msg[index + 7] << 0);
return val;
}
private static Int64 getLong(byte[] msg, int index)
{
byte[] var = new byte[8];
var[7] = msg[index];
var[6] = msg[index+1];
var[5] = msg[index+2];
var[4] = msg[index+3];
var[3] = msg[index+4];
var[2] = msg[index+5];
var[1] = msg[index+6];
var[0] = msg[index+7];
Int64 val = BitConverter.ToInt64(var, 0);
return val;
}
private static double getDouble(byte[] msg, int index)
{
byte[] var = new byte[8];
var[7] = msg[index];
var[6] = msg[index + 1];
var[5] = msg[index + 2];
var[4] = msg[index + 3];
var[3] = msg[index + 4];
var[2] = msg[index + 5];
var[1] = msg[index + 6];
var[0] = msg[index + 7];
double val = BitConverter.ToDouble(var, 0);
return val;
}
private static char getChar(byte[] msg, int index)
{
return (char)msg[index + 3];
}
private static RGBA getRGBA(byte[] msg, int index)
{
return new RGBA(msg[index], msg[index + 1], msg[index + 2], msg[index + 3]);
}
private static Midi getMidi(byte[] msg, int index)
{
return new Midi(msg[index], msg[index + 1], msg[index + 2], msg[index + 3]);
}
#endregion
#region Create byte arrays for arguments
protected static byte[] setInt(int value)
{
byte[] msg = new byte[4];
var bytes = BitConverter.GetBytes(value);
msg[0] = bytes[3];
msg[1] = bytes[2];
msg[2] = bytes[1];
msg[3] = bytes[0];
return msg;
}
protected static byte[] setFloat(float value)
{
byte[] msg = new byte[4];
var bytes = BitConverter.GetBytes(value);
msg[0] = bytes[3];
msg[1] = bytes[2];
msg[2] = bytes[1];
msg[3] = bytes[0];
return msg;
}
protected static byte[] setString(string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
int len = bytes.Length + (4 - bytes.Length % 4);
if (len <= bytes.Length) len = len + 4;
byte[] msg = new byte[len];
bytes.CopyTo(msg, 0);
return msg;
}
protected static byte[] setBlob(byte[] value)
{
int len = value.Length + 4;
len = len + (4 - len % 4);
byte[] msg = new byte[len];
byte[] size = setInt(value.Length);
size.CopyTo(msg, 0);
value.CopyTo(msg, 4);
return msg;
}
protected static byte[] setLong(Int64 value)
{
byte[] rev = BitConverter.GetBytes(value);
byte[] output = new byte[8];
output[0] = rev[7];
output[1] = rev[6];
output[2] = rev[5];
output[3] = rev[4];
output[4] = rev[3];
output[5] = rev[2];
output[6] = rev[1];
output[7] = rev[0];
return output;
}
protected static byte[] setULong(UInt64 value)
{
byte[] rev = BitConverter.GetBytes(value);
byte[] output = new byte[8];
output[0] = rev[7];
output[1] = rev[6];
output[2] = rev[5];
output[3] = rev[4];
output[4] = rev[3];
output[5] = rev[2];
output[6] = rev[1];
output[7] = rev[0];
return output;
}
protected static byte[] setDouble(double value)
{
byte[] rev = BitConverter.GetBytes(value);
byte[] output = new byte[8];
output[0] = rev[7];
output[1] = rev[6];
output[2] = rev[5];
output[3] = rev[4];
output[4] = rev[3];
output[5] = rev[2];
output[6] = rev[1];
output[7] = rev[0];
return output;
}
protected static byte[] setChar(char value)
{
byte[] output = new byte[4];
output[0] = 0;
output[1] = 0;
output[2] = 0;
output[3] = (byte)value;
return output;
}
protected static byte[] setRGBA(RGBA value)
{
byte[] output = new byte[4];
output[0] = value.Red;
output[1] = value.Green;
output[2] = value.Blue;
output[3] = value.Alpha;
return output;
}
protected static byte[] setMidi(Midi value)
{
byte[] output = new byte[4];
output[0] = value.Port;
output[1] = value.Status;
output[2] = value.Data1;
output[3] = value.Data2;
return output;
}
#endregion
}
}
@@ -0,0 +1,59 @@
namespace SharpOSC
{
public struct RGBA
{
public byte Red;
public byte Green;
public byte Blue;
public byte Alpha;
public RGBA(byte red, byte green, byte blue, byte alpha)
{
this.Red = red;
this.Green = green;
this.Blue = blue;
this.Alpha = alpha;
}
public override bool Equals(System.Object obj)
{
if (obj.GetType() == typeof(RGBA))
{
if (this.Red == ((RGBA)obj).Red && this.Green == ((RGBA)obj).Green && this.Blue == ((RGBA)obj).Blue && this.Alpha == ((RGBA)obj).Alpha)
return true;
else
return false;
}
else if (obj.GetType() == typeof(byte[]))
{
if (this.Red == ((byte[])obj)[0] && this.Green == ((byte[])obj)[1] && this.Blue == ((byte[])obj)[2] && this.Alpha == ((byte[])obj)[3])
return true;
else
return false;
}
else
return false;
}
public static bool operator ==(RGBA a, RGBA b)
{
if (a.Equals(b))
return true;
else
return false;
}
public static bool operator !=(RGBA a, RGBA b)
{
if (!a.Equals(b))
return true;
else
return false;
}
public override int GetHashCode()
{
return (Red << 24) + (Green << 16) + (Blue << 8) + (Alpha);
}
}
}
@@ -0,0 +1,82 @@
using System.Collections.Generic;
namespace SharpOSC
{
public static class SlipFrame
{
public static readonly byte END = 0xc0;
public static readonly byte ESC = 0xdb;
public static readonly byte ESC_END = 0xDC;
public static readonly byte ESC_ESC = 0xDD;
public static List<byte[]> Decode(byte[] data)
{
List<byte[]> messages = new List<byte[]>();
List<byte> buffer = new List<byte>();
bool escapeNext = false;
for (int i = 0; i < data.Length; i++)
{
if (data[i] == ESC)
{
escapeNext = true;
continue;
}
if (escapeNext)
{
if (data[i] == ESC_END)
{
buffer.Add(END);
}
else if (data[i] == ESC_ESC)
{
buffer.Add(ESC);
}
escapeNext = false;
}
else if (data[i] == END)
{
if(buffer.Count > 0)
{
messages.Add(buffer.ToArray());
}
buffer.Clear();
}
else
{
buffer.Add(data[i]);
}
}
return messages;
}
public static byte[] Encode(byte[] data)
{
List<byte> slipData = new List<byte>();
byte[] esc_end = { ESC, ESC_END };
byte[] esc_esc = { ESC, ESC_ESC };
byte[] end = { END };
int length = data.Length;
for (int i = 0; i < length; i++)
{
if (data[i] == END)
{
slipData.AddRange(esc_end);
}
else if (data[i] == ESC)
{
slipData.AddRange(esc_esc);
}
else
{
slipData.Add(data[i]);
}
}
slipData.AddRange(end);
return slipData.ToArray();
}
}
}
@@ -0,0 +1,64 @@
namespace SharpOSC
{
public class Symbol
{
public string Value;
public Symbol()
{
Value = "";
}
public Symbol(string value)
{
this.Value = value;
}
override
public string ToString()
{
return Value;
}
public override bool Equals(System.Object obj)
{
if (obj.GetType() == typeof(Symbol))
{
if (this.Value == ((Symbol)obj).Value)
return true;
else
return false;
}
else if (obj.GetType() == typeof(string))
{
if (this.Value == ((string)obj))
return true;
else
return false;
}
else
return false;
}
public static bool operator ==(Symbol a, Symbol b)
{
if (a.Equals(b))
return true;
else
return false;
}
public static bool operator !=(Symbol a, Symbol b)
{
if (!a.Equals(b))
return true;
else
return false;
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
}
}
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Serilog;
using TcpSharp;
namespace SharpOSC
{
public class MessageEventArgs : EventArgs
{
public OscMessage Message
{
get;
set;
}
}
public class TCPClient
{
private ILogger _log = Log.Logger.ForContext<TCPClient>();
public int Port
{
get { return _port; }
}
int _port;
public string Address
{
get { return _address; }
}
public delegate void MessageReceivedHandler(object source, MessageEventArgs args);
public event MessageReceivedHandler? MessageReceived;
string _address;
TcpSharpSocketClient? tcpClient;
private List<byte> frameStream = new List<byte>();
public TCPClient(string address, int port)
{
_port = port;
_address = address;
}
public bool Connect()
{
try
{
tcpClient = new TcpSharpSocketClient(_address, _port);
tcpClient.OnConnected += ClientConnected;
tcpClient.OnDataReceived += DataReceived;
tcpClient.OnDisconnected += ClientDisconneted;
tcpClient.OnError += ClientError;
tcpClient.Connect();
return true;
}
catch (Exception e)
{
_log.Error(e.StackTrace);
return false;
}
}
private void ClientError(object sender, OnClientErrorEventArgs e)
{
_log.Error("client error");
_log.Error(e.Exception.ToString());
}
private void ClientDisconneted(object sender, OnClientDisconnectedEventArgs e)
{
_log.Information($"client disconnected: {e.Reason}");
Close();
}
private void DataReceived(object sender, OnClientDataReceivedEventArgs e)
{
frameStream.AddRange(e.Data);
int i = 0;
int frameEnd = frameStream.FindIndex(1, frameByte => frameByte.Equals(SlipFrame.END));
while (frameEnd > 0)
{
List<byte> frame = frameStream.GetRange(0, frameEnd + 1);
frameStream.RemoveRange(0, frameEnd + 1);
List<byte[]> messages = SlipFrame.Decode(frame.ToArray());
foreach (var message in messages)
{
try
{
OscPacket packet = OscPacket.GetPacket(message);
OscMessage responseMessage = (OscMessage)packet;
if (packet == null)
{
_log.Error("packet is null");
}
if (responseMessage == null)
{
_log.Error("responeMessage is null");
}
OnMessageReceived(responseMessage);
}
catch (Exception ex)
{
_log.Error($"Exception parsing OSC message: {ex.ToString()}");
}
}
frameEnd = frameStream.FindIndex(0, frameByte => frameByte.Equals(SlipFrame.END));
}
}
private void ClientConnected(object sender, OnClientConnectedEventArgs e)
{
_log.Debug($"connected to <{_address}:{_port}>");
}
public void Send(byte[] message)
{
byte[] slipData = SlipFrame.Encode(message);
tcpClient.SendBytes(slipData.ToArray());
}
public void Send(OscPacket packet)
{
byte[] data = packet.GetBytes();
Send(data);
}
public bool IsConnected
{
get
{
if (tcpClient == null)
return false;
else
return tcpClient.Connected;
}
}
public void Close()
{
if (tcpClient != null)
{
tcpClient.OnConnected -= ClientConnected;
tcpClient.OnDataReceived -= DataReceived;
tcpClient.OnDisconnected -= ClientDisconneted;
if (tcpClient.Connected)
{
_log.Debug($"closing connection to {_address}");
tcpClient.Disconnect();
}
}
}
protected virtual void OnMessageReceived(OscMessage msg)
{
MessageReceived?.Invoke(this, new MessageEventArgs() { Message = msg });
}
}
}
@@ -0,0 +1,89 @@
using System;
namespace SharpOSC
{
public struct Timetag
{
public UInt64 Tag;
public DateTime Timestamp
{
get
{
return Utils.TimetagToDateTime(Tag);
}
set
{
Tag = Utils.DateTimeToTimetag(value);
}
}
/// <summary>
/// Gets or sets the fraction of a second in the timestamp. the double precision number is multiplied by 2^32
/// giving an accuracy down to about 230 picoseconds ( 1/(2^32) of a second)
/// </summary>
public double Fraction
{
get
{
return Utils.TimetagToFraction(Tag);
}
set
{
Tag = (Tag & 0xFFFFFFFF00000000) + (UInt32)(value * 0xFFFFFFFF);
}
}
public Timetag(UInt64 value)
{
this.Tag = value;
}
public Timetag(DateTime value)
{
Tag = 0;
this.Timestamp = value;
}
public override bool Equals(System.Object obj)
{
if (obj.GetType() == typeof(Timetag))
{
if (this.Tag == ((Timetag)obj).Tag)
return true;
else
return false;
}
else if (obj.GetType() == typeof(UInt64))
{
if (this.Tag == ((UInt64)obj))
return true;
else
return false;
}
else
return false;
}
public static bool operator ==(Timetag a, Timetag b)
{
if (a.Equals(b))
return true;
else
return false;
}
public static bool operator !=(Timetag a, Timetag b)
{
if (a.Equals(b))
return true;
else
return false;
}
public override int GetHashCode()
{
return (int)( ((uint)(Tag >> 32) + (uint)(Tag & 0x00000000FFFFFFFF)) / 2);
}
}
}
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Threading;
using Serilog;
namespace SharpOSC
{
public delegate void HandleOscPacket(OscPacket packet);
public delegate void HandleBytePacket(byte[] packet);
public class UDPListener : IDisposable
{
public int Port { get; private set; }
object callbackLock;
UdpClient receivingUdpClient;
IPEndPoint RemoteIpEndPoint;
HandleBytePacket BytePacketCallback = null;
HandleOscPacket OscPacketCallback = null;
Queue<byte[]> queue;
ManualResetEvent ClosingEvent;
public UDPListener(int port)
{
Port = port;
queue = new Queue<byte[]>();
ClosingEvent = new ManualResetEvent(false);
callbackLock = new object();
// try to open the port 10 times, else fail
for (int i = 0; i < 10; i++)
{
try
{
receivingUdpClient = new UdpClient(port);
break;
}
catch (Exception)
{
// Failed in ten tries, throw the exception and give up
if (i >= 9)
throw;
Thread.Sleep(5);
}
}
RemoteIpEndPoint = new IPEndPoint(IPAddress.Any, 0);
// setup first async event
AsyncCallback callBack = new AsyncCallback(ReceiveCallback);
receivingUdpClient.BeginReceive(callBack, null);
}
public UDPListener(int port, HandleOscPacket callback) : this(port)
{
this.OscPacketCallback = callback;
}
public UDPListener(int port, HandleBytePacket callback) : this(port)
{
this.BytePacketCallback = callback;
}
void ReceiveCallback(IAsyncResult result)
{
Monitor.Enter(callbackLock);
Byte[] bytes = null;
try
{
bytes = receivingUdpClient.EndReceive(result, ref RemoteIpEndPoint);
}
catch (ObjectDisposedException e)
{
// Ignore if disposed. This happens when closing the listener
Log.Debug(e.ToString());
}
// Process bytes
if (bytes != null && bytes.Length > 0)
{
if (BytePacketCallback != null)
{
BytePacketCallback(bytes);
}
else if (OscPacketCallback != null)
{
OscPacket packet = null;
try
{
packet = OscPacket.GetPacket(bytes);
}
catch (Exception e)
{
Log.Debug(e.ToString());
// If there is an error reading the packet, null is sent to the callback
}
//Log.Debug("Raw UDP In: " + System.Text.Encoding.ASCII.GetString(bytes));
OscPacketCallback(packet);
}
else
{
lock (queue)
{
queue.Enqueue(bytes);
}
}
}
if (closing)
ClosingEvent.Set();
else
{
// Setup next async event
AsyncCallback callBack = new AsyncCallback(ReceiveCallback);
receivingUdpClient.BeginReceive(callBack, null);
}
Monitor.Exit(callbackLock);
}
bool closing = false;
public void Close()
{
lock (callbackLock)
{
ClosingEvent.Reset();
closing = true;
receivingUdpClient.Close();
}
ClosingEvent.WaitOne();
}
public void Dispose()
{
this.Close();
}
public OscPacket Receive()
{
if (closing) throw new Exception("UDPListener has been closed.");
lock (queue)
{
if (queue.Count() > 0)
{
byte[] bytes = queue.Dequeue();
var packet = OscPacket.GetPacket(bytes);
//Log.Debug("Raw UDP In: " + System.Text.Encoding.ASCII.GetString(bytes));
return packet;
}
else
return null;
}
}
public byte[] ReceiveBytes()
{
if (closing) throw new Exception("UDPListener has been closed.");
lock (queue)
{
if (queue.Count() > 0)
{
byte[] bytes = queue.Dequeue();
return bytes;
}
else
return null;
}
}
}
}
@@ -0,0 +1,53 @@
using System;
using System.Net.Sockets;
using System.Net;
namespace SharpOSC
{
public class UDPSender
{
public int Port
{
get { return _port; }
}
int _port;
public string Address
{
get { return _address; }
}
string _address;
IPEndPoint RemoteIpEndPoint;
Socket sock;
public UDPSender(string address, int port)
{
_port = port;
_address = address;
sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
var addresses = System.Net.Dns.GetHostAddresses(address);
if (addresses.Length == 0) throw new Exception("Unable to find IP address for " + address);
RemoteIpEndPoint = new IPEndPoint(addresses[0], port);
}
public void Send(byte[] message)
{
sock.SendTo(message, RemoteIpEndPoint);
}
public void Send(OscPacket packet)
{
byte[] data = packet.GetBytes();
Send(data);
}
public void Close()
{
sock.Close();
}
}
}
@@ -0,0 +1,47 @@
using System;
namespace SharpOSC
{
public class Utils
{
public static DateTime TimetagToDateTime(UInt64 val)
{
if (val == 1)
return DateTime.Now;
UInt32 seconds = (UInt32)(val >> 32);
var time = DateTime.Parse("1900-01-01 00:00:00");
time = time.AddSeconds(seconds);
var fraction = TimetagToFraction(val);
time = time.AddSeconds(fraction);
return time;
}
public static double TimetagToFraction(UInt64 val)
{
if (val == 1)
return 0.0;
UInt32 seconds = (UInt32)(val & 0x00000000FFFFFFFF);
double fraction = (double)seconds / (UInt32)(0xFFFFFFFF);
return fraction;
}
public static UInt64 DateTimeToTimetag(DateTime value)
{
UInt64 seconds = (UInt32)(value - DateTime.Parse("1900-01-01 00:00:00.000")).TotalSeconds;
UInt64 fraction = (UInt32)(0xFFFFFFFF * ((double)value.Millisecond / 1000));
UInt64 output = (seconds << 32) + fraction;
return output;
}
public static int AlignedStringLength(string val)
{
int len = val.Length + (4 - val.Length % 4);
if (len <= val.Length) len += 4;
return len;
}
}
}
@@ -0,0 +1,157 @@
namespace TcpSharp;
public class ConnectedClient
{
/* Public Properties */
public TcpClient Client { get; internal set; }
public string ConnectionId { get; internal set; }
public bool Connected { get { return this.Client != null && this.Client.Connected; } }
public bool AcceptData { get; internal set; } = true;
public long BytesReceived { get; private set; }
public long BytesSent { get; private set; }
/* Reference Fields */
private readonly TcpSharpSocketServer _server;
/* Private Fields */
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly CancellationToken _cancellationToken;
internal ConnectedClient(TcpSharpSocketServer server, TcpClient client, string connectionId)
{
this.Client = client;
this.ConnectionId = connectionId;
this._server = server;
this._cancellationTokenSource = new CancellationTokenSource();
this._cancellationToken = this._cancellationTokenSource.Token;
}
internal void StartReceiving()
{
Task.Factory.StartNew(ReceivingTask, TaskCreationOptions.LongRunning);
}
internal void StopReceiving()
{
this._cancellationTokenSource.Cancel();
}
private async Task ReceivingTask()
{
var stream = this.Client.GetStream();
var buffer = new byte[this.Client.ReceiveBufferSize];
try
{
var bytesCount = 0;
while (!this._cancellationToken.IsCancellationRequested && (bytesCount = await stream.ReadAsync(buffer, 0, buffer.Length, this._cancellationToken)) != 0)
{
// Increase BytesReceived
BytesReceived += bytesCount;
this._server.AddReceivedBytes(bytesCount);
// Invoke OnDataReceived
if (this.AcceptData)
{
var bytesReceived = new byte[bytesCount];
Array.Copy(buffer, bytesReceived, bytesCount);
this._server.InvokeOnDataReceived(new OnServerDataReceivedEventArgs
{
Client = this.Client,
ConnectionId = this.ConnectionId,
Data = bytesReceived
});
}
}
}
catch (IOException ex)
{
// Disconnect
this._server.Disconnect(this.ConnectionId, DisconnectReason.Exception);
}
catch (Exception ex)
{
// Invoke OnError
this._server.InvokeOnError(new OnServerErrorEventArgs
{
Client = this.Client,
ConnectionId = this.ConnectionId,
Exception = ex
});
// Disconnect
this._server.Disconnect(this.ConnectionId, DisconnectReason.Exception);
}
}
public long SendBytes(byte[] bytes)
{
if (!this.Connected) return 0;
this.BytesSent += bytes.Length;
this._server.AddSentBytes(bytes.Length);
return this.Client.Client.Send(bytes);
}
public long SendString(string data)
{
if (!this.Connected) return 0;
var bytes = Encoding.UTF8.GetBytes(data);
this.BytesSent += bytes.Length;
this._server.AddSentBytes(bytes.Length);
return this.Client.Client.Send(bytes);
}
public long SendString(string data, Encoding encoding)
{
if (!this.Connected) return 0;
var bytes = encoding.GetBytes(data);
this.BytesSent += bytes.Length;
this._server.AddSentBytes(bytes.Length);
return this.Client.Client.Send(bytes);
}
public long SendFile(string filePath)
{
// Check Point
if (!this.Connected) return 0;
if (!File.Exists(filePath)) return 0;
// FileInfo
var fileInfo = new FileInfo(filePath);
if (fileInfo == null) return 0;
// Action
this.Client.Client.SendFile(filePath);
this.BytesSent += fileInfo.Length;
this._server.AddSentBytes(fileInfo.Length);
// Return
return fileInfo.Length;
}
public long SendFile(string filePath, byte[] preBuffer, byte[] postBuffer, TransmitFileOptions flags)
{
// Check Point
if (!this.Connected) return 0;
if (!File.Exists(filePath)) return 0;
// FileInfo
var fileInfo = new FileInfo(filePath);
if (fileInfo == null) return 0;
// Action
this.Client.Client.SendFile(filePath, preBuffer, postBuffer, flags);
this.BytesSent += fileInfo.Length;
this._server.AddSentBytes(fileInfo.Length);
// Return
return fileInfo.Length;
}
}
@@ -0,0 +1,9 @@
namespace TcpSharp;
public enum DisconnectReason : byte
{
None = 0,
Exception = 1,
ServerAborted = 2,
ServerStopped = 3,
}
@@ -0,0 +1,13 @@
namespace TcpSharp;
public class OnClientConnectedEventArgs : EventArgs
{
public IPAddress ServerIPAddress
{
get { return IPAddress.Parse(ServerHost); }
}
public string ServerHost { get; internal set; }
public int ServerPort { get; internal set; }
}
@@ -0,0 +1,6 @@
namespace TcpSharp;
public class OnClientDataReceivedEventArgs : EventArgs
{
public byte[] Data { get; internal set; }
}
@@ -0,0 +1,6 @@
namespace TcpSharp;
public class OnClientDisconnectedEventArgs : EventArgs
{
public DisconnectReason Reason { get; internal set; }
}
@@ -0,0 +1,6 @@
namespace TcpSharp;
public class OnClientErrorEventArgs : EventArgs
{
public Exception Exception { get; internal set; }
}
@@ -0,0 +1,8 @@
namespace TcpSharp;
public class OnClientReconnectedEventArgs : EventArgs
{
public IPAddress ServerIPAddress => IPAddress.Parse(ServerHost);
public string ServerHost { get; internal set; }
public int ServerPort { get; internal set; }
}
@@ -0,0 +1,9 @@
namespace TcpSharp;
public class OnServerConnectedEventArgs : EventArgs
{
public IPEndPoint IPEndPoint { get; internal set; }
public string IPAddress { get; internal set; }
public int Port { get; internal set; }
public string ConnectionId { get; internal set; }
}
@@ -0,0 +1,9 @@
namespace TcpSharp;
public class OnServerConnectionRequestEventArgs : EventArgs
{
public IPEndPoint IPEndPoint { get; internal set; }
public string IPAddress { get; internal set; }
public int Port { get; internal set; }
public bool Accept { get; set; } = true;
}
@@ -0,0 +1,8 @@
namespace TcpSharp;
public class OnServerDataReceivedEventArgs : EventArgs
{
public TcpClient Client { get; internal set; }
public string ConnectionId { get; internal set; }
public byte[] Data { get; internal set; }
}
@@ -0,0 +1,7 @@
namespace TcpSharp;
public class OnServerDisconnectedEventArgs : EventArgs
{
public string ConnectionId { get; internal set; }
public DisconnectReason Reason { get; internal set; }
}
@@ -0,0 +1,8 @@
namespace TcpSharp;
public class OnServerErrorEventArgs : EventArgs
{
public TcpClient Client { get; internal set; }
public string ConnectionId { get; internal set; }
public Exception Exception { get; internal set; }
}
@@ -0,0 +1,6 @@
namespace TcpSharp;
public class OnServerStartedEventArgs : EventArgs
{
public bool IsStarted { get; internal set; }
}
@@ -0,0 +1,6 @@
namespace TcpSharp;
public class OnServerStoppedEventArgs : EventArgs
{
public bool IsStopped { get; internal set; }
}
@@ -0,0 +1,226 @@
namespace TcpSharp;
/// <summary>
/// Generated id is composed of
/// <list type="bullet">
/// <item><description>time - 41 bits (millisecond precision w/ a custom epoch gives us 69 years)</description></item>
/// <item><description>configured machine id - 10 bits (5 bit worker id, 5 bits datacenter id) - gives us up to 1024 machines</description></item>
/// <item><description>sequence number - 12 bits - rolls over every 4096 per machine (with protection to avoid rollover in the same ms)</description></item>
/// </list>
/// </summary>
internal class SnowflakeGenerator : IEnumerable<long>
{
#region Private Constant
/// <summary>
/// 1 January 1970. Used to calculate timestamp (in milliseconds)
/// </summary>
private static readonly DateTime Jan1st1970 = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long Epoch = 1668124800000L;
/// <summary>
/// Number of bits allocated for a worker id in the generated identifier. 5 bits indicates values from 0 to 31
/// </summary>
private const int WorkerIdBits = 5;
/// <summary>
/// Datacenter identifier this worker belongs to. 5 bits indicates values from 0 to 31
/// </summary>
private const int DatacenterIdBits = 5;
/// <summary>
/// Generator identifier. 10 bits indicates values from 0 to 1023
/// </summary>
private const int GeneratorIdBits = 10;
/// <summary>
/// Maximum generator identifier
/// </summary>
private const long MaxGeneratorId = -1 ^ -1L << GeneratorIdBits;
/// <summary>
/// Maximum worker identifier
/// </summary>
private const long MaxWorkerId = -1L ^ -1L << WorkerIdBits;
/// <summary>
/// Maximum datacenter identifier
/// </summary>
private const long MaxDatacenterId = -1L ^ -1L << DatacenterIdBits;
/// <summary>
/// Number of bits allocated for sequence in the generated identifier
/// </summary>
private const int SequenceBits = 12;
private const int WorkerIdShift = SequenceBits;
private const int DatacenterIdShift = SequenceBits + WorkerIdBits;
private const int TimestampLeftShift = SequenceBits + WorkerIdBits + DatacenterIdBits;
private const long SequenceMask = -1L ^ -1L << SequenceBits;
#endregion Private Constant
#region Private Fields
/// <summary>
/// Object used as a monitor for threads synchronization.
/// </summary>
private readonly object monitor = new object();
/// <summary>
/// The timestamp used to generate last id by the worker
/// </summary>
private long lastTimestamp = -1L;
private long sequence = 0;
#endregion Private Fields
#region Public Properties
/// <summary>
/// Indicates how many times the given generator had to wait
/// for next millisecond <see cref="TillNextMillis"/> since startup.
/// </summary>
public int NextMillisecondWait { get; set; }
#endregion Public Properties
#region Public Constructors
public SnowflakeGenerator(int generatorId = 0, int sequence = 0) : this((int)(generatorId & MaxWorkerId), (int)(generatorId >> WorkerIdBits & MaxDatacenterId), sequence)
{
// sanity check for generatorId
if (generatorId > MaxGeneratorId || generatorId < 0)
{
throw new InvalidOperationException(
string.Format("Generator Id can't be greater than {0} or less than 0", MaxGeneratorId));
}
}
public SnowflakeGenerator(int workerId, int datacenterId, int sequence = 0)
{
// sanity check for workerId
if (workerId > MaxWorkerId || workerId < 0)
{
throw new InvalidOperationException(
string.Format("Worker Id can't be greater than {0} or less than 0", MaxWorkerId));
}
// sanity check for datacenterId
if (datacenterId > MaxDatacenterId || datacenterId < 0)
{
throw new InvalidOperationException(
string.Format("Datacenter Id can't be greater than {0} or less than 0", MaxDatacenterId));
}
WorkerId = workerId;
DatacenterId = datacenterId;
this.sequence = sequence;
}
#endregion Public Constructors
#region Public Properties
/// <summary>
/// The identifier of the worker
/// </summary>
public long WorkerId { get; private set; }
/// <summary>
/// Identifier of datacenter the worker belongs to
/// </summary>
public long DatacenterId { get; private set; }
#endregion Public Properties
#region Public Methods
public long GenerateId()
{
lock (monitor)
{
return NextId();
}
}
public IEnumerator<long> GetEnumerator()
{
while (true)
{
yield return GenerateId();
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion Public Methods
#region Private Properties
private static long CurrentTime
{
get { return (long)(DateTime.UtcNow - Jan1st1970).TotalMilliseconds; }
}
#endregion Private Properties
#region Public Methods
#endregion Public Methods
#region Private Static Methods
#endregion Private Static Methods
#region Private Methods
private long TillNextMillis(long lastTimestamp)
{
NextMillisecondWait++;
var timestamp = CurrentTime;
SpinWait.SpinUntil(() => (timestamp = CurrentTime) > lastTimestamp);
return timestamp;
}
private long NextId()
{
var timestamp = CurrentTime;
if (timestamp < lastTimestamp)
{
throw new InvalidOperationException(string.Format("Clock moved backwards. Refusing to generate id for {0} milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp)
{
sequence = sequence + 1 & SequenceMask;
if (sequence == 0)
{
timestamp = TillNextMillis(lastTimestamp);
}
}
else
{
sequence = 0;
}
lastTimestamp = timestamp;
return timestamp - Epoch << TimestampLeftShift |
DatacenterId << DatacenterIdShift |
WorkerId << WorkerIdShift |
sequence;
}
#endregion Private Methods
}
@@ -0,0 +1,519 @@
namespace TcpSharp;
public class TcpSharpSocketClient
{
#region Public Properties
public string Host
{
get { return _host; }
set
{
if (Connected)
throw (new Exception("Socket Client is already connected. You cant change this property while connected."));
_host = value;
}
}
public int Port
{
get { return _port; }
set
{
if (Connected)
throw (new Exception("Socket Client is already connected. You cant change this property while connected."));
_port = value;
}
}
public bool NoDelay
{
get { return _nodelay; }
set
{
_nodelay = value;
if (_socket != null) _socket.NoDelay = value;
}
}
public bool KeepAlive
{
get { return _keepAlive; }
set
{
if (Connected)
throw (new Exception("Socket Client is already connected. You cant change this property while connected."));
_keepAlive = value;
}
}
public int KeepAliveTime
{
get { return _keepAliveTime; }
set
{
if (Connected)
throw (new Exception("Socket Client is already connected. You cant change this property while connected."));
_keepAliveTime = value;
}
}
/// <summary>
/// Keep-alive interval in seconds
/// </summary>
public int KeepAliveInterval
{
get { return _keepAliveInterval; }
set
{
if (Connected)
throw (new Exception("Socket Client is already connected. You cant change this property while connected."));
_keepAliveInterval = value;
}
}
public int KeepAliveRetryCount
{
get { return _keepAliveRetryCount; }
set
{
if (Connected)
throw (new Exception("Socket Client is already connected. You cant change this property while connected."));
_keepAliveRetryCount = value;
}
}
public int ReceiveBufferSize
{
get { return _receiveBufferSize; }
set
{
_receiveBufferSize = value;
_recvBuffer = new byte[value];
if (_socket != null) _socket.ReceiveBufferSize = value;
}
}
public int ReceiveTimeout
{
get { return _receiveTimeout; }
set
{
_receiveTimeout = value;
if (_socket != null) _socket.ReceiveTimeout = value;
}
}
public int SendBufferSize
{
get { return _sendBufferSize; }
set
{
_sendBufferSize = value;
_sendBuffer = new byte[value];
if (_socket != null) _socket.SendBufferSize = value;
}
}
public int SendTimeout
{
get { return _sendTimeout; }
set
{
_sendTimeout = value;
if (_socket != null) _socket.SendTimeout = value;
}
}
public long BytesReceived
{
get { return _bytesReceived; }
internal set { _bytesReceived = value; }
}
public long BytesSent
{
get { return _bytesSent; }
internal set { _bytesSent = value; }
}
public bool Reconnect
{
get { return _reconnect; }
set { _reconnect = value; }
}
public bool Reconnecting { get; private set; }
public int ReconnectDelayInSeconds
{
get { return _reconnectDelay; }
set { _reconnectDelay = value; }
}
public bool AcceptData
{
get { return _acceptData; }
set { _acceptData = value; }
}
public bool Connected { get { return this._socket != null && this._socket.Connected; } }
#endregion
#region Private Properties
private string _host;
private int _port;
private bool _nodelay = true;
private bool _keepAlive = false;
private int _keepAliveTime = 900;
private int _keepAliveInterval = 300;
private int _keepAliveRetryCount = 5;
private int _receiveBufferSize = 8192;
private int _receiveTimeout = 0;
private int _sendBufferSize = 8192;
private int _sendTimeout = 0;
private long _bytesReceived;
private long _bytesSent;
private bool _reconnect = false;
private int _reconnectDelay = 5;
private bool _acceptData = true;
#endregion
#region Public Events
public event EventHandler<OnClientErrorEventArgs> OnError = delegate { };
public event EventHandler<OnClientConnectedEventArgs> OnConnected = delegate { };
public event EventHandler<OnClientReconnectedEventArgs> OnReconnected = delegate { };
public event EventHandler<OnClientDisconnectedEventArgs> OnDisconnected = delegate { };
public event EventHandler<OnClientDataReceivedEventArgs> OnDataReceived = delegate { };
#endregion
#region Event Invokers
internal void InvokeOnError(OnClientErrorEventArgs args) => this.OnError?.Invoke(this, args);
internal void InvokeOnConnected(OnClientConnectedEventArgs args) => this.OnConnected?.Invoke(this, args);
internal void InvokeOnReconnected(OnClientReconnectedEventArgs args) => this.OnReconnected?.Invoke(this, args);
internal void InvokeOnDisconnected(OnClientDisconnectedEventArgs args) => this.OnDisconnected?.Invoke(this, args);
internal void InvokeOnDataReceived(OnClientDataReceivedEventArgs args) => this.OnDataReceived?.Invoke(this, args);
#endregion
#region Private Fields
private Socket _socket;
private byte[] _recvBuffer;
private byte[] _sendBuffer;
#endregion
#region Receiver Task
private CancellationTokenSource _cancellationTokenSource;
private CancellationToken _cancellationToken;
#endregion
#region Constructors
public TcpSharpSocketClient() : this("127.0.0.1", 1024)
{
}
public TcpSharpSocketClient(string host, int port)
{
this.Host = host;
this.Port = port;
}
#endregion
#region Public Methods
public void Connect()
{
// Buffers
this._recvBuffer = new byte[ReceiveBufferSize];
this._sendBuffer = new byte[SendBufferSize];
// Get Host IP Address that is used to establish a connection
// In this case, we get one IP address of localhost that is IP : 127.0.0.1
// If a host has multiple addresses, you will get a list of addresses
IPAddress serverIPAddress;
if (!IPAddress.TryParse(Host, out serverIPAddress))
{
var serverIPHost = Dns.GetHostEntry(Host);
if (serverIPHost.AddressList.Length == 0) throw new Exception("Unable to solve host address");
serverIPAddress = serverIPHost.AddressList[0];
if (serverIPAddress.ToString() == "::1") serverIPAddress = new IPAddress(16777343); // 127.0.0.1
}
var serverIPEndPoint = new IPEndPoint(serverIPAddress, Port);
// Create a TCP/IP socket.
this._socket = new Socket(serverIPAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
// Set Properties
this._socket.NoDelay = this.NoDelay;
this._socket.ReceiveBufferSize = this.ReceiveBufferSize;
this._socket.ReceiveTimeout = this.ReceiveTimeout;
this._socket.SendBufferSize = this.SendBufferSize;
this._socket.SendTimeout = this.SendTimeout;
/* Keep Alive */
if (this.KeepAlive && this.KeepAliveInterval > 0)
{
#if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER
_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
_socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, this.KeepAliveTime);
_socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, this.KeepAliveInterval);
_socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, this.KeepAliveRetryCount);
#elif NETFRAMEWORK
// Get the size of the uint to use to back the byte array
// int size = Marshal.SizeOf((uint)0);
int size = sizeof(uint);
// Create the byte array
byte[] keepAlive = new byte[size * 3];
// Pack the byte array:
// Turn keepalive on
Buffer.BlockCopy(BitConverter.GetBytes((uint)1), 0, keepAlive, 0, size);
// How long does it take to start the first probe (in milliseconds)
Buffer.BlockCopy(BitConverter.GetBytes((uint)(KeepAliveTime * 1000)), 0, keepAlive, size, size);
// Detection time interval (in milliseconds)
Buffer.BlockCopy(BitConverter.GetBytes((uint)(KeepAliveInterval * 1000)), 0, keepAlive, size * 2, size);
// Set the keep-alive settings on the underlying Socket
_socket.IOControl(IOControlCode.KeepAliveValues, keepAlive, null);
#elif NETSTANDARD
// Set the keep-alive settings on the underlying Socket
_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
#endif
}
// Connect to Remote EndPoint
_socket.Connect(serverIPEndPoint);
// Start Receiver Thread
if (this._cancellationTokenSource != null) this._cancellationTokenSource.Cancel();
this._cancellationTokenSource = new CancellationTokenSource();
this._cancellationToken = this._cancellationTokenSource.Token;
Task.Factory.StartNew(ReceiverTask, TaskCreationOptions.LongRunning);
// Invoke OnConnected
this.InvokeOnConnected(new OnClientConnectedEventArgs
{
ServerHost = this.Host,
ServerPort = this.Port,
});
}
public void Disconnect()
{
this.Disconnect(DisconnectReason.None);
}
public long SendBytes(byte[] bytes)
{
// Check Point
if (!this.Connected) return 0;
// Action
var sent = this._socket.Send(bytes);
this.BytesSent += sent;
// Return
return sent;
}
public async Task<long> SendBytesAsync(byte[] bytes, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendBytes(bytes);
}
public long SendString(string data)
{
// Check Point
if (!this.Connected) return 0;
// Action
var bytes = Encoding.UTF8.GetBytes(data);
var sent = this._socket.Send(bytes);
this.BytesSent += sent;
// Return
return sent;
}
public async Task<long> SendStringAsync(string data, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendString(data);
}
public long SendString(string data, Encoding encoding)
{
// Check Point
if (!this.Connected) return 0;
// Action
var bytes = encoding.GetBytes(data);
var sent = this._socket.Send(bytes);
this.BytesSent += sent;
// Return
return sent;
}
public async Task<long> SendStringAsync(string data, Encoding encoding, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendString(data, encoding);
}
public long SendFile(string fileName)
{
// Check Point
if (!this.Connected) return 0;
if (!File.Exists(fileName)) return 0;
// FileInfo
var fileInfo = new FileInfo(fileName);
if (fileInfo == null) return 0;
// Action
this._socket.SendFile(fileName);
this.BytesSent += fileInfo.Length;
// Return
return fileInfo.Length;
}
public async Task<long> SendFileAsync(string fileName, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendFile(fileName);
}
public long SendFile(string fileName, byte[] preBuffer, byte[] postBuffer, TransmitFileOptions flags)
{
// Check Point
if (!this.Connected) return 0;
if (!File.Exists(fileName)) return 0;
// FileInfo
var fileInfo = new FileInfo(fileName);
if (fileInfo == null) return 0;
// Action
this._socket.SendFile(fileName, preBuffer, postBuffer, flags);
this.BytesSent += fileInfo.Length;
// Return
return fileInfo.Length;
}
public async Task<long> SendFileAsync(string fileName, byte[] preBuffer, byte[] postBuffer, TransmitFileOptions flags, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendFile(fileName, preBuffer, postBuffer, flags);
}
#endregion
#region Private Methods
private void ReceiverTask()
{
try
{
while (!_cancellationToken.IsCancellationRequested)
{
// Receive the response from the remote device.
var bytesCount = _socket.Receive(_recvBuffer);
if (bytesCount > 0)
{
BytesReceived += bytesCount;
if (this.AcceptData)
{
var bytes = new byte[bytesCount];
Array.Copy(_recvBuffer, bytes, bytesCount);
// Invoke OnDataReceived
this.InvokeOnDataReceived(new OnClientDataReceivedEventArgs
{
Data = bytes
});
}
}
}
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.ConnectionAborted)
{
Disconnect(DisconnectReason.ServerAborted);
}
else
{
Disconnect(DisconnectReason.None);
}
}
catch (Exception ex)
{
// Invoke OnError
this.InvokeOnError(new OnClientErrorEventArgs
{
Exception = ex
});
// Disconnect
Disconnect(DisconnectReason.Exception);
}
}
private void Disconnect(DisconnectReason reason)
{
try
{
// Stop Receiver Task
this._cancellationTokenSource.Cancel();
}
catch { }
try
{
// Release the socket.
_socket.Shutdown(SocketShutdown.Both);
_socket.Close();
}
catch { }
try
{
// Dispose
_socket.Dispose();
}
catch { }
// Invoke OnDisconnected
this.InvokeOnDisconnected(new OnClientDisconnectedEventArgs());
// Reconnect
if (this.Reconnect && !Reconnecting)
{
Reconnecting = true;
while (!this.Connected)
{
try
{
Task.Delay(this.ReconnectDelayInSeconds * 1000);
this.Connect();
break;
}
catch { }
}
Reconnecting = false;
// Invoke OnReconnected
this.InvokeOnReconnected(new OnClientReconnectedEventArgs
{
ServerHost = this.Host,
ServerPort = this.Port,
});
}
}
#endregion
}
@@ -0,0 +1,463 @@
namespace TcpSharp;
public class TcpSharpSocketServer
{
#region Public Properties
public bool Listening
{
get { return _isListening; }
private set { _isListening = value; }
}
public int Port
{
get { return _port; }
set
{
if (Listening)
throw (new Exception("Socket Server is already listening. You cant change this property while listening."));
_port = value;
}
}
public bool NoDelay
{
get { return _nodelay; }
private set { _nodelay = value; }
}
public bool KeepAlive
{
get { return _keepAlive; }
set
{
if (Listening)
throw (new Exception("Socket Server is already listening. You cant change this property while listening."));
_keepAlive = value;
}
}
public int KeepAliveTime
{
get { return _keepAliveTime; }
set
{
if (Listening)
throw (new Exception("Socket Server is already listening. You cant change this property while listening."));
_keepAliveTime = value;
}
}
/// <summary>
/// Keep-alive interval in seconds
/// </summary>
public int KeepAliveInterval
{
get { return _keepAliveInterval; }
set
{
if (Listening)
throw (new Exception("Socket Server is already listening. You cant change this property while listening."));
_keepAliveInterval = value;
}
}
public int KeepAliveRetryCount
{
get { return _keepAliveRetryCount; }
set
{
if (Listening)
throw (new Exception("Socket Server is already listening. You cant change this property while listening."));
_keepAliveRetryCount = value;
}
}
public int ReceiveBufferSize
{
get { return _receiveBufferSize; }
set { _receiveBufferSize = value; }
}
public int ReceiveTimeout
{
get { return _receiveTimeout; }
set { _receiveTimeout = value; }
}
public int SendBufferSize
{
get { return _sendBufferSize; }
set { _sendBufferSize = value; }
}
public int SendTimeout
{
get { return _sendTimeout; }
set { _sendTimeout = value; }
}
public long BytesReceived
{
get { return _bytesReceived; }
internal set { _bytesReceived = value; }
}
public long BytesSent
{
get { return _bytesSent; }
internal set { _bytesSent = value; }
}
#endregion
#region Private Properties
private bool _isListening;
private int _port;
private bool _nodelay = true;
private bool _keepAlive = false;
private int _keepAliveTime = 900;
private int _keepAliveInterval = 300;
private int _keepAliveRetryCount = 5;
private int _receiveBufferSize = 8192;
private int _receiveTimeout = 0;
private int _sendBufferSize = 8192;
private int _sendTimeout = 0;
private long _bytesReceived = 0;
private long _bytesSent = 0;
#endregion
#region Public Events
public event EventHandler<OnServerStartedEventArgs> OnStarted = delegate { };
public event EventHandler<OnServerStoppedEventArgs> OnStopped = delegate { };
public event EventHandler<OnServerErrorEventArgs> OnError = delegate { };
public event EventHandler<OnServerConnectionRequestEventArgs> OnConnectionRequest = delegate { };
public event EventHandler<OnServerConnectedEventArgs> OnConnected = delegate { };
public event EventHandler<OnServerDisconnectedEventArgs> OnDisconnected = delegate { };
public event EventHandler<OnServerDataReceivedEventArgs> OnDataReceived = delegate { };
#endregion
#region Event Invokers
internal void InvokeOnStarted(OnServerStartedEventArgs args) => this.OnStarted?.Invoke(this, args);
internal void InvokeOnStopped(OnServerStoppedEventArgs args) => this.OnStopped?.Invoke(this, args);
internal void InvokeOnError(OnServerErrorEventArgs args) => this.OnError?.Invoke(this, args);
internal void InvokeOnConnectionRequest(OnServerConnectionRequestEventArgs args) => this.OnConnectionRequest?.Invoke(this, args);
internal void InvokeOnConnected(OnServerConnectedEventArgs args) => this.OnConnected?.Invoke(this, args);
internal void InvokeOnDisconnected(OnServerDisconnectedEventArgs args) => this.OnDisconnected?.Invoke(this, args);
internal void InvokeOnDataReceived(OnServerDataReceivedEventArgs args) => this.OnDataReceived?.Invoke(this, args);
#endregion
#region Readonly Properties
private TcpListener _listener;
private readonly SnowflakeGenerator _idGenerator;
private readonly ConcurrentDictionary<string, ConnectedClient> _clients;
#endregion
#region Listener Task
private CancellationTokenSource _cancellationTokenSource;
private CancellationToken _cancellationToken;
#endregion
#region Constructors
public TcpSharpSocketServer() : this(1024)
{
}
public TcpSharpSocketServer(int port)
{
this.Port = port;
this._idGenerator = new SnowflakeGenerator();
this._clients = new ConcurrentDictionary<string, ConnectedClient>();
}
#endregion
#region Public Methods
public void StartListening()
{
this._clients.Clear();
this._cancellationTokenSource = new CancellationTokenSource();
this._cancellationToken = this._cancellationTokenSource.Token;
Task.Factory.StartNew(ListeningTask, TaskCreationOptions.LongRunning);
}
public void StopListening()
{
// Disconnect All Clients
var connectionIds = _clients.Keys.ToList();
foreach (var connectionId in connectionIds)
{
Disconnect(connectionId, DisconnectReason.ServerStopped);
}
// Stop Listener
this._listener.Stop();
this.Listening = false;
// Stop Thread
this._cancellationTokenSource.Cancel();
// Invoke OnStopped
InvokeOnStopped(new OnServerStoppedEventArgs
{
IsStopped = true,
});
}
public ConnectedClient GetClient(string connectionId)
{
// Check Point
if (!_clients.ContainsKey(connectionId)) return null;
// Return Client
return _clients[connectionId];
}
public long SendBytes(string connectionId, byte[] bytes)
{
// Get Client
var client = GetClient(connectionId);
if (client == null) return 0;
// Send Bytes
return client.SendBytes(bytes);
}
public async Task<long> SendBytesAsync(string connectionId, byte[] bytes, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendBytes(connectionId, bytes);
}
public long SendString(string connectionId, string data)
{
// Get Client
var client = GetClient(connectionId);
if (client == null) return 0;
// Send Bytes
return client.SendString(data);
}
public async Task<long> SendStringAsync(string connectionId, string data, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendString(connectionId, data);
}
public long SendString(string connectionId, string data, Encoding encoding)
{
// Get Client
var client = GetClient(connectionId);
if (client == null) return 0;
// Send Bytes
return client.SendString(data, encoding);
}
public async Task<long> SendStringAsync(string connectionId, string data, Encoding encoding, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendString(connectionId, data, encoding);
}
public long SendFile(string connectionId, string fileName)
{
// Get Client
var client = GetClient(connectionId);
if (client == null) return 0;
// Send Bytes
return client.SendFile(fileName);
}
public async Task<long> SendFileAsync(string connectionId, string fileName, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendFile(connectionId, fileName);
}
public long SendFile(string connectionId, string fileName, byte[] preBuffer, byte[] postBuffer, TransmitFileOptions flags)
{
// Get Client
var client = GetClient(connectionId);
if (client == null) return 0;
// Send Bytes
return client.SendFile(fileName, preBuffer, postBuffer, flags);
}
public async Task<long> SendFileAsync(string connectionId, string fileName, byte[] preBuffer, byte[] postBuffer, TransmitFileOptions flags, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.CompletedTask;
return SendFile(connectionId, fileName, preBuffer, postBuffer, flags);
}
public void Disconnect(string connectionId, DisconnectReason reason = DisconnectReason.None)
{
// Check Point
if (!_clients.ContainsKey(connectionId)) return;
// Get Client
var client = _clients[connectionId];
// Check Point
// if (!client.Connected) return;
// Stop Receiving
client.StopReceiving();
// Disconnect
this.Disconnect(client.Client);
// Remove From Clients
_clients.TryRemove(connectionId, out _);
// Invoke OnDisconnected
InvokeOnDisconnected(new OnServerDisconnectedEventArgs
{
ConnectionId = connectionId,
Reason = reason,
});
}
#endregion
#region Internal Methods
internal void AddReceivedBytes(long bytesCount)
{
Interlocked.Add(ref _bytesReceived, bytesCount);
}
internal void AddSentBytes(long bytesCount)
{
Interlocked.Add(ref _bytesSent, bytesCount);
}
#endregion
#region Private Methods
private void Disconnect(TcpClient client)
{
try
{
client.GetStream().Close();
client.Close();
client.Dispose();
}
catch { }
}
private void ListeningTask()
{
this._listener = new TcpListener(IPAddress.Any, this.Port);
// NoDelay
this._listener.Server.NoDelay = this.NoDelay;
/* Keep Alive */
if (this.KeepAlive && this.KeepAliveInterval > 0)
{
#if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER
_listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
_listener.Server.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, this.KeepAliveTime);
_listener.Server.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, this.KeepAliveInterval);
_listener.Server.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, this.KeepAliveRetryCount);
#elif NETFRAMEWORK
// Get the size of the uint to use to back the byte array
// int size = Marshal.SizeOf((uint)0);
int size = sizeof(uint);
// Create the byte array
byte[] keepAlive = new byte[size * 3];
// Pack the byte array:
// Turn keepalive on
Buffer.BlockCopy(BitConverter.GetBytes((uint)1), 0, keepAlive, 0, size);
// How long does it take to start the first probe (in milliseconds)
Buffer.BlockCopy(BitConverter.GetBytes((uint)(KeepAliveTime*1000)), 0, keepAlive, size, size);
// Detection time interval (in milliseconds)
Buffer.BlockCopy(BitConverter.GetBytes((uint)(KeepAliveInterval*1000)), 0, keepAlive, size * 2, size);
// Set the keep-alive settings on the underlying Socket
_listener.Server.IOControl(IOControlCode.KeepAliveValues, keepAlive, null);
#elif NETSTANDARD
// Set the keep-alive settings on the underlying Socket
_listener.Server.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
#endif
}
// Start
this._listener.Start();
// Listening
this.Listening = true;
// Invoke OnStarted Event
InvokeOnStarted(new OnServerStartedEventArgs
{
IsStarted = true
});
// Loop for new connections
while (!this._cancellationToken.IsCancellationRequested)
{
// Getting new connections
var tcpClient = this._listener.AcceptTcpClient();
var ipEndPoint = (IPEndPoint)tcpClient.Client.RemoteEndPoint;
var ipAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address.ToString();
var port = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Port;
var cr_args = new OnServerConnectionRequestEventArgs
{
IPEndPoint = ipEndPoint,
IPAddress = ipAddress,
Port = port,
Accept = true
};
// Decide Accept or Reject
// Invoke OnConnectionRequest Event
InvokeOnConnectionRequest(cr_args);
// Reject
if (!cr_args.Accept)
{
this.Disconnect(tcpClient);
continue;
}
// Accept
tcpClient.NoDelay = this.NoDelay;
tcpClient.ReceiveBufferSize = this.ReceiveBufferSize;
tcpClient.ReceiveTimeout = this.ReceiveTimeout;
tcpClient.SendBufferSize = this.SendBufferSize;
tcpClient.SendTimeout = this.SendTimeout;
var nanoClient = new ConnectedClient(this, tcpClient, this._idGenerator.GenerateId().ToString());
this._clients[nanoClient.ConnectionId] = nanoClient;
// Start Receiving
nanoClient.StartReceiving();
// Invoke OnConnected Event
var c_args = new OnServerConnectedEventArgs
{
IPEndPoint = ipEndPoint,
IPAddress = ipAddress,
Port = port,
ConnectionId = nanoClient.ConnectionId
};
InvokeOnConnected(c_args);
}
// Listening
this.Listening = false;
}
#endregion
}
@@ -0,0 +1,10 @@
global using System;
global using System.Collections.Concurrent;
global using System.Collections.Generic;
global using System.IO;
global using System.Linq;
global using System.Net;
global using System.Net.Sockets;
global using System.Text;
global using System.Threading;
global using System.Threading.Tasks;
@@ -0,0 +1,41 @@
using Acr.UserDialogs;
using qController.Communication;
namespace qController.Dialogs
{
public class AddInstancePrompt : PromptConfig
{
public AddInstancePrompt()
{
Title = "Enter IP of QLab Computer";
Message = "Enter an IP Address to save";
OkText = "Next";
OnTextChanged = args =>
{
//IPAddress ugh
args.IsValid = IPHelper.IsValidAddress(args.Value);
};
OnAction = (qAddress) =>
{
if (!qAddress.Ok)
return;
UserDialogs.Instance.Prompt(new PromptConfig
{
Title = "Enter Name",
Message = "Enter a Name for " + qAddress.Text,
OkText = "Save",
OnAction = (qName) => {
Console.WriteLine("manual workspace added");
if (!qName.Ok)
return;
QStorage.AddInstance(qName.Text, qAddress.Text);
App.showToast("Manual Workspace Added!");
}
});
};
}
}
}
@@ -0,0 +1,22 @@
using System;
using Acr.UserDialogs;
using Microsoft.Maui.ApplicationModel;
namespace qController.Dialogs
{
public class DonatePrompt : ConfirmConfig
{
public DonatePrompt()
{
Title = "Support the Project";
Message = "This application will never be a paid app or contain any sort of ads, but if you choose to show your support you may do so by clicking the Donate button below. Thank you!";
OkText = "Donate";
OnAction = (response) =>
{
if (response)
Launcher.OpenAsync(new Uri("https://linktr.ee/JoelWetzell"));
};
}
}
}
@@ -0,0 +1,15 @@
using System;
using Acr.UserDialogs;
namespace qController.Dialogs
{
public class NoWorkspacesConfig : ConfirmConfig
{
public NoWorkspacesConfig(Action<bool> action)
{
Message = "QLab doesn't have any workspaces open?";
OkText = "Disconnect";
OnAction = action;
}
}
}
@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using Acr.UserDialogs;
using qController.QItems;
using Serilog;
namespace qController.Dialogs
{
public class WorkspacePromptArgs : EventArgs
{
public QWorkspace 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("CONTROLPAGE - Workspace Selected " + workspace.displayName);
if (!workspace.hasPasscode)
{
OnWorkspaceSelected(new QWorkspace(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 QWorkspace(workspace_id,resp.Value));
}
else
{
OnWorkspaceSelected(null);
}
}
});
}
protected virtual void OnWorkspaceSelected(QWorkspace workspace)
{
if (WorkspaceSelected != null)
WorkspaceSelected(this, new WorkspacePromptArgs() { SelectedWorkspace = workspace });
}
}
}
+51
View File
@@ -0,0 +1,51 @@
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);
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
using System.Collections.Generic;
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 QIcon.GetIconFromType("Group");
}
}
}
}
@@ -0,0 +1,10 @@
using System;
namespace qController.Classes.Cues
{
public class QDefinitions
{
public QDefinitions()
{
}
}
}
+32
View File
@@ -0,0 +1,32 @@
namespace qController.QItems
{
public class QInstance
{
public string name { get; set; }
public string address { get; set; }
public QInstance()
{
}
public QInstance(string tName, string tAddress)
{
name = tName;
address = tAddress;
}
public bool IsReachable()
{
/*try
{
IPAddress instanceIP = IPAddress.Parse(address);
Ping p = new System.Net.NetworkInformation.Ping();
PingReply reply = p.Send(instanceIP);
return reply.Status == IPStatus.Success ? true : false;
}
catch
{
return false;
}*/
return true;
}
}
}
+126
View File
@@ -0,0 +1,126 @@
//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 QWorkspace
{
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 QWorkspace(string id)
{
workspace_id = id;
}
public QWorkspace(string id, string passcode)
{
workspace_id = id;
this.passcode = passcode;
}
public QWorkspace()
{
}
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()
{
if(data == null)
{
IsPopulated = false;
return false;
}
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)");
}
}
}
}
@@ -0,0 +1,11 @@
//General information about a QLab Workspace
namespace qController.QItems
{
public class QWorkspaceInfo
{
public string version { get; set; }
public string displayName { get; set; }
public string uniqueID { get; set; }
public bool hasPasscode { get; set; }
}
}
@@ -0,0 +1,11 @@
namespace qController
{
public class MenuPageItem
{
public string Title { get; set; }
public string Icon { get; set; }
public string Command { get; set; }
}
}
@@ -0,0 +1,15 @@
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 = "!";
}
}
}
@@ -0,0 +1,11 @@
namespace qController
{
public class OSCListItem
{
public string Text { get; set; }
public string Icon { get; set; }
public string Command { get; set; }
}
}

Some files were not shown because too many files have changed in this diff Show More