Use new dynamic item icon in web app search and table views (#1976)

This commit is contained in:
Leendert de Borst
2026-04-29 10:37:23 +02:00
committed by Leendert de Borst
parent 3eaf2ac5c6
commit f0c8f96a1c
8 changed files with 13 additions and 221 deletions

View File

@@ -1,79 +0,0 @@
@inject NavigationManager NavigationManager
<div @onclick="ShowDetails" class="credential-card overflow-hidden p-4 space-y-2 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700
dark:bg-gray-800 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200">
<div class="px-4 py-2 text-gray-400 rounded text-center flex flex-col items-center">
<DisplayFavicon FaviconBytes="@Obj.Logo" Padding="true"></DisplayFavicon>
<div class="flex items-center justify-center gap-1.5 w-full">
<div class="text-gray-900 dark:text-gray-100 break-words">@GetServiceName()</div>
@if (Obj.HasPasskey)
{
<svg class="w-3.5 h-3.5 text-gray-500 dark:text-gray-400 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="Has passkey">
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
</svg>
}
@if (Obj.HasAttachment)
{
<svg class="w-3.5 h-3.5 text-gray-500 dark:text-gray-400 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-label="Has attachments">
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
</svg>
}
</div>
<div class="text-gray-500 dark:text-gray-400 break-words w-full text-sm">@GetDisplayText()</div>
</div>
</div>
@code {
/// <summary>
/// Gets or sets the credentials object to show.
/// </summary>
[Parameter]
public required ItemListEntry Obj { get; set; }
/// <summary>
/// Gets the display text for the credential, showing username by default,
/// falling back to email only if username is null/empty.
/// </summary>
private string GetDisplayText()
{
var returnValue = string.Empty;
// Show username if available
if (!string.IsNullOrEmpty(Obj.Username))
{
returnValue = Obj.Username;
}
// Show email if username is not available
if (!string.IsNullOrEmpty(Obj.Email))
{
returnValue = Obj.Email;
}
return returnValue;
}
/// <summary>
/// Get the service name for a credential.
/// </summary>
private string GetServiceName()
{
var returnValue = "Untitled";
if (!string.IsNullOrEmpty(Obj.Service))
{
returnValue = Obj.Service;
}
return returnValue;
}
/// <summary>
/// Navigate to the details page of the credential.
/// </summary>
private void ShowDetails()
{
// Redirect to view page instead for now.
NavigationManager.NavigateTo($"/items/{Obj.Id}");
}
}

View File

@@ -1,81 +0,0 @@
@if (FaviconBytes != null)
{
<img src="@_faviconDataUrl" style="width: @(Width)px;" class="rounded-lg w-28 @(Padding ? "mb-4 sm:mb-0 xl:mb-4 2xl:mb-0" : "")" alt="Favicon" />
}
else
{
<img src="img/service-placeholder.webp" style="width: @(Width)px;" class="@(Padding ? "mb-4 sm:mb-0 xl:mb-4 2xl:mb-0" : "")" alt="Favicon" />
}
@code {
/// <summary>
/// Byte[] of the favicon.
/// </summary>
[Parameter]
public byte[]? FaviconBytes { get; set; }
/// <summary>
/// The width (in pixels) to show the icon as. Defaults to 50px.
/// </summary>
[Parameter]
public int Width { get; set; } = 50;
/// <summary>
/// Boolean indicating whether to add padding to the icon. Defaults to false.
/// </summary>
[Parameter]
public bool Padding { get; set; }
/// <summary>
/// The data URL of the favicon.
/// </summary>
private string? _faviconDataUrl;
/// <inheritdoc />
protected override void OnParametersSet()
{
if (FaviconBytes is not null)
{
string mimeType = DetectMimeType(FaviconBytes);
string base64String = Convert.ToBase64String(FaviconBytes);
_faviconDataUrl = $"data:{mimeType};base64,{base64String}";
}
}
/// <summary>
/// Detect the mime type of the favicon.
/// </summary>
/// <param name="bytes">The bytes of the favicon.</param>
/// <returns>The mime type of the favicon.</returns>
private static string DetectMimeType(byte[] bytes)
{
// Check for SVG.
if (bytes.Length >= 5)
{
string header = System.Text.Encoding.ASCII.GetString(bytes.Take(5).ToArray()).ToLower();
if (header.Contains("<?xml") || header.Contains("<svg"))
{
return "image/svg+xml";
}
}
// Check for ICO.
if (bytes.Length >= 4 &&
bytes[0] == 0x00 && bytes[1] == 0x00 &&
bytes[2] == 0x01 && bytes[3] == 0x00)
{
return "image/x-icon";
}
// Check for PNG.
if (bytes.Length >= 4 &&
bytes[0] == 0x89 && bytes[1] == 0x50 &&
bytes[2] == 0x4E && bytes[3] == 0x47)
{
return "image/png";
}
// Default to x-icon if unknown.
return "image/x-icon";
}
}

View File

@@ -1,37 +0,0 @@
@using System.Text.RegularExpressions
@using Microsoft.Extensions.Localization
@inject IStringLocalizerFactory LocalizerFactory
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-xl font-semibold dark:text-white">@SharedLocalizer["Notes"]</h3>
<div class="dark:text-gray-300">
@((MarkupString)ConvertUrlsToLinks(Notes).Replace(Environment.NewLine, "<br>"))
</div>
</div>
@code {
private IStringLocalizer SharedLocalizer => LocalizerFactory.Create("SharedResources", "AliasVault.Client");
/// <summary>
/// The notes to display.
/// </summary>
[Parameter]
public string Notes { get; set; } = "";
private static string ConvertUrlsToLinks(string text)
{
// HTML-encode the text before processing URLs
var encodedText = System.Web.HttpUtility.HtmlEncode(text);
string urlPattern = @"(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})";
return Regex.Replace(encodedText, urlPattern, match =>
{
string url = match.Value;
if (!url.StartsWith("http://") && !url.StartsWith("https://"))
{
url = "http://" + url;
}
return $"<a href=\"{url}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"text-blue-500 hover:underline\">{match.Value}</a>";
}, RegexOptions.None, TimeSpan.FromMilliseconds(200));
}
}

View File

@@ -8,7 +8,12 @@
<SortableTableRow Class="cursor-pointer" OnClick="@(() => NavigateToCredential(credential.Id))">
<SortableTableColumn Padding="false">
<div class="flex items-center space-x-2 py-2 pl-2">
<DisplayFavicon FaviconBytes="@credential.Logo" Width="24" />
<ItemIcon
ItemType="@credential.ItemType"
Logo="@credential.Logo"
CardNumber="@credential.CardNumber"
AltText="@credential.Service"
SizeClass="w-6 h-6" />
<span class="font-bold ml-2">@credential.Service</span>
</div>
</SortableTableColumn>

View File

@@ -69,7 +69,12 @@
<div
class="search-result @(i == SelectedIndex ? "bg-gray-100 dark:bg-gray-700" : "") px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center"
@onclick="() => SelectResult(result)">
<DisplayFavicon FaviconBytes="@result.Logo?.FileData" Width="24" />
<ItemIcon
ItemType="@result.ItemType"
Logo="@result.Logo?.FileData"
CardNumber="@ItemService.GetFieldValue(result, FieldKey.CardNumber)"
AltText="@(result.Name ?? string.Empty)"
SizeClass="w-6 h-6" />
<div class="ml-2">
<div class="font-medium text-gray-900 dark:text-white">
@if (result.FolderId.HasValue && FolderPathCache.TryGetValue(result.FolderId.Value, out var folderPath))

View File

@@ -14,9 +14,9 @@
@using AliasVault.Client.Main.Components
@using AliasVault.Client.Main.Components.Alerts
@using AliasVault.Client.Main.Components.Attachments
@using AliasVault.Client.Main.Components.Credentials
@using AliasVault.Client.Main.Components.Email
@using AliasVault.Client.Main.Components.Forms
@using AliasVault.Client.Main.Components.Items
@using AliasVault.Client.Main.Components.Layout
@using AliasVault.Client.Main.Components.Loading
@using AliasVault.Client.Main.Components.Settings

View File

@@ -1165,10 +1165,6 @@ video {
width: 6rem;
}
.w-28 {
width: 7rem;
}
.w-3 {
width: 0.75rem;
}
@@ -3959,11 +3955,6 @@ video {
--tw-ring-color: rgb(153 27 27 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-red-900:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-offset-gray-800:focus:is(.dark *) {
--tw-ring-offset-color: #1f2937;
}
@@ -3991,10 +3982,6 @@ video {
margin-bottom: 2rem;
}
.sm\:mb-0 {
margin-bottom: 0px;
}
.sm\:ml-1 {
margin-left: 0.25rem;
}
@@ -4354,10 +4341,6 @@ video {
margin-bottom: 0.5rem;
}
.xl\:mb-4 {
margin-bottom: 1rem;
}
.xl\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
@@ -4381,10 +4364,6 @@ video {
.\32xl\:col-span-2 {
grid-column: span 2 / span 2;
}
.\32xl\:mb-0 {
margin-bottom: 0px;
}
}
.\[\&\>svg\]\:h-full>svg {

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB