Implement ImageRenderer (fixes #44)
This commit is contained in:
parent
8a27f336bd
commit
4573596464
|
@ -25,10 +25,13 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
};
|
};
|
||||||
app.UseWebSockets (webSocketOptions);
|
app.UseWebSockets (webSocketOptions);
|
||||||
|
|
||||||
|
Ooui.UI.ServerEnabled = false;
|
||||||
|
|
||||||
app.Use (async (context, next) =>
|
app.Use (async (context, next) =>
|
||||||
{
|
{
|
||||||
if (context.Request.Path == jsPath) {
|
|
||||||
var response = context.Response;
|
var response = context.Response;
|
||||||
|
|
||||||
|
if (context.Request.Path == jsPath) {
|
||||||
var clientJsBytes = Ooui.UI.ClientJsBytes;
|
var clientJsBytes = Ooui.UI.ClientJsBytes;
|
||||||
var clientJsEtag = Ooui.UI.ClientJsEtag;
|
var clientJsEtag = Ooui.UI.ClientJsEtag;
|
||||||
if (context.Request.Headers.TryGetValue ("If-None-Match", out var inms) && inms.Count > 0 && inms[0] == clientJsEtag) {
|
if (context.Request.Headers.TryGetValue ("If-None-Match", out var inms) && inms.Count > 0 && inms[0] == clientJsEtag) {
|
||||||
|
@ -53,6 +56,21 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
context.Response.StatusCode = 400;
|
context.Response.StatusCode = 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (Ooui.UI.TryGetFileContentAtPath (context.Request.Path, out var file)) {
|
||||||
|
if (context.Request.Headers.TryGetValue ("If-None-Match", out var inms) && inms.Count > 0 && inms[0] == file.Etag) {
|
||||||
|
response.StatusCode = 304;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
response.StatusCode = 200;
|
||||||
|
response.ContentLength = file.Content.Length;
|
||||||
|
response.ContentType = file.ContentType;
|
||||||
|
response.Headers.Add ("Cache-Control", "public, max-age=60");
|
||||||
|
response.Headers.Add ("Etag", file.Etag);
|
||||||
|
using (var s = response.Body) {
|
||||||
|
await s.WriteAsync (file.Content, 0, file.Content.Length).ConfigureAwait (false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
await next ().ConfigureAwait (false);
|
await next ().ConfigureAwait (false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,13 @@ using Xamarin.Forms.Internals;
|
||||||
[assembly: ExportRenderer (typeof (Editor), typeof (EditorRenderer))]
|
[assembly: ExportRenderer (typeof (Editor), typeof (EditorRenderer))]
|
||||||
[assembly: ExportRenderer (typeof (Entry), typeof (EntryRenderer))]
|
[assembly: ExportRenderer (typeof (Entry), typeof (EntryRenderer))]
|
||||||
[assembly: ExportRenderer (typeof (Frame), typeof (FrameRenderer))]
|
[assembly: ExportRenderer (typeof (Frame), typeof (FrameRenderer))]
|
||||||
|
[assembly: ExportRenderer (typeof (Image), typeof (ImageRenderer))]
|
||||||
[assembly: ExportRenderer (typeof (Label), typeof (LabelRenderer))]
|
[assembly: ExportRenderer (typeof (Label), typeof (LabelRenderer))]
|
||||||
[assembly: ExportRenderer (typeof (ProgressBar), typeof (ProgressBarRenderer))]
|
[assembly: ExportRenderer (typeof (ProgressBar), typeof (ProgressBarRenderer))]
|
||||||
[assembly: ExportRenderer (typeof (Switch), typeof (SwitchRenderer))]
|
[assembly: ExportRenderer (typeof (Switch), typeof (SwitchRenderer))]
|
||||||
|
[assembly: ExportImageSourceHandler (typeof (FileImageSource), typeof (FileImageSourceHandler))]
|
||||||
|
[assembly: ExportImageSourceHandler (typeof (StreamImageSource), typeof (StreamImagesourceHandler))]
|
||||||
|
[assembly: ExportImageSourceHandler (typeof (UriImageSource), typeof (ImageLoaderSourceHandler))]
|
||||||
|
|
||||||
namespace Ooui.Forms
|
namespace Ooui.Forms
|
||||||
{
|
{
|
||||||
|
@ -26,4 +30,13 @@ namespace Ooui.Forms
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
|
||||||
|
public sealed class ExportImageSourceHandlerAttribute : HandlerAttribute
|
||||||
|
{
|
||||||
|
public ExportImageSourceHandlerAttribute (Type handler, Type target)
|
||||||
|
: base (handler, target)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ namespace Xamarin.Forms
|
||||||
Registrar.RegisterAll (new[] {
|
Registrar.RegisterAll (new[] {
|
||||||
typeof(ExportRendererAttribute),
|
typeof(ExportRendererAttribute),
|
||||||
//typeof(ExportCellAttribute),
|
//typeof(ExportCellAttribute),
|
||||||
//typeof(ExportImageSourceHandlerAttribute),
|
typeof(ExportImageSourceHandlerAttribute),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xamarin.Forms;
|
||||||
|
|
||||||
|
namespace Ooui.Forms.Renderers
|
||||||
|
{
|
||||||
|
public class ImageRenderer : ViewRenderer<Xamarin.Forms.Image, Ooui.Image>
|
||||||
|
{
|
||||||
|
bool _isDisposed;
|
||||||
|
|
||||||
|
protected override void Dispose (bool disposing)
|
||||||
|
{
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (disposing) {
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
|
||||||
|
base.Dispose (disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnElementChanged (ElementChangedEventArgs<Xamarin.Forms.Image> e)
|
||||||
|
{
|
||||||
|
if (Control == null) {
|
||||||
|
var imageView = new Ooui.Image ();
|
||||||
|
SetNativeControl (imageView);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.NewElement != null) {
|
||||||
|
SetAspect ();
|
||||||
|
await TrySetImage (e.OldElement);
|
||||||
|
SetOpacity ();
|
||||||
|
}
|
||||||
|
|
||||||
|
base.OnElementChanged (e);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async void OnElementPropertyChanged (object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnElementPropertyChanged (sender, e);
|
||||||
|
if (e.PropertyName == Xamarin.Forms.Image.SourceProperty.PropertyName)
|
||||||
|
await TrySetImage ();
|
||||||
|
else if (e.PropertyName == Xamarin.Forms.Image.IsOpaqueProperty.PropertyName)
|
||||||
|
SetOpacity ();
|
||||||
|
else if (e.PropertyName == Xamarin.Forms.Image.AspectProperty.PropertyName)
|
||||||
|
SetAspect ();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetAspect ()
|
||||||
|
{
|
||||||
|
if (_isDisposed || Element == null || Control == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual async Task TrySetImage (Xamarin.Forms.Image previous = null)
|
||||||
|
{
|
||||||
|
// By default we'll just catch and log any exceptions thrown by SetImage so they don't bring down
|
||||||
|
// the application; a custom renderer can override this method and handle exceptions from
|
||||||
|
// SetImage differently if it wants to
|
||||||
|
|
||||||
|
try {
|
||||||
|
await SetImage (previous).ConfigureAwait (false);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
System.Diagnostics.Debug.WriteLine ("Error loading image: {0}", ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
((IImageController)Element)?.SetIsLoading (false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task SetImage (Xamarin.Forms.Image oldElement = null)
|
||||||
|
{
|
||||||
|
if (_isDisposed || Element == null || Control == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = Element.Source;
|
||||||
|
|
||||||
|
if (oldElement != null) {
|
||||||
|
var oldSource = oldElement.Source;
|
||||||
|
if (Equals (oldSource, source))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (oldSource is FileImageSource && source is FileImageSource && ((FileImageSource)oldSource).File == ((FileImageSource)source).File)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Control.Source = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
IImageSourceHandler handler;
|
||||||
|
|
||||||
|
Element.SetIsLoading (true);
|
||||||
|
|
||||||
|
if (source != null &&
|
||||||
|
(handler = Xamarin.Forms.Internals.Registrar.Registered.GetHandler<IImageSourceHandler> (source.GetType ())) != null) {
|
||||||
|
string uiimage;
|
||||||
|
try {
|
||||||
|
uiimage = await handler.LoadImageAsync (source, scale: 1.0f);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) {
|
||||||
|
uiimage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var imageView = Control;
|
||||||
|
if (imageView != null)
|
||||||
|
imageView.Source = uiimage;
|
||||||
|
|
||||||
|
((IVisualElementController)Element).NativeSizeChanged ();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Control.Source = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
Element.SetIsLoading (false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetOpacity ()
|
||||||
|
{
|
||||||
|
if (_isDisposed || Element == null || Control == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IImageSourceHandler : IRegisterable
|
||||||
|
{
|
||||||
|
Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class FileImageSourceHandler : IImageSourceHandler
|
||||||
|
{
|
||||||
|
public async Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
|
||||||
|
{
|
||||||
|
string image = null;
|
||||||
|
var filesource = imagesource as FileImageSource;
|
||||||
|
var file = filesource?.File;
|
||||||
|
if (!string.IsNullOrEmpty (file)) {
|
||||||
|
var name = System.IO.Path.GetFileName (file);
|
||||||
|
image = "/images/" + name;
|
||||||
|
if (Ooui.UI.TryGetFileContentAtPath (image, out var f)) {
|
||||||
|
// Already published
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await Task.Run (() => Ooui.UI.PublishFile (image, file), cancelationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class StreamImagesourceHandler : IImageSourceHandler
|
||||||
|
{
|
||||||
|
public async Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
|
||||||
|
{
|
||||||
|
string image = null;
|
||||||
|
var streamsource = imagesource as StreamImageSource;
|
||||||
|
if (streamsource?.Stream != null) {
|
||||||
|
using (var streamImage = await ((IStreamImageSource)streamsource).GetStreamAsync (cancelationToken).ConfigureAwait (false)) {
|
||||||
|
if (streamImage != null) {
|
||||||
|
var data = new byte[streamImage.Length];
|
||||||
|
using (var outputStream = new System.IO.MemoryStream (data)) {
|
||||||
|
await streamImage.CopyToAsync (outputStream, 4096, cancelationToken).ConfigureAwait (false);
|
||||||
|
}
|
||||||
|
var hash = Ooui.UI.Hash (data);
|
||||||
|
var etag = "\"" + hash + "\"";
|
||||||
|
image = "/images/" + hash;
|
||||||
|
if (Ooui.UI.TryGetFileContentAtPath (image, out var file) && file.Etag == etag) {
|
||||||
|
// Already published
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Ooui.UI.PublishFile (image, data, etag, "image");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image == null) {
|
||||||
|
System.Diagnostics.Debug.WriteLine ("Could not load image: {0}", streamsource);
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class ImageLoaderSourceHandler : IImageSourceHandler
|
||||||
|
{
|
||||||
|
public Task<string> LoadImageAsync (ImageSource imagesource, CancellationToken cancelationToken = default (CancellationToken), float scale = 1f)
|
||||||
|
{
|
||||||
|
var imageLoader = imagesource as UriImageSource;
|
||||||
|
return Task.FromResult (imageLoader?.Uri.ToString () ?? "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
Ooui/UI.cs
76
Ooui/UI.cs
|
@ -73,6 +73,19 @@ namespace Ooui
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
static bool serverEnabled = true;
|
||||||
|
public static bool ServerEnabled {
|
||||||
|
get => serverEnabled;
|
||||||
|
set {
|
||||||
|
if (serverEnabled != value) {
|
||||||
|
serverEnabled = value;
|
||||||
|
if (serverEnabled)
|
||||||
|
Restart ();
|
||||||
|
else
|
||||||
|
Stop ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static UI ()
|
static UI ()
|
||||||
{
|
{
|
||||||
|
@ -91,7 +104,7 @@ namespace Ooui
|
||||||
clientJsEtag = "\"" + Hash (clientJsBytes) + "\"";
|
clientJsEtag = "\"" + Hash (clientJsBytes) + "\"";
|
||||||
}
|
}
|
||||||
|
|
||||||
static string Hash (byte[] bytes)
|
public static string Hash (byte[] bytes)
|
||||||
{
|
{
|
||||||
var sha = sha256;
|
var sha = sha256;
|
||||||
if (sha == null) {
|
if (sha == null) {
|
||||||
|
@ -135,7 +148,47 @@ namespace Ooui
|
||||||
if (contentType == null) {
|
if (contentType == null) {
|
||||||
contentType = GuessContentType (path, filePath);
|
contentType = GuessContentType (path, filePath);
|
||||||
}
|
}
|
||||||
Publish (path, new DataHandler (data, contentType));
|
var etag = "\"" + Hash (data) + "\"";
|
||||||
|
Publish (path, new DataHandler (data, etag, contentType));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PublishFile (string path, byte[] data, string contentType)
|
||||||
|
{
|
||||||
|
var etag = "\"" + Hash (data) + "\"";
|
||||||
|
Publish (path, new DataHandler (data, etag, contentType));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PublishFile (string path, byte[] data, string etag, string contentType)
|
||||||
|
{
|
||||||
|
Publish (path, new DataHandler (data, etag, contentType));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetFileContentAtPath (string path, out FileContent file)
|
||||||
|
{
|
||||||
|
RequestHandler handler;
|
||||||
|
lock (publishedPaths) {
|
||||||
|
if (!publishedPaths.TryGetValue (path, out handler)) {
|
||||||
|
file = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (handler is DataHandler dh) {
|
||||||
|
file = new FileContent {
|
||||||
|
Etag = dh.Etag,
|
||||||
|
Content = dh.Data,
|
||||||
|
ContentType = dh.ContentType,
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
file = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FileContent
|
||||||
|
{
|
||||||
|
public string ContentType { get; set; }
|
||||||
|
public string Etag { get; set; }
|
||||||
|
public byte[] Content { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PublishJson (string path, Func<object> ctor)
|
public static void PublishJson (string path, Func<object> ctor)
|
||||||
|
@ -146,7 +199,8 @@ 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);
|
||||||
Publish (path, new DataHandler (data, JsonHandler.ContentType));
|
var etag = "\"" + Hash (data) + "\"";
|
||||||
|
Publish (path, new DataHandler (data, etag, JsonHandler.ContentType));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PublishCustomResponse (string path, Action<HttpListenerContext, CancellationToken> responder)
|
public static void PublishCustomResponse (string path, Action<HttpListenerContext, CancellationToken> responder)
|
||||||
|
@ -178,6 +232,7 @@ namespace Ooui
|
||||||
|
|
||||||
static void Start ()
|
static void Start ()
|
||||||
{
|
{
|
||||||
|
if (!serverEnabled) return;
|
||||||
if (serverCts != null) return;
|
if (serverCts != null) return;
|
||||||
serverCts = new CancellationTokenSource ();
|
serverCts = new CancellationTokenSource ();
|
||||||
var token = serverCts.Token;
|
var token = serverCts.Token;
|
||||||
|
@ -343,11 +398,17 @@ namespace Ooui
|
||||||
class DataHandler : RequestHandler
|
class DataHandler : RequestHandler
|
||||||
{
|
{
|
||||||
readonly byte[] data;
|
readonly byte[] data;
|
||||||
|
readonly string etag;
|
||||||
readonly string contentType;
|
readonly string contentType;
|
||||||
|
|
||||||
public DataHandler (byte[] data, string contentType = null)
|
public byte[] Data => data;
|
||||||
|
public string Etag => etag;
|
||||||
|
public string ContentType => contentType;
|
||||||
|
|
||||||
|
public DataHandler (byte[] data, string etag, string contentType = null)
|
||||||
{
|
{
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
this.etag = etag;
|
||||||
this.contentType = contentType;
|
this.contentType = contentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -357,7 +418,13 @@ namespace Ooui
|
||||||
var path = url.LocalPath;
|
var path = url.LocalPath;
|
||||||
var response = listenerContext.Response;
|
var response = listenerContext.Response;
|
||||||
|
|
||||||
|
var inm = listenerContext.Request.Headers.Get ("If-None-Match");
|
||||||
|
if (!string.IsNullOrEmpty (inm) && inm == etag) {
|
||||||
|
response.StatusCode = 304;
|
||||||
|
}
|
||||||
|
else {
|
||||||
response.StatusCode = 200;
|
response.StatusCode = 200;
|
||||||
|
response.AddHeader ("Etag", etag);
|
||||||
if (!string.IsNullOrEmpty (contentType))
|
if (!string.IsNullOrEmpty (contentType))
|
||||||
response.ContentType = contentType;
|
response.ContentType = contentType;
|
||||||
response.ContentLength64 = data.LongLength;
|
response.ContentLength64 = data.LongLength;
|
||||||
|
@ -365,6 +432,7 @@ namespace Ooui
|
||||||
using (var s = response.OutputStream) {
|
using (var s = response.OutputStream) {
|
||||||
s.Write (data, 0, data.Length);
|
s.Write (data, 0, data.Length);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
response.Close ();
|
response.Close ();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue