Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Matthew Kilgore committed Sep 27, 2024
0 parents commit 6e4cee9
Show file tree
Hide file tree
Showing 10 changed files with 1,034 additions and 0 deletions.
694 changes: 694 additions & 0 deletions .gitignore

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions Extensions/DictionaryExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace SysAdminsMedia.BlazorIconify.Extensions;

public static class DictionaryExtension
{
public static string Get(this Dictionary<string, object> data, string className)
{
if (data is null)
return string.Empty;

return data.TryGetValue(className, out var value) ? value.ToString()! : string.Empty;
}
}
23 changes: 23 additions & 0 deletions Extensions/IconifyExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Blazored.LocalStorage;
using Microsoft.Extensions.DependencyInjection;

namespace SysAdminsMedia.BlazorIconify.Extensions;

public static class IconifyExtension
{
public static IServiceCollection AddBlazorIconify(this IServiceCollection services) => services
.AddScoped<Registry>()
.AddBlazoredLocalStorage(
config =>
{
config.JsonSerializerOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
config.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
config.JsonSerializerOptions.IgnoreReadOnlyProperties = true;
config.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
config.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
config.JsonSerializerOptions.ReadCommentHandling = JsonCommentHandling.Skip;
config.JsonSerializerOptions.WriteIndented = false;
});
}
12 changes: 12 additions & 0 deletions IconMetaData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;

namespace SysAdminsMedia.BlazorIconify;

public class IconMetaData
{
[JsonPropertyName("name")] public required string Name { get; set; }
[JsonPropertyName("content")] public required string Content { get; set; }
[JsonPropertyName("time_fetched")] public DateTime TimeFetched { get; set; }

[JsonPropertyName("color")] public string? Color { get; set; }
}
15 changes: 15 additions & 0 deletions Iconify.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@using BlazorIconify.Extensions

<main @attributes="@Attributes"
class="@Attributes.Get("class")"
style="@Attributes.Get("style")">
@((MarkupString)_svg)
</main>

<style>
.icon {
display: inline-block;
width: 1em;
height: 1em;
}
</style>
159 changes: 159 additions & 0 deletions Iconify.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System.Text;
using System.Text.Encodings.Web;
using System.Xml;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components;
using SysAdminsMedia.BlazorIconify.Extensions;

namespace SysAdminsMedia.BlazorIconify;

public partial class Iconify : ComponentBase
{
private const string API = "https://api.iconify.design/";
private const string ErrorIcon = "ic:baseline-do-not-disturb";

private string _svg = string.Empty;
private bool _initialized;

[Inject] public HttpClient HttpClient { get; set; } = null!;
[Inject] public ILocalStorageService LocalStorage { get; set; } = null!;
[Inject] public Registry Registry { get; set; } = null!;

[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> Attributes { get; set; } = null!;

[Parameter] public string Icon { get; set; } = string.Empty;

[Parameter] public string Color { get; set; } = string.Empty;


protected override async Task OnAfterRenderAsync(bool firstRender)
{
_initialized = true;

if (!firstRender) return;

if (string.IsNullOrEmpty(Icon))
{
// Fallback to error icon if no icon is provided
Icon = ErrorIcon;
return;
}

// Only fetch the icon if it has changed
if (await Registry.IsCached(Icon, Color))
{
var metadata = await Registry.GetIcon(Icon, Color);
if (metadata is null) return;

var svg = TryParseToXml(metadata.Content);
if (svg is null) return;

UpdateSvg(svg);
}
else
{
string iconUrl = $"{API}{Icon.Replace(':', '/')}.svg";
if (!string.IsNullOrEmpty(Color))
{
iconUrl += $"?color={UrlEncoder.Default.Encode(Color)}";
}
_svg = await FetchIconAsync(iconUrl);

if (string.IsNullOrEmpty(_svg))
{
Console.WriteLine($"Failed to fetch icon {(!string.IsNullOrEmpty(Icon) ? Icon : "\"null\"")}");
return;
}

var svg = TryParseToXml(_svg);
if (svg is null) return;

UpdateSvg(svg);

await Registry.AddIcon(new IconMetaData
{
Name = Icon,
Content = _svg,
Color = Color,
TimeFetched = DateTime.Now
});
}

if (string.IsNullOrEmpty(_svg))
Console.WriteLine($"Failed to fetch icon {this}");

StateHasChanged();
Console.WriteLine("RENDER ICONIFY");
}

protected override async Task OnParametersSetAsync()
{
if (!_initialized) return;
await OnAfterRenderAsync(true);
}

private async Task<string> FetchIconAsync(string url)
{
if (string.IsNullOrEmpty(Icon)) return string.Empty;

var response = await HttpClient.GetByteArrayAsync(url);
var iconContents = Encoding.UTF8.GetString(response);

if (iconContents is not "404" && response is not ({ Length: 0 } or null))
return iconContents;

iconContents = string.Empty;
return iconContents;
}

private static XmlDocument? TryParseToXml(string content)
{
try
{
var document = new XmlDocument();
document.LoadXml(content);

return document;
}
catch (XmlException ex)
{
Console.WriteLine("Failed to parse xml from svg file.");
}

return null;
}

private void UpdateSvg(XmlDocument document)
{
var rootElement = document.DocumentElement;

switch (rootElement)
{
case null:
Console.WriteLine("No root element.");
return;
case not { Name: "svg" } or null:
Console.WriteLine("Failed to find svg element.");
return;
}

if (rootElement is null)
{
Console.WriteLine("Failed to find svg element.");
return;
}

rootElement.SetAttribute("class", $"{Attributes.Get("i-class")} icon");
rootElement.SetAttribute("style", Attributes.Get("i-style"));
rootElement.RemoveAttribute("width");
rootElement.RemoveAttribute("height");

foreach (XmlElement child in rootElement.ChildNodes)
{
if (child.Name.ToLower() is not "path") continue;
}

_svg = rootElement.OuterXml;
}
}
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# SysAdminsMedia.BlazorIconify
BlazorIconify is a Blazor component library for Iconify, a unified icon framework that provides a consistent icon experience across all platforms. BlazorIconify is a wrapper around the Iconify API that allows you to easily add icons to your Blazor applications.

## Installation
You can install BlazorIconify via NuGet. Run the following command in the Package Manager Console:
```
Install-Package SysAdminsMedia.BlazorIconify
```
or via the .NET Core CLI:
```
dotnet add package SysAdminsMedia.BlazorIconify
```

## Usage
First, you need to add the following line to your `_Imports.razor` file:
```csharp
@using SysAdminsMedia.BlazorIconify
```

Then, you can use the `Iconify` component in your Blazor components like this:
```html
<Iconify Icon="mdi:home" />
```

You can also adjust the color and other properties of the icon:
```html
<Iconify Icon="mdi:home" Color="red" Class="my-custom-class" Style="align-content: center;" />
```
50 changes: 50 additions & 0 deletions Registry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Blazored.LocalStorage;

namespace SysAdminsMedia.BlazorIconify;

public sealed class Registry(ILocalStorageService LocalStorage)
{
private const string CachedIconsKey = "cached-icons";

private List<IconMetaData> _icons = [];

public async Task AddIcon(IconMetaData metadata)
{
if(string.IsNullOrEmpty(metadata.Name)) return;
if (IsRegistered(metadata.Name)) return;

_icons.Add(metadata);
await LocalStorage.SetItemAsync(CachedIconsKey, _icons);
}

public async Task<IconMetaData?> GetIcon(string icon, string? color = "")
{
if (string.IsNullOrEmpty(icon)) return null;

var icons = await GetCachedIcons();
return icons.FirstOrDefault(x => x.Name == icon && x.Color == color);
}

public async Task<bool> IsCached(string icon, string? color = "")
{
if (string.IsNullOrEmpty(icon)) return false;

var icons = await GetCachedIcons();
return icons.Exists(x => x.Name == icon && x.Color == color);
}

public async Task Clear()
{
_icons.Clear();
await LocalStorage.RemoveItemAsync(CachedIconsKey);
}

private async Task<List<IconMetaData>> GetCachedIcons()
{
if (_icons.Count > 0) return _icons;
return _icons = await LocalStorage.GetItemAsync<List<IconMetaData>>(CachedIconsKey) ?? [];
}

private bool IsRegistered(string icon) =>
_icons.Exists(x => x.Name == icon);
}
25 changes: 25 additions & 0 deletions SysAdminsMedia.BlazorIconify.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>SysAdminsMedia.BlazorIconify</AssemblyName>
<RootNamespace>SysAdminsMedia.BlazorIconify</RootNamespace>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>SysAdminsMedia.BlazorIconify</PackageId>
<Authors>SysAdmins Media</Authors>
<RepositoryUrl>https://github.com/sysadminsmedia/BlazorIconify</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Company>SysAdmins Media</Company>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.8" />
<None Include="README.md" Pack="true" PackagePath="\"/>
</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions SysAdminsMedia.BlazorIconify.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SysAdminsMedia.BlazorIconify", "SysAdminsMedia.BlazorIconify.csproj", "{B841266B-8C1E-49BC-BF53-5EA4064FCC41}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B841266B-8C1E-49BC-BF53-5EA4064FCC41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B841266B-8C1E-49BC-BF53-5EA4064FCC41}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B841266B-8C1E-49BC-BF53-5EA4064FCC41}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B841266B-8C1E-49BC-BF53-5EA4064FCC41}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

0 comments on commit 6e4cee9

Please sign in to comment.