Implement ImageRenderer (fixes #44)
This commit is contained in:
parent
8a27f336bd
commit
4573596464
|
@ -25,10 +25,13 @@ namespace Microsoft.AspNetCore.Builder
|
|||
};
|
||||
app.UseWebSockets (webSocketOptions);
|
||||
|
||||
Ooui.UI.ServerEnabled = false;
|
||||
|
||||
app.Use (async (context, next) =>
|
||||
{
|
||||
var response = context.Response;
|
||||
|
||||
if (context.Request.Path == jsPath) {
|
||||
var response = context.Response;
|
||||
var clientJsBytes = Ooui.UI.ClientJsBytes;
|
||||
var clientJsEtag = Ooui.UI.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;
|
||||
}
|
||||
}
|
||||
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 {
|
||||
await next ().ConfigureAwait (false);
|
||||
}
|
||||
|
|
|
@ -12,9 +12,13 @@ using Xamarin.Forms.Internals;
|
|||
[assembly: ExportRenderer (typeof (Editor), typeof (EditorRenderer))]
|
||||
[assembly: ExportRenderer (typeof (Entry), typeof (EntryRenderer))]
|
||||
[assembly: ExportRenderer (typeof (Frame), typeof (FrameRenderer))]
|
||||
[assembly: ExportRenderer (typeof (Image), typeof (ImageRenderer))]
|
||||
[assembly: ExportRenderer (typeof (Label), typeof (LabelRenderer))]
|
||||
[assembly: ExportRenderer (typeof (ProgressBar), typeof (ProgressBarRenderer))]
|
||||
[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
|
||||
{
|
||||
|
@ -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[] {
|
||||
typeof(ExportRendererAttribute),
|
||||
//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 () ?? "");
|
||||
}
|
||||
}
|
||||
}
|
88
Ooui/UI.cs
88
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 ()
|
||||
{
|
||||
|
@ -91,7 +104,7 @@ namespace Ooui
|
|||
clientJsEtag = "\"" + Hash (clientJsBytes) + "\"";
|
||||
}
|
||||
|
||||
static string Hash (byte[] bytes)
|
||||
public static string Hash (byte[] bytes)
|
||||
{
|
||||
var sha = sha256;
|
||||
if (sha == null) {
|
||||
|
@ -135,7 +148,47 @@ namespace Ooui
|
|||
if (contentType == null) {
|
||||
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)
|
||||
|
@ -146,7 +199,8 @@ namespace Ooui
|
|||
public static void PublishJson (string path, object 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)
|
||||
|
@ -178,6 +232,7 @@ namespace Ooui
|
|||
|
||||
static void Start ()
|
||||
{
|
||||
if (!serverEnabled) return;
|
||||
if (serverCts != null) return;
|
||||
serverCts = new CancellationTokenSource ();
|
||||
var token = serverCts.Token;
|
||||
|
@ -343,11 +398,17 @@ namespace Ooui
|
|||
class DataHandler : RequestHandler
|
||||
{
|
||||
readonly byte[] data;
|
||||
readonly string etag;
|
||||
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.etag = etag;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
|
@ -357,13 +418,20 @@ namespace Ooui
|
|||
var path = url.LocalPath;
|
||||
var response = listenerContext.Response;
|
||||
|
||||
response.StatusCode = 200;
|
||||
if (!string.IsNullOrEmpty (contentType))
|
||||
response.ContentType = contentType;
|
||||
response.ContentLength64 = data.LongLength;
|
||||
var inm = listenerContext.Request.Headers.Get ("If-None-Match");
|
||||
if (!string.IsNullOrEmpty (inm) && inm == etag) {
|
||||
response.StatusCode = 304;
|
||||
}
|
||||
else {
|
||||
response.StatusCode = 200;
|
||||
response.AddHeader ("Etag", etag);
|
||||
if (!string.IsNullOrEmpty (contentType))
|
||||
response.ContentType = contentType;
|
||||
response.ContentLength64 = data.LongLength;
|
||||
|
||||
using (var s = response.OutputStream) {
|
||||
s.Write (data, 0, data.Length);
|
||||
using (var s = response.OutputStream) {
|
||||
s.Write (data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
response.Close ();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue