110 gravatar image source (#531)
* Initial implementation. * Added handler implementation and AppBuilder extension. * Removed unnecessary csproj additions made by VS * Moved default image enum to Primatives folder. * removed unnecessary create and await task. * Use Math.Clamp instead of Switch. * Return Stream.Null instead of Null. * Removed pragma * Made SourceService use base or UriImageSourceService. * Added unit tests * Removed GravatarImageSourceService Mapped to use UriImageSourceService Use IUriImageSource instead of IStreamImageSource Request Gravatars image if no EmailProperty Updated and added unit tests to handle gravatars image * Wording change for samples. * Moved enum to seperate class. * Fixed spelling mistake * Removed unnecessary interfaces from core. * Removed comment * Static singleton HttpClient * ReadOnlySpan<char> instead of string?. * Removed unnecessary configuring of image sources. * Reorganised to follow Style.Cop rules. * Get size of parent without being control type specific. Unit tests for ALL controls that support ImageSource * Fix for formatting errors (an extra SPACE!?!?!) * Eliminate multiple calls with Delay and ContinueWith. * Using debug instead of ImageSourceService logger. * Replaced reflection with is VisualElement. Allow smaller than default size. * Improved accessibility for sample. * Cancellation token source timeout constant added. Tried to improve efficiency. * Moved to ImageSources folder. Moved tests to mirror folder. Added XamlNameSpace alias. * Move sample out to ImageSources section * Moved samples to ImageSources. Added ImageSources gallery. * Improved test code coverage. * Push to get a rebuild * Added tests to cover DefaultStream. * Changes IsEmpty to check email instead of Uri. Improved unit test code coverage. * Improved unit test code coverage. Added parent size invalidation, to help get size. * Initial play with parental bindings * Binding to parent Width/Height. Better size handling. Removed redundant code. * Removed nullable from BindableProperty as not required. * Test cater for parent sizing. * Improved code coverage. Removed redundant code. * Removed debug for debounce sanity checks. * Renamed tests * Swapped to Pascal case * Update src/CommunityToolkit.Maui/ImageSources/GravatarImageSource.shared.cs * Update src/CommunityToolkit.Maui/ImageSources/GravatarImageSource.shared.cs * Update src/CommunityToolkit.Maui/ImageSources/GravatarImageSource.shared.cs * Update src/CommunityToolkit.Maui/ImageSources/GravatarImageSource.shared.cs * Fixed naming rules * Removed variable and used directly in IF * Added cryptography string extension. * Updated comments to make clear size restrictions. * Changed to use unset value of -1 instead of 0. * Removed HTTP call verification debug output. * Use IsEmpty property. * Unified spaces after semicolons. * Moved conversion to after if, for efficiency. * Updated test for string with spaces after semicolons. * Added HttpClient extensions. * Update src/CommunityToolkit.Maui/Extensions/HttpClientExtensions.shares.cs Co-authored-by: Vladislav Antonyuk <33021114+VladislavAntonyuk@users.noreply.github.com> * Update src/CommunityToolkit.Maui/Extensions/HttpClientExtensions.shares.cs Co-authored-by: Vladislav Antonyuk <33021114+VladislavAntonyuk@users.noreply.github.com> * Update src/CommunityToolkit.Maui/Extensions/HttpClientExtensions.shares.cs Co-authored-by: Vladislav Antonyuk <33021114+VladislavAntonyuk@users.noreply.github.com> * Fix of bad formatted param. * Updated summary for internal GETs. * Only dispatch HTTP request if not matching previous. * Added IDisposable * Added disposable to test that disposable types are behaving as expected. * Improved code coverage testing. Renamed to IsDisposed to match standard. * Resolved issue where size to small, then to larger didn't result in the larger size being requested. * Fix for breaking merge differences AddTransient-> AddTransientWithShellRoute * Refactor async/await * Change `public` to `internal` * Ensure `SingletonHttpClient` is thread-safe * Update `ToString()` test * Update Sample App * Add `ArgumentNullException.ThrowIfNull(uri)` * `dotnet format` Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Shaun Lawrence <shaunrlawrence@gmail.com> Co-authored-by: Pedro Jesus <pedrojesus.cefet@gmail.com> Co-authored-by: Vladislav Antonyuk <33021114+VladislavAntonyuk@users.noreply.github.com>
This commit is contained in:
Родитель
328e27c581
Коммит
7908c8878c
|
@ -79,7 +79,7 @@ dotnet_diagnostic.IDE1006.severity = error
|
|||
|
||||
## Public Fields are kept Pascal Case
|
||||
dotnet_naming_symbols.public_symbols.applicable_kinds = field
|
||||
dotnet_naming_symbols.public_symbols.applicable_accessibilities = public
|
||||
dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal
|
||||
|
||||
dotnet_naming_style.first_word_upper_case_style.capitalization = first_word_upper
|
||||
|
||||
|
@ -104,7 +104,7 @@ dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
|
|||
|
||||
dotnet_naming_symbols.static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.static_fields.required_modifiers = static
|
||||
dotnet_naming_symbols.static_fields.applicable_accessibilities = internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.static_fields.applicable_accessibilities = private, protected, protected_internal, private_protected
|
||||
|
||||
dotnet_naming_style.static_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.static_field_style.required_prefix =
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
xmlns:behaviors="clr-namespace:CommunityToolkit.Maui.Sample.Pages.Behaviors"
|
||||
xmlns:converters="clr-namespace:CommunityToolkit.Maui.Sample.Pages.Converters"
|
||||
xmlns:extensions="clr-namespace:CommunityToolkit.Maui.Sample.Pages.Extensions"
|
||||
xmlns:imagesources="clr-namespace:CommunityToolkit.Maui.Sample.Pages.ImageSources"
|
||||
xmlns:layouts="clr-namespace:CommunityToolkit.Maui.Sample.Pages.Layouts"
|
||||
xmlns:views="clr-namespace:CommunityToolkit.Maui.Sample.Pages.Views"
|
||||
xmlns:pages="clr-namespace:CommunityToolkit.Maui.Sample.Pages"
|
||||
|
@ -57,6 +58,12 @@
|
|||
<ShellContent ContentTemplate="{DataTemplate extensions:ExtensionsGalleryPage}" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="ImageSources"
|
||||
Route="ImageSourcesGalleryPage"
|
||||
Icon="{OnPlatform Default='dotnet_bot.png', MacCatalyst=''}">
|
||||
<ShellContent ContentTemplate="{DataTemplate imagesources:ImageSourcesGalleryPage}" />
|
||||
</FlyoutItem>
|
||||
|
||||
<FlyoutItem Title="Layouts"
|
||||
Route="LayoutsGalleryPage"
|
||||
Icon="{OnPlatform Default='dotnet_bot.png', MacCatalyst=''}">
|
||||
|
|
|
@ -3,12 +3,14 @@ using CommunityToolkit.Maui.Sample.Pages.Alerts;
|
|||
using CommunityToolkit.Maui.Sample.Pages.Behaviors;
|
||||
using CommunityToolkit.Maui.Sample.Pages.Converters;
|
||||
using CommunityToolkit.Maui.Sample.Pages.Extensions;
|
||||
using CommunityToolkit.Maui.Sample.Pages.ImageSources;
|
||||
using CommunityToolkit.Maui.Sample.Pages.Layouts;
|
||||
using CommunityToolkit.Maui.Sample.Pages.Views;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Alerts;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Behaviors;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Converters;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.ImageSources;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Layouts;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Views;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Views.AvatarView;
|
||||
|
@ -78,6 +80,9 @@ public partial class AppShell : Shell
|
|||
// Add Extensions View Models
|
||||
CreateViewModelMapping<ColorAnimationExtensionsPage, ColorAnimationExtensionsViewModel, ExtensionsGalleryPage, ExtensionsGalleryViewModel>(),
|
||||
|
||||
// Add ImageSources View Models
|
||||
CreateViewModelMapping<GravatarImageSourcePage, GravatarImageSourceViewModel, ImageSourcesGalleryPage, ImageSourcesGalleryViewModel>(),
|
||||
|
||||
// Add Layouts View Models
|
||||
CreateViewModelMapping<UniformItemsLayoutPage, UniformItemsLayoutViewModel, LayoutsGalleryPage, LayoutsGalleryViewModel>(),
|
||||
|
||||
|
|
|
@ -5,12 +5,14 @@ using CommunityToolkit.Maui.Sample.Pages.Alerts;
|
|||
using CommunityToolkit.Maui.Sample.Pages.Behaviors;
|
||||
using CommunityToolkit.Maui.Sample.Pages.Converters;
|
||||
using CommunityToolkit.Maui.Sample.Pages.Extensions;
|
||||
using CommunityToolkit.Maui.Sample.Pages.ImageSources;
|
||||
using CommunityToolkit.Maui.Sample.Pages.Layouts;
|
||||
using CommunityToolkit.Maui.Sample.Pages.Views;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Alerts;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Behaviors;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Converters;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.ImageSources;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Layouts;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Views;
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.Views.AvatarView;
|
||||
|
@ -58,6 +60,7 @@ public static class MauiProgram
|
|||
services.AddTransient<BehaviorsGalleryPage, BehaviorsGalleryViewModel>();
|
||||
services.AddTransient<ConvertersGalleryPage, ConvertersGalleryViewModel>();
|
||||
services.AddTransient<ExtensionsGalleryPage, ExtensionsGalleryViewModel>();
|
||||
services.AddTransient<ImageSourcesGalleryPage, ImageSourcesGalleryViewModel>();
|
||||
services.AddTransient<LayoutsGalleryPage, LayoutsGalleryViewModel>();
|
||||
services.AddTransient<ViewsGalleryPage, ViewsGalleryViewModel>();
|
||||
|
||||
|
@ -133,6 +136,9 @@ public static class MauiProgram
|
|||
// Add Extensions Pages + ViewModels
|
||||
services.AddTransientWithShellRoute<ColorAnimationExtensionsPage, ColorAnimationExtensionsViewModel>();
|
||||
|
||||
// Add ImageSources pages + ViewModels
|
||||
services.AddTransientWithShellRoute<GravatarImageSourcePage, GravatarImageSourceViewModel>();
|
||||
|
||||
// Add Layouts Pages + ViewModels
|
||||
services.AddTransientWithShellRoute<UniformItemsLayoutPage, UniformItemsLayoutViewModel>();
|
||||
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<pages:BasePage
|
||||
x:Class="CommunityToolkit.Maui.Sample.Pages.ImageSources.GravatarImageSourcePage"
|
||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
|
||||
xmlns:pages="clr-namespace:CommunityToolkit.Maui.Sample.Pages"
|
||||
xmlns:vm="clr-namespace:CommunityToolkit.Maui.Sample.ViewModels.ImageSources"
|
||||
Title="GravatarImageSource"
|
||||
x:DataType="vm:GravatarImageSourceViewModel"
|
||||
x:TypeArguments="vm:GravatarImageSourceViewModel">
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Spacing="12">
|
||||
<VerticalStackLayout.Resources>
|
||||
<ResourceDictionary>
|
||||
<Style x:Key="Description" TargetType="Label">
|
||||
<Setter Property="VerticalTextAlignment" Value="Start" />
|
||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||
<Setter Property="LineBreakMode" Value="WordWrap" />
|
||||
<Setter Property="Margin" Value="4,0" />
|
||||
</Style>
|
||||
<Style x:Key="HR" TargetType="Line">
|
||||
<Setter Property="Stroke" Value="{AppThemeBinding Light=Black, Dark=White}" />
|
||||
<Setter Property="X2" Value="300" />
|
||||
<Setter Property="HorizontalOptions" Value="Center" />
|
||||
</Style>
|
||||
<Style TargetType="Switch">
|
||||
<Setter Property="OnColor" Value="{StaticResource Gray400}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
|
||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||
<VisualStateGroupList>
|
||||
<VisualStateGroup x:Name="CommonStates">
|
||||
<VisualState x:Name="On">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
<VisualState x:Name="Off">
|
||||
<VisualState.Setters>
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
</VisualStateGroup>
|
||||
</VisualStateGroupList>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style TargetType="Slider">
|
||||
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
||||
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</VerticalStackLayout.Resources>
|
||||
|
||||
<Label SemanticProperties.HeadingLevel="Level1" Style="{StaticResource Description}">
|
||||
<Label.FormattedText>
|
||||
<FormattedString>
|
||||
<Span Text="GravatarImageSource allows you to use as an Image " />
|
||||
<Span FontAttributes="Italic" Text="source" />
|
||||
<Span Text=", a users Gravatar registered image via their email address." />
|
||||
</FormattedString>
|
||||
</Label.FormattedText>
|
||||
</Label>
|
||||
<Line Style="{StaticResource HR}" />
|
||||
<Label SemanticProperties.HeadingLevel="Level2" Text="with Image control" />
|
||||
<Image HeightRequest="128" WidthRequest="128">
|
||||
<Image.Source>
|
||||
<mct:GravatarImageSource
|
||||
CacheValidity="{Binding CacheValidityTimespan}"
|
||||
CachingEnabled="{Binding EnableCache}"
|
||||
Email="{Binding Email}"
|
||||
Image="{Binding DefaultGravatarSelected}"
|
||||
SemanticProperties.Description="GravatarImageSource for an Image control, binding all properties." />
|
||||
</Image.Source>
|
||||
</Image>
|
||||
<Label SemanticProperties.HeadingLevel="Level2" Text="with Button control" />
|
||||
<Button Text="Button">
|
||||
<Button.ImageSource>
|
||||
<mct:GravatarImageSource
|
||||
CacheValidity="{Binding CacheValidityTimespan}"
|
||||
CachingEnabled="{Binding EnableCache}"
|
||||
Email="{Binding Email}"
|
||||
Image="{Binding DefaultGravatarSelected}"
|
||||
SemanticProperties.Description="GravatarImageSource for a Button control, binding all properties." />
|
||||
</Button.ImageSource>
|
||||
</Button>
|
||||
<Label SemanticProperties.HeadingLevel="Level2" Text="with ImageButton control" />
|
||||
<ImageButton HeightRequest="73" WidthRequest="72">
|
||||
<ImageButton.Source>
|
||||
<mct:GravatarImageSource
|
||||
CacheValidity="{Binding CacheValidityTimespan}"
|
||||
CachingEnabled="{Binding EnableCache}"
|
||||
Email="{Binding Email}"
|
||||
Image="{Binding DefaultGravatarSelected}"
|
||||
SemanticProperties.Description="GravatarImageSource for an ImageButton control, binding all properties." />
|
||||
</ImageButton.Source>
|
||||
</ImageButton>
|
||||
<Label SemanticProperties.HeadingLevel="Level2" Text="with AvatarView Control" />
|
||||
<mct:AvatarView
|
||||
BorderWidth="2"
|
||||
CornerRadius="32,0,0,32"
|
||||
HeightRequest="128"
|
||||
WidthRequest="128">
|
||||
<mct:AvatarView.Stroke>
|
||||
<LinearGradientBrush EndPoint="0,1">
|
||||
<GradientStop Offset="0.1" Color="Blue" />
|
||||
<GradientStop Offset="1.0" Color="Red" />
|
||||
</LinearGradientBrush>
|
||||
</mct:AvatarView.Stroke>
|
||||
<mct:AvatarView.ImageSource>
|
||||
<mct:GravatarImageSource
|
||||
CacheValidity="{Binding CacheValidityTimespan}"
|
||||
CachingEnabled="{Binding EnableCache}"
|
||||
Email="{Binding Email}"
|
||||
Image="{Binding DefaultGravatarSelected}"
|
||||
SemanticProperties.Description="GravatarImageSourse used with AvatarView." />
|
||||
</mct:AvatarView.ImageSource>
|
||||
</mct:AvatarView>
|
||||
<Label x:Name="LabelEmail" Text="Email:" />
|
||||
<Entry
|
||||
AutomationProperties.LabeledBy="{x:Reference LabelEmail}"
|
||||
Keyboard="Email"
|
||||
SemanticProperties.Hint="Enter your own gravatar registered email address"
|
||||
Text="{Binding Email}" />
|
||||
<Label x:Name="LabelDefaultImage" Text="Default Image:" />
|
||||
<Picker
|
||||
AutomationProperties.LabeledBy="{x:Reference LabelDefaultImage}"
|
||||
ItemsSource="{Binding DefaultGravatarItems}"
|
||||
SelectedItem="{Binding DefaultGravatarSelected}"
|
||||
SemanticProperties.Hint="A default image is displayed if there is no image associated with the requested email hash." />
|
||||
<Label x:Name="LabelEnableCache" Text="Enable cache:" />
|
||||
<Switch
|
||||
AutomationProperties.LabeledBy="{x:Reference LabelEnableCache}"
|
||||
IsToggled="{Binding EnableCache}"
|
||||
SemanticProperties.Hint="Defines whether image caching is enabled. The default value of this property is: True" />
|
||||
<Label
|
||||
x:Name="LabelCacheValidity"
|
||||
IsEnabled="{Binding EnableCache}"
|
||||
SemanticProperties.Description="Display how many days for cache validity.">
|
||||
<Label.FormattedText>
|
||||
<FormattedString>
|
||||
<Span Text="Cache Validity (in days): '" />
|
||||
<Span Text="{Binding CacheValidityInDays}" />
|
||||
<Span Text="'" />
|
||||
</FormattedString>
|
||||
</Label.FormattedText>
|
||||
</Label>
|
||||
<Slider
|
||||
AutomationProperties.LabeledBy="{x:Reference LabelCacheValidity}"
|
||||
IsEnabled="{Binding EnableCache}"
|
||||
Maximum="365"
|
||||
Minimum="1"
|
||||
SemanticProperties.Hint="Specify how long the image will be stored locally for. The default of this property is 1 day."
|
||||
Value="{Binding CacheValidityInDays}" />
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</pages:BasePage>
|
|
@ -0,0 +1,13 @@
|
|||
namespace CommunityToolkit.Maui.Sample.Pages.ImageSources;
|
||||
|
||||
using CommunityToolkit.Maui.Sample.ViewModels.ImageSources;
|
||||
|
||||
public partial class GravatarImageSourcePage : BasePage<GravatarImageSourceViewModel>
|
||||
{
|
||||
public GravatarImageSourcePage(GravatarImageSourceViewModel viewModel) : base(viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
Padding = new Thickness(Padding.Left, Padding.Top, Padding.Right, 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
using CommunityToolkit.Maui.Sample.ViewModels.ImageSources;
|
||||
|
||||
namespace CommunityToolkit.Maui.Sample.Pages.ImageSources;
|
||||
|
||||
public class ImageSourcesGalleryPage : BaseGalleryPage<ImageSourcesGalleryViewModel>
|
||||
{
|
||||
public ImageSourcesGalleryPage(IDeviceInfo deviceInfo, ImageSourcesGalleryViewModel imageSourcesGalleryViewModel)
|
||||
: base("Image Sources", deviceInfo, imageSourcesGalleryViewModel)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
namespace CommunityToolkit.Maui.Sample.ViewModels.ImageSources;
|
||||
|
||||
using CommunityToolkit.Maui.ImageSources;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
public partial class GravatarImageSourceViewModel : BaseViewModel
|
||||
{
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(CacheValidityTimespan))]
|
||||
int cacheValidityInDays = 1;
|
||||
|
||||
[ObservableProperty]
|
||||
DefaultImage defaultGravatarSelected = DefaultImage.MysteryPerson;
|
||||
|
||||
[ObservableProperty]
|
||||
string email = "dsiegel@avantipoint.com";
|
||||
|
||||
[ObservableProperty]
|
||||
bool enableCache = true;
|
||||
|
||||
public TimeSpan CacheValidityTimespan => TimeSpan.FromDays(CacheValidityInDays);
|
||||
|
||||
public IReadOnlyList<DefaultImage> DefaultGravatarItems { get; } = new[]
|
||||
{
|
||||
DefaultImage.MysteryPerson,
|
||||
DefaultImage.FileNotFound,
|
||||
DefaultImage.Identicon,
|
||||
DefaultImage.MonsterId,
|
||||
DefaultImage.Retro,
|
||||
DefaultImage.Robohash,
|
||||
DefaultImage.Wavatar,
|
||||
DefaultImage.Blank
|
||||
};
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using CommunityToolkit.Maui.Sample.Models;
|
||||
|
||||
namespace CommunityToolkit.Maui.Sample.ViewModels.ImageSources;
|
||||
|
||||
public class ImageSourcesGalleryViewModel : BaseGalleryViewModel
|
||||
{
|
||||
public ImageSourcesGalleryViewModel()
|
||||
: base(new[]
|
||||
{
|
||||
SectionModel.Create<GravatarImageSourceViewModel>("GravatarImageSource", Colors.Red, "GravatarImageSource allows you to use as an Image source, a users Gravatar registered image via their email address."),
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
|
@ -35,7 +35,7 @@ public class PlatformSnackbar : PlatformToast
|
|||
NFloat padding)
|
||||
: base(message, backgroundColor, cornerRadius, textColor, textFont, characterSpacing, padding)
|
||||
{
|
||||
padding += defaultPadding;
|
||||
padding += DefaultPadding;
|
||||
|
||||
actionButton = new PaddedButton(padding, padding, padding, padding);
|
||||
actionButton.SetContentCompressionResistancePriority((float)UILayoutPriority.Required, UILayoutConstraintAxis.Horizontal);
|
||||
|
|
|
@ -9,8 +9,9 @@ namespace CommunityToolkit.Maui.Core.Views;
|
|||
/// </summary>
|
||||
public class PlatformToast : Alert, IDisposable
|
||||
{
|
||||
internal const float DefaultPadding = 10;
|
||||
|
||||
readonly PaddedLabel messageLabel;
|
||||
internal const float defaultPadding = 10;
|
||||
|
||||
bool isDisposed;
|
||||
|
||||
|
@ -33,7 +34,7 @@ public class PlatformToast : Alert, IDisposable
|
|||
double characterSpacing,
|
||||
NFloat padding)
|
||||
{
|
||||
padding += defaultPadding;
|
||||
padding += DefaultPadding;
|
||||
|
||||
messageLabel = new PaddedLabel(padding, padding, padding, padding)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,438 @@
|
|||
namespace CommunityToolkit.Maui.UnitTests.ImageSources.GravatarImageSource;
|
||||
|
||||
using CommunityToolkit.Maui.ImageSources;
|
||||
using Xunit;
|
||||
|
||||
public class GravatarImageSourceTests : BaseHandlerTest
|
||||
{
|
||||
readonly TimeSpan cacheValidity = new(1, 0, 0);
|
||||
readonly bool cachingEnabled = false;
|
||||
readonly string email = "dsiegel@avantipoint.com";
|
||||
readonly DefaultImage image = DefaultImage.MonsterId;
|
||||
|
||||
[Fact]
|
||||
public void ChangingEmailAndImageWithNoSizeDoesNotUpdateUri()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource
|
||||
{
|
||||
Email = email,
|
||||
Image = image
|
||||
};
|
||||
Assert.Equal(image, gravatarImageSource.Image);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), gravatarImageSource.Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangingEmailWithNoSizeDoesNotUpdateUri()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource
|
||||
{
|
||||
Email = email
|
||||
};
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), gravatarImageSource.Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ChangingImageWithNoSizeDoesNotUpdateUri()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource
|
||||
{
|
||||
Image = image
|
||||
};
|
||||
Assert.Equal(image, gravatarImageSource.Image);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), gravatarImageSource.Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstructorTest()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource()
|
||||
{
|
||||
CacheValidity = cacheValidity,
|
||||
CachingEnabled = cachingEnabled,
|
||||
Email = email,
|
||||
Image = image
|
||||
};
|
||||
|
||||
Assert.Equal(cacheValidity, gravatarImageSource.CacheValidity);
|
||||
Assert.True(cachingEnabled == gravatarImageSource.CachingEnabled);
|
||||
Assert.Equal(email, gravatarImageSource.Email);
|
||||
Assert.Equal(image, gravatarImageSource.Image);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default404Image()
|
||||
{
|
||||
Image testControl = new()
|
||||
{
|
||||
Source = new GravatarImageSource()
|
||||
{
|
||||
Email = email,
|
||||
}
|
||||
};
|
||||
|
||||
((GravatarImageSource)testControl.Source).Image = DefaultImage.FileNotFound;
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(DefaultImage.FileNotFound, ((GravatarImageSource)testControl.Source).Image);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultCacheValidity()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
Assert.Equal(new TimeSpan(1, 0, 0, 0), gravatarImageSource.CacheValidity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultCachingEnabled()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
Assert.True(gravatarImageSource.CachingEnabled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultDefaultImage()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
Assert.Equal(DefaultImage.MysteryPerson, gravatarImageSource.Image);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultEmail()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
Assert.Null(gravatarImageSource.Email);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultUri()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), gravatarImageSource.Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsDisposed()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
Assert.False(gravatarImageSource.IsDisposed);
|
||||
gravatarImageSource.Dispose();
|
||||
Assert.True(gravatarImageSource.IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsDisposedDisposeTokenSource()
|
||||
{
|
||||
Image testControl = new()
|
||||
{
|
||||
Source = new GravatarImageSource()
|
||||
};
|
||||
Assert.True(testControl.Source is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
|
||||
Assert.False(((GravatarImageSource)testControl.Source).IsDisposed);
|
||||
((GravatarImageSource)testControl.Source).Dispose();
|
||||
Assert.True(((GravatarImageSource)testControl.Source).IsDisposed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEmpty()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
Assert.True(gravatarImageSource.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsEmptyNot()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource()
|
||||
{
|
||||
Email = email,
|
||||
};
|
||||
Assert.False(gravatarImageSource.IsEmpty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullEmailDoesNotCrash()
|
||||
{
|
||||
var loader = new GravatarImageSource();
|
||||
var exception = Record.Exception(() => loader.Email = null);
|
||||
Assert.Null(exception);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBindableObjectBackButtonBehavior()
|
||||
{
|
||||
BackButtonBehavior testControl = new()
|
||||
{
|
||||
IconOverride = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.IconOverride is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.IconOverride).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestBindableObjectSearchHandler()
|
||||
{
|
||||
SearchHandler testControl = new()
|
||||
{
|
||||
QueryIcon = new GravatarImageSource(),
|
||||
ClearPlaceholderIcon = new GravatarImageSource(),
|
||||
ClearIcon = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.QueryIcon is GravatarImageSource);
|
||||
Assert.True(testControl.ClearPlaceholderIcon is GravatarImageSource);
|
||||
Assert.True(testControl.ClearIcon is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.QueryIcon).Uri);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.ClearPlaceholderIcon).Uri);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.ClearIcon).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlButton()
|
||||
{
|
||||
Button testControl = new()
|
||||
{
|
||||
ImageSource = new GravatarImageSource(),
|
||||
};
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.True(testControl.ImageSource is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/?s=37"), ((GravatarImageSource)testControl.ImageSource).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlButtonWithEmail()
|
||||
{
|
||||
Button testControl = new()
|
||||
{
|
||||
ImageSource = new GravatarImageSource() { Email = email },
|
||||
};
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.True(testControl.ImageSource is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/b65a519785f69fbe7236dd0fd6396094?s=37&d=mp"), ((GravatarImageSource)testControl.ImageSource).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlImage()
|
||||
{
|
||||
Image testControl = new()
|
||||
{
|
||||
Source = new GravatarImageSource()
|
||||
};
|
||||
Assert.True(testControl.Source is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/?s=37"), ((GravatarImageSource)testControl.Source).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlImageButton()
|
||||
{
|
||||
ImageButton testControl = new()
|
||||
{
|
||||
Source = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.Source is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/?s=37"), ((GravatarImageSource)testControl.Source).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlImageButtonWithEmail()
|
||||
{
|
||||
ImageButton testControl = new()
|
||||
{
|
||||
Source = new GravatarImageSource() { Email = email },
|
||||
};
|
||||
Assert.True(testControl.Source is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/b65a519785f69fbe7236dd0fd6396094?s=37&d=mp"), ((GravatarImageSource)testControl.Source).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlImageButtonWithoutEmail()
|
||||
{
|
||||
ImageButton testControl = new()
|
||||
{
|
||||
Source = new GravatarImageSource() { Image = image },
|
||||
};
|
||||
Assert.True(testControl.Source is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/?s=37"), ((GravatarImageSource)testControl.Source).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlImageWithEmail()
|
||||
{
|
||||
Image testControl = new()
|
||||
{
|
||||
Source = new GravatarImageSource()
|
||||
{
|
||||
Email = email,
|
||||
}
|
||||
};
|
||||
Assert.True(testControl.Source is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/b65a519785f69fbe7236dd0fd6396094?s=37&d=mp"), ((GravatarImageSource)testControl.Source).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlImageWithEmailAndImage()
|
||||
{
|
||||
Image testControl = new()
|
||||
{
|
||||
Source = new GravatarImageSource()
|
||||
{
|
||||
Email = email,
|
||||
Image = image
|
||||
}
|
||||
};
|
||||
Assert.True(testControl.Source is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/b65a519785f69fbe7236dd0fd6396094?s=37&d=monsterid"), ((GravatarImageSource)testControl.Source).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlPage()
|
||||
{
|
||||
Page testControl = new()
|
||||
{
|
||||
IconImageSource = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.IconImageSource is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.IconImageSource).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlShell()
|
||||
{
|
||||
Shell testControl = new()
|
||||
{
|
||||
FlyoutBackgroundImage = new GravatarImageSource(),
|
||||
FlyoutIcon = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.FlyoutBackgroundImage is GravatarImageSource);
|
||||
Assert.True(testControl.FlyoutIcon is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.FlyoutBackgroundImage).Uri);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.FlyoutIcon).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestControlSlider()
|
||||
{
|
||||
Slider testControl = new()
|
||||
{
|
||||
ThumbImageSource = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.ThumbImageSource is GravatarImageSource);
|
||||
testControl.Layout(new Rect(0, 0, 37, 73));
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.ThumbImageSource).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestDataPackage()
|
||||
{
|
||||
DataPackage testControl = new()
|
||||
{
|
||||
Image = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.Image is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.Image).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestDefaultStream()
|
||||
{
|
||||
CancellationTokenSource cts = new();
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
Stream stream = await gravatarImageSource.Stream(cts.Token);
|
||||
Assert.Equal(2637, stream.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TestDefaultStreamCanceled()
|
||||
{
|
||||
CancellationTokenSource cts = new();
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
cts.Cancel();
|
||||
Stream stream = await gravatarImageSource.Stream(cts.Token);
|
||||
Assert.Equal(Stream.Null, stream);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestElementAppLinkEntry()
|
||||
{
|
||||
AppLinkEntry testControl = new()
|
||||
{
|
||||
Thumbnail = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.Thumbnail is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.Thumbnail).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestElementAppLinkEntryWithEmail()
|
||||
{
|
||||
AppLinkEntry testControl = new()
|
||||
{
|
||||
Thumbnail = new GravatarImageSource() { Email = email },
|
||||
};
|
||||
Assert.True(testControl.Thumbnail is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.Thumbnail).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestElementImageCell()
|
||||
{
|
||||
ImageCell testControl = new()
|
||||
{
|
||||
ImageSource = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.ImageSource is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.ImageSource).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestElementMenuItem()
|
||||
{
|
||||
MenuItem testControl = new()
|
||||
{
|
||||
IconImageSource = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.IconImageSource is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.IconImageSource).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestNavigableElementBaseShellItem()
|
||||
{
|
||||
BaseShellItem testControl = new()
|
||||
{
|
||||
Icon = new GravatarImageSource(),
|
||||
FlyoutIcon = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.Icon is GravatarImageSource);
|
||||
Assert.True(testControl.FlyoutIcon is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.Icon).Uri);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.FlyoutIcon).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestToolbar()
|
||||
{
|
||||
Toolbar testControl = new(new View())
|
||||
{
|
||||
TitleIcon = new GravatarImageSource(),
|
||||
};
|
||||
Assert.True(testControl.TitleIcon is GravatarImageSource);
|
||||
Assert.Equal(new Uri("https://www.gravatar.com/avatar/"), ((GravatarImageSource)testControl.TitleIcon).Uri);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToStringTest()
|
||||
{
|
||||
var gravatarImageSource = new GravatarImageSource();
|
||||
Assert.Equal("Uri: https://www.gravatar.com/avatar/\nEmail: \nSize: -1\nImage: mp\nCacheValidity: 1.00:00:00\nCachingEnabled: True", gravatarImageSource.ToString());
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Alerts))]
|
||||
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Behaviors))]
|
||||
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Converters))]
|
||||
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.ImageSources))]
|
||||
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Views))]
|
||||
[assembly: XmlnsDefinition(Constants.XamlNamespace, Constants.CommunityToolkitNamespacePrefix + nameof(CommunityToolkit.Maui.Layouts))]
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace CommunityToolkit.Maui.Extensions;
|
||||
|
||||
/// <summary>Cryptography extensions.</summary>
|
||||
static class CryptographyExtensions
|
||||
{
|
||||
/// <summary>A hash function producing a 128-bit hash value (16 Bytes, 32 Hexadecimal characters)</summary>
|
||||
/// <param name="source">Source string.</param>
|
||||
/// <param name="separator">Separator.</param>
|
||||
/// <returns>Array of 16 bytes.</returns>
|
||||
public static string GetMd5Hash(this string source, string separator = "-")
|
||||
{
|
||||
using var md5 = MD5.Create();
|
||||
var hash = md5.ComputeHash(Encoding.UTF8.GetBytes(source.AsSpan().ToArray())).AsSpan();
|
||||
|
||||
return BitConverter.ToString(hash.ToArray(), 0, hash.Length).Replace("-", separator, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace CommunityToolkit.Maui.Extensions;
|
||||
|
||||
/// <summary>HttpClient extensions.</summary>
|
||||
static partial class HttpClientExtensions
|
||||
{
|
||||
/// <summary>Get stream from Uri.</summary>
|
||||
/// <param name="client"><see href="HttpClient" />.</param>
|
||||
/// <param name="uri">Target Uri.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Task <see cref="Task{TResult}"/> result.</returns>
|
||||
public static async Task<Stream> DownloadStreamAsync(this HttpClient client, Uri uri, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(client);
|
||||
ArgumentNullException.ThrowIfNull(uri);
|
||||
|
||||
try
|
||||
{
|
||||
return await StreamWrapper.GetStreamAsync(uri, cancellationToken, client).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Error getting stream for {uri}: {ex}");
|
||||
return Stream.Null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
namespace CommunityToolkit.Maui.ImageSources;
|
||||
|
||||
using CommunityToolkit.Maui.Extensions;
|
||||
using Microsoft.Maui.Controls;
|
||||
|
||||
/// <summary>Gravatar image source.</summary>
|
||||
/// <remarks>Note that <see cref="UriImageSource"/> is sealed and can't be used as a parent!</remarks>
|
||||
public class GravatarImageSource : StreamImageSource, IDisposable
|
||||
{
|
||||
/// <summary>The backing store for the <see cref="CacheValidity" /> bindable property.</summary>
|
||||
public static readonly BindableProperty CacheValidityProperty = BindableProperty.Create(nameof(CacheValidity), typeof(TimeSpan), typeof(GravatarImageSource), TimeSpan.FromDays(1));
|
||||
|
||||
/// <summary>The backing store for the <see cref="CachingEnabled" /> bindable property.</summary>
|
||||
public static readonly BindableProperty CachingEnabledProperty = BindableProperty.Create(nameof(CachingEnabled), typeof(bool), typeof(GravatarImageSource), true);
|
||||
|
||||
/// <summary>The backing store for the <see cref="Email" /> bindable property.</summary>
|
||||
public static readonly BindableProperty EmailProperty = BindableProperty.Create(nameof(Email), typeof(string), typeof(GravatarImageSource), defaultValue: null, propertyChanged: OnEmailPropertyChanged);
|
||||
|
||||
/// <summary>The backing store for the <see cref="Image" /> bindable property.</summary>
|
||||
public static readonly BindableProperty ImageProperty = BindableProperty.Create(nameof(Image), typeof(DefaultImage), typeof(GravatarImageSource), defaultValue: DefaultImage.MysteryPerson, propertyChanged: OnDefaultImagePropertyChanged);
|
||||
|
||||
/// <summary>The backing store for the <see cref="ParentHeight" /> bindable property.</summary>
|
||||
internal static readonly BindableProperty ParentHeightProperty = BindableProperty.Create(nameof(ParentHeight), typeof(int), typeof(GravatarImageSource), defaultValue: defaultSize, propertyChanged: OnSizePropertyChanged);
|
||||
|
||||
/// <summary>The backing store for the <see cref="ParentWidth" /> bindable property.</summary>
|
||||
internal static readonly BindableProperty ParentWidthProperty = BindableProperty.Create(nameof(ParentWidth), typeof(int), typeof(GravatarImageSource), defaultValue: defaultSize, propertyChanged: OnSizePropertyChanged);
|
||||
|
||||
const int cancellationTokenSourceTimeout = 737;
|
||||
const string defaultGravatarImageAddress = "https://www.gravatar.com/avatar/";
|
||||
const int defaultSize = 80;
|
||||
|
||||
static readonly Lazy<HttpClient> singletonHttpClientHolder = new();
|
||||
|
||||
readonly CancellationTokenSource? tokenSource;
|
||||
|
||||
int gravatarSize = -1;
|
||||
Uri? lastDispatch;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="GravatarImageSource"/> class.</summary>
|
||||
public GravatarImageSource()
|
||||
{
|
||||
Uri = new Uri(defaultGravatarImageAddress);
|
||||
Stream = new Func<CancellationToken, Task<Stream>>(cancelationToken => SingletonHttpClient.DownloadStreamAsync(Uri, cancelationToken));
|
||||
}
|
||||
|
||||
/// <summary>Gets a value indicating whether the control email is empty.</summary>
|
||||
public override bool IsEmpty => string.IsNullOrEmpty(Email);
|
||||
|
||||
/// <summary>Gets or sets a <see cref="TimeSpan"/> structure that indicates the length of time after which the image cache becomes invalid.</summary>
|
||||
public TimeSpan CacheValidity
|
||||
{
|
||||
get => (TimeSpan)GetValue(CacheValidityProperty);
|
||||
set => SetValue(CacheValidityProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets a Boolean value that indicates whether caching is enabled on this <see cref="GravatarImageSource"/> object.</summary>
|
||||
public bool CachingEnabled
|
||||
{
|
||||
get => (bool)GetValue(CachingEnabledProperty);
|
||||
set => SetValue(CachingEnabledProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets a value indicating whether <see cref="GravatarImageSource"/> has been disposed.</summary>
|
||||
public bool IsDisposed { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the email address.</summary>
|
||||
public string? Email
|
||||
{
|
||||
get => (string)GetValue(EmailProperty);
|
||||
set => SetValue(EmailProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the default image.</summary>
|
||||
public DefaultImage Image
|
||||
{
|
||||
get => (DefaultImage)GetValue(ImageProperty);
|
||||
set => SetValue(ImageProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the URI for the image to get.</summary>
|
||||
[System.ComponentModel.TypeConverter(typeof(UriTypeConverter))]
|
||||
public Uri Uri { get; set; }
|
||||
|
||||
/// <summary>Gets the parent height.</summary>
|
||||
internal int ParentHeight => (int)GetValue(ParentHeightProperty);
|
||||
|
||||
/// <summary>Gets the parent width.</summary>
|
||||
internal int ParentWidth => (int)GetValue(ParentWidthProperty);
|
||||
|
||||
/// <summary>Gets or sets the image size.</summary>
|
||||
/// <remarks>Size is limited to be in the range of 1 to 2048.</remarks>
|
||||
int GravatarSize
|
||||
{
|
||||
get => gravatarSize;
|
||||
set => gravatarSize = Math.Clamp(value, 1, 2048);
|
||||
}
|
||||
|
||||
HttpClient SingletonHttpClient => singletonHttpClientHolder.Value;
|
||||
|
||||
/// <summary>Dispose <see cref="GravatarImageSource"/>.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>Returns the Uri as a string.</summary>
|
||||
/// <returns>String of the URI.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Uri: {Uri}\nEmail: {Email}\nSize: {GravatarSize}\nImage: {DefaultGravatarName(Image)}\nCacheValidity: {CacheValidity}\nCachingEnabled: {CachingEnabled}";
|
||||
}
|
||||
|
||||
/// <summary>Dispose <see cref="GravatarImageSource"/>.</summary>
|
||||
/// <param name="isDisposing">Is disposing.</param>
|
||||
protected virtual void Dispose(bool isDisposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
IsDisposed = true;
|
||||
|
||||
if (isDisposing)
|
||||
{
|
||||
tokenSource?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>On parent set.</summary>
|
||||
protected override void OnParentSet()
|
||||
{
|
||||
base.OnParentSet();
|
||||
|
||||
if (Parent is not VisualElement parentElement || parentElement is null)
|
||||
{
|
||||
GravatarSize = defaultSize;
|
||||
return;
|
||||
}
|
||||
|
||||
SetBinding(ParentWidthProperty, new Binding(nameof(VisualElement.Width), BindingMode.OneWay, source: parentElement));
|
||||
SetBinding(ParentHeightProperty, new Binding(nameof(VisualElement.Height), BindingMode.OneWay, source: parentElement));
|
||||
}
|
||||
|
||||
static string DefaultGravatarName(DefaultImage defaultGravatar) => defaultGravatar switch
|
||||
{
|
||||
DefaultImage.FileNotFound => "404",
|
||||
DefaultImage.MysteryPerson => "mp",
|
||||
_ => defaultGravatar.ToString().ToLower(),
|
||||
};
|
||||
|
||||
static async void OnDefaultImagePropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||
{
|
||||
GravatarImageSource gravatarImageSource = (GravatarImageSource)bindable;
|
||||
await gravatarImageSource.HandleNewUriRequested(gravatarImageSource.Email, (DefaultImage)newValue);
|
||||
}
|
||||
|
||||
static async void OnEmailPropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||
{
|
||||
GravatarImageSource gravatarImageSource = (GravatarImageSource)bindable;
|
||||
await gravatarImageSource.HandleNewUriRequested((string?)newValue, gravatarImageSource.Image);
|
||||
}
|
||||
|
||||
static async void OnSizePropertyChanged(BindableObject bindable, object oldValue, object newValue)
|
||||
{
|
||||
if (newValue is not int intNewValue || intNewValue <= -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GravatarImageSource gravatarImageSource = (GravatarImageSource)bindable;
|
||||
if (gravatarImageSource.GravatarSize is -1)
|
||||
{
|
||||
gravatarImageSource.GravatarSize = intNewValue;
|
||||
return;
|
||||
}
|
||||
|
||||
gravatarImageSource.GravatarSize = Math.Min(gravatarImageSource.ParentWidth, gravatarImageSource.ParentHeight);
|
||||
await gravatarImageSource.HandleNewUriRequested(gravatarImageSource.Email, gravatarImageSource.Image);
|
||||
}
|
||||
|
||||
Task HandleNewUriRequested(string? email, DefaultImage image)
|
||||
{
|
||||
if (GravatarSize is -1)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Uri = IsEmpty
|
||||
? new Uri($"{defaultGravatarImageAddress}?s={GravatarSize}")
|
||||
: new Uri($"{defaultGravatarImageAddress}{email?.GetMd5Hash(string.Empty).ToLowerInvariant()}?s={GravatarSize}&d={DefaultGravatarName(image)}");
|
||||
|
||||
return OnUriChanged();
|
||||
}
|
||||
|
||||
async Task OnUriChanged()
|
||||
{
|
||||
if (tokenSource is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
tokenSource.Cancel();
|
||||
tokenSource.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Left intentionally empty, as we don't need to catch anything.
|
||||
}
|
||||
}
|
||||
|
||||
if (Uri.Equals(lastDispatch))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (tokenSource?.Token is not null)
|
||||
{
|
||||
await Task.Delay(cancellationTokenSourceTimeout, tokenSource.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(cancellationTokenSourceTimeout);
|
||||
}
|
||||
|
||||
CancellationTokenSource?.Cancel();
|
||||
lastDispatch = Uri;
|
||||
await Dispatcher.DispatchIfRequiredAsync(OnSourceChanged);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Default image enumerator.</summary>
|
||||
public enum DefaultImage
|
||||
{
|
||||
/// <summary>(mystery-person) A simple, cartoon-style silhouetted outline of a person (does not vary by email hash)</summary>
|
||||
MysteryPerson = 0,
|
||||
|
||||
/// <summary>404: Do not load any image if none is associated with the email hash, instead return an HTTP 404 (File Not Found) response.</summary>
|
||||
FileNotFound,
|
||||
|
||||
/// <summary>A geometric pattern based on an email hash.</summary>
|
||||
Identicon,
|
||||
|
||||
/// <summary>A generated 'monster' with different colours, faces, etc.</summary>
|
||||
MonsterId,
|
||||
|
||||
/// <summary>Generated faces with differing features and backgrounds.</summary>
|
||||
Wavatar,
|
||||
|
||||
/// <summary>Awesome generated, 8-bit arcade-style pixilated faces.</summary>
|
||||
Retro,
|
||||
|
||||
/// <summary>A generated robot with different colours, faces, etc.</summary>
|
||||
Robohash,
|
||||
|
||||
/// <summary>A transparent PNG image.</summary>
|
||||
Blank,
|
||||
}
|
Загрузка…
Ссылка в новой задаче