Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
f0180b7246
|
@ -1,3 +1,9 @@
|
||||||
|
|
||||||
|
# Wasm SDK
|
||||||
|
/Ooui.Wasm/*.nupkg
|
||||||
|
/Ooui.Wasm.Test
|
||||||
|
/Ooui.Wasm.Old
|
||||||
|
|
||||||
# Social media files
|
# Social media files
|
||||||
/Media
|
/Media
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "Ooui.Wasm.Build.Tasks/linker"]
|
||||||
|
path = Ooui.Wasm.Build.Tasks/linker
|
||||||
|
url = git@github.com:mono/linker.git
|
|
@ -0,0 +1,428 @@
|
||||||
|
# Ooui.Forms Status
|
||||||
|
|
||||||
|
### ActivityIndicator
|
||||||
|
|
||||||
|
The [ActivityIndicator](https://developer.xamarin.com/api/type/Xamarin.Forms.ActivityIndicator/) control gives a visual clue to the user that something is happening, without information about its progress.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
IsRunning | Done
|
||||||
|
Color | Pending
|
||||||
|
|
||||||
|
### ActionSheets
|
||||||
|
|
||||||
|
The UIActionSheet is a common UI element in iOS. The Xamarin.Forms [DisplayActionSheet](https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/navigation/pop-ups/) method lets you include this control in cross-platforms apps, rendering native alternatives.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
Title | Pending
|
||||||
|
Cancel | Pending
|
||||||
|
Extras | Pending
|
||||||
|
Destruction | Pending
|
||||||
|
|
||||||
|
### BoxView
|
||||||
|
|
||||||
|
[BoxView](https://developer.xamarin.com/api/type/Xamarin.Forms.BoxView/) is a useful stand-in for images or custom elements when doing initial prototyping.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
Color | Done
|
||||||
|
|
||||||
|
### Button
|
||||||
|
|
||||||
|
A [button](https://developer.xamarin.com/api/type/Xamarin.Forms.Button/) View that reacts to touch events.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
IsEnabled | Done
|
||||||
|
Command | Done
|
||||||
|
CommandParameter | Done
|
||||||
|
ContentLayout | Done
|
||||||
|
Text | Done
|
||||||
|
TextColor | Done
|
||||||
|
Font | Done
|
||||||
|
FontFamily | Done
|
||||||
|
FontAttributes | Done
|
||||||
|
BorderWidth | Done
|
||||||
|
BorderColor | Done
|
||||||
|
BorderRadius | Done
|
||||||
|
Image | Pending
|
||||||
|
Clicked | Done
|
||||||
|
Pressed | Done
|
||||||
|
Released | Done
|
||||||
|
|
||||||
|
### CarouselPage
|
||||||
|
|
||||||
|
The Xamarin.Forms [CarouselPage](https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/navigation/carousel-page/) is a page that users can swipe from side to side to navigate through pages of content, like a gallery.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
ItemsSource | Pending
|
||||||
|
ItemTemplate | Pending
|
||||||
|
CurrentPage | Pending
|
||||||
|
Children | Pending
|
||||||
|
BackgrondImage | Pending
|
||||||
|
Icon | Pending
|
||||||
|
IsBusy | Pending
|
||||||
|
Padding | Pending
|
||||||
|
Tittle | Pending
|
||||||
|
ToolbarItems | Pending
|
||||||
|
CurrentPageChanged | Pending
|
||||||
|
PagesChanged | Pending
|
||||||
|
LayoutChanged | Pending
|
||||||
|
Appearing | Pending
|
||||||
|
Disappearing | Pending
|
||||||
|
|
||||||
|
### ContentPage
|
||||||
|
|
||||||
|
A [ContentPage](https://developer.xamarin.com/api/type/Xamarin.Forms.ContentPage/) is a Page displaying a single View, often a container like a StackLayout or ScrollView.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
Appearing | Pending
|
||||||
|
Disappearing | Pending
|
||||||
|
|
||||||
|
DatePicker
|
||||||
|
|
||||||
|
The visual representation of a [DatePicker](https://developer.xamarin.com/api/type/Xamarin.Forms.DatePicker/) is very similar to the one of Entry, except that a special control for picking a date appears in place of a keyboard.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
Format | Done
|
||||||
|
Date | Done
|
||||||
|
MinimumDate | Pending
|
||||||
|
MaximumDate | Pending
|
||||||
|
TextColor | Pending
|
||||||
|
DateSelected | Pending
|
||||||
|
|
||||||
|
### Editor
|
||||||
|
|
||||||
|
The [Editor](https://developer.xamarin.com/guides/xamarin-forms/user-interface/text/editor/) control is used to accept multi-line input.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
IsEnabled | Done
|
||||||
|
Text | Done
|
||||||
|
FontFamily | Done
|
||||||
|
FontSize | Done
|
||||||
|
FontAttributes | Done
|
||||||
|
TextColor | Done
|
||||||
|
TextChanged | Done
|
||||||
|
Completed | Done
|
||||||
|
|
||||||
|
### Entry
|
||||||
|
|
||||||
|
[Entry](https://developer.xamarin.com/api/type/Xamarin.Forms.Entry/) is a single line text entry. It is best used for collecting small discrete pieces of information, like usernames and passwords.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
IsEnabled | Done
|
||||||
|
Placeholder | Done
|
||||||
|
PlaceholderColor | Pending
|
||||||
|
Text | Done
|
||||||
|
TextColor | Done
|
||||||
|
FontFamily | Done
|
||||||
|
FontSize | Done
|
||||||
|
FontAttributes | Done
|
||||||
|
TextColor | Done
|
||||||
|
TextChanged | Done
|
||||||
|
Completed | Done
|
||||||
|
|
||||||
|
### Frame
|
||||||
|
|
||||||
|
[Frame](https://developer.xamarin.com/api/type/Xamarin.Forms.Frame/) is an element containing a single child, with some framing options.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
OutlineColor | Done
|
||||||
|
HasShadow | Pending
|
||||||
|
|
||||||
|
|
||||||
|
### Image
|
||||||
|
|
||||||
|
[View](https://developer.xamarin.com/api/type/Xamarin.Forms.Image/) that holds an image.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
Source | Done
|
||||||
|
Aspect | Pending
|
||||||
|
IsOpaque | Pending
|
||||||
|
IsLoading | Done
|
||||||
|
|
||||||
|
Supported FileSource, StreamSource and UriSource.
|
||||||
|
|
||||||
|
|
||||||
|
### Label
|
||||||
|
|
||||||
|
A [Label](https://developer.xamarin.com/api/type/Xamarin.Forms.Label/) is used to display single-line text elements as well as multi-line blocks of text.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
IsEnabled | Done
|
||||||
|
FormattedText | Done
|
||||||
|
HorizontalTextAlignment | Done
|
||||||
|
LineBreakMode | Pending
|
||||||
|
Text | Done
|
||||||
|
TextColor | Done
|
||||||
|
VerticalTextAlignment | Done
|
||||||
|
FontAttributes | Done
|
||||||
|
FontFamily | Done
|
||||||
|
FontSize | Done
|
||||||
|
XAlign | Done
|
||||||
|
YAlign | Done
|
||||||
|
|
||||||
|
### ListView
|
||||||
|
|
||||||
|
An [ItemsView](https://developer.xamarin.com/api/type/Xamarin.Forms.ListView/) that displays a collection of data as a vertical list.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
IsPullToRefreshEnabled | Pending
|
||||||
|
IsRefreshing | Pending
|
||||||
|
ItemsSource | Done
|
||||||
|
RefreshCommand | Pending
|
||||||
|
Header | Pending
|
||||||
|
HeaderTemplate | Pending
|
||||||
|
Footer | Pending
|
||||||
|
FooterTemplate | Pending
|
||||||
|
SelectedItem | Done
|
||||||
|
HasUnevenRows | Pending
|
||||||
|
RowHeight | Pending
|
||||||
|
GroupHeaderTemplate | Done
|
||||||
|
IsGroupingEnabled | Pending
|
||||||
|
SeparatorVisibility | Pending
|
||||||
|
SeparatorColor | Pending
|
||||||
|
ContextActions | Pending
|
||||||
|
ScrollTo | Pending
|
||||||
|
ItemAppearing | Pending
|
||||||
|
ItemDisappearing | Pending
|
||||||
|
ItemSelected | Pending
|
||||||
|
ItemTapped | Done
|
||||||
|
Refresing | Pending
|
||||||
|
|
||||||
|
### MasterDetailPage
|
||||||
|
|
||||||
|
A [Page](https://developer.xamarin.com/api/type/Xamarin.Forms.MasterDetailPage/) that manages two panes of information: A master page that presents data at a high level, and a detail page that displays low-level details about information in the master.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
Detail | Pending
|
||||||
|
IsGestureEnabled | Pending
|
||||||
|
IsPresented | Pending
|
||||||
|
Master | Pending
|
||||||
|
MasterBehavior | Pending
|
||||||
|
ShouldShowToolBarButton | Pending
|
||||||
|
IsPresentedChanged | Pending
|
||||||
|
|
||||||
|
### Map
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Maps.Map/) that shows a map provided by a platform-specific service.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
Pins | Pending
|
||||||
|
MapType | Pending
|
||||||
|
IsShowingUser | Pending
|
||||||
|
HasScrollEnabled | Pending
|
||||||
|
HasZoomEnabled | Pending
|
||||||
|
VisibleRegion | Pending
|
||||||
|
MoveToRegion | Pending
|
||||||
|
|
||||||
|
### NavigationPage
|
||||||
|
|
||||||
|
A [Page](https://developer.xamarin.com/api/type/Xamarin.Forms.NavigationPage/) that manages the navigation and user-experience of a stack of other pages.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
Animate | Pending
|
||||||
|
Add | Pending
|
||||||
|
Remove | Pending
|
||||||
|
|
||||||
|
### OpenGLView
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.OpenGLView/) that displays OpenGL content.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
HasRenderLoop | Pending
|
||||||
|
|
||||||
|
|
||||||
|
### Picker
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Picker/) control for picking an element in a list.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
Title | Pending
|
||||||
|
SelectedIndex | Pending
|
||||||
|
ItemsSource | Pending
|
||||||
|
SelectedItem | Pending
|
||||||
|
Items | Pending
|
||||||
|
itemDisplayBinding | Pending
|
||||||
|
SelectedIndexChanged | Pending
|
||||||
|
|
||||||
|
### Progress
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.ProgressBar/) control that displays progress.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Pending
|
||||||
|
Progress | Done
|
||||||
|
ProgressTo | Pending
|
||||||
|
|
||||||
|
### ScrollView
|
||||||
|
|
||||||
|
An [element](https://developer.xamarin.com/api/type/Xamarin.Forms.ScrollView/) capable of scrolling if its Content requires.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Pending
|
||||||
|
Orientation | Pending
|
||||||
|
ScrollX | Pending
|
||||||
|
ScrollY | Pending
|
||||||
|
Content | Pending
|
||||||
|
ContentSize | Pending
|
||||||
|
ScrollToAsync | Pending
|
||||||
|
Scrolled | Pending
|
||||||
|
|
||||||
|
### SearchBar
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.SearchBar/) control that provides a search box.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
SearchCommand | Done
|
||||||
|
SearchCommandParameter | Done
|
||||||
|
CancelButtonColor | Pending
|
||||||
|
Placeholder | Done
|
||||||
|
FontFamily | Pending
|
||||||
|
FontSize | Pending
|
||||||
|
FontAtttributes | Pending
|
||||||
|
HorizontalTextAlignment | Done
|
||||||
|
TextColor | Done
|
||||||
|
PlaceholderColor | Pending
|
||||||
|
FontFamily | Pending
|
||||||
|
SearchButtonPressed | Done
|
||||||
|
TextChanged | Done
|
||||||
|
|
||||||
|
### Slider
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Slider/) control that inputs a linear value.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
Minimum | Done
|
||||||
|
Maximum | Done
|
||||||
|
Value | Done
|
||||||
|
ValueChanged | Done
|
||||||
|
|
||||||
|
### Stepper
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Stepper/) control that inputs a discrete value, constrained to a range.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Pending
|
||||||
|
Minimum | Pending
|
||||||
|
Maximum | Pending
|
||||||
|
Value | Pending
|
||||||
|
Increment | Pending
|
||||||
|
ValueChanged | Pending
|
||||||
|
|
||||||
|
### Switch
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.Switch/) control that provides a toggled value.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
IsToggled | Done
|
||||||
|
Toggled | Done
|
||||||
|
|
||||||
|
### TabbedPage
|
||||||
|
|
||||||
|
[Displays](https://developer.xamarin.com/api/type/Xamarin.Forms.TabbedPage/) an array of tabs across the top of the screen, each of which loads content onto the screen.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BarBackgroundColor | Pending
|
||||||
|
BarTextColor | Pending
|
||||||
|
ItemsSource | Pending
|
||||||
|
ItemTemplate | Pending
|
||||||
|
SelectedItem | Pending
|
||||||
|
GetIndex | Pending
|
||||||
|
GetPageByIndex | Pending
|
||||||
|
SetIndex | Pending
|
||||||
|
Appearing | Pending
|
||||||
|
Disappearing | Pending
|
||||||
|
CurrentPageChanged | Pending
|
||||||
|
PagesChanged | Pending
|
||||||
|
|
||||||
|
### TableView
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.TableView/) that holds rows of Cell elements.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Pending
|
||||||
|
HasUnevenRows | Pending
|
||||||
|
TableIntent | Pending
|
||||||
|
RowHeight | Pending
|
||||||
|
Root | Pending
|
||||||
|
ContextActions | Pending
|
||||||
|
|
||||||
|
### TimePicker
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.TimePicker/) control that provides time picking.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
Format | Done
|
||||||
|
TextColor | Done
|
||||||
|
Time | Done
|
||||||
|
|
||||||
|
### WebView
|
||||||
|
|
||||||
|
A [View](https://developer.xamarin.com/api/type/Xamarin.Forms.WebView/) that presents HTML content.
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
CanGoBack | Pending
|
||||||
|
CamGoForward | Pending
|
||||||
|
Source | Done
|
||||||
|
Eval | Pending
|
||||||
|
GoBack | Pending
|
||||||
|
GoForward | Pending
|
||||||
|
Navigated | Pending
|
||||||
|
Navigating | Pending
|
||||||
|
|
||||||
|
### VisualElement
|
||||||
|
|
||||||
|
Property | Status
|
||||||
|
------ | ------
|
||||||
|
BackgroundColor | Done
|
||||||
|
AnchorX | Done
|
||||||
|
AnchorY | Done
|
||||||
|
IsEnabled | Done
|
||||||
|
IsFocused | Done
|
||||||
|
IsVisible | Done
|
||||||
|
Opacity | Done
|
||||||
|
RotationX | Pending
|
||||||
|
RotationY | Pending
|
||||||
|
Scale | Done
|
||||||
|
TranslationX | Done
|
||||||
|
TranslationY | Done
|
|
@ -22,6 +22,7 @@ namespace Ooui.AspNetCore
|
||||||
var response = context.HttpContext.Response;
|
var response = context.HttpContext.Response;
|
||||||
response.StatusCode = 200;
|
response.StatusCode = 200;
|
||||||
response.ContentType = "text/html; charset=utf-8";
|
response.ContentType = "text/html; charset=utf-8";
|
||||||
|
response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
|
||||||
|
|
||||||
if (element.WantsFullScreen) {
|
if (element.WantsFullScreen) {
|
||||||
element.Style.Width = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowWidth", 32, 640, 10000);
|
element.Style.Width = GetCookieDouble (context.HttpContext.Request.Cookies, "oouiWindowWidth", 32, 640, 10000);
|
||||||
|
|
|
@ -11,21 +11,21 @@ namespace Ooui.AspNetCore
|
||||||
{
|
{
|
||||||
public static string WebSocketPath { get; set; } = "/ooui.ws";
|
public static string WebSocketPath { get; set; } = "/ooui.ws";
|
||||||
|
|
||||||
public static TimeSpan SessionTimeout { get; set; } = TimeSpan.FromMinutes (5);
|
public static TimeSpan SessionTimeout { get; set; } = TimeSpan.FromMinutes (1);
|
||||||
|
|
||||||
static readonly ConcurrentDictionary<string, ActiveSession> activeSessions =
|
static readonly ConcurrentDictionary<string, PendingSession> pendingSessions =
|
||||||
new ConcurrentDictionary<string, ActiveSession> ();
|
new ConcurrentDictionary<string, PendingSession> ();
|
||||||
|
|
||||||
public static string BeginSession (HttpContext context, Element element)
|
public static string BeginSession (HttpContext context, Element element)
|
||||||
{
|
{
|
||||||
var id = Guid.NewGuid ().ToString ("N");
|
var id = Guid.NewGuid ().ToString ("N");
|
||||||
|
|
||||||
var s = new ActiveSession {
|
var s = new PendingSession {
|
||||||
Element = element,
|
Element = element,
|
||||||
LastConnectTimeUtc = DateTime.UtcNow,
|
CreateTimeUtc = DateTime.UtcNow,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!activeSessions.TryAdd (id, s)) {
|
if (!pendingSessions.TryAdd (id, s)) {
|
||||||
throw new Exception ("Failed to schedule pending session");
|
throw new Exception ("Failed to schedule pending session");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,19 +62,18 @@ namespace Ooui.AspNetCore
|
||||||
//
|
//
|
||||||
// Clear old sessions
|
// Clear old sessions
|
||||||
//
|
//
|
||||||
var toClear = activeSessions.Where (x => (DateTime.UtcNow - x.Value.LastConnectTimeUtc) > SessionTimeout).ToList ();
|
var toClear = pendingSessions.Where (x => (DateTime.UtcNow - x.Value.CreateTimeUtc) > SessionTimeout).ToList ();
|
||||||
foreach (var c in toClear) {
|
foreach (var c in toClear) {
|
||||||
activeSessions.TryRemove (c.Key, out var _);
|
pendingSessions.TryRemove (c.Key, out var _);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Find the pending session
|
// Find the pending session
|
||||||
//
|
//
|
||||||
if (!activeSessions.TryGetValue (id, out var activeSession)) {
|
if (!pendingSessions.TryRemove (id, out var activeSession)) {
|
||||||
BadRequest ("Unknown `id`");
|
BadRequest ("Unknown `id`");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
activeSession.LastConnectTimeUtc = DateTime.UtcNow;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Set the element's dimensions
|
// Set the element's dimensions
|
||||||
|
@ -98,14 +97,14 @@ namespace Ooui.AspNetCore
|
||||||
//
|
//
|
||||||
var token = CancellationToken.None;
|
var token = CancellationToken.None;
|
||||||
var webSocket = await context.WebSockets.AcceptWebSocketAsync ("ooui");
|
var webSocket = await context.WebSockets.AcceptWebSocketAsync ("ooui");
|
||||||
var session = new Ooui.UI.Session (webSocket, activeSession.Element, w, h, token);
|
var session = new Ooui.WebSocketSession (webSocket, activeSession.Element, w, h, token);
|
||||||
await session.RunAsync ().ConfigureAwait (false);
|
await session.RunAsync ().ConfigureAwait (false);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActiveSession
|
class PendingSession
|
||||||
{
|
{
|
||||||
public Element Element;
|
public Element Element;
|
||||||
public DateTime LastConnectTimeUtc;
|
public DateTime CreateTimeUtc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Web;
|
using System;
|
||||||
using Xamarin.Forms.Internals;
|
using Xamarin.Forms.Internals;
|
||||||
|
|
||||||
namespace Ooui.Forms
|
namespace Ooui.Forms
|
||||||
|
@ -31,7 +31,7 @@ namespace Ooui.Forms
|
||||||
ClassName = "close"
|
ClassName = "close"
|
||||||
};
|
};
|
||||||
|
|
||||||
_closeButton.AppendChild(new Span(HttpUtility.HtmlDecode("×")));
|
_closeButton.AppendChild(new Span("×"));
|
||||||
|
|
||||||
var h4 = new Heading(4)
|
var h4 = new Heading(4)
|
||||||
{
|
{
|
||||||
|
|
|
@ -21,7 +21,7 @@ namespace Xamarin.Forms
|
||||||
return;
|
return;
|
||||||
IsInitialized = true;
|
IsInitialized = true;
|
||||||
|
|
||||||
Log.Listeners.Add (new DelegateLogListener ((c, m) => Trace.WriteLine (m, c)));
|
Log.Listeners.Add (new DelegateLogListener ((c, m) => System.Diagnostics.Debug.WriteLine (m, c)));
|
||||||
|
|
||||||
Device.SetIdiom (TargetIdiom.Desktop);
|
Device.SetIdiom (TargetIdiom.Desktop);
|
||||||
Device.PlatformServices = new OouiPlatformServices ();
|
Device.PlatformServices = new OouiPlatformServices ();
|
||||||
|
@ -133,7 +133,7 @@ namespace Xamarin.Forms
|
||||||
{
|
{
|
||||||
if (timer != null)
|
if (timer != null)
|
||||||
return;
|
return;
|
||||||
var interval = TimeSpan.FromSeconds (1.0 / Ooui.UI.Session.MaxFps);
|
var interval = TimeSpan.FromSeconds (1.0 / Ooui.UI.MaxFps);
|
||||||
timer = new Timer ((_ => {
|
timer = new Timer ((_ => {
|
||||||
this.SendSignals ();
|
this.SendSignals ();
|
||||||
}), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds);
|
}), null, (int)interval.TotalMilliseconds, (int)interval.TotalMilliseconds);
|
||||||
|
|
|
@ -9,16 +9,14 @@
|
||||||
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
|
||||||
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>
|
||||||
<RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" $(TargetFramework) == 'netstandard1.0' ">
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
<DefineConstants>PCL</DefineConstants>
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
|
||||||
<DebugType></DebugType>
|
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Xamarin.Forms" Version="2.5.0.122203" />
|
<PackageReference Include="Xamarin.Forms" Version="2.5.0.122203" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
@ -4,7 +4,6 @@ using System.Threading.Tasks;
|
||||||
using Ooui.Forms.Renderers;
|
using Ooui.Forms.Renderers;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
using Xamarin.Forms.Internals;
|
using Xamarin.Forms.Internals;
|
||||||
using System.Web;
|
|
||||||
|
|
||||||
namespace Ooui.Forms
|
namespace Ooui.Forms
|
||||||
{
|
{
|
||||||
|
@ -142,7 +141,7 @@ namespace Ooui.Forms
|
||||||
void AddChild (VisualElement view)
|
void AddChild (VisualElement view)
|
||||||
{
|
{
|
||||||
if (!Application.IsApplicationOrNull (view.RealParent))
|
if (!Application.IsApplicationOrNull (view.RealParent))
|
||||||
Console.Error.WriteLine ("Tried to add parented view to canvas directly");
|
System.Diagnostics.Debug.WriteLine ("Tried to add parented view to canvas directly");
|
||||||
|
|
||||||
if (GetRenderer (view) == null) {
|
if (GetRenderer (view) == null) {
|
||||||
var viewRenderer = CreateRenderer (view);
|
var viewRenderer = CreateRenderer (view);
|
||||||
|
@ -152,7 +151,7 @@ namespace Ooui.Forms
|
||||||
viewRenderer.SetElementSize (new Size (640, 480));
|
viewRenderer.SetElementSize (new Size (640, 480));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
Console.Error.WriteLine ("Potential view double add");
|
System.Diagnostics.Debug.WriteLine ("Potential view double add");
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleRendererStyle_PropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
void HandleRendererStyle_PropertyChanged (object sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||||
|
|
|
@ -139,6 +139,7 @@ namespace Ooui.Forms.Renderers
|
||||||
|
|
||||||
public sealed class FileImageSourceHandler : IImageSourceHandler
|
public sealed class FileImageSourceHandler : IImageSourceHandler
|
||||||
{
|
{
|
||||||
|
#pragma warning disable 1998
|
||||||
public async Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
|
public async Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
|
||||||
{
|
{
|
||||||
string image = null;
|
string image = null;
|
||||||
|
@ -171,7 +172,7 @@ namespace Ooui.Forms.Renderers
|
||||||
using (var outputStream = new System.IO.MemoryStream (data)) {
|
using (var outputStream = new System.IO.MemoryStream (data)) {
|
||||||
await streamImage.CopyToAsync (outputStream, 4096, cancelationToken).ConfigureAwait (false);
|
await streamImage.CopyToAsync (outputStream, 4096, cancelationToken).ConfigureAwait (false);
|
||||||
}
|
}
|
||||||
var hash = Ooui.UI.Hash (data);
|
var hash = Ooui.Utilities.Hash (data);
|
||||||
var etag = "\"" + hash + "\"";
|
var etag = "\"" + hash + "\"";
|
||||||
image = "/images/" + hash;
|
image = "/images/" + hash;
|
||||||
if (Ooui.UI.TryGetFileContentAtPath (image, out var file) && file.Etag == etag) {
|
if (Ooui.UI.TryGetFileContentAtPath (image, out var file) && file.Etag == etag) {
|
||||||
|
|
|
@ -0,0 +1,295 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
using Microsoft.Build.Framework;
|
||||||
|
using Microsoft.Build.Utilities;
|
||||||
|
using Mono.Cecil;
|
||||||
|
using Mono.Linker;
|
||||||
|
using Mono.Linker.Steps;
|
||||||
|
|
||||||
|
namespace Ooui.Wasm.Build.Tasks
|
||||||
|
{
|
||||||
|
public class BuildDistTask : Task
|
||||||
|
{
|
||||||
|
const string SdkUrl = "https://jenkins.mono-project.com/job/test-mono-mainline-webassembly/62/label=highsierra/Azure/processDownloadRequest/62/highsierra/sdks/wasm/mono-wasm-ddf4e7be31b.zip";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string Assembly { get; set; }
|
||||||
|
[Required]
|
||||||
|
public string OutputPath { get; set; }
|
||||||
|
public string ReferencePath { get; set; }
|
||||||
|
|
||||||
|
public override bool Execute ()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
InstallSdk ();
|
||||||
|
GetBcl ();
|
||||||
|
CreateDist ();
|
||||||
|
CopyRuntime ();
|
||||||
|
LinkAssemblies ();
|
||||||
|
ExtractClientJs ();
|
||||||
|
DiscoverEntryPoint ();
|
||||||
|
GenerateHtml ();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Log.LogErrorFromException (ex);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string sdkPath;
|
||||||
|
|
||||||
|
void InstallSdk ()
|
||||||
|
{
|
||||||
|
var sdkName = Path.GetFileNameWithoutExtension (new Uri (SdkUrl).AbsolutePath.Replace ('/', Path.DirectorySeparatorChar));
|
||||||
|
Log.LogMessage ("SDK: " + sdkName);
|
||||||
|
sdkPath = Path.Combine (Path.GetTempPath (), sdkName);
|
||||||
|
Log.LogMessage ("SDK Path: " + sdkPath);
|
||||||
|
if (Directory.Exists (sdkPath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var client = new WebClient ();
|
||||||
|
var zipPath = sdkPath + ".zip";
|
||||||
|
Log.LogMessage ($"Downloading {sdkName} to {zipPath}");
|
||||||
|
client.DownloadFile (SdkUrl, zipPath);
|
||||||
|
|
||||||
|
ZipFile.ExtractToDirectory (zipPath, sdkPath);
|
||||||
|
Log.LogMessage ($"Extracted {sdkName} to {sdkPath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
string bclPath;
|
||||||
|
Dictionary<string, string> bclAssemblies;
|
||||||
|
|
||||||
|
void GetBcl ()
|
||||||
|
{
|
||||||
|
bclPath = Path.Combine (sdkPath, "bcl");
|
||||||
|
var reals = Directory.GetFiles (bclPath, "*.dll");
|
||||||
|
var facades = Directory.GetFiles (Path.Combine (bclPath, "Facades"), "*.dll");
|
||||||
|
var allFiles = reals.Concat (facades);
|
||||||
|
bclAssemblies = allFiles.ToDictionary (x => Path.GetFileName (x));
|
||||||
|
}
|
||||||
|
|
||||||
|
string distPath;
|
||||||
|
string managedPath;
|
||||||
|
|
||||||
|
void CreateDist ()
|
||||||
|
{
|
||||||
|
var outputPath = Path.GetFullPath (OutputPath);
|
||||||
|
distPath = Path.Combine (outputPath, "dist");
|
||||||
|
managedPath = Path.Combine (distPath, "managed");
|
||||||
|
Directory.CreateDirectory (managedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CopyRuntime ()
|
||||||
|
{
|
||||||
|
var rtPath = Path.Combine (sdkPath, "release");
|
||||||
|
var files = new[] { "mono.wasm", "mono.js" };
|
||||||
|
foreach (var f in files) {
|
||||||
|
var src = Path.Combine (rtPath, f);
|
||||||
|
var dest = Path.Combine (distPath, f);
|
||||||
|
Log.LogMessage ($"Runtime {src} -> {dest}");
|
||||||
|
File.Copy (src, dest, true);
|
||||||
|
}
|
||||||
|
File.Copy (Path.Combine (sdkPath, "server.py"), Path.Combine (distPath, "server.py"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> linkedAsmPaths;
|
||||||
|
List<string> refpaths;
|
||||||
|
|
||||||
|
void LinkAssemblies ()
|
||||||
|
{
|
||||||
|
var references = ReferencePath.Split (';').Select (x => x.Trim ()).Where (x => x.Length > 0).ToList ();
|
||||||
|
refpaths = new List<string> ();
|
||||||
|
foreach (var r in references) {
|
||||||
|
var name = Path.GetFileName (r);
|
||||||
|
if (bclAssemblies.ContainsKey (name)) {
|
||||||
|
refpaths.Add (bclAssemblies[name]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
refpaths.Add (r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var asmPath = Path.GetFullPath (Assembly);
|
||||||
|
|
||||||
|
var pipeline = GetLinkerPipeline ();
|
||||||
|
using (var context = new LinkContext (pipeline)) {
|
||||||
|
context.CoreAction = AssemblyAction.CopyUsed;
|
||||||
|
context.UserAction = AssemblyAction.CopyUsed;
|
||||||
|
context.OutputDirectory = managedPath;
|
||||||
|
|
||||||
|
pipeline.PrependStep (new ResolveFromAssemblyStep (asmPath, ResolveFromAssemblyStep.RootVisibility.Any));
|
||||||
|
|
||||||
|
var refdirs = refpaths.Select (x => Path.GetDirectoryName (x)).Distinct ().ToList ();
|
||||||
|
refdirs.Insert (0, Path.Combine (bclPath, "Facades"));
|
||||||
|
refdirs.Insert (0, bclPath);
|
||||||
|
foreach (var d in refdirs.Distinct ()) {
|
||||||
|
context.Resolver.AddSearchDirectory (d);
|
||||||
|
}
|
||||||
|
|
||||||
|
pipeline.AddStepAfter (typeof (LoadReferencesStep), new LoadI18nAssemblies (I18nAssemblies.None));
|
||||||
|
|
||||||
|
foreach (var dll in Directory.GetFiles (managedPath, "*.dll")) {
|
||||||
|
File.Delete (dll);
|
||||||
|
}
|
||||||
|
pipeline.Process (context);
|
||||||
|
}
|
||||||
|
|
||||||
|
linkedAsmPaths = Directory.GetFiles (managedPath, "*.dll").OrderBy (x => Path.GetFileName (x)).ToList ();
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreserveUsingAttributesStep : ResolveStep
|
||||||
|
{
|
||||||
|
readonly HashSet<string> ignoreAsmNames;
|
||||||
|
|
||||||
|
public PreserveUsingAttributesStep (IEnumerable<string> ignoreAsmNames)
|
||||||
|
{
|
||||||
|
this.ignoreAsmNames = new HashSet<string> (ignoreAsmNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Process ()
|
||||||
|
{
|
||||||
|
var asms = Context.GetAssemblies ();
|
||||||
|
foreach (var a in asms.Where (x => !ignoreAsmNames.Contains (x.Name.Name))) {
|
||||||
|
foreach (var m in a.Modules) {
|
||||||
|
foreach (var t in m.Types) {
|
||||||
|
PreserveTypeIfRequested (t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PreserveTypeIfRequested (TypeDefinition type)
|
||||||
|
{
|
||||||
|
var typePreserved = IsTypePreserved (type);
|
||||||
|
if (IsTypePreserved (type)) {
|
||||||
|
MarkAndPreserveAll (type);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
foreach (var m in type.Methods.Where (IsMethodPreserved)) {
|
||||||
|
Annotations.AddPreservedMethod (type, m);
|
||||||
|
}
|
||||||
|
foreach (var t in type.NestedTypes) {
|
||||||
|
PreserveTypeIfRequested (t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsTypePreserved (TypeDefinition m)
|
||||||
|
{
|
||||||
|
return m.CustomAttributes.FirstOrDefault (x => x.AttributeType.Name.StartsWith ("Preserve", StringComparison.Ordinal)) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsMethodPreserved (MethodDefinition m)
|
||||||
|
{
|
||||||
|
return m.CustomAttributes.FirstOrDefault (x => x.AttributeType.Name.StartsWith ("Preserve", StringComparison.Ordinal)) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MarkAndPreserveAll (TypeDefinition type)
|
||||||
|
{
|
||||||
|
Annotations.MarkAndPush (type);
|
||||||
|
Annotations.SetPreserve (type, TypePreserve.All);
|
||||||
|
if (!type.HasNestedTypes) {
|
||||||
|
Tracer.Pop ();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (TypeDefinition nested in type.NestedTypes)
|
||||||
|
MarkAndPreserveAll (nested);
|
||||||
|
Tracer.Pop ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Pipeline GetLinkerPipeline ()
|
||||||
|
{
|
||||||
|
var p = new Pipeline ();
|
||||||
|
p.AppendStep (new LoadReferencesStep ());
|
||||||
|
p.AppendStep (new PreserveUsingAttributesStep (bclAssemblies.Values.Select (Path.GetFileNameWithoutExtension)));
|
||||||
|
p.AppendStep (new BlacklistStep ());
|
||||||
|
p.AppendStep (new TypeMapStep ());
|
||||||
|
p.AppendStep (new MarkStep ());
|
||||||
|
p.AppendStep (new SweepStep ());
|
||||||
|
p.AppendStep (new CleanStep ());
|
||||||
|
p.AppendStep (new RegenerateGuidStep ());
|
||||||
|
p.AppendStep (new OutputStep ());
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExtractClientJs ()
|
||||||
|
{
|
||||||
|
var oouiPath = refpaths.FirstOrDefault (x => Path.GetFileName (x).Equals ("Ooui.dll", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
if (oouiPath == null) {
|
||||||
|
Log.LogError ("Ooui.dll not included in the project");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var oouiAsm = AssemblyDefinition.ReadAssembly (oouiPath);
|
||||||
|
var clientJs = oouiAsm.MainModule.Resources.FirstOrDefault (x => x.Name.EndsWith ("Client.js", StringComparison.InvariantCultureIgnoreCase)) as EmbeddedResource;
|
||||||
|
if (clientJs == null) {
|
||||||
|
Log.LogError ("Ooui.dll missing client javascript");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dest = Path.Combine (distPath, "ooui.js");
|
||||||
|
using (var srcs = clientJs.GetResourceStream ()) {
|
||||||
|
using (var dests = new FileStream (dest, FileMode.Create, FileAccess.Write)) {
|
||||||
|
srcs.CopyTo (dests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.LogMessage ($"Client JS {dest}");
|
||||||
|
}
|
||||||
|
|
||||||
|
MethodDefinition entryPoint;
|
||||||
|
|
||||||
|
void DiscoverEntryPoint ()
|
||||||
|
{
|
||||||
|
var asm = AssemblyDefinition.ReadAssembly (Assembly);
|
||||||
|
entryPoint = asm.EntryPoint;
|
||||||
|
if (entryPoint == null) {
|
||||||
|
throw new Exception ($"{Path.GetFileName (Assembly)} is missing an entry point");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void GenerateHtml ()
|
||||||
|
{
|
||||||
|
var htmlPath = Path.Combine (distPath, "index.html");
|
||||||
|
using (var w = new StreamWriter (htmlPath, false, new UTF8Encoding (false))) {
|
||||||
|
w.Write (@"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=""utf-8"" />
|
||||||
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
|
||||||
|
<link rel=""stylesheet"" href=""https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"" />
|
||||||
|
<link rel=""stylesheet"" href=""https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id=""ooui-body"" class=""container-fluid"">
|
||||||
|
<p id=""loading""><i class=""fa fa-refresh fa-spin"" style=""font-size:14px;margin-right:0.5em;""></i> Loading...</p>
|
||||||
|
</div>
|
||||||
|
<script defer type=""text/javascript"" src=""ooui.js""></script>
|
||||||
|
<script type=""text/javascript"">
|
||||||
|
var assemblies = [");
|
||||||
|
var head = "";
|
||||||
|
foreach (var l in linkedAsmPaths.Select (x => Path.GetFileName (x))) {
|
||||||
|
w.Write (head);
|
||||||
|
w.Write ('\"');
|
||||||
|
w.Write (l);
|
||||||
|
w.Write ('\"');
|
||||||
|
head = ",";
|
||||||
|
}
|
||||||
|
w.WriteLine ($@"];
|
||||||
|
document.addEventListener(""DOMContentLoaded"", function(event) {{
|
||||||
|
oouiWasm(""{entryPoint.DeclaringType.Module.Assembly.Name.Name}"", ""{entryPoint.DeclaringType.Namespace}"", ""{entryPoint.DeclaringType.Name}"", ""{entryPoint.Name}"", assemblies);
|
||||||
|
}});
|
||||||
|
</script>
|
||||||
|
<script defer type=""text/javascript"" src=""mono.js""></script>
|
||||||
|
</body>
|
||||||
|
</html>");
|
||||||
|
}
|
||||||
|
Log.LogMessage ($"HTML {htmlPath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||||
|
<DefineConstants>NET_CORE</DefineConstants>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Build" Version="15.3.409" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Framework" Version="15.3.409" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="15.3.409" />
|
||||||
|
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="15.3.409" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="linker\**" />
|
||||||
|
<Compile Include="linker\linker\Linker\**" />
|
||||||
|
<Compile Include="linker\linker\Linker.Steps\**" />
|
||||||
|
<Compile Include="linker\cecil\Mono.Cecil\**" />
|
||||||
|
<Compile Include="linker\cecil\Mono.Cecil.Cil\**" />
|
||||||
|
<Compile Include="linker\cecil\Mono.Cecil.Metadata\**" />
|
||||||
|
<Compile Include="linker\cecil\Mono.Cecil.PE\**" />
|
||||||
|
<Compile Include="linker\cecil\Mono.Collections.Generic\**" />
|
||||||
|
<Compile Include="linker\cecil\Mono\**" />
|
||||||
|
<Compile Remove="linker\linker\Linker\Driver.cs" />
|
||||||
|
<Compile Remove="linker\linker\Linker\AssemblyInfo.cs" />
|
||||||
|
<Compile Remove="linker\cecil\Mono.Cecil\AssemblyInfo.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1dcc9afa256c8e94050b6a21f03b503508e47f05
|
|
@ -0,0 +1,4 @@
|
||||||
|
all:
|
||||||
|
msbuild /p:Configuration=Release /t:Restore ../Ooui.Wasm.Build.Tasks/Ooui.Wasm.Build.Tasks.csproj
|
||||||
|
msbuild /p:Configuration=Release ../Ooui.Wasm.Build.Tasks/Ooui.Wasm.Build.Tasks.csproj
|
||||||
|
nuget pack
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<package >
|
||||||
|
<metadata>
|
||||||
|
<id>Ooui.Wasm</id>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<title>Ooui.Wasm</title>
|
||||||
|
<authors>praeclarum</authors>
|
||||||
|
<owners>praeclarum</owners>
|
||||||
|
<licenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</licenseUrl>
|
||||||
|
<projectUrl>https://github.com/praeclarum/Ooui</projectUrl>
|
||||||
|
<iconUrl>https://github.com/praeclarum/Ooui/raw/master/Documentation/Icon.png</iconUrl>
|
||||||
|
<requireLicenseAcceptance>false</requireLicenseAcceptance>
|
||||||
|
<description>WebAssembly support for Ooui apps</description>
|
||||||
|
<tags>Ooui UI CrossPlatform WebAssembly Wasm</tags>
|
||||||
|
<dependencies>
|
||||||
|
<group>
|
||||||
|
<dependency id="Ooui" version="1.0.0" />
|
||||||
|
</group>
|
||||||
|
</dependencies>
|
||||||
|
</metadata>
|
||||||
|
<files>
|
||||||
|
<file src="Ooui.Wasm.targets" target="build/netstandard2.0/Ooui.Wasm.targets" />
|
||||||
|
<file src="../Ooui.Wasm.Build.Tasks/bin/Release/netstandard2.0/Ooui.Wasm.Build.Tasks.dll" target="build/netstandard2.0/Ooui.Wasm.Build.Tasks.dll" />
|
||||||
|
</files>
|
||||||
|
</package>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
|
||||||
|
<UsingTask TaskName="Ooui.Wasm.Build.Tasks.BuildDistTask" AssemblyFile="$(MSBuildThisFileDirectory)Ooui.Wasm.Build.Tasks.dll" />
|
||||||
|
|
||||||
|
<!-- BuildDist -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<CompileDependsOn>
|
||||||
|
$(CompileDependsOn);
|
||||||
|
BuildDist;
|
||||||
|
</CompileDependsOn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<Target Name="BuildDist" AfterTargets="AfterBuild" Condition="'$(_BuildDistAlreadyExecuted)'!='true'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<_BuildDistAlreadyExecuted>true</_BuildDistAlreadyExecuted>
|
||||||
|
</PropertyGroup>
|
||||||
|
<BuildDistTask
|
||||||
|
Assembly = "$(IntermediateOutputPath)$(TargetFileName)"
|
||||||
|
OutputPath = "$(OutputPath)"
|
||||||
|
ReferencePath = "@(ReferencePath)" />
|
||||||
|
</Target>
|
||||||
|
|
||||||
|
</Project>
|
26
Ooui.sln
26
Ooui.sln
|
@ -2,22 +2,24 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 15
|
# Visual Studio 15
|
||||||
VisualStudioVersion = 15.0.27130.2010
|
VisualStudioVersion = 15.0.27130.2010
|
||||||
MinimumVisualStudioVersion = 15.0.26124.0
|
MinimumVisualStudioVersion = 15.0.26124.0
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ooui", "Ooui\Ooui.csproj", "{DFDFD036-BF48-4D3A-BF99-88CA1EA8E4B9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ooui", "Ooui\Ooui.csproj", "{DFDFD036-BF48-4D3A-BF99-88CA1EA8E4B9}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Samples", "Samples\Samples.csproj", "{CDF8BB01-40BB-402F-8446-47AA6F1628F3}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Samples", "Samples\Samples.csproj", "{CDF8BB01-40BB-402F-8446-47AA6F1628F3}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{78F6E9E7-4322-4F87-8CE9-1EEF1B16D268}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{78F6E9E7-4322-4F87-8CE9-1EEF1B16D268}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ooui.Forms", "Ooui.Forms\Ooui.Forms.csproj", "{DB819A2F-91E1-40FB-8D48-6544169966B8}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ooui.Forms", "Ooui.Forms\Ooui.Forms.csproj", "{DB819A2F-91E1-40FB-8D48-6544169966B8}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ooui.AspNetCore", "Ooui.AspNetCore\Ooui.AspNetCore.csproj", "{2EDF0328-698B-458A-B10C-AB1B4786A6CA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ooui.AspNetCore", "Ooui.AspNetCore\Ooui.AspNetCore.csproj", "{2EDF0328-698B-458A-B10C-AB1B4786A6CA}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PlatformSamples", "PlatformSamples", "{12ADF328-BBA8-48FC-9AF1-F11B7921D9EA}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PlatformSamples", "PlatformSamples", "{12ADF328-BBA8-48FC-9AF1-F11B7921D9EA}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCoreMvc", "PlatformSamples\AspNetCoreMvc\AspNetCoreMvc.csproj", "{7C6D477C-3378-4A86-9C31-AAD51204120B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCoreMvc", "PlatformSamples\AspNetCoreMvc\AspNetCoreMvc.csproj", "{7C6D477C-3378-4A86-9C31-AAD51204120B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OouiTemplates", "Templates\OouiTemplates\OouiTemplates.csproj", "{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OouiTemplates", "Templates\OouiTemplates\OouiTemplates.csproj", "{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ooui.Wasm.Build.Tasks", "Ooui.Wasm.Build.Tasks\Ooui.Wasm.Build.Tasks.csproj", "{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -112,6 +114,18 @@ Global
|
||||||
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x64.Build.0 = Release|Any CPU
|
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x86.ActiveCfg = Release|Any CPU
|
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x86.Build.0 = Release|Any CPU
|
{1DA10AAB-EB41-49CF-9441-B4D28D0A7F96}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{6E9C9582-0DA8-4496-BAE0-23EFAF4A10C2}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -7,7 +7,6 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public class Button : FormControl
|
public class Button : FormControl
|
||||||
{
|
{
|
||||||
ButtonType typ = ButtonType.Submit;
|
|
||||||
public ButtonType Type {
|
public ButtonType Type {
|
||||||
get => GetAttribute ("type", ButtonType.Submit);
|
get => GetAttribute ("type", ButtonType.Submit);
|
||||||
set => SetAttributeProperty ("type", value);
|
set => SetAttributeProperty ("type", value);
|
||||||
|
|
|
@ -36,8 +36,10 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
if (message.TargetId == Id) {
|
if (message.TargetId == Id) {
|
||||||
switch (message.MessageType) {
|
switch (message.MessageType) {
|
||||||
case MessageType.Call when message.Key == "getContext" && message.Value is Array a && a.Length == 1 && "2d".Equals (a.GetValue (0)):
|
case MessageType.Call:
|
||||||
|
if (message.Key == "getContext" && message.Value is Array a && a.Length == 1 && "2d".Equals (a.GetValue (0))) {
|
||||||
AddStateMessage (message);
|
AddStateMessage (message);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
201
Ooui/Client.js
201
Ooui/Client.js
|
@ -6,6 +6,17 @@ const nodes = {};
|
||||||
const hasText = {};
|
const hasText = {};
|
||||||
|
|
||||||
let socket = null;
|
let socket = null;
|
||||||
|
let wasmSession = null;
|
||||||
|
|
||||||
|
function send (json) {
|
||||||
|
if (debug) console.log ("Send", json);
|
||||||
|
if (socket != null) {
|
||||||
|
socket.send (json);
|
||||||
|
}
|
||||||
|
else if (wasmSession != null) {
|
||||||
|
WebAssemblyApp.receiveMessagesJson (wasmSession, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mouseEvents = {
|
const mouseEvents = {
|
||||||
click: true,
|
click: true,
|
||||||
|
@ -94,17 +105,26 @@ function ooui (rootElementPath) {
|
||||||
|
|
||||||
console.log("Web socket created");
|
console.log("Web socket created");
|
||||||
|
|
||||||
// Throttled window resize event
|
monitorSizeChanges (1000/10);
|
||||||
(function() {
|
}
|
||||||
window.addEventListener("resize", resizeThrottler, false);
|
|
||||||
|
|
||||||
|
function oouiWasm (mainAsmName, mainNamespace, mainClassName, mainMethodName, assemblies)
|
||||||
|
{
|
||||||
|
Module.entryPoint = { "a": mainAsmName, "n": mainNamespace, "t": mainClassName, "m": mainMethodName };
|
||||||
|
Module.assemblies = assemblies;
|
||||||
|
|
||||||
|
monitorSizeChanges (1000/30);
|
||||||
|
}
|
||||||
|
|
||||||
|
function monitorSizeChanges (millis)
|
||||||
|
{
|
||||||
var resizeTimeout;
|
var resizeTimeout;
|
||||||
function resizeThrottler() {
|
function resizeThrottler() {
|
||||||
if (!resizeTimeout) {
|
if (!resizeTimeout) {
|
||||||
resizeTimeout = setTimeout(function() {
|
resizeTimeout = setTimeout(function() {
|
||||||
resizeTimeout = null;
|
resizeTimeout = null;
|
||||||
resizeHandler();
|
resizeHandler();
|
||||||
}, 100);
|
}, millis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,11 +137,11 @@ function ooui (rootElementPath) {
|
||||||
};
|
};
|
||||||
saveSize (em.v);
|
saveSize (em.v);
|
||||||
const ems = JSON.stringify (em);
|
const ems = JSON.stringify (em);
|
||||||
if (socket != null)
|
send (ems);
|
||||||
socket.send (ems);
|
|
||||||
if (debug) console.log ("Event", em);
|
if (debug) console.log ("Event", em);
|
||||||
}
|
}
|
||||||
}());
|
|
||||||
|
window.addEventListener("resize", resizeThrottler, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNode (id) {
|
function getNode (id) {
|
||||||
|
@ -244,8 +264,7 @@ function msgListen (m) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const ems = JSON.stringify (em);
|
const ems = JSON.stringify (em);
|
||||||
if (socket != null)
|
send (ems);
|
||||||
socket.send (ems);
|
|
||||||
if (debug) console.log ("Event", em);
|
if (debug) console.log ("Event", em);
|
||||||
if (em.k === "submit")
|
if (em.k === "submit")
|
||||||
e.preventDefault ();
|
e.preventDefault ();
|
||||||
|
@ -294,3 +313,167 @@ function fixupValue (v) {
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// == WASM Support ==
|
||||||
|
|
||||||
|
window["__oouiReceiveMessages"] = function (sessionId, messages)
|
||||||
|
{
|
||||||
|
if (debug) console.log ("WebAssembly Receive", messages);
|
||||||
|
if (wasmSession != null) {
|
||||||
|
messages.forEach (function (m) {
|
||||||
|
// console.log ('Raw value from server', m.v);
|
||||||
|
m.v = fixupValue (m.v);
|
||||||
|
processMessage (m);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var Module = {
|
||||||
|
onRuntimeInitialized: function () {
|
||||||
|
if (debug) console.log ("Done with WASM module instantiation.");
|
||||||
|
|
||||||
|
Module.FS_createPath ("/", "managed", true, true);
|
||||||
|
|
||||||
|
var pending = 0;
|
||||||
|
this.assemblies.forEach (function(asm_name) {
|
||||||
|
if (debug) console.log ("Loading", asm_name);
|
||||||
|
++pending;
|
||||||
|
fetch ("managed/" + asm_name, { credentials: 'same-origin' }).then (function (response) {
|
||||||
|
if (!response.ok)
|
||||||
|
throw "failed to load Assembly '" + asm_name + "'";
|
||||||
|
return response['arrayBuffer']();
|
||||||
|
}).then (function (blob) {
|
||||||
|
var asm = new Uint8Array (blob);
|
||||||
|
Module.FS_createDataFile ("managed/" + asm_name, null, asm, true, true, true);
|
||||||
|
--pending;
|
||||||
|
if (pending == 0)
|
||||||
|
Module.bclLoadingDone ();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
bclLoadingDone: function () {
|
||||||
|
if (debug) console.log ("Done loading the BCL.");
|
||||||
|
MonoRuntime.init ();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var MonoRuntime = {
|
||||||
|
init: function () {
|
||||||
|
this.load_runtime = Module.cwrap ('mono_wasm_load_runtime', null, ['string', 'number']);
|
||||||
|
this.assembly_load = Module.cwrap ('mono_wasm_assembly_load', 'number', ['string']);
|
||||||
|
this.find_class = Module.cwrap ('mono_wasm_assembly_find_class', 'number', ['number', 'string', 'string']);
|
||||||
|
this.find_method = Module.cwrap ('mono_wasm_assembly_find_method', 'number', ['number', 'string', 'number']);
|
||||||
|
this.invoke_method = Module.cwrap ('mono_wasm_invoke_method', 'number', ['number', 'number', 'number']);
|
||||||
|
this.mono_string_get_utf8 = Module.cwrap ('mono_wasm_string_get_utf8', 'number', ['number']);
|
||||||
|
this.mono_string = Module.cwrap ('mono_wasm_string_from_js', 'number', ['string']);
|
||||||
|
|
||||||
|
this.load_runtime ("managed", 1);
|
||||||
|
|
||||||
|
if (debug) console.log ("Done initializing the runtime.");
|
||||||
|
|
||||||
|
WebAssemblyApp.init ();
|
||||||
|
},
|
||||||
|
|
||||||
|
conv_string: function (mono_obj) {
|
||||||
|
if (mono_obj == 0)
|
||||||
|
return null;
|
||||||
|
var raw = this.mono_string_get_utf8 (mono_obj);
|
||||||
|
var res = Module.UTF8ToString (raw);
|
||||||
|
Module._free (raw);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
call_method: function (method, this_arg, args) {
|
||||||
|
var args_mem = Module._malloc (args.length * 4);
|
||||||
|
var eh_throw = Module._malloc (4);
|
||||||
|
for (var i = 0; i < args.length; ++i)
|
||||||
|
Module.setValue (args_mem + i * 4, args [i], "i32");
|
||||||
|
Module.setValue (eh_throw, 0, "i32");
|
||||||
|
|
||||||
|
var res = this.invoke_method (method, this_arg, args_mem, eh_throw);
|
||||||
|
|
||||||
|
var eh_res = Module.getValue (eh_throw, "i32");
|
||||||
|
|
||||||
|
Module._free (args_mem);
|
||||||
|
Module._free (eh_throw);
|
||||||
|
|
||||||
|
if (eh_res != 0) {
|
||||||
|
var msg = this.conv_string (res);
|
||||||
|
throw new Error (msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var WebAssemblyApp = {
|
||||||
|
init: function () {
|
||||||
|
this.loading = document.getElementById ("loading");
|
||||||
|
|
||||||
|
this.findMethods ();
|
||||||
|
|
||||||
|
this.runApp ("1", "2");
|
||||||
|
|
||||||
|
this.loading.hidden = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
runApp: function (a, b) {
|
||||||
|
try {
|
||||||
|
var sessionId = "main";
|
||||||
|
if (!!this.ooui_DisableServer_method) {
|
||||||
|
MonoRuntime.call_method (this.ooui_DisableServer_method, null, []);
|
||||||
|
}
|
||||||
|
MonoRuntime.call_method (this.main_method, null, [MonoRuntime.mono_string (a), MonoRuntime.mono_string (b)]);
|
||||||
|
wasmSession = sessionId;
|
||||||
|
if (!!this.ooui_StartWebAssemblySession_method) {
|
||||||
|
var initialSize = getSize ();
|
||||||
|
MonoRuntime.call_method (this.ooui_StartWebAssemblySession_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string ("/"), MonoRuntime.mono_string (Math.round(initialSize.width) + " " + Math.round(initialSize.height))]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
receiveMessagesJson: function (sessionId, json) {
|
||||||
|
if (!!this.ooui_ReceiveWebAssemblySessionMessageJson_method) {
|
||||||
|
MonoRuntime.call_method (this.ooui_ReceiveWebAssemblySessionMessageJson_method, null, [MonoRuntime.mono_string (sessionId), MonoRuntime.mono_string (json)]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
findMethods: function () {
|
||||||
|
this.main_module = MonoRuntime.assembly_load (Module.entryPoint.a);
|
||||||
|
if (!this.main_module)
|
||||||
|
throw "Could not find Main Module " + Module.entryPoint.a + ".dll";
|
||||||
|
|
||||||
|
this.main_class = MonoRuntime.find_class (this.main_module, Module.entryPoint.n, Module.entryPoint.t)
|
||||||
|
if (!this.main_class)
|
||||||
|
throw "Could not find Program class in main module";
|
||||||
|
|
||||||
|
this.main_method = MonoRuntime.find_method (this.main_class, Module.entryPoint.m, -1)
|
||||||
|
if (!this.main_method)
|
||||||
|
throw "Could not find Main method";
|
||||||
|
|
||||||
|
this.ooui_module = MonoRuntime.assembly_load ("Ooui");
|
||||||
|
if (!!this.ooui_module) {
|
||||||
|
|
||||||
|
this.ooui_class = MonoRuntime.find_class (this.ooui_module, "Ooui", "UI");
|
||||||
|
if (!this.ooui_class)
|
||||||
|
throw "Could not find UI class in Ooui module";
|
||||||
|
|
||||||
|
this.ooui_DisableServer_method = MonoRuntime.find_method (this.ooui_class, "DisableServer", -1);
|
||||||
|
if (!this.ooui_DisableServer_method)
|
||||||
|
throw "Could not find DisableServer method";
|
||||||
|
|
||||||
|
this.ooui_StartWebAssemblySession_method = MonoRuntime.find_method (this.ooui_class, "StartWebAssemblySession", -1);
|
||||||
|
if (!this.ooui_StartWebAssemblySession_method)
|
||||||
|
throw "Could not find StartWebAssemblySession method";
|
||||||
|
|
||||||
|
this.ooui_ReceiveWebAssemblySessionMessageJson_method = MonoRuntime.find_method (this.ooui_class, "ReceiveWebAssemblySessionMessageJson", -1);
|
||||||
|
if (!this.ooui_ReceiveWebAssemblySessionMessageJson_method)
|
||||||
|
throw "Could not find ReceiveWebAssemblySessionMessageJson method";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,6 @@ namespace Ooui
|
||||||
set => SetAttributeProperty ("title", value);
|
set => SetAttributeProperty ("title", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hidden = false;
|
|
||||||
public bool IsHidden {
|
public bool IsHidden {
|
||||||
get => GetBooleanAttribute ("hidden");
|
get => GetBooleanAttribute ("hidden");
|
||||||
set => SetBooleanAttributeProperty ("hidden", value);
|
set => SetBooleanAttributeProperty ("hidden", value);
|
||||||
|
@ -232,6 +231,8 @@ namespace Ooui
|
||||||
|
|
||||||
protected virtual bool HtmlNeedsFullEndElement => false;
|
protected virtual bool HtmlNeedsFullEndElement => false;
|
||||||
|
|
||||||
|
#if !NO_XML
|
||||||
|
|
||||||
public override void WriteOuterHtml (System.Xml.XmlWriter w)
|
public override void WriteOuterHtml (System.Xml.XmlWriter w)
|
||||||
{
|
{
|
||||||
w.WriteStartElement (TagName);
|
w.WriteStartElement (TagName);
|
||||||
|
@ -262,5 +263,7 @@ namespace Ooui
|
||||||
c.WriteOuterHtml (w);
|
c.WriteOuterHtml (w);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Ooui
|
namespace Ooui
|
||||||
{
|
{
|
||||||
|
@ -21,7 +22,7 @@ namespace Ooui
|
||||||
public IReadOnlyList<Message> StateMessages {
|
public IReadOnlyList<Message> StateMessages {
|
||||||
get {
|
get {
|
||||||
lock (stateMessages) {
|
lock (stateMessages) {
|
||||||
return new List<Message> (stateMessages).AsReadOnly ();
|
return new ReadOnlyList<Message> (stateMessages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -242,7 +243,7 @@ namespace Ooui
|
||||||
|
|
||||||
public override bool CanConvert (Type objectType)
|
public override bool CanConvert (Type objectType)
|
||||||
{
|
{
|
||||||
return typeof (EventTarget).IsAssignableFrom (objectType);
|
return typeof (EventTarget).GetTypeInfo ().IsAssignableFrom (objectType.GetTypeInfo ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ namespace Ooui
|
||||||
{
|
{
|
||||||
public class Form : Element
|
public class Form : Element
|
||||||
{
|
{
|
||||||
string action = "";
|
|
||||||
public string Action {
|
public string Action {
|
||||||
get => GetStringAttribute ("action", "");
|
get => GetStringAttribute ("action", "");
|
||||||
set => SetAttributeProperty ("action", value ?? "");
|
set => SetAttributeProperty ("action", value ?? "");
|
||||||
|
|
|
@ -9,7 +9,6 @@ namespace Ooui
|
||||||
set => SetAttributeProperty ("name", value);
|
set => SetAttributeProperty ("name", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isDisabled = false;
|
|
||||||
public bool IsDisabled {
|
public bool IsDisabled {
|
||||||
get => GetBooleanAttribute ("disabled");
|
get => GetBooleanAttribute ("disabled");
|
||||||
set => SetBooleanAttributeProperty ("disabled", value);
|
set => SetBooleanAttributeProperty ("disabled", value);
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ooui
|
||||||
|
{
|
||||||
|
class JsonConvert
|
||||||
|
{
|
||||||
|
static void WriteJsonString (System.IO.TextWriter w, string s)
|
||||||
|
{
|
||||||
|
w.Write ('\"');
|
||||||
|
for (var i = 0; i < s.Length; i++) {
|
||||||
|
var c = s[i];
|
||||||
|
if (c == '\"') {
|
||||||
|
w.Write ("\\\"");
|
||||||
|
}
|
||||||
|
else if (c == '\r') {
|
||||||
|
w.Write ("\\r");
|
||||||
|
}
|
||||||
|
else if (c == '\n') {
|
||||||
|
w.Write ("\\n");
|
||||||
|
}
|
||||||
|
else if (c == '\t') {
|
||||||
|
w.Write ("\\t");
|
||||||
|
}
|
||||||
|
else if (c == '\b') {
|
||||||
|
w.Write ("\\b");
|
||||||
|
}
|
||||||
|
else if (c == '\\') {
|
||||||
|
w.Write ("\\");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
w.Write (c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Write ('\"');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WriteJsonValue (System.IO.TextWriter w, object value)
|
||||||
|
{
|
||||||
|
if (value == null) {
|
||||||
|
w.Write ("null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var s = value as string;
|
||||||
|
if (s != null) {
|
||||||
|
WriteJsonString (w, s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var a = value as Array;
|
||||||
|
if (a != null) {
|
||||||
|
w.Write ('[');
|
||||||
|
var head = "";
|
||||||
|
foreach (var o in a) {
|
||||||
|
w.Write (head);
|
||||||
|
WriteJsonValue (w, o);
|
||||||
|
head = ",";
|
||||||
|
}
|
||||||
|
w.Write (']');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var e = value as EventTarget;
|
||||||
|
if (e != null) {
|
||||||
|
w.Write ('\"');
|
||||||
|
w.Write (e.Id);
|
||||||
|
w.Write ('\"');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is Color) {
|
||||||
|
WriteJsonString (w, ((Color)value).ToString ());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var icult = System.Globalization.CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
if (value is double) {
|
||||||
|
w.Write (((double)value).ToString (icult));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is int) {
|
||||||
|
w.Write (((int)value).ToString (icult));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is float) {
|
||||||
|
w.Write (((float)value).ToString (icult));
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write (Newtonsoft.Json.JsonConvert.SerializeObject (value));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string SerializeObject (object value)
|
||||||
|
{
|
||||||
|
using (var sw = new System.IO.StringWriter ()) {
|
||||||
|
WriteJsonValue (sw, value);
|
||||||
|
return sw.ToString ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ReadJsonArray (string j, ref int i)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException ();
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ReadJsonObject (string json, ref int i)
|
||||||
|
{
|
||||||
|
var e = json.Length;
|
||||||
|
while (i < e) {
|
||||||
|
while (i < e && char.IsWhiteSpace (json[i]))
|
||||||
|
i++;
|
||||||
|
if (i >= e)
|
||||||
|
throw new Exception ("JSON Unexpected end");
|
||||||
|
var n = e - i;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
throw new NotImplementedException ();
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ReadJsonString (string j, ref int i)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException ();
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ReadJsonNumber (string j, ref int i)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException ();
|
||||||
|
}
|
||||||
|
|
||||||
|
static object ReadJsonValue (string json, ref int i)
|
||||||
|
{
|
||||||
|
var e = json.Length;
|
||||||
|
while (i < e && char.IsWhiteSpace (json[i]))
|
||||||
|
i++;
|
||||||
|
if (i >= e)
|
||||||
|
throw new Exception ("JSON Unexpected end");
|
||||||
|
var n = e - i;
|
||||||
|
switch (json[i]) {
|
||||||
|
case '[':
|
||||||
|
return ReadJsonArray (json, ref i);
|
||||||
|
case '{':
|
||||||
|
return ReadJsonObject (json, ref i);
|
||||||
|
case '\"':
|
||||||
|
return ReadJsonString (json, ref i);
|
||||||
|
case 'f':
|
||||||
|
i += 5;
|
||||||
|
return false;
|
||||||
|
case 't':
|
||||||
|
i += 4;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return ReadJsonNumber (json, ref i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static object ReadJsonValue (string json, int startIndex)
|
||||||
|
{
|
||||||
|
var i = startIndex;
|
||||||
|
return ReadJsonValue (json, ref i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,5 +13,12 @@ namespace Ooui
|
||||||
: base ("label")
|
: base ("label")
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Label (string text)
|
||||||
|
: this ()
|
||||||
|
{
|
||||||
|
Text = text;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,105 @@ namespace Ooui
|
||||||
Key = eventType,
|
Key = eventType,
|
||||||
Value = value,
|
Value = value,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public void WriteJson (System.IO.TextWriter w)
|
||||||
|
{
|
||||||
|
w.Write ('{');
|
||||||
|
switch (MessageType) {
|
||||||
|
case MessageType.Call: w.Write ("\"m\":\"call\",\"id\":\""); break;
|
||||||
|
case MessageType.Create: w.Write ("\"m\":\"create\",\"id\":\""); break;
|
||||||
|
case MessageType.Event: w.Write ("\"m\":\"event\",\"id\":\""); break;
|
||||||
|
case MessageType.Listen: w.Write ("\"m\":\"listen\",\"id\":\""); break;
|
||||||
|
case MessageType.Nop: w.Write ("\"m\":\"nop\",\"id\":\""); break;
|
||||||
|
case MessageType.RemoveAttribute: w.Write ("\"m\":\"remAttr\",\"id\":\""); break;
|
||||||
|
case MessageType.Set: w.Write ("\"m\":\"set\",\"id\":\""); break;
|
||||||
|
case MessageType.SetAttribute: w.Write ("\"m\":\"setAttr\",\"id\":\""); break;
|
||||||
|
}
|
||||||
|
w.Write (TargetId);
|
||||||
|
w.Write ("\",\"k\":\"");
|
||||||
|
w.Write (Key);
|
||||||
|
if (Value != null) {
|
||||||
|
w.Write ("\",\"v\":");
|
||||||
|
JsonConvert.WriteJsonValue (w, Value);
|
||||||
|
w.Write ('}');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
w.Write ("\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ToJson ()
|
||||||
|
{
|
||||||
|
using (var sw = new System.IO.StringWriter ()) {
|
||||||
|
WriteJson (sw);
|
||||||
|
return sw.ToString ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Message FromJson (string json)
|
||||||
|
{
|
||||||
|
var m = new Message ();
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
var e = json.Length;
|
||||||
|
|
||||||
|
while (i < e) {
|
||||||
|
while (i < e && (json[i]==',' || json[i]=='{' || char.IsWhiteSpace (json[i])))
|
||||||
|
i++;
|
||||||
|
if (i >= e)
|
||||||
|
throw new Exception ("JSON Unexpected end");
|
||||||
|
var n = e - i;
|
||||||
|
if (json[i] == '}')
|
||||||
|
break;
|
||||||
|
if (n > 4 && json[i] == '\"' && json[i+2] == '\"' && json[i+3] == ':') {
|
||||||
|
switch (json[i + 1]) {
|
||||||
|
case 'm':
|
||||||
|
if (json[i + 4] == '\"' && json[i + 5] == 'e') {
|
||||||
|
m.MessageType = MessageType.Event;
|
||||||
|
}
|
||||||
|
i += 5;
|
||||||
|
while (i < e && json[i] != '\"') i++;
|
||||||
|
i++;
|
||||||
|
break;
|
||||||
|
case 'k': {
|
||||||
|
i += 5;
|
||||||
|
var se = i;
|
||||||
|
while (se < e && json[se] != '\"')
|
||||||
|
se++;
|
||||||
|
m.Key = json.Substring (i, se - i);
|
||||||
|
i = se + 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'v':
|
||||||
|
m.Value = JsonConvert.ReadJsonValue (json, i + 4);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (n > 5 && json[i] == '\"' && json[i + 3] == '\"' && json[i + 4] == ':' && json[i+5] == '\"') {
|
||||||
|
switch (json[i + 1]) {
|
||||||
|
case 'i': {
|
||||||
|
i += 6;
|
||||||
|
var se = i;
|
||||||
|
while (se < e && json[se] != '\"')
|
||||||
|
se++;
|
||||||
|
m.TargetId = json.Substring (i, se - i);
|
||||||
|
i = se + 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Exception ("JSON Expected property");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString ()
|
||||||
|
{
|
||||||
|
return ToJson ();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter (typeof (StringEnumConverter))]
|
[JsonConverter (typeof (StringEnumConverter))]
|
||||||
|
|
49
Ooui/Node.cs
49
Ooui/Node.cs
|
@ -1,6 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace Ooui
|
namespace Ooui
|
||||||
{
|
{
|
||||||
|
@ -11,7 +11,7 @@ namespace Ooui
|
||||||
public IReadOnlyList<Node> Children {
|
public IReadOnlyList<Node> Children {
|
||||||
get {
|
get {
|
||||||
lock (children) {
|
lock (children) {
|
||||||
return new List<Node> (children).AsReadOnly ();
|
return new ReadOnlyList<Node> (children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,13 @@ namespace Ooui
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual string Text {
|
public virtual string Text {
|
||||||
get { return String.Join ("", from c in Children select c.Text); }
|
get {
|
||||||
|
var sb = new System.Text.StringBuilder ();
|
||||||
|
foreach (var c in Children) {
|
||||||
|
sb.Append (c.Text);
|
||||||
|
}
|
||||||
|
return sb.ToString ();
|
||||||
|
}
|
||||||
set {
|
set {
|
||||||
ReplaceAll (new TextNode (value ?? ""));
|
ReplaceAll (new TextNode (value ?? ""));
|
||||||
}
|
}
|
||||||
|
@ -84,7 +90,7 @@ namespace Ooui
|
||||||
return null;
|
return null;
|
||||||
lock (children) {
|
lock (children) {
|
||||||
if (!children.Remove (child)) {
|
if (!children.Remove (child)) {
|
||||||
throw new ArgumentException ("Child not contained in this element", nameof(child));
|
throw new ArgumentException ("Child not contained in this element", nameof (child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
child.MessageSent -= HandleChildMessageSent;
|
child.MessageSent -= HandleChildMessageSent;
|
||||||
|
@ -123,6 +129,7 @@ namespace Ooui
|
||||||
protected override bool SaveStateMessageIfNeeded (Message message)
|
protected override bool SaveStateMessageIfNeeded (Message message)
|
||||||
{
|
{
|
||||||
if (message.TargetId == Id) {
|
if (message.TargetId == Id) {
|
||||||
|
var handled = false;
|
||||||
switch (message.MessageType) {
|
switch (message.MessageType) {
|
||||||
case MessageType.Call when message.Key == "insertBefore":
|
case MessageType.Call when message.Key == "insertBefore":
|
||||||
AddStateMessage (message);
|
AddStateMessage (message);
|
||||||
|
@ -131,7 +138,7 @@ namespace Ooui
|
||||||
UpdateStateMessages (state => {
|
UpdateStateMessages (state => {
|
||||||
var mchild = ma.GetValue (0);
|
var mchild = ma.GetValue (0);
|
||||||
Node nextChild = null;
|
Node nextChild = null;
|
||||||
for (var i = 0; i < state.Count; ) {
|
for (var i = 0; i < state.Count;) {
|
||||||
var x = state[i];
|
var x = state[i];
|
||||||
if (x.Key == "insertBefore" && x.Value is Array xa && xa.Length == 2 && ReferenceEquals (xa.GetValue (0), mchild)) {
|
if (x.Key == "insertBefore" && x.Value is Array xa && xa.Length == 2 && ReferenceEquals (xa.GetValue (0), mchild)) {
|
||||||
// Remove any inserts for this node
|
// Remove any inserts for this node
|
||||||
|
@ -149,9 +156,9 @@ namespace Ooui
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
}
|
||||||
|
if (!handled) {
|
||||||
base.SaveStateMessageIfNeeded (message);
|
base.SaveStateMessageIfNeeded (message);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -181,6 +188,8 @@ namespace Ooui
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !NO_XML
|
||||||
|
|
||||||
public virtual string OuterHtml {
|
public virtual string OuterHtml {
|
||||||
get {
|
get {
|
||||||
using (var stream = new System.IO.MemoryStream ()) {
|
using (var stream = new System.IO.MemoryStream ()) {
|
||||||
|
@ -199,5 +208,31 @@ namespace Ooui
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void WriteOuterHtml (System.Xml.XmlWriter w);
|
public abstract void WriteOuterHtml (System.Xml.XmlWriter w);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReadOnlyList<T> : IReadOnlyList<T>
|
||||||
|
{
|
||||||
|
readonly List<T> list;
|
||||||
|
|
||||||
|
public ReadOnlyList (List<T> items)
|
||||||
|
{
|
||||||
|
list = new List<T> (items);
|
||||||
|
}
|
||||||
|
|
||||||
|
T IReadOnlyList<T>.this[int index] => list[index];
|
||||||
|
|
||||||
|
int IReadOnlyCollection<T>.Count => list.Count;
|
||||||
|
|
||||||
|
IEnumerator<T> IEnumerable<T>.GetEnumerator ()
|
||||||
|
{
|
||||||
|
return ((IEnumerable<T>)list).GetEnumerator ();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator ()
|
||||||
|
{
|
||||||
|
return ((IEnumerable)list).GetEnumerator ();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,15 @@
|
||||||
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
|
<PackageProjectUrl>https://github.com/praeclarum/Ooui</PackageProjectUrl>
|
||||||
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/praeclarum/Ooui/blob/master/LICENSE</PackageLicenseUrl>
|
||||||
<RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl>
|
<RepositoryUrl>https://github.com/praeclarum/Ooui.git</RepositoryUrl>
|
||||||
<TargetFramework>netstandard2.0</TargetFramework>
|
<TargetFrameworks>netstandard2.0</TargetFrameworks>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Client.js" />
|
<EmbeddedResource Include="Client.js" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Ooui
|
namespace Ooui
|
||||||
{
|
{
|
||||||
|
@ -62,10 +62,10 @@ namespace Ooui
|
||||||
}
|
}
|
||||||
|
|
||||||
if (presenterWebView == null) {
|
if (presenterWebView == null) {
|
||||||
throw new ArgumentException ("Presenter must be a WebView", nameof(presenter));
|
throw new ArgumentException ("Presenter must be a WebView", nameof (presenter));
|
||||||
}
|
}
|
||||||
|
|
||||||
var m = androidWebViewType.GetMethod ("LoadUrl", BindingFlags.Public|BindingFlags.Instance, null, CallingConventions.Any, new Type[] { typeof(string) }, null);
|
var m = androidWebViewType.GetMethod ("LoadUrl", BindingFlags.Public | BindingFlags.Instance, null, CallingConventions.Any, new Type[] { typeof (string) }, null);
|
||||||
m.Invoke (presenterWebView, new object[] { url });
|
m.Invoke (presenterWebView, new object[] { url });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,7 +130,7 @@ namespace Ooui
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Process StartBrowserProcess (string url)
|
static void StartBrowserProcess (string url)
|
||||||
{
|
{
|
||||||
// var vs = Environment.GetEnvironmentVariables ();
|
// var vs = Environment.GetEnvironmentVariables ();
|
||||||
// foreach (System.Collections.DictionaryEntry kv in vs) {
|
// foreach (System.Collections.DictionaryEntry kv in vs) {
|
||||||
|
@ -139,9 +139,12 @@ namespace Ooui
|
||||||
|
|
||||||
// Console.WriteLine ($"Process.Start {cmd} {args}");
|
// Console.WriteLine ($"Process.Start {cmd} {args}");
|
||||||
|
|
||||||
return Environment.OSVersion.Platform == PlatformID.Unix
|
if (Environment.OSVersion.Platform == PlatformID.Unix) {
|
||||||
? Process.Start ("open", url)
|
Process.Start ("open", url);
|
||||||
: Process.Start (new ProcessStartInfo (url) { UseShellExecute = true });
|
}
|
||||||
|
else {
|
||||||
|
Process.Start (new ProcessStartInfo (url) { UseShellExecute = true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Ooui
|
||||||
|
{
|
||||||
|
class PreserveAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Ooui
|
||||||
|
{
|
||||||
|
public abstract class Session
|
||||||
|
{
|
||||||
|
protected readonly Element element;
|
||||||
|
protected readonly double initialWidth;
|
||||||
|
protected readonly double initialHeight;
|
||||||
|
|
||||||
|
protected readonly HashSet<string> createdIds;
|
||||||
|
|
||||||
|
protected readonly List<Message> queuedMessages = new List<Message> ();
|
||||||
|
|
||||||
|
public Session (Element element, double initialWidth, double initialHeight)
|
||||||
|
{
|
||||||
|
this.element = element;
|
||||||
|
this.initialWidth = initialWidth;
|
||||||
|
this.initialHeight = initialHeight;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Keep a list of all the elements for which we've transmitted the initial state
|
||||||
|
//
|
||||||
|
createdIds = new HashSet<string> {
|
||||||
|
"window",
|
||||||
|
"document",
|
||||||
|
"document.body",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void QueueStateMessagesLocked (EventTarget target)
|
||||||
|
{
|
||||||
|
if (target == null) return;
|
||||||
|
var created = false;
|
||||||
|
foreach (var m in target.StateMessages) {
|
||||||
|
if (m.MessageType == MessageType.Create) {
|
||||||
|
createdIds.Add (m.TargetId);
|
||||||
|
created = true;
|
||||||
|
}
|
||||||
|
if (created) {
|
||||||
|
QueueMessageLocked (m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void QueueMessageLocked (Message message)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Make sure all the referenced objects have been created
|
||||||
|
//
|
||||||
|
if (!createdIds.Contains (message.TargetId)) {
|
||||||
|
QueueStateMessagesLocked (element.GetElementById (message.TargetId));
|
||||||
|
}
|
||||||
|
if (message.Value is EventTarget ve) {
|
||||||
|
if (!createdIds.Contains (ve.Id)) {
|
||||||
|
QueueStateMessagesLocked (ve);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (message.Value is Array a) {
|
||||||
|
for (var i = 0; i < a.Length; i++) {
|
||||||
|
// Console.WriteLine ($"A{i} = {a.GetValue(i)}");
|
||||||
|
if (a.GetValue (i) is EventTarget e && !createdIds.Contains (e.Id)) {
|
||||||
|
QueueStateMessagesLocked (e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Add it to the queue
|
||||||
|
//
|
||||||
|
//Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}");
|
||||||
|
queuedMessages.Add (message);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void QueueMessage (Message message)
|
||||||
|
{
|
||||||
|
lock (queuedMessages) {
|
||||||
|
QueueMessageLocked (message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Error (string message, Exception ex)
|
||||||
|
{
|
||||||
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
|
Console.WriteLine ("{0}: {1}", message, ex);
|
||||||
|
Console.ResetColor ();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Info (string message)
|
||||||
|
{
|
||||||
|
Console.WriteLine (message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -412,8 +412,17 @@ namespace Ooui
|
||||||
return null;
|
return null;
|
||||||
if (val is string s)
|
if (val is string s)
|
||||||
return s;
|
return s;
|
||||||
|
|
||||||
|
if (val is int i)
|
||||||
|
return i + units;
|
||||||
|
if (val is double d)
|
||||||
|
return d.ToString (System.Globalization.CultureInfo.InvariantCulture) + units;
|
||||||
|
if (val is float f)
|
||||||
|
return f.ToString (System.Globalization.CultureInfo.InvariantCulture) + units;
|
||||||
|
|
||||||
if (val is IConvertible c)
|
if (val is IConvertible c)
|
||||||
return c.ToString (System.Globalization.CultureInfo.InvariantCulture) + units;
|
return c.ToString (System.Globalization.CultureInfo.InvariantCulture) + units;
|
||||||
|
|
||||||
return val.ToString ();
|
return val.ToString ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -431,6 +440,13 @@ namespace Ooui
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (v is int i)
|
||||||
|
return i;
|
||||||
|
if (v is double d)
|
||||||
|
return d;
|
||||||
|
if (v is float f)
|
||||||
|
return f;
|
||||||
|
|
||||||
if (v is IConvertible c)
|
if (v is IConvertible c)
|
||||||
return c.ToDouble (System.Globalization.CultureInfo.InvariantCulture);
|
return c.ToDouble (System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ namespace Ooui
|
||||||
set => SetAttributeProperty ("rows", value);
|
set => SetAttributeProperty ("rows", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
int cols = 20;
|
|
||||||
public int Columns {
|
public int Columns {
|
||||||
get => GetAttribute ("cols", 20);
|
get => GetAttribute ("cols", 20);
|
||||||
set => SetAttributeProperty ("cols", value);
|
set => SetAttributeProperty ("cols", value);
|
||||||
|
@ -55,9 +54,13 @@ namespace Ooui
|
||||||
return base.TriggerEventFromMessage (message);
|
return base.TriggerEventFromMessage (message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !NO_XML
|
||||||
|
|
||||||
public override void WriteInnerHtml (System.Xml.XmlWriter w)
|
public override void WriteInnerHtml (System.Xml.XmlWriter w)
|
||||||
{
|
{
|
||||||
w.WriteString (val ?? "");
|
w.WriteString (val ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,9 +21,13 @@ namespace Ooui
|
||||||
Text = text;
|
Text = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !NO_XML
|
||||||
|
|
||||||
public override void WriteOuterHtml (System.Xml.XmlWriter w)
|
public override void WriteOuterHtml (System.Xml.XmlWriter w)
|
||||||
{
|
{
|
||||||
w.WriteString (text);
|
w.WriteString (text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
351
Ooui/UI.cs
351
Ooui/UI.cs
|
@ -6,52 +6,30 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.WebSockets;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Ooui
|
namespace Ooui
|
||||||
{
|
{
|
||||||
public static class UI
|
public static class UI
|
||||||
{
|
{
|
||||||
static readonly ManualResetEvent started = new ManualResetEvent (false);
|
public const int MaxFps = 30;
|
||||||
|
|
||||||
[ThreadStatic]
|
static readonly ManualResetEvent started = new ManualResetEvent (false);
|
||||||
static System.Security.Cryptography.SHA256 sha256;
|
|
||||||
|
|
||||||
static CancellationTokenSource serverCts;
|
static CancellationTokenSource serverCts;
|
||||||
|
|
||||||
static readonly Dictionary<string, RequestHandler> publishedPaths =
|
static readonly Dictionary<string, RequestHandler> publishedPaths =
|
||||||
new Dictionary<string, RequestHandler> ();
|
new Dictionary<string, RequestHandler> ();
|
||||||
|
|
||||||
static readonly Dictionary<string, Style> styles =
|
|
||||||
new Dictionary<string, Style> ();
|
|
||||||
static readonly StyleSelectors rules = new StyleSelectors ();
|
|
||||||
|
|
||||||
public static StyleSelectors Styles => rules;
|
|
||||||
|
|
||||||
static readonly byte[] clientJsBytes;
|
static readonly byte[] clientJsBytes;
|
||||||
static readonly string clientJsEtag;
|
static readonly string clientJsEtag;
|
||||||
|
|
||||||
public static byte[] ClientJsBytes => clientJsBytes;
|
public static byte[] ClientJsBytes => clientJsBytes;
|
||||||
public static string ClientJsEtag => clientJsEtag;
|
public static string ClientJsEtag => clientJsEtag;
|
||||||
|
|
||||||
public static string Template { get; set; } = $@"<!DOCTYPE html>
|
public static string HeadHtml { get; set; } = @"<link rel=""stylesheet"" href=""https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"" />";
|
||||||
<html>
|
public static string BodyHeaderHtml { get; set; } = @"";
|
||||||
<head>
|
public static string BodyFooterHtml { get; set; } = @"";
|
||||||
<title>@Title</title>
|
|
||||||
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
|
|
||||||
<link rel=""stylesheet"" href=""https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"" />
|
|
||||||
<style>@Styles</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id=""ooui-body"" class=""container-fluid"">
|
|
||||||
@InitialHtml
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src=""/ooui.js""></script>
|
|
||||||
<script>ooui(""@WebSocketPath"");</script>
|
|
||||||
</body>
|
|
||||||
</html>";
|
|
||||||
|
|
||||||
static string host = "*";
|
static string host = "*";
|
||||||
public static string Host {
|
public static string Host {
|
||||||
|
@ -87,6 +65,12 @@ namespace Ooui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Preserve]
|
||||||
|
static void DisableServer ()
|
||||||
|
{
|
||||||
|
ServerEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
static UI ()
|
static UI ()
|
||||||
{
|
{
|
||||||
var asm = typeof(UI).Assembly;
|
var asm = typeof(UI).Assembly;
|
||||||
|
@ -101,27 +85,12 @@ namespace Ooui
|
||||||
clientJsBytes = Encoding.UTF8.GetBytes (r.ReadToEnd ());
|
clientJsBytes = Encoding.UTF8.GetBytes (r.ReadToEnd ());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clientJsEtag = "\"" + Hash (clientJsBytes) + "\"";
|
clientJsEtag = "\"" + Utilities.Hash (clientJsBytes) + "\"";
|
||||||
}
|
|
||||||
|
|
||||||
public static string Hash (byte[] bytes)
|
|
||||||
{
|
|
||||||
var sha = sha256;
|
|
||||||
if (sha == null) {
|
|
||||||
sha = System.Security.Cryptography.SHA256.Create ();
|
|
||||||
sha256 = sha;
|
|
||||||
}
|
|
||||||
var data = sha.ComputeHash (bytes);
|
|
||||||
StringBuilder sBuilder = new StringBuilder ();
|
|
||||||
for (int i = 0; i < data.Length; i++) {
|
|
||||||
sBuilder.Append (data[i].ToString ("x2"));
|
|
||||||
}
|
|
||||||
return sBuilder.ToString ();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Publish (string path, RequestHandler handler)
|
static void Publish (string path, RequestHandler handler)
|
||||||
{
|
{
|
||||||
Console.WriteLine ($"PUBLISH {path} {handler}");
|
//Console.WriteLine ($"PUBLISH {path} {handler}");
|
||||||
lock (publishedPaths) publishedPaths[path] = handler;
|
lock (publishedPaths) publishedPaths[path] = handler;
|
||||||
Start ();
|
Start ();
|
||||||
}
|
}
|
||||||
|
@ -148,13 +117,13 @@ namespace Ooui
|
||||||
if (contentType == null) {
|
if (contentType == null) {
|
||||||
contentType = GuessContentType (path, filePath);
|
contentType = GuessContentType (path, filePath);
|
||||||
}
|
}
|
||||||
var etag = "\"" + Hash (data) + "\"";
|
var etag = "\"" + Utilities.Hash (data) + "\"";
|
||||||
Publish (path, new DataHandler (data, etag, contentType));
|
Publish (path, new DataHandler (data, etag, contentType));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PublishFile (string path, byte[] data, string contentType)
|
public static void PublishFile (string path, byte[] data, string contentType)
|
||||||
{
|
{
|
||||||
var etag = "\"" + Hash (data) + "\"";
|
var etag = "\"" + Utilities.Hash (data) + "\"";
|
||||||
Publish (path, new DataHandler (data, etag, contentType));
|
Publish (path, new DataHandler (data, etag, contentType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,7 +168,7 @@ namespace Ooui
|
||||||
public static void PublishJson (string path, object value)
|
public static void PublishJson (string path, object value)
|
||||||
{
|
{
|
||||||
var data = JsonHandler.GetData (value);
|
var data = JsonHandler.GetData (value);
|
||||||
var etag = "\"" + Hash (data) + "\"";
|
var etag = "\"" + Utilities.Hash (data) + "\"";
|
||||||
Publish (path, new DataHandler (data, etag, JsonHandler.ContentType));
|
Publish (path, new DataHandler (data, etag, JsonHandler.ContentType));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -392,7 +361,45 @@ namespace Ooui
|
||||||
|
|
||||||
public static string RenderTemplate (string webSocketPath, string title = "", string initialHtml = "")
|
public static string RenderTemplate (string webSocketPath, string title = "", string initialHtml = "")
|
||||||
{
|
{
|
||||||
return Template.Replace ("@WebSocketPath", webSocketPath).Replace ("@Styles", rules.ToString ()).Replace ("@Title", title).Replace ("@InitialHtml", initialHtml);
|
using (var w = new System.IO.StringWriter ()) {
|
||||||
|
RenderTemplate (w, webSocketPath, title, initialHtml);
|
||||||
|
return w.ToString ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string EscapeHtml (string text)
|
||||||
|
{
|
||||||
|
return text.Replace ("&", "&").Replace ("<", "<");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RenderTemplate (TextWriter writer, string webSocketPath, string title, string initialHtml)
|
||||||
|
{
|
||||||
|
writer.Write (@"<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>");
|
||||||
|
writer.Write (EscapeHtml (title));
|
||||||
|
writer.Write (@"</title>
|
||||||
|
<meta name=""viewport"" content=""width=device-width, initial-scale=1"" />
|
||||||
|
");
|
||||||
|
writer.WriteLine (HeadHtml);
|
||||||
|
writer.WriteLine (@" <style>");
|
||||||
|
writer.WriteLine (rules.ToString ());
|
||||||
|
writer.WriteLine (@" </style>
|
||||||
|
</head>
|
||||||
|
<body>");
|
||||||
|
writer.WriteLine (BodyHeaderHtml);
|
||||||
|
writer.WriteLine (@"<div id=""ooui-body"" class=""container-fluid"">");
|
||||||
|
writer.WriteLine (initialHtml);
|
||||||
|
writer.Write (@"</div>
|
||||||
|
|
||||||
|
<script src=""/ooui.js""></script>
|
||||||
|
<script>ooui(""");
|
||||||
|
writer.Write (webSocketPath);
|
||||||
|
writer.WriteLine (@""");</script>");
|
||||||
|
writer.WriteLine (BodyFooterHtml);
|
||||||
|
writer.WriteLine (@"</body>
|
||||||
|
</html>");
|
||||||
}
|
}
|
||||||
|
|
||||||
class DataHandler : RequestHandler
|
class DataHandler : RequestHandler
|
||||||
|
@ -450,8 +457,9 @@ namespace Ooui
|
||||||
|
|
||||||
public static byte[] GetData (object obj)
|
public static byte[] GetData (object obj)
|
||||||
{
|
{
|
||||||
var r = Newtonsoft.Json.JsonConvert.SerializeObject (obj);
|
var r = Ooui.JsonConvert.SerializeObject (obj);
|
||||||
return System.Text.Encoding.UTF8.GetBytes (r);
|
var e = new UTF8Encoding (false);
|
||||||
|
return e.GetBytes (r);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Respond (HttpListenerContext listenerContext, CancellationToken token)
|
public override void Respond (HttpListenerContext listenerContext, CancellationToken token)
|
||||||
|
@ -521,8 +529,8 @@ namespace Ooui
|
||||||
//
|
//
|
||||||
// Connect the web socket
|
// Connect the web socket
|
||||||
//
|
//
|
||||||
WebSocketContext webSocketContext = null;
|
System.Net.WebSockets.WebSocketContext webSocketContext = null;
|
||||||
WebSocket webSocket = null;
|
System.Net.WebSockets.WebSocket webSocket = null;
|
||||||
try {
|
try {
|
||||||
webSocketContext = await listenerContext.AcceptWebSocketAsync (subProtocol: "ooui").ConfigureAwait (false);
|
webSocketContext = await listenerContext.AcceptWebSocketAsync (subProtocol: "ooui").ConfigureAwait (false);
|
||||||
webSocket = webSocketContext.WebSocket;
|
webSocket = webSocketContext.WebSocket;
|
||||||
|
@ -560,10 +568,10 @@ namespace Ooui
|
||||||
// Create a new session and let it handle everything from here
|
// Create a new session and let it handle everything from here
|
||||||
//
|
//
|
||||||
try {
|
try {
|
||||||
var session = new Session (webSocket, element, w, h, serverToken);
|
var session = new WebSocketSession (webSocket, element, w, h, serverToken);
|
||||||
await session.RunAsync ().ConfigureAwait (false);
|
await session.RunAsync ().ConfigureAwait (false);
|
||||||
}
|
}
|
||||||
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) {
|
catch (System.Net.WebSockets.WebSocketException ex) when (ex.WebSocketErrorCode == System.Net.WebSockets.WebSocketError.ConnectionClosedPrematurely) {
|
||||||
// The remote party closed the WebSocket connection without completing the close handshake.
|
// The remote party closed the WebSocket connection without completing the close handshake.
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
|
@ -581,215 +589,50 @@ namespace Ooui
|
||||||
Console.ResetColor ();
|
Console.ResetColor ();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Session
|
static readonly Dictionary<string, WebAssemblySession> globalElementSessions = new Dictionary<string, WebAssemblySession> ();
|
||||||
|
|
||||||
|
[Preserve]
|
||||||
|
public static void StartWebAssemblySession (string sessionId, string elementPath, string initialSize)
|
||||||
{
|
{
|
||||||
readonly WebSocket webSocket;
|
Element element;
|
||||||
readonly Element element;
|
RequestHandler handler;
|
||||||
readonly Action<Message> handleElementMessageSent;
|
lock (publishedPaths) {
|
||||||
|
publishedPaths.TryGetValue (elementPath, out handler);
|
||||||
readonly CancellationTokenSource sessionCts = new CancellationTokenSource ();
|
|
||||||
readonly CancellationTokenSource linkedCts;
|
|
||||||
readonly CancellationToken token;
|
|
||||||
|
|
||||||
readonly HashSet<string> createdIds;
|
|
||||||
readonly List<Message> queuedMessages = new List<Message> ();
|
|
||||||
|
|
||||||
public const int MaxFps = 30;
|
|
||||||
|
|
||||||
readonly System.Timers.Timer sendThrottle;
|
|
||||||
DateTime lastTransmitTime = DateTime.MinValue;
|
|
||||||
readonly TimeSpan throttleInterval = TimeSpan.FromSeconds (1.0 / MaxFps);
|
|
||||||
readonly double initialWidth;
|
|
||||||
readonly double initialHeight;
|
|
||||||
|
|
||||||
public Session (WebSocket webSocket, Element element, double initialWidth, double initialHeight, CancellationToken serverToken)
|
|
||||||
{
|
|
||||||
this.webSocket = webSocket;
|
|
||||||
this.element = element;
|
|
||||||
this.initialWidth = initialWidth;
|
|
||||||
this.initialHeight = initialHeight;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Create a new session cancellation token that will trigger
|
|
||||||
// automatically if the server shutsdown or the session shutsdown.
|
|
||||||
//
|
|
||||||
linkedCts = CancellationTokenSource.CreateLinkedTokenSource (serverToken, sessionCts.Token);
|
|
||||||
token = linkedCts.Token;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Keep a list of all the elements for which we've transmitted the initial state
|
|
||||||
//
|
|
||||||
createdIds = new HashSet<string> {
|
|
||||||
"window",
|
|
||||||
"document",
|
|
||||||
"document.body",
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Preparse handlers for the element
|
|
||||||
//
|
|
||||||
handleElementMessageSent = QueueMessage;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Create a timer to use as a throttle when sending messages
|
|
||||||
//
|
|
||||||
sendThrottle = new System.Timers.Timer (throttleInterval.TotalMilliseconds);
|
|
||||||
sendThrottle.Elapsed += (s, e) => {
|
|
||||||
// System.Console.WriteLine ("TICK SEND THROTTLE FOR {0}", element);
|
|
||||||
if ((e.SignalTime - lastTransmitTime) >= throttleInterval) {
|
|
||||||
sendThrottle.Enabled = false;
|
|
||||||
lastTransmitTime = e.SignalTime;
|
|
||||||
TransmitQueuedMessages ();
|
|
||||||
}
|
}
|
||||||
};
|
if (handler is ElementHandler eh) {
|
||||||
}
|
element = eh.GetElement ();
|
||||||
|
|
||||||
public async Task RunAsync ()
|
|
||||||
{
|
|
||||||
//
|
|
||||||
// Start watching for changes in the element
|
|
||||||
//
|
|
||||||
element.MessageSent += handleElementMessageSent;
|
|
||||||
|
|
||||||
try {
|
|
||||||
//
|
|
||||||
// Add it to the document body
|
|
||||||
//
|
|
||||||
if (element.WantsFullScreen) {
|
|
||||||
element.Style.Width = initialWidth;
|
|
||||||
element.Style.Height = initialHeight;
|
|
||||||
}
|
|
||||||
QueueMessage (Message.Call ("document.body", "appendChild", element));
|
|
||||||
|
|
||||||
//
|
|
||||||
// Start the Read Loop
|
|
||||||
//
|
|
||||||
var receiveBuffer = new byte[64*1024];
|
|
||||||
|
|
||||||
while (webSocket.State == WebSocketState.Open && !token.IsCancellationRequested) {
|
|
||||||
var receiveResult = await webSocket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), token).ConfigureAwait (false);
|
|
||||||
|
|
||||||
if (receiveResult.MessageType == WebSocketMessageType.Close) {
|
|
||||||
await webSocket.CloseAsync (WebSocketCloseStatus.NormalClosure, "", token).ConfigureAwait (false);
|
|
||||||
sessionCts.Cancel ();
|
|
||||||
}
|
|
||||||
else if (receiveResult.MessageType == WebSocketMessageType.Binary) {
|
|
||||||
await webSocket.CloseAsync (WebSocketCloseStatus.InvalidMessageType, "Cannot accept binary frame", token).ConfigureAwait (false);
|
|
||||||
sessionCts.Cancel ();
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var size = receiveResult.Count;
|
element = new Div ();
|
||||||
while (!receiveResult.EndOfMessage) {
|
}
|
||||||
if (size >= receiveBuffer.Length) {
|
|
||||||
await webSocket.CloseAsync (WebSocketCloseStatus.MessageTooBig, "Message too big", token).ConfigureAwait (false);
|
var ops = initialSize.Split (' ');
|
||||||
|
var initialWidth = double.Parse (ops[0]);
|
||||||
|
var initialHeight = double.Parse (ops[1]);
|
||||||
|
var g = new WebAssemblySession (sessionId, element, initialWidth, initialHeight);
|
||||||
|
lock (globalElementSessions) {
|
||||||
|
globalElementSessions[sessionId] = g;
|
||||||
|
}
|
||||||
|
g.StartSession ();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Preserve]
|
||||||
|
public static void ReceiveWebAssemblySessionMessageJson (string sessionId, string json)
|
||||||
|
{
|
||||||
|
WebAssemblySession g;
|
||||||
|
lock (globalElementSessions) {
|
||||||
|
if (!globalElementSessions.TryGetValue (sessionId, out g))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
receiveResult = await webSocket.ReceiveAsync (new ArraySegment<byte>(receiveBuffer, size, receiveBuffer.Length - size), token).ConfigureAwait (false);
|
g.ReceiveMessageJson (json);
|
||||||
size += receiveResult.Count;
|
|
||||||
}
|
|
||||||
var receivedString = Encoding.UTF8.GetString (receiveBuffer, 0, size);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Console.WriteLine ("RECEIVED: {0}", receivedString);
|
|
||||||
var message = Newtonsoft.Json.JsonConvert.DeserializeObject<Message> (receivedString);
|
|
||||||
element.Receive (message);
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
Error ("Failed to process received message", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
element.MessageSent -= handleElementMessageSent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void QueueStateMessagesLocked (EventTarget target)
|
|
||||||
{
|
|
||||||
if (target == null) return;
|
|
||||||
var created = false;
|
|
||||||
foreach (var m in target.StateMessages) {
|
|
||||||
if (m.MessageType == MessageType.Create) {
|
|
||||||
createdIds.Add (m.TargetId);
|
|
||||||
created = true;
|
|
||||||
}
|
|
||||||
if (created) {
|
|
||||||
QueueMessageLocked (m);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void QueueMessageLocked (Message message)
|
static readonly Dictionary<string, Style> styles =
|
||||||
{
|
new Dictionary<string, Style> ();
|
||||||
//
|
static readonly StyleSelectors rules = new StyleSelectors ();
|
||||||
// Make sure all the referenced objects have been created
|
|
||||||
//
|
|
||||||
if (!createdIds.Contains (message.TargetId)) {
|
|
||||||
QueueStateMessagesLocked (element.GetElementById (message.TargetId));
|
|
||||||
}
|
|
||||||
if (message.Value is EventTarget ve) {
|
|
||||||
if (!createdIds.Contains (ve.Id)) {
|
|
||||||
QueueStateMessagesLocked (ve);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (message.Value is Array a) {
|
|
||||||
for (var i = 0; i < a.Length; i++) {
|
|
||||||
// Console.WriteLine ($"A{i} = {a.GetValue(i)}");
|
|
||||||
if (a.GetValue (i) is EventTarget e && !createdIds.Contains (e.Id)) {
|
|
||||||
QueueStateMessagesLocked (e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
public static StyleSelectors Styles => rules;
|
||||||
// Add it to the queue
|
|
||||||
//
|
|
||||||
//Console.WriteLine ($"QM {message.MessageType} {message.TargetId} {message.Key} {message.Value}");
|
|
||||||
queuedMessages.Add (message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void QueueMessage (Message message)
|
|
||||||
{
|
|
||||||
lock (queuedMessages) {
|
|
||||||
QueueMessageLocked (message);
|
|
||||||
}
|
|
||||||
sendThrottle.Enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async void TransmitQueuedMessages ()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
//
|
|
||||||
// Dequeue as many messages as we can
|
|
||||||
//
|
|
||||||
var messagesToSend = new List<Message> ();
|
|
||||||
System.Runtime.CompilerServices.ConfiguredTaskAwaitable task;
|
|
||||||
lock (queuedMessages) {
|
|
||||||
messagesToSend.AddRange (queuedMessages);
|
|
||||||
queuedMessages.Clear ();
|
|
||||||
|
|
||||||
if (messagesToSend.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
//
|
|
||||||
// Now actually send this message
|
|
||||||
// Do this while locked to make sure SendAsync is called in the right order
|
|
||||||
//
|
|
||||||
var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend);
|
|
||||||
var outputBuffer = new ArraySegment<byte> (Encoding.UTF8.GetBytes (json));
|
|
||||||
//Console.WriteLine ("TRANSMIT " + json);
|
|
||||||
task = webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false);
|
|
||||||
}
|
|
||||||
await task;
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
Error ("Failed to send queued messages, aborting session", ex);
|
|
||||||
element.MessageSent -= handleElementMessageSent;
|
|
||||||
sessionCts.Cancel ();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StyleSelectors
|
public class StyleSelectors
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Ooui
|
||||||
|
{
|
||||||
|
public static class Utilities
|
||||||
|
{
|
||||||
|
[ThreadStatic]
|
||||||
|
static System.Security.Cryptography.SHA256 sha256;
|
||||||
|
|
||||||
|
public static string Hash (byte[] bytes)
|
||||||
|
{
|
||||||
|
var sha = sha256;
|
||||||
|
if (sha == null) {
|
||||||
|
sha = System.Security.Cryptography.SHA256.Create ();
|
||||||
|
sha256 = sha;
|
||||||
|
}
|
||||||
|
var data = sha.ComputeHash (bytes);
|
||||||
|
StringBuilder sBuilder = new StringBuilder ();
|
||||||
|
for (int i = 0; i < data.Length; i++) {
|
||||||
|
sBuilder.Append (data[i].ToString ("x2"));
|
||||||
|
}
|
||||||
|
return sBuilder.ToString ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,107 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Ooui
|
||||||
|
{
|
||||||
|
public class WebAssemblySession : Session
|
||||||
|
{
|
||||||
|
readonly string id;
|
||||||
|
readonly Action<Message> handleElementMessageSent;
|
||||||
|
|
||||||
|
public WebAssemblySession (string id, Element element, double initialWidth, double initialHeight)
|
||||||
|
: base (element, initialWidth, initialHeight)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
handleElementMessageSent = QueueMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void QueueMessage (Message message)
|
||||||
|
{
|
||||||
|
lock (queuedMessages) {
|
||||||
|
QueueMessageLocked (message);
|
||||||
|
var max = 1;
|
||||||
|
var i = 0;
|
||||||
|
while (i < queuedMessages.Count) {
|
||||||
|
TransmitQueuedMessagesLocked (queuedMessages, i, max);
|
||||||
|
i += max;
|
||||||
|
}
|
||||||
|
// WebAssembly.Runtime.InvokeJS("console.log('TRANSMITTED'," + queuedMessages.Count + ")");
|
||||||
|
queuedMessages.Clear ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TransmitQueuedMessagesLocked (List<Message> messagesToSend, int startIndex, int max)
|
||||||
|
{
|
||||||
|
if (messagesToSend.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Now actually send the messages
|
||||||
|
//
|
||||||
|
var sb = new System.IO.StringWriter ();
|
||||||
|
sb.Write ("__oouiReceiveMessages(\"");
|
||||||
|
sb.Write (id);
|
||||||
|
sb.Write ("\",");
|
||||||
|
sb.Write ("[");
|
||||||
|
var head = "";
|
||||||
|
int n = 0;
|
||||||
|
for (var i = startIndex; i < messagesToSend.Count && n < max; i++, n++) {
|
||||||
|
sb.Write (head);
|
||||||
|
messagesToSend[i].WriteJson (sb);
|
||||||
|
head = ",";
|
||||||
|
}
|
||||||
|
sb.Write ("])");
|
||||||
|
var jsonp = sb.ToString ();
|
||||||
|
// WebAssembly.Runtime.InvokeJS("console.log('TRANSMIT',"+n+")");
|
||||||
|
WebAssembly.Runtime.InvokeJS (jsonp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReceiveMessageJson (string json)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
var message = Newtonsoft.Json.JsonConvert.DeserializeObject<Message> (json);
|
||||||
|
element.Receive (message);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Error ("Failed to process received message", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartSession ()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Start watching for changes in the element
|
||||||
|
//
|
||||||
|
element.MessageSent += handleElementMessageSent;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Add it to the document body
|
||||||
|
//
|
||||||
|
if (element.WantsFullScreen) {
|
||||||
|
element.Style.Width = initialWidth;
|
||||||
|
element.Style.Height = initialHeight;
|
||||||
|
}
|
||||||
|
QueueMessage (Message.Call ("document.body", "appendChild", element));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopSession ()
|
||||||
|
{
|
||||||
|
element.MessageSent -= handleElementMessageSent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace WebAssembly
|
||||||
|
{
|
||||||
|
public sealed class Runtime
|
||||||
|
{
|
||||||
|
[System.Runtime.CompilerServices.MethodImplAttribute ((System.Runtime.CompilerServices.MethodImplOptions)4096)]
|
||||||
|
static extern string InvokeJS (string str, out int exceptional_result);
|
||||||
|
|
||||||
|
public static string InvokeJS (string str)
|
||||||
|
{
|
||||||
|
return InvokeJS (str, out var _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
|
namespace Ooui
|
||||||
|
{
|
||||||
|
public class WebSocketSession : Session
|
||||||
|
{
|
||||||
|
readonly WebSocket webSocket;
|
||||||
|
readonly Action<Message> handleElementMessageSent;
|
||||||
|
|
||||||
|
readonly CancellationTokenSource sessionCts = new CancellationTokenSource ();
|
||||||
|
readonly CancellationTokenSource linkedCts;
|
||||||
|
readonly CancellationToken token;
|
||||||
|
|
||||||
|
readonly System.Timers.Timer sendThrottle;
|
||||||
|
DateTime lastTransmitTime = DateTime.MinValue;
|
||||||
|
readonly TimeSpan throttleInterval = TimeSpan.FromSeconds (1.0 / UI.MaxFps);
|
||||||
|
|
||||||
|
public WebSocketSession (WebSocket webSocket, Element element, double initialWidth, double initialHeight, CancellationToken serverToken)
|
||||||
|
: base (element, initialWidth, initialHeight)
|
||||||
|
{
|
||||||
|
this.webSocket = webSocket;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create a new session cancellation token that will trigger
|
||||||
|
// automatically if the server shutsdown or the session shutsdown.
|
||||||
|
//
|
||||||
|
linkedCts = CancellationTokenSource.CreateLinkedTokenSource (serverToken, sessionCts.Token);
|
||||||
|
token = linkedCts.Token;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Preparse handlers for the element
|
||||||
|
//
|
||||||
|
handleElementMessageSent = QueueMessage;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Create a timer to use as a throttle when sending messages
|
||||||
|
//
|
||||||
|
sendThrottle = new System.Timers.Timer (throttleInterval.TotalMilliseconds);
|
||||||
|
sendThrottle.Elapsed += (s, e) => {
|
||||||
|
// System.Console.WriteLine ("TICK SEND THROTTLE FOR {0}", element);
|
||||||
|
if ((e.SignalTime - lastTransmitTime) >= throttleInterval) {
|
||||||
|
sendThrottle.Enabled = false;
|
||||||
|
lastTransmitTime = e.SignalTime;
|
||||||
|
TransmitQueuedMessages ();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RunAsync ()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// Start watching for changes in the element
|
||||||
|
//
|
||||||
|
element.MessageSent += handleElementMessageSent;
|
||||||
|
|
||||||
|
try {
|
||||||
|
//
|
||||||
|
// Add it to the document body
|
||||||
|
//
|
||||||
|
if (element.WantsFullScreen) {
|
||||||
|
element.Style.Width = initialWidth;
|
||||||
|
element.Style.Height = initialHeight;
|
||||||
|
}
|
||||||
|
QueueMessage (Message.Call ("document.body", "appendChild", element));
|
||||||
|
|
||||||
|
//
|
||||||
|
// Start the Read Loop
|
||||||
|
//
|
||||||
|
var receiveBuffer = new byte[64 * 1024];
|
||||||
|
|
||||||
|
while (webSocket.State == WebSocketState.Open && !token.IsCancellationRequested) {
|
||||||
|
var receiveResult = await webSocket.ReceiveAsync (new ArraySegment<byte> (receiveBuffer), token).ConfigureAwait (false);
|
||||||
|
|
||||||
|
if (receiveResult.MessageType == WebSocketMessageType.Close) {
|
||||||
|
await webSocket.CloseAsync (WebSocketCloseStatus.NormalClosure, "", token).ConfigureAwait (false);
|
||||||
|
sessionCts.Cancel ();
|
||||||
|
}
|
||||||
|
else if (receiveResult.MessageType == WebSocketMessageType.Binary) {
|
||||||
|
await webSocket.CloseAsync (WebSocketCloseStatus.InvalidMessageType, "Cannot accept binary frame", token).ConfigureAwait (false);
|
||||||
|
sessionCts.Cancel ();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var size = receiveResult.Count;
|
||||||
|
while (!receiveResult.EndOfMessage) {
|
||||||
|
if (size >= receiveBuffer.Length) {
|
||||||
|
await webSocket.CloseAsync (WebSocketCloseStatus.MessageTooBig, "Message too big", token).ConfigureAwait (false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
receiveResult = await webSocket.ReceiveAsync (new ArraySegment<byte> (receiveBuffer, size, receiveBuffer.Length - size), token).ConfigureAwait (false);
|
||||||
|
size += receiveResult.Count;
|
||||||
|
}
|
||||||
|
var receivedString = Encoding.UTF8.GetString (receiveBuffer, 0, size);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Console.WriteLine ("RECEIVED: {0}", receivedString);
|
||||||
|
var message = Newtonsoft.Json.JsonConvert.DeserializeObject<Message> (receivedString);
|
||||||
|
element.Receive (message);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Error ("Failed to process received message", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
element.MessageSent -= handleElementMessageSent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void QueueMessage (Message message)
|
||||||
|
{
|
||||||
|
base.QueueMessage (message);
|
||||||
|
sendThrottle.Enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async void TransmitQueuedMessages ()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
//
|
||||||
|
// Dequeue as many messages as we can
|
||||||
|
//
|
||||||
|
var messagesToSend = new List<Message> ();
|
||||||
|
System.Runtime.CompilerServices.ConfiguredTaskAwaitable task;
|
||||||
|
lock (queuedMessages) {
|
||||||
|
messagesToSend.AddRange (queuedMessages);
|
||||||
|
queuedMessages.Clear ();
|
||||||
|
|
||||||
|
if (messagesToSend.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Now actually send this message
|
||||||
|
// Do this while locked to make sure SendAsync is called in the right order
|
||||||
|
//
|
||||||
|
var json = Newtonsoft.Json.JsonConvert.SerializeObject (messagesToSend);
|
||||||
|
var outputBuffer = new ArraySegment<byte> (Encoding.UTF8.GetBytes (json));
|
||||||
|
//Console.WriteLine ("TRANSMIT " + json);
|
||||||
|
task = webSocket.SendAsync (outputBuffer, WebSocketMessageType.Text, true, token).ConfigureAwait (false);
|
||||||
|
}
|
||||||
|
await task;
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
Error ("Failed to send queued messages, aborting session", ex);
|
||||||
|
element.MessageSent -= handleElementMessageSent;
|
||||||
|
sessionCts.Cancel ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
README.md
30
README.md
|
@ -4,7 +4,8 @@
|
||||||
| ------- | ------- | ----------- |
|
| ------- | ------- | ----------- |
|
||||||
| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.svg)](https://www.nuget.org/packages/Ooui) | [Ooui](https://www.nuget.org/packages/Ooui) | Core library with HTML elements and a server |
|
| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.svg)](https://www.nuget.org/packages/Ooui) | [Ooui](https://www.nuget.org/packages/Ooui) | Core library with HTML elements and a server |
|
||||||
| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.AspNetCore.svg)](https://www.nuget.org/packages/Ooui.AspNetCore) | [Ooui.AspNetCore](https://www.nuget.org/packages/Ooui.AspNetCore) | Integration with ASP.NET Core |
|
| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.AspNetCore.svg)](https://www.nuget.org/packages/Ooui.AspNetCore) | [Ooui.AspNetCore](https://www.nuget.org/packages/Ooui.AspNetCore) | Integration with ASP.NET Core |
|
||||||
| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.Forms.svg)](https://www.nuget.org/packages/Ooui.Forms) | [Ooui.Forms](https://www.nuget.org/packages/Ooui.Forms) | Xamarin.Forms backend using Ooui |
|
| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.Forms.svg)](https://www.nuget.org/packages/Ooui.Forms) | [Ooui.Forms](https://www.nuget.org/packages/Ooui.Forms) | Xamarin.Forms backend using Ooui ([Status](Documentation/OouiFormsStatus.md)) |
|
||||||
|
| [![NuGet Package](https://img.shields.io/nuget/v/Ooui.Wasm.svg)](https://www.nuget.org/packages/Ooui.Wasm) | [Ooui.Wasm](https://www.nuget.org/packages/Ooui.Wasm) | Package your app into a web assembly |
|
||||||
|
|
||||||
Ooui (pronounced *weee!*) is a small cross-platform UI library for .NET that uses web technologies.
|
Ooui (pronounced *weee!*) is a small cross-platform UI library for .NET that uses web technologies.
|
||||||
|
|
||||||
|
@ -15,6 +16,8 @@ It presents a classic object-oriented UI API that controls a dumb browser. With
|
||||||
|
|
||||||
Head on over to [http://ooui.mecha.parts](http://ooui.mecha.parts) to tryout the samples.
|
Head on over to [http://ooui.mecha.parts](http://ooui.mecha.parts) to tryout the samples.
|
||||||
|
|
||||||
|
You can also load [https://s3.amazonaws.com/praeclarum.org/wasm/index.html](https://s3.amazonaws.com/praeclarum.org/wasm/index.html) to try the WebAssembly mode of Ooui running Xamarin.Forms. (That's Xamarin.Forms running right in your browser!)
|
||||||
|
|
||||||
|
|
||||||
## Try the Samples Locally
|
## Try the Samples Locally
|
||||||
|
|
||||||
|
@ -57,7 +60,7 @@ class Program
|
||||||
};
|
};
|
||||||
|
|
||||||
// Publishing makes an object available at a given URL
|
// Publishing makes an object available at a given URL
|
||||||
// The user should be directed to http://localhost:8080/button
|
// The user should be directed to http://localhost:8080/shared-button
|
||||||
UI.Publish ("/shared-button", button);
|
UI.Publish ("/shared-button", button);
|
||||||
|
|
||||||
// Don't exit the app until someone hits return
|
// Don't exit the app until someone hits return
|
||||||
|
@ -82,19 +85,30 @@ With just that code, a web server that serves the HTML and web socket logic nece
|
||||||
Ooui has been broken up into several packages to increase the variety of ways that it can be used. Here are some combinations to help you decide which way is best for you.
|
Ooui has been broken up into several packages to increase the variety of ways that it can be used. Here are some combinations to help you decide which way is best for you.
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Ooui</th><th>Ooui.AspNetCore</th><th>Ooui.Forms</th><th></th></tr></thead>
|
<thead><tr><th>Ooui</th><th>Ooui.AspNetCore</th><th>Ooui.Forms</th><th>Ooui.Wasm</th><th></th></tr></thead>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>✓</td><td></td><td></td><td><a href="https://github.com/praeclarum/Ooui/wiki/Write-the-UI-using-the-web-DOM-and-use-the-built-in-web-server">Write the UI using the web DOM and use the built-in web server</a></td>
|
<td>✓</td><td></td><td></td><td></td><td><a href="https://github.com/praeclarum/Ooui/wiki/Web-DOM-with-the-Built-in-Web-Server">Web DOM with the Built-in Web Server</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>✓</td><td>✓</td><td></td><td>Write the UI using the web DOM and serve it with ASP.NET Core</td>
|
<td>✓</td><td>✓</td><td></td><td></td><td>Web DOM with ASP.NET Core</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>✓</td><td>✓</td><td>✓</td><td>Write the UI using Xamarin.Forms and serve it with ASP.NET Core</td>
|
<td>✓</td><td>✓</td><td>✓</td><td></td><td>Xamarin.Forms with ASP.NET Core</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>✓</td><td></td><td>✓</td><td>Write the UI using Xamarin.Forms and use the built-in web server</td>
|
<td>✓</td><td></td><td>✓</td><td></td><td>Xamarin.Forms with the built-in web server</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>✓</td><td></td><td></td><td>✓</td><td><a href="https://github.com/praeclarum/Ooui/wiki/Web DOM-with-Web-Assembly">Web DOM with Web Assembly</a></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>✓</td><td></td><td>✓</td><td>✓</td><td><a href="https://github.com/praeclarum/Ooui/wiki/Xamarin.Forms-with-Web-Assembly">Xamarin.Forms with Web Assembly</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
@ -106,6 +120,8 @@ When the user requests a page, the page will connect to the server using a web s
|
||||||
|
|
||||||
When the user clicks or otherwise interacts with the UI, those events are sent back over the web socket so that your code can deal with them.
|
When the user clicks or otherwise interacts with the UI, those events are sent back over the web socket so that your code can deal with them.
|
||||||
|
|
||||||
|
In the case of web assembly, this same dataflow takes place. However, sockets are not used as all communication is done locally in the browser process.
|
||||||
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
@ -32,9 +32,6 @@
|
||||||
<Compile Update="DisplayAlertPage.xaml.cs">
|
<Compile Update="DisplayAlertPage.xaml.cs">
|
||||||
<DependentUpon>DisplayAlertPage.xaml</DependentUpon>
|
<DependentUpon>DisplayAlertPage.xaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Update="XamlPreviewPage.xaml.cs">
|
|
||||||
<DependentUpon>XamlPreviewPage.xaml</DependentUpon>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -56,9 +53,6 @@
|
||||||
<EmbeddedResource Update="WeatherApp\WeatherPage.xaml">
|
<EmbeddedResource Update="WeatherApp\WeatherPage.xaml">
|
||||||
<Generator>MSBuild:Compile</Generator>
|
<Generator>MSBuild:Compile</Generator>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<EmbeddedResource Update="XamlPreviewPage.xaml">
|
|
||||||
<Generator>MSBuild:UpdateDesignTimeXaml</Generator>
|
|
||||||
</EmbeddedResource>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Samples.XamlPreviewPage">
|
|
||||||
<ContentPage.Content>
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<StackLayout Orientation="Vertical" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
|
|
||||||
<Label Text="Xamarin.Forms XAML Editor" FontSize="24" FontAttributes="Bold" Margin="8,8,8,0" />
|
|
||||||
<Label Text="Edit the XAML below to see a live preview on the right" Margin="8,0,8,8" />
|
|
||||||
</StackLayout>
|
|
||||||
<Editor x:Name="editor" FontFamily="monospace" FontSize="12" Grid.Row="1" Grid.Column="0" />
|
|
||||||
<ContentView x:Name="results" Grid.Row="1" Grid.Column="1" BackgroundColor="White" />
|
|
||||||
</Grid>
|
|
||||||
</ContentPage.Content>
|
|
||||||
</ContentPage>
|
|
|
@ -1,72 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using Xamarin.Forms;
|
|
||||||
|
|
||||||
namespace Samples
|
|
||||||
{
|
|
||||||
public partial class XamlPreviewPage : ContentPage
|
|
||||||
{
|
|
||||||
public XamlPreviewPage ()
|
|
||||||
{
|
|
||||||
InitializeComponent ();
|
|
||||||
|
|
||||||
editor.Text = @"<ContentView
|
|
||||||
xmlns=""http://xamarin.com/schemas/2014/forms""
|
|
||||||
xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml"">
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height=""*"" />
|
|
||||||
<RowDefinition Height=""*"" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width=""*"" />
|
|
||||||
<ColumnDefinition Width=""*"" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<StackLayout Grid.Row=""0"" Grid.Column=""0"">
|
|
||||||
<Label Text=""Top Left"" />
|
|
||||||
<Entry Placeholder=""I'm ready for some text"" />
|
|
||||||
<Button Text=""I'm a button, but I don't do anything"" />
|
|
||||||
</StackLayout>
|
|
||||||
<Label Text=""Top Right"" Grid.Row=""0"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#c5000b"" />
|
|
||||||
<Label Text=""Bottom Left"" Grid.Row=""1"" Grid.Column=""0"" TextColor=""Black"" BackgroundColor=""#ffd320"" />
|
|
||||||
<Label Text=""Bottom Right"" Grid.Row=""1"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#008000"" />
|
|
||||||
</Grid>
|
|
||||||
</ContentView>";
|
|
||||||
editor.TextChanged += (sender, e) => DisplayXaml ();
|
|
||||||
DisplayXaml ();
|
|
||||||
}
|
|
||||||
|
|
||||||
CancellationTokenSource lastCts = null;
|
|
||||||
|
|
||||||
public void DisplayXaml ()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
var cts = new CancellationTokenSource ();
|
|
||||||
var token = cts.Token;
|
|
||||||
lastCts?.Cancel ();
|
|
||||||
lastCts = cts;
|
|
||||||
|
|
||||||
var asm = typeof (Xamarin.Forms.Xaml.Internals.XamlTypeResolver).Assembly;
|
|
||||||
var xamlLoaderType = asm.GetType ("Xamarin.Forms.Xaml.XamlLoader");
|
|
||||||
var loadArgTypes = new[] { typeof (object), typeof (string) };
|
|
||||||
var loadMethod = xamlLoaderType.GetMethod ("Load", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public, null, System.Reflection.CallingConventions.Any, loadArgTypes, null);
|
|
||||||
var contentView = new ContentView ();
|
|
||||||
loadMethod.Invoke (null, new object[] { contentView, editor.Text });
|
|
||||||
|
|
||||||
if (!token.IsCancellationRequested) {
|
|
||||||
results.Content = contentView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) {
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
results.Content = new Label {
|
|
||||||
TextColor = Color.DarkRed,
|
|
||||||
FontSize = 12,
|
|
||||||
Text = ex.ToString (),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading;
|
||||||
using Xamarin.Forms;
|
using Xamarin.Forms;
|
||||||
|
using Xamarin.Forms.Xaml;
|
||||||
|
|
||||||
namespace Samples
|
namespace Samples
|
||||||
{
|
{
|
||||||
|
@ -10,8 +11,108 @@ namespace Samples
|
||||||
|
|
||||||
public Ooui.Element CreateElement ()
|
public Ooui.Element CreateElement ()
|
||||||
{
|
{
|
||||||
var page = new XamlPreviewPage ();
|
var page = new XamlEditorPage ();
|
||||||
return page.GetOouiElement ();
|
return page.GetOouiElement ();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public partial class XamlEditorPage : ContentPage
|
||||||
|
{
|
||||||
|
Editor editor;
|
||||||
|
ContentView results;
|
||||||
|
|
||||||
|
public XamlEditorPage ()
|
||||||
|
{
|
||||||
|
InitializeComponent ();
|
||||||
|
|
||||||
|
editor.Text = @"<ContentView
|
||||||
|
xmlns=""http://xamarin.com/schemas/2014/forms""
|
||||||
|
xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml"">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height=""*"" />
|
||||||
|
<RowDefinition Height=""*"" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width=""*"" />
|
||||||
|
<ColumnDefinition Width=""*"" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackLayout Grid.Row=""0"" Grid.Column=""0"">
|
||||||
|
<Label Text=""Top Left"" />
|
||||||
|
<Entry Placeholder=""I'm ready for some text"" />
|
||||||
|
<Button Text=""I'm a button, but I don't do anything"" />
|
||||||
|
</StackLayout>
|
||||||
|
<Label Text=""Top Right"" Grid.Row=""0"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#c5000b"" />
|
||||||
|
<Label Text=""Bottom Left"" Grid.Row=""1"" Grid.Column=""0"" TextColor=""Black"" BackgroundColor=""#ffd320"" />
|
||||||
|
<Label Text=""Bottom Right"" Grid.Row=""1"" Grid.Column=""1"" TextColor=""White"" BackgroundColor=""#008000"" />
|
||||||
|
</Grid>
|
||||||
|
</ContentView>";
|
||||||
|
editor.TextChanged += (sender, e) => DisplayXaml ();
|
||||||
|
DisplayXaml ();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InitializeComponent ()
|
||||||
|
{
|
||||||
|
var grid = new Grid ();
|
||||||
|
|
||||||
|
grid.RowDefinitions.Add (new RowDefinition { Height = GridLength.Auto });
|
||||||
|
grid.RowDefinitions.Add (new RowDefinition { Height = GridLength.Star });
|
||||||
|
grid.ColumnDefinitions.Add (new ColumnDefinition { Width = GridLength.Star });
|
||||||
|
grid.ColumnDefinitions.Add (new ColumnDefinition { Width = GridLength.Star });
|
||||||
|
|
||||||
|
editor = new Editor {
|
||||||
|
FontSize = 12,
|
||||||
|
FontFamily = "monospace",
|
||||||
|
};
|
||||||
|
editor.SetValue (Grid.ColumnProperty, 0);
|
||||||
|
editor.SetValue (Grid.RowProperty, 1);
|
||||||
|
|
||||||
|
results = new ContentView ();
|
||||||
|
results.SetValue (Grid.ColumnProperty, 1);
|
||||||
|
results.SetValue (Grid.RowProperty, 1);
|
||||||
|
|
||||||
|
var title = new Label {
|
||||||
|
Text = "XAML Editor",
|
||||||
|
FontSize = 24,
|
||||||
|
FontAttributes = FontAttributes.Bold,
|
||||||
|
Margin = new Thickness (8),
|
||||||
|
};
|
||||||
|
title.SetValue (Grid.ColumnProperty, 0);
|
||||||
|
title.SetValue (Grid.RowProperty, 0);
|
||||||
|
|
||||||
|
grid.Children.Add (title);
|
||||||
|
grid.Children.Add (editor);
|
||||||
|
grid.Children.Add (results);
|
||||||
|
|
||||||
|
Content = grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
CancellationTokenSource lastCts = null;
|
||||||
|
|
||||||
|
public void DisplayXaml ()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
var cts = new CancellationTokenSource ();
|
||||||
|
var token = cts.Token;
|
||||||
|
lastCts?.Cancel ();
|
||||||
|
lastCts = cts;
|
||||||
|
|
||||||
|
var contentView = new ContentView ();
|
||||||
|
contentView.LoadFromXaml (editor.Text);
|
||||||
|
|
||||||
|
if (!token.IsCancellationRequested) {
|
||||||
|
results.Content = contentView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) {
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
results.Content = new Label {
|
||||||
|
TextColor = Color.DarkRed,
|
||||||
|
FontSize = 12,
|
||||||
|
Text = ex.ToString (),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System;
|
||||||
|
|
||||||
|
#if NUNIT
|
||||||
|
using NUnit.Framework;
|
||||||
|
using TestClassAttribute = NUnit.Framework.TestFixtureAttribute;
|
||||||
|
using TestMethodAttribute = NUnit.Framework.TestCaseAttribute;
|
||||||
|
#else
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using Ooui;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Tests
|
||||||
|
{
|
||||||
|
[TestClass]
|
||||||
|
public class JsonTests
|
||||||
|
{
|
||||||
|
static readonly Regex noid = new Regex ("⦙\\d+");
|
||||||
|
static string NoId (string s)
|
||||||
|
{
|
||||||
|
return noid.Replace (s, "⦙");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ButtonIndividualMessages ()
|
||||||
|
{
|
||||||
|
var b = new Button ();
|
||||||
|
b.Text = "Hello";
|
||||||
|
b.Click += (sender, e) => { };
|
||||||
|
b.Title = "\"Quote\"";
|
||||||
|
Assert.AreEqual ("{\"m\":\"create\",\"id\":\"⦙\",\"k\":\"button\"}", NoId (b.StateMessages[0].ToJson ()));
|
||||||
|
Assert.AreEqual ("{\"m\":\"call\",\"id\":\"⦙\",\"k\":\"insertBefore\",\"v\":[\"⦙\",null]}", NoId (b.StateMessages[1].ToJson ()));
|
||||||
|
Assert.AreEqual ("{\"m\":\"listen\",\"id\":\"⦙\",\"k\":\"click\"}", NoId (b.StateMessages[2].ToJson ()));
|
||||||
|
Assert.AreEqual ("{\"m\":\"setAttr\",\"id\":\"⦙\",\"k\":\"title\",\"v\":\"\\\"Quote\\\"\"}", NoId (b.StateMessages[3].ToJson ()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ButtonWriteMessages ()
|
||||||
|
{
|
||||||
|
var b = new Button ();
|
||||||
|
b.Text = "Hello";
|
||||||
|
b.Click += (sender, e) => { };
|
||||||
|
var sw = new StringWriter ();
|
||||||
|
foreach (var m in b.StateMessages) {
|
||||||
|
m.WriteJson (sw);
|
||||||
|
}
|
||||||
|
Assert.AreEqual ("{\"m\":\"create\",\"id\":\"⦙\",\"k\":\"button\"}" +
|
||||||
|
"{\"m\":\"call\",\"id\":\"⦙\",\"k\":\"insertBefore\",\"v\":[\"⦙\",null]}" +
|
||||||
|
"{\"m\":\"listen\",\"id\":\"⦙\",\"k\":\"click\"}", NoId (sw.ToString ()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue