Updated UI. Fixed circle search performance.
@@ -1,143 +1,206 @@
|
||||
using JSMR.Application.Circles.Queries.Search;
|
||||
using JSMR.Application.Common.Search;
|
||||
using JSMR.Domain.Entities;
|
||||
using JSMR.Infrastructure.Common.Queries;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace JSMR.Infrastructure.Data.Repositories.Circles;
|
||||
|
||||
public class CircleSearchProvider(AppDbContext context) : SearchProvider<CircleSearchItem, CircleSearchCriteria, CircleSortField, CircleSearchItem>, ICircleSearchProvider
|
||||
public class CircleQuery
|
||||
{
|
||||
protected override IQueryable<CircleSearchItem> GetBaseQuery()
|
||||
public required Circle Circle { get; init; }
|
||||
}
|
||||
|
||||
public class CircleSearchProvider(AppDbContext context) : SearchProvider<CircleSearchItem, CircleSearchCriteria, CircleSortField, CircleQuery>, ICircleSearchProvider
|
||||
{
|
||||
protected override IQueryable<CircleQuery> GetBaseQuery()
|
||||
{
|
||||
// Project from Circles so we can use correlated subqueries per CircleId.
|
||||
var q =
|
||||
from c in context.Circles.AsNoTracking()
|
||||
select new CircleSearchItem
|
||||
select new CircleQuery
|
||||
{
|
||||
CircleId = c.CircleId,
|
||||
Name = c.Name,
|
||||
MakerId = c.MakerId,
|
||||
Favorite = c.Favorite,
|
||||
Blacklisted = c.Blacklisted,
|
||||
Spam = c.Spam,
|
||||
|
||||
// Aggregates
|
||||
Downloads = context.VoiceWorks
|
||||
.Where(v => v.CircleId == c.CircleId)
|
||||
.Select(v => (int?)v.Downloads) // make nullable for Sum over empty set
|
||||
.Sum() ?? 0,
|
||||
|
||||
Releases = context.VoiceWorks
|
||||
.Count(v => v.CircleId == c.CircleId && v.SalesDate != null),
|
||||
|
||||
Pending = context.VoiceWorks
|
||||
.Count(v => v.CircleId == c.CircleId && v.ExpectedDate != null),
|
||||
|
||||
FirstReleaseDate = context.VoiceWorks
|
||||
.Where(v => v.CircleId == c.CircleId)
|
||||
.Select(v => v.SalesDate)
|
||||
.Min(),
|
||||
|
||||
LatestReleaseDate = context.VoiceWorks
|
||||
.Where(v => v.CircleId == c.CircleId)
|
||||
.Select(v => v.SalesDate)
|
||||
.Max(),
|
||||
|
||||
// "Latest" by ProductId length, then value
|
||||
LatestProductId = context.VoiceWorks
|
||||
.Where(v => v.CircleId == c.CircleId)
|
||||
.OrderByDescending(v => v.ProductId.Length)
|
||||
.ThenByDescending(v => v.ProductId)
|
||||
.Select(v => v.ProductId)
|
||||
.FirstOrDefault(),
|
||||
|
||||
// If you want these two in base query too:
|
||||
LatestVoiceWorkHasImage = context.VoiceWorks
|
||||
.Where(v => v.CircleId == c.CircleId)
|
||||
.OrderByDescending(v => v.ProductId.Length)
|
||||
.ThenByDescending(v => v.ProductId)
|
||||
.Select(v => (bool?)v.HasImage)
|
||||
.FirstOrDefault(),
|
||||
|
||||
LatestVoiceWorkSalesDate = context.VoiceWorks
|
||||
.Where(v => v.CircleId == c.CircleId)
|
||||
.OrderByDescending(v => v.ProductId.Length)
|
||||
.ThenByDescending(v => v.ProductId)
|
||||
.Select(v => v.SalesDate)
|
||||
.FirstOrDefault()
|
||||
Circle = c
|
||||
};
|
||||
|
||||
return q;
|
||||
}
|
||||
|
||||
protected override IQueryable<CircleSearchItem> ApplyFilters(IQueryable<CircleSearchItem> query, CircleSearchCriteria criteria)
|
||||
//protected override IQueryable<CircleSearchItem> GetBaseQuery()
|
||||
//{
|
||||
// // Project from Circles so we can use correlated subqueries per CircleId.
|
||||
// var q =
|
||||
// from c in context.Circles.AsNoTracking()
|
||||
// select new CircleSearchItem
|
||||
// {
|
||||
// CircleId = c.CircleId,
|
||||
// Name = c.Name,
|
||||
// MakerId = c.MakerId,
|
||||
// Favorite = c.Favorite,
|
||||
// Blacklisted = c.Blacklisted,
|
||||
// Spam = c.Spam,
|
||||
|
||||
// // Aggregates
|
||||
// Downloads = context.VoiceWorks
|
||||
// .Where(v => v.CircleId == c.CircleId)
|
||||
// .Select(v => (int?)v.Downloads) // make nullable for Sum over empty set
|
||||
// .Sum() ?? 0,
|
||||
|
||||
// Releases = context.VoiceWorks
|
||||
// .Count(v => v.CircleId == c.CircleId && v.SalesDate != null),
|
||||
|
||||
// Pending = context.VoiceWorks
|
||||
// .Count(v => v.CircleId == c.CircleId && v.ExpectedDate != null),
|
||||
|
||||
// FirstReleaseDate = context.VoiceWorks
|
||||
// .Where(v => v.CircleId == c.CircleId)
|
||||
// .Select(v => v.SalesDate)
|
||||
// .Min(),
|
||||
|
||||
// LatestReleaseDate = context.VoiceWorks
|
||||
// .Where(v => v.CircleId == c.CircleId)
|
||||
// .Select(v => v.SalesDate)
|
||||
// .Max(),
|
||||
|
||||
// // "Latest" by ProductId length, then value
|
||||
// LatestProductId = context.VoiceWorks
|
||||
// .Where(v => v.CircleId == c.CircleId)
|
||||
// .OrderByDescending(v => v.ProductId.Length)
|
||||
// .ThenByDescending(v => v.ProductId)
|
||||
// .Select(v => v.ProductId)
|
||||
// .FirstOrDefault(),
|
||||
|
||||
// // If you want these two in base query too:
|
||||
// LatestVoiceWorkHasImage = context.VoiceWorks
|
||||
// .Where(v => v.CircleId == c.CircleId)
|
||||
// .OrderByDescending(v => v.ProductId.Length)
|
||||
// .ThenByDescending(v => v.ProductId)
|
||||
// .Select(v => (bool?)v.HasImage)
|
||||
// .FirstOrDefault(),
|
||||
|
||||
// LatestVoiceWorkSalesDate = context.VoiceWorks
|
||||
// .Where(v => v.CircleId == c.CircleId)
|
||||
// .OrderByDescending(v => v.ProductId.Length)
|
||||
// .ThenByDescending(v => v.ProductId)
|
||||
// .Select(v => v.SalesDate)
|
||||
// .FirstOrDefault()
|
||||
// };
|
||||
|
||||
// return q;
|
||||
//}
|
||||
|
||||
protected override IQueryable<CircleQuery> ApplyFilters(IQueryable<CircleQuery> query, CircleSearchCriteria criteria)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(criteria.Name))
|
||||
{
|
||||
var term = $"%{criteria.Name.Trim()}%";
|
||||
|
||||
query = query.Where(x =>
|
||||
EF.Functions.Like(x.Name, term) ||
|
||||
EF.Functions.Like(x.MakerId, term));
|
||||
EF.Functions.Like(x.Circle.Name, term) ||
|
||||
EF.Functions.Like(x.Circle.MakerId, term));
|
||||
}
|
||||
|
||||
switch (criteria.Status)
|
||||
{
|
||||
case CircleStatus.NotBlacklisted:
|
||||
query = query.Where(x => !x.Blacklisted);
|
||||
query = query.Where(x => !x.Circle.Blacklisted);
|
||||
break;
|
||||
case CircleStatus.Favorited:
|
||||
query = query.Where(x => x.Favorite);
|
||||
query = query.Where(x => x.Circle.Favorite);
|
||||
break;
|
||||
case CircleStatus.Blacklisted:
|
||||
query = query.Where(x => x.Blacklisted);
|
||||
query = query.Where(x => x.Circle.Blacklisted);
|
||||
break;
|
||||
case CircleStatus.Spam:
|
||||
query = query.Where(x => x.Spam);
|
||||
query = query.Where(x => x.Circle.Spam);
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
protected override Expression<Func<CircleSearchItem, object>> GetSortExpression(CircleSortField field) => field switch
|
||||
protected override Expression<Func<CircleQuery, object>> GetSortExpression(CircleSortField field) => field switch
|
||||
{
|
||||
CircleSortField.Favorite => x => !x.Favorite,
|
||||
CircleSortField.Blacklisted => x => !x.Blacklisted,
|
||||
CircleSortField.Spam => x => !x.Spam,
|
||||
_ => x => x.Name
|
||||
CircleSortField.Favorite => x => !x.Circle.Favorite,
|
||||
CircleSortField.Blacklisted => x => !x.Circle.Blacklisted,
|
||||
CircleSortField.Spam => x => !x.Circle.Spam,
|
||||
_ => x => x.Circle.Name
|
||||
};
|
||||
|
||||
protected override IEnumerable<(Expression<Func<CircleSearchItem, object>> Selector, SortDirection Dir)> GetDefaultSortChain()
|
||||
protected override IEnumerable<(Expression<Func<CircleQuery, object>> Selector, SortDirection Dir)> GetDefaultSortChain()
|
||||
{
|
||||
yield return (x => x.Name, SortDirection.Ascending);
|
||||
yield return (x => x.MakerId, SortDirection.Ascending);
|
||||
yield return (x => x.Circle.Name, SortDirection.Ascending);
|
||||
yield return (x => x.Circle.MakerId, SortDirection.Ascending);
|
||||
}
|
||||
|
||||
protected override IQueryable<CircleSearchItem> GetSelectQuery(IOrderedQueryable<CircleSearchItem> query)
|
||||
protected override IQueryable<CircleSearchItem> GetSelectQuery(IOrderedQueryable<CircleQuery> query)
|
||||
{
|
||||
// Join to VoiceWorks by LatestProductId to fill HasImage / SalesDate
|
||||
var selected =
|
||||
from item in query
|
||||
join vw in context.VoiceWorks.AsNoTracking() on item.LatestProductId equals vw.ProductId into vws
|
||||
from latest in vws.DefaultIfEmpty()
|
||||
//join vw in context.VoiceWorks.AsNoTracking() on item.LatestProductId equals vw.ProductId into vws
|
||||
//from latest in vws.DefaultIfEmpty()
|
||||
select new CircleSearchItem
|
||||
{
|
||||
CircleId = item.CircleId,
|
||||
Name = item.Name,
|
||||
MakerId = item.MakerId,
|
||||
Favorite = item.Favorite,
|
||||
Blacklisted = item.Blacklisted,
|
||||
Spam = item.Spam,
|
||||
Downloads = item.Downloads,
|
||||
Releases = item.Releases,
|
||||
Pending = item.Pending,
|
||||
FirstReleaseDate = item.FirstReleaseDate,
|
||||
LatestReleaseDate = item.LatestReleaseDate,
|
||||
LatestProductId = item.LatestProductId,
|
||||
LatestVoiceWorkHasImage = latest != null ? latest.HasImage : (bool?)null,
|
||||
LatestVoiceWorkSalesDate = latest != null ? latest.SalesDate : null
|
||||
CircleId = item.Circle.CircleId,
|
||||
Name = item.Circle.Name,
|
||||
MakerId = item.Circle.MakerId,
|
||||
Favorite = item.Circle.Favorite,
|
||||
Blacklisted = item.Circle.Blacklisted,
|
||||
Spam = item.Circle.Spam,
|
||||
// Aggregates
|
||||
Downloads = context.VoiceWorks
|
||||
.Where(v => v.CircleId == item.Circle.CircleId)
|
||||
.Select(v => (int?)v.Downloads) // make nullable for Sum over empty set
|
||||
.Sum() ?? 0,
|
||||
|
||||
Releases = context.VoiceWorks
|
||||
.Count(v => v.CircleId == item.Circle.CircleId && v.SalesDate != null),
|
||||
|
||||
Pending = context.VoiceWorks
|
||||
.Count(v => v.CircleId == item.Circle.CircleId && v.ExpectedDate != null),
|
||||
|
||||
FirstReleaseDate = context.VoiceWorks
|
||||
.Where(v => v.CircleId == item.Circle.CircleId)
|
||||
.Select(v => v.SalesDate)
|
||||
.Min(),
|
||||
|
||||
LatestReleaseDate = context.VoiceWorks
|
||||
.Where(v => v.CircleId == item.Circle.CircleId)
|
||||
.Select(v => v.SalesDate)
|
||||
.Max(),
|
||||
|
||||
// "Latest" by ProductId length, then value
|
||||
LatestProductId = context.VoiceWorks
|
||||
.Where(v => v.CircleId == item.Circle.CircleId)
|
||||
.OrderByDescending(v => v.ProductId.Length)
|
||||
.ThenByDescending(v => v.ProductId)
|
||||
.Select(v => v.ProductId)
|
||||
.FirstOrDefault(),
|
||||
|
||||
// If you want these two in base query too:
|
||||
LatestVoiceWorkHasImage = context.VoiceWorks
|
||||
.Where(v => v.CircleId == item.Circle.CircleId)
|
||||
.OrderByDescending(v => v.ProductId.Length)
|
||||
.ThenByDescending(v => v.ProductId)
|
||||
.Select(v => (bool?)v.HasImage)
|
||||
.FirstOrDefault(),
|
||||
|
||||
LatestVoiceWorkSalesDate = context.VoiceWorks
|
||||
.Where(v => v.CircleId == item.Circle.CircleId)
|
||||
.OrderByDescending(v => v.ProductId.Length)
|
||||
.ThenByDescending(v => v.ProductId)
|
||||
.Select(v => v.SalesDate)
|
||||
.FirstOrDefault(),
|
||||
//Downloads = item.Downloads,
|
||||
//Releases = item.Releases,
|
||||
//Pending = item.Pending,
|
||||
//FirstReleaseDate = item.FirstReleaseDate,
|
||||
//LatestReleaseDate = item.LatestReleaseDate,
|
||||
//LatestProductId = item.LatestProductId,
|
||||
//LatestVoiceWorkHasImage = latest != null ? latest.HasImage : (bool?)null,
|
||||
//LatestVoiceWorkSalesDate = latest != null ? latest.SalesDate : null
|
||||
};
|
||||
|
||||
return selected;
|
||||
|
||||
84
JSMR.UI.Blazor/Components/JImage.razor
Normal file
@@ -0,0 +1,84 @@
|
||||
<div class="@ContainerClassees">
|
||||
<div class="j-image-overlay"></div>
|
||||
<img class="@ImageClasses" loading="@LoadingAttribute" src="@Source" @onload="OnImageLoaded">
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public required string Source { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string FallbackSource { get; set; } = "images/home/no_img_main.gif";
|
||||
|
||||
[Parameter]
|
||||
public bool LazyLoading { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public string? ContainerClass { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? ImageClass { get; set; }
|
||||
|
||||
private bool _isLoaded;
|
||||
private string? _lastSource;
|
||||
|
||||
private string ContainerClassees => GetContainerClasses();
|
||||
private string ImageClasses => GetImageClasses();
|
||||
|
||||
private string? LoadingAttribute => LazyLoading ? "lazy" : null;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (!string.Equals(_lastSource, Source, StringComparison.Ordinal))
|
||||
{
|
||||
_lastSource = Source;
|
||||
_isLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetContainerClasses()
|
||||
{
|
||||
List<string> classNames = ["j-image-container"];
|
||||
|
||||
if (!string.IsNullOrEmpty(ContainerClass))
|
||||
{
|
||||
List<string> customClassNames = ContainerClass
|
||||
.Split(" ")
|
||||
.Select(className => className.Trim())
|
||||
.Where(className => !string.IsNullOrWhiteSpace(className))
|
||||
.ToList();
|
||||
|
||||
classNames.AddRange(customClassNames);
|
||||
}
|
||||
|
||||
return string.Join(" ", classNames);
|
||||
}
|
||||
|
||||
private string GetImageClasses()
|
||||
{
|
||||
List<string> classNames = ["j-image"];
|
||||
|
||||
if (!_isLoaded)
|
||||
{
|
||||
classNames.Add("j-lazy-load");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ImageClass))
|
||||
{
|
||||
List<string> customClassNames = ImageClass
|
||||
.Split(" ")
|
||||
.Select(className => className.Trim())
|
||||
.Where(className => !string.IsNullOrWhiteSpace(className))
|
||||
.ToList();
|
||||
|
||||
classNames.AddRange(customClassNames);
|
||||
}
|
||||
|
||||
return string.Join(" ", classNames);
|
||||
}
|
||||
|
||||
private void OnImageLoaded()
|
||||
{
|
||||
_isLoaded = true;
|
||||
}
|
||||
}
|
||||
37
JSMR.UI.Blazor/Components/JPagination.razor
Normal file
@@ -0,0 +1,37 @@
|
||||
<div class="pagination">
|
||||
<div>
|
||||
<label>@IndexInfo</label>
|
||||
</div>
|
||||
<MudPagination ShowFirstButton="true" ShowLastButton="true" Count="@((int)Math.Ceiling((decimal)TotalItems / (decimal)PageSize))" Selected="@PageNumber" SelectedChanged="OnSelectedChanged" />
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public int PageNumber { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<int> PageNumberChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int PageSize { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<int> PageSizeChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int TotalItems { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<int> TotalItemsChanged { get; set; }
|
||||
|
||||
public string IndexInfo => TotalItems == 0 ? "No items" : $"{StartIndex.ToString("n0")} - {EndIndex.ToString("n0")} of {TotalItems.ToString("n0")} items";
|
||||
|
||||
public int StartIndex => (PageNumber - 1) * PageSize + 1;
|
||||
public int EndIndex => PageNumber * PageSize < TotalItems ? PageNumber * PageSize : TotalItems;
|
||||
|
||||
private async Task OnSelectedChanged(int newPage)
|
||||
{
|
||||
PageNumber = newPage;
|
||||
await PageNumberChanged.InvokeAsync(newPage);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,265 @@
|
||||
@page "/circles"
|
||||
@using JSMR.Application.Circles.Queries.Search
|
||||
@using JSMR.Application.Common.Search
|
||||
@using JSMR.UI.Blazor.Components
|
||||
@using JSMR.UI.Blazor.Services
|
||||
@inject VoiceWorksClient Client
|
||||
@inject IJSRuntime JS
|
||||
@inject HttpClient Http
|
||||
|
||||
<PageTitle>Circles</PageTitle>
|
||||
|
||||
<h1>Circles</h1>
|
||||
|
||||
<p>This component demonstrates fetching data from the server.</p>
|
||||
<div class="search-filter-control-container">
|
||||
<div class="search-filter-control-span-3">
|
||||
<MudTextField @bind-Value="Keywords" Placeholder="Circle Name or Maker Id" Immediate="true" DebounceInterval="500" Variant="Variant.Outlined" Margin="Margin.Dense" Adornment="@Adornment.End" AdornmentIcon="@Icons.Material.Outlined.Search" />
|
||||
</div>
|
||||
<div class="search-filter-control-span-1">
|
||||
<MudSelect @bind-Value="SelectedCircleStatus" Placeholder="Circle Status" Dense="true" Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||
<MudSelectItem Value="@CircleStatus.NotBlacklisted.ToString()">Not Blacklisted</MudSelectItem>
|
||||
<MudSelectItem Value="@CircleStatus.Favorited.ToString()">Favorite</MudSelectItem>
|
||||
<MudSelectItem Value="@CircleStatus.Blacklisted.ToString()">Blacklisted</MudSelectItem>
|
||||
<MudSelectItem Value="@CircleStatus.Spam.ToString()">Spam</MudSelectItem>
|
||||
<MudSelectItem Value="@String.Empty">All</MudSelectItem>
|
||||
</MudSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@* @if (forecasts == null)
|
||||
@if (searchResults is null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
<p>Loading…</p>
|
||||
}
|
||||
else if (searchResults.Items.Length == 0)
|
||||
{
|
||||
<p>No results.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th aria-label="Temperature in Celsius">Temp. (C)</th>
|
||||
<th aria-label="Temperature in Farenheit">Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
} *@
|
||||
<MudDataGrid Items="@searchResults.Items" Style="table-layout: fixed">
|
||||
<Columns>
|
||||
<TemplateColumn HeaderStyle="width: 12em">
|
||||
<CellTemplate>
|
||||
<JImage ContainerClass="j-circle-image-container-mini" ImageClass="j-circle-image-mini" Source="@ImageUrlProvider.GetImageURL(context.Item.LatestProductId, context.Item.LatestVoiceWorkHasImage ?? false, context.Item.LatestVoiceWorkSalesDate, "main")" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.Name" Title="Name" />
|
||||
<PropertyColumn Property="x => x.MakerId" Title="Maker Id" HeaderStyle="width: 12em" />
|
||||
<TemplateColumn Title="Status" HeaderStyle="width: 10em">
|
||||
<CellTemplate>
|
||||
@if (context.Item.Favorite)
|
||||
{
|
||||
<MudChip T="string" Label="true" Color="Color.Info" Style="width: 100%">Favorite</MudChip>
|
||||
}
|
||||
else if (context.Item.Blacklisted)
|
||||
{
|
||||
<MudChip T="string" Label="true" Color="Color.Warning" Style="color:black; width: 100%">Blacklisted</MudChip>
|
||||
}
|
||||
else if (context.Item.Spam)
|
||||
{
|
||||
<MudChip T="string" Label="true" Color="Color.Error" Style="width: 100%">Spam</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Label="true" Style="width: 100%">Normal</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="Rating" HeaderStyle="width: 8em">
|
||||
<CellTemplate>
|
||||
@if (context.Item.Releases > 0)
|
||||
{
|
||||
<div class="circle-star-container">
|
||||
<div class="circle-star @GetStarRatingClass(context.Item)"></div>
|
||||
</div>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Property="x => x.Downloads" Title="Downloads" HeaderStyle="width: 12em" />
|
||||
<PropertyColumn Property="x => x.Releases" Title="Releases" HeaderStyle="width: 12em" />
|
||||
<PropertyColumn Property="x => x.Pending" Title="Pending" HeaderStyle="width: 12em" />
|
||||
<TemplateColumn Title="First Release Date" HeaderStyle="width: 14em">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.FirstReleaseDate?.ToString("MMMM d, yyyy")</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="Latest Release Date" HeaderStyle="width: 14em">
|
||||
<CellTemplate>
|
||||
<MudText>@context.Item.LatestReleaseDate?.ToString("MMMM d, yyyy")</MudText>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
|
||||
@* <div class="items-container circle-items-container">
|
||||
@foreach (var item in searchResults.Items)
|
||||
{
|
||||
<MudPaper Outlined="true">
|
||||
<JImage @key="item.CircleId" ContainerClass="j-circle-image-container" ImageClass="j-circle-image" Source="@ImageUrlProvider.GetImageURL(item.LatestProductId, item.LatestVoiceWorkHasImage ?? false, item.LatestVoiceWorkSalesDate, "main")" />
|
||||
<MudText Typo="Typo.h6">@item.Name</MudText>
|
||||
<div>@item.Releases</div>
|
||||
<div>@item.Pending</div>
|
||||
|
||||
@if (item.Releases > 0)
|
||||
{
|
||||
<div class="circle-star-container">
|
||||
<div class="circle-star @GetStarRatingClass(item)"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>@item.Downloads</div>
|
||||
<MudChip T="string" Variant="Variant.Outlined" Icon="@Icons.Material.Filled.Download" Color="Color.Info">@item.Downloads.ToString("n0")</MudChip>
|
||||
<div>@item.FirstReleaseDate?.ToString("MMMM d, yyyy")</div>
|
||||
<div>@item.LatestReleaseDate?.ToString("MMMM d, yyyy")</div>
|
||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Favorite" Color="@(item.Favorite? Color.Secondary: Color.Dark)" />
|
||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Block" Color="@(item.Blacklisted? Color.Secondary: Color.Dark)" />
|
||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.RestoreFromTrash" Color="@(item.Spam? Color.Secondary: Color.Dark)" />
|
||||
</MudPaper>
|
||||
}
|
||||
</div> *@
|
||||
|
||||
<JPagination @bind-PageNumber="PageNumber" @bind-PageSize="pageSize" @bind-TotalItems="searchResults.TotalItems" />
|
||||
@* <MudPagination ShowFirstButton="true" ShowLastButton="true" Count="@((int)Math.Ceiling((decimal)searchResults.TotalItems / (decimal)100))" @bind-Selected="@PageNumber" Class="j-pager" /> *@
|
||||
}
|
||||
|
||||
<style>
|
||||
.mud-table-root {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.j-pager {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
background: var(--mud-palette-background);
|
||||
padding: .5em 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
// private WeatherForecast[]? forecasts;
|
||||
private string? keywords;
|
||||
|
||||
// protected override async Task OnInitializedAsync()
|
||||
// {
|
||||
// forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json");
|
||||
// }
|
||||
public string? Keywords
|
||||
{
|
||||
get { return keywords; }
|
||||
set
|
||||
{
|
||||
keywords = value;
|
||||
_ = UpdateDataAsync(true);
|
||||
}
|
||||
}
|
||||
|
||||
// public class WeatherForecast
|
||||
// {
|
||||
// public DateOnly Date { get; set; }
|
||||
private string circleStatus = string.Empty;
|
||||
|
||||
// public int TemperatureC { get; set; }
|
||||
public string SelectedCircleStatus
|
||||
{
|
||||
get { return circleStatus; }
|
||||
set
|
||||
{
|
||||
circleStatus = value;
|
||||
_ = UpdateDataAsync(true);
|
||||
}
|
||||
}
|
||||
|
||||
// public string? Summary { get; set; }
|
||||
private int pageNumber = 1;
|
||||
|
||||
// public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
// }
|
||||
public int PageNumber
|
||||
{
|
||||
get { return pageNumber; }
|
||||
set
|
||||
{
|
||||
pageNumber = value;
|
||||
_ = UpdateDataAsync(false);
|
||||
}
|
||||
}
|
||||
|
||||
int pageSize = 100;
|
||||
SearchResult<CircleSearchItem>? searchResults;
|
||||
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
_ = LoadCirclesAsync();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task LoadCirclesAsync()
|
||||
{
|
||||
SearchCirclesRequest request = new(
|
||||
Options: new()
|
||||
{
|
||||
PageNumber = PageNumber,
|
||||
PageSize = pageSize,
|
||||
Criteria = new()
|
||||
{
|
||||
Name = Keywords,
|
||||
Status = string.IsNullOrWhiteSpace(SelectedCircleStatus) == false ? Enum.Parse<CircleStatus>(SelectedCircleStatus) : null
|
||||
},
|
||||
SortOptions =
|
||||
[
|
||||
new(CircleSortField.Name, Application.Common.Search.SortDirection.Ascending)
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
|
||||
var result = await Client.SearchAsync(request);
|
||||
|
||||
searchResults = result?.Results ?? new();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task UpdateDataAsync(bool resetPageNumber)
|
||||
{
|
||||
if (resetPageNumber)
|
||||
pageNumber = 1;
|
||||
|
||||
await LoadCirclesAsync();
|
||||
}
|
||||
|
||||
private string GetStarRatingClass(CircleSearchItem item)
|
||||
{
|
||||
double averageDownloads = item.Downloads / item.Releases;
|
||||
|
||||
if (averageDownloads < 100)
|
||||
{
|
||||
return "circle-star-poor";
|
||||
}
|
||||
|
||||
if (averageDownloads < 250)
|
||||
{
|
||||
return "circle-star-below-average";
|
||||
}
|
||||
|
||||
if (averageDownloads < 500)
|
||||
{
|
||||
return "circle-star-average";
|
||||
}
|
||||
|
||||
if (averageDownloads < 1000)
|
||||
{
|
||||
return "circle-star-above-average";
|
||||
}
|
||||
|
||||
if (averageDownloads < 2000)
|
||||
{
|
||||
return "circle-star-popular";
|
||||
}
|
||||
|
||||
if (averageDownloads < 4000)
|
||||
{
|
||||
return "circle-star-super-popular";
|
||||
}
|
||||
|
||||
if (averageDownloads < 8000)
|
||||
{
|
||||
return "circle-star-ultra-popular";
|
||||
}
|
||||
|
||||
return "circle-star-god-tier";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,122 @@
|
||||
@page "/creators"
|
||||
@inject VoiceWorksClient Client
|
||||
@inject IJSRuntime JS
|
||||
@using JSMR.Application.Common.Search
|
||||
@using JSMR.Application.Creators.Queries.Search
|
||||
@using JSMR.Application.Creators.Queries.Search.Contracts
|
||||
@using JSMR.UI.Blazor.Services
|
||||
|
||||
<PageTitle>Creators</PageTitle>
|
||||
|
||||
<h1>Creators</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
<MudTextField @bind-Value="Keywords" Immediate="true" DebounceInterval="500" Label="Filter" Variant="Variant.Text" Adornment="@Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search" />
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
@if (searchResults is null)
|
||||
{
|
||||
<p>Loading…</p>
|
||||
}
|
||||
else if (searchResults.Items.Length == 0)
|
||||
{
|
||||
<p>No results.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid Items="@searchResults.Items" Style="table-layout: fixed">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Name" Title="Name" />
|
||||
<PropertyColumn Property="x => x.VoiceWorkCount" Title="Voice Works" HeaderStyle="width: 14em" />
|
||||
<TemplateColumn Title="Favorite" HeaderStyle="width: 8em">
|
||||
<CellTemplate>
|
||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Favorite" Color="@(context.Item.Favorite? Color.Secondary: Color.Dark)" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="Blacklisted" HeaderStyle="width: 8em">
|
||||
<CellTemplate>
|
||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Block" Color="@(context.Item.Blacklisted? Color.Secondary: Color.Dark)" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
|
||||
<MudPagination ShowFirstButton="true" ShowLastButton="true" Count="@((int)Math.Ceiling((decimal)searchResults.TotalItems / (decimal)100))" @bind-Selected="@PageNumber" Class="j-pager" />
|
||||
}
|
||||
|
||||
<style>
|
||||
.mud-table-root {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.j-pager {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
background: var(--mud-palette-background);
|
||||
padding: .5em 1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
private string? keywords;
|
||||
|
||||
private void IncrementCount()
|
||||
public string? Keywords
|
||||
{
|
||||
currentCount++;
|
||||
get { return keywords; }
|
||||
set
|
||||
{
|
||||
keywords = value;
|
||||
_ = LoadTagsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int pageNumber = 1;
|
||||
|
||||
public int PageNumber
|
||||
{
|
||||
get { return pageNumber; }
|
||||
set
|
||||
{
|
||||
pageNumber = value;
|
||||
_ = LoadTagsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
int pageSize = 100;
|
||||
SearchResult<CreatorSearchItem>? searchResults;
|
||||
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
_ = LoadTagsAsync();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task LoadTagsAsync()
|
||||
{
|
||||
SearchCreatorsRequest request = new(
|
||||
Options: new()
|
||||
{
|
||||
PageNumber = PageNumber,
|
||||
PageSize = pageSize,
|
||||
Criteria = new()
|
||||
{
|
||||
Name = Keywords
|
||||
},
|
||||
SortOptions =
|
||||
[
|
||||
new(CreatorSortField.Name, Application.Common.Search.SortDirection.Ascending)
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
|
||||
var result = await Client.SearchAsync(request);
|
||||
|
||||
searchResults = result?.Results ?? new();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
@page "/"
|
||||
@inject VoiceWorksClient Client
|
||||
@using JSMR.Application.VoiceWorks.Queries.Search
|
||||
@using JSMR.UI.Blazor.Services
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
@page "/tags"
|
||||
@inject VoiceWorksClient Client
|
||||
@inject IJSRuntime JS
|
||||
@using JSMR.Application.Common.Search
|
||||
@using JSMR.Application.Tags.Queries.Search
|
||||
@using JSMR.Application.Tags.Queries.Search.Contracts
|
||||
@using JSMR.UI.Blazor.Services
|
||||
|
||||
<PageTitle>Tags</PageTitle>
|
||||
|
||||
<h1>Tags</h1>
|
||||
|
||||
<MudTextField @bind-Value="Keywords" Immediate="true" DebounceInterval="500" Label="Filter" Variant="Variant.Text" Adornment="@Adornment.Start" AdornmentIcon="@Icons.Material.Outlined.Search" />
|
||||
|
||||
@if (searchResults is null)
|
||||
{
|
||||
<p>Loading…</p>
|
||||
@@ -18,13 +22,21 @@ else if (searchResults.Items.Length == 0)
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDataGrid Items="@searchResults.Items">
|
||||
<MudDataGrid Items="@searchResults.Items" Style="table-layout: fixed">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Name" Title="Name" />
|
||||
<PropertyColumn Property="x => x.EnglishName" Title="English Name" />
|
||||
<PropertyColumn Property="x => x.VoiceWorkCount" Title="Voice Works" />
|
||||
<PropertyColumn Property="x => x.Favorite" Title="Favorite" />
|
||||
<PropertyColumn Property="x => x.Blacklisted" Title="Blacklisted" />
|
||||
<PropertyColumn Property="x => x.VoiceWorkCount" Title="Voice Works" HeaderStyle="width: 14em" />
|
||||
<TemplateColumn Title="Favorite" HeaderStyle="width: 8em">
|
||||
<CellTemplate>
|
||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Favorite" Color="@(context.Item.Favorite ? Color.Secondary : Color.Dark)" @onclick="@(e => IncrementCount(context.Item))" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="Blacklisted" HeaderStyle="width: 8em">
|
||||
<CellTemplate>
|
||||
<MudIconButton Size="@Size.Small" Icon="@Icons.Material.Filled.Block" Color="@(context.Item.Blacklisted? Color.Secondary: Color.Dark)" />
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
</MudDataGrid>
|
||||
|
||||
@@ -32,6 +44,10 @@ else
|
||||
}
|
||||
|
||||
<style>
|
||||
.mud-table-root {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.j-pager {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
@@ -45,6 +61,18 @@ else
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private string? keywords;
|
||||
|
||||
public string? Keywords
|
||||
{
|
||||
get { return keywords; }
|
||||
set
|
||||
{
|
||||
keywords = value;
|
||||
_ = LoadTagsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private int pageNumber = 1;
|
||||
|
||||
public int PageNumber
|
||||
@@ -76,7 +104,7 @@ else
|
||||
PageSize = pageSize,
|
||||
Criteria = new()
|
||||
{
|
||||
|
||||
Name = Keywords
|
||||
},
|
||||
SortOptions =
|
||||
[
|
||||
@@ -85,6 +113,7 @@ else
|
||||
}
|
||||
);
|
||||
|
||||
await JS.InvokeVoidAsync("pageHelpers.scrollToTop");
|
||||
var result = await Client.SearchAsync(request);
|
||||
|
||||
searchResults = result?.Results ?? new();
|
||||
@@ -92,4 +121,8 @@ else
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private void IncrementCount(TagSearchItem item)
|
||||
{
|
||||
//item.Favorite = !item.Favorite;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@page "/voiceworks"
|
||||
@using JSMR.Application.VoiceWorks.Queries.Search
|
||||
@using JSMR.UI.Blazor.Services
|
||||
@inject VoiceWorksClient Client
|
||||
|
||||
<PageTitle>Voice Works</PageTitle>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
using JSMR.Application.Tags.Queries.Search;
|
||||
using JSMR.Application.VoiceWorks.Queries.Search;
|
||||
using JSMR.UI.Blazor;
|
||||
using JSMR.UI.Blazor.Services;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MudBlazor.Services;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
@@ -21,28 +18,4 @@ builder.Services.AddMudServices();
|
||||
|
||||
builder.Services.AddScoped<VoiceWorksClient>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
public class VoiceWorksClient(HttpClient http)
|
||||
{
|
||||
public async Task<SearchVoiceWorksResponse?> SearchAsync(SearchVoiceWorksRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/voiceworks/search", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<SearchVoiceWorksResponse>(cancellationToken: ct);
|
||||
}
|
||||
|
||||
public async Task<SearchTagsResponse?> SearchAsync(SearchTagsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/tags/search", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<SearchTagsResponse>(cancellationToken: ct);
|
||||
}
|
||||
}
|
||||
|
||||
public record VoiceWorkDto(string ProductId, string ProductName);
|
||||
|
||||
// Tiny helper result type
|
||||
public readonly record struct Result<T>(bool Ok, T? Value, string? Error)
|
||||
{
|
||||
public static Result<T> Success(T v) => new(true, v, null);
|
||||
public static Result<T> Failure(string e) => new(false, default, e);
|
||||
}
|
||||
await builder.Build().RunAsync();
|
||||
78
JSMR.UI.Blazor/Services/ImageUrlProvider.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
namespace JSMR.UI.Blazor.Services;
|
||||
|
||||
public static class ImageUrlProvider
|
||||
{
|
||||
public static string GetImageURL(string? productId, bool hasImage, DateTime? salesDate, string size)
|
||||
{
|
||||
string folder = "modpub";
|
||||
string imageSize = "main";
|
||||
|
||||
string imageWorkType = productId != null ? productId.StartsWith("RJ") ? "doujin" : "professional" : "doujin";
|
||||
|
||||
switch (size)
|
||||
{
|
||||
case "small":
|
||||
imageSize = "sam";
|
||||
folder = "modpub";
|
||||
break;
|
||||
case "300x300":
|
||||
imageSize = hasImage ? "main_300x300" : "main";
|
||||
folder = "resize";
|
||||
break;
|
||||
case "240x":
|
||||
//imageSize = hasImage ? imageWorkType == "doujin" ? "main_240x240" : "sam_170x" : "main";
|
||||
imageSize = hasImage ? imageWorkType == "doujin" ? "main_240x240" : "main_240x240" : "main";
|
||||
folder = "resize";
|
||||
break;
|
||||
case "main":
|
||||
default:
|
||||
imageSize = "main";
|
||||
folder = "modpub";
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasImage == false || productId == null)
|
||||
{
|
||||
string noImageUrlTemplate = "/images/web/home/no_img_[imageSize].gif";
|
||||
string noImageUrl = noImageUrlTemplate.Replace("[imageSize]", imageSize);
|
||||
|
||||
return noImageUrl;
|
||||
}
|
||||
|
||||
var imageUrlTemplate = "//img.dlsite.jp/[folder]/images2/[imageType1]/[imageWorkType]/[fullRoundedProductId]/[productId][imageType2]_img_[imageSize].jpg";
|
||||
|
||||
var productIdWithoutPrefixString = productId.Substring(2);
|
||||
int productIdWithoutPrefix = Convert.ToInt32(productId.Substring(2));
|
||||
|
||||
string productIdPrefix = productId.Substring(0, 2);
|
||||
|
||||
double something = (double)((productIdWithoutPrefix / 1000) * 1000);
|
||||
int roundedProductId = (int)Math.Round(Math.Ceiling((double)productIdWithoutPrefix / 1000) * 1000);
|
||||
|
||||
//string actualRoundedProductId = ("000000" + roundedProductId.ToString()).Substring(roundedProductId.ToString().Length);
|
||||
//string fullRoundedProductId = productIdPrefix + actualRoundedProductId;
|
||||
|
||||
var productIdWithPrefixStringLength = productIdWithoutPrefixString.Length;
|
||||
var zeroPadLength = productIdWithPrefixStringLength - roundedProductId.ToString().Length;
|
||||
|
||||
var fullRoundedProductId = productIdPrefix.PadRight(productIdPrefix.Length + zeroPadLength, '0') + roundedProductId;
|
||||
|
||||
bool hasSalesDate = salesDate.HasValue;
|
||||
|
||||
string imageType1 = hasSalesDate ? "work" : "ana";
|
||||
string imageType2 = hasSalesDate ? "" : "_ana";
|
||||
|
||||
string productLinkPage = salesDate.HasValue ? "work" : "announce";
|
||||
|
||||
string imageUrl = imageUrlTemplate
|
||||
.Replace("[folder]", folder)
|
||||
.Replace("[imageType1]", imageType1)
|
||||
.Replace("[imageWorkType]", imageWorkType)
|
||||
.Replace("[fullRoundedProductId]", fullRoundedProductId)
|
||||
.Replace("[productId]", productId)
|
||||
.Replace("[imageType2]", imageType2)
|
||||
.Replace("[imageSize]", imageSize);
|
||||
|
||||
return imageUrl;
|
||||
}
|
||||
}
|
||||
34
JSMR.UI.Blazor/Services/VoiceWorksClient.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using JSMR.Application.Circles.Queries.Search;
|
||||
using JSMR.Application.Creators.Queries.Search;
|
||||
using JSMR.Application.Tags.Queries.Search;
|
||||
using JSMR.Application.VoiceWorks.Queries.Search;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace JSMR.UI.Blazor.Services;
|
||||
|
||||
public class VoiceWorksClient(HttpClient http)
|
||||
{
|
||||
public async Task<SearchVoiceWorksResponse?> SearchAsync(SearchVoiceWorksRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/voiceworks/search", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<SearchVoiceWorksResponse>(cancellationToken: ct);
|
||||
}
|
||||
|
||||
public async Task<SearchCirclesResponse?> SearchAsync(SearchCirclesRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/circles/search", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<SearchCirclesResponse>(cancellationToken: ct);
|
||||
}
|
||||
|
||||
public async Task<SearchCreatorsResponse?> SearchAsync(SearchCreatorsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/creators/search", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<SearchCreatorsResponse>(cancellationToken: ct);
|
||||
}
|
||||
|
||||
public async Task<SearchTagsResponse?> SearchAsync(SearchTagsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
using var resp = await http.PostAsJsonAsync("/api/tags/search", request, ct);
|
||||
return await resp.Content.ReadFromJsonAsync<SearchTagsResponse>(cancellationToken: ct);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
/*font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;*/
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
@@ -111,4 +111,197 @@ code {
|
||||
|
||||
.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
/* Custom */
|
||||
|
||||
/* Search Filters */
|
||||
.search-filter-control-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-column-gap: 16px;
|
||||
grid-row-gap: 12px;
|
||||
background-color: #0f2031;
|
||||
padding: 12px;
|
||||
border: 1px solid #304562;
|
||||
margin-bottom: 24px
|
||||
}
|
||||
|
||||
.search-filter-control-container > .search-filter-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-filter-control-container > .search-filter-control-span-2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.search-filter-control-container > .search-filter-control-span-3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
.search-filter-control-container > .search-filter-control-span-4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-column: span 4;
|
||||
}
|
||||
|
||||
.items-container {
|
||||
display: grid;
|
||||
grid-column-gap: 1.2em;
|
||||
grid-row-gap: 1.2em;
|
||||
}
|
||||
|
||||
.circle-items-container {
|
||||
grid-template-columns: repeat(auto-fill,minmax(300px,1fr));
|
||||
}
|
||||
|
||||
.j-image-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.j-image-overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
background-color: #000;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.j-image.j-lazy-load {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.j-image[loading="lazy"] {
|
||||
transition: .5s linear;
|
||||
}
|
||||
|
||||
/* Pagination */
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 0px;
|
||||
bottom: 0;
|
||||
background: var(--background-color);
|
||||
padding: .5rem 0;
|
||||
margin: 0;
|
||||
position: sticky;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Circle */
|
||||
.j-circle-image-container {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.j-circle-image-container-mini {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.j-circle-image {
|
||||
display: block;
|
||||
min-width: 200px;
|
||||
width: 226px;
|
||||
height: 170px;
|
||||
object-fit: cover;
|
||||
background-color: black;
|
||||
border: 1px solid #949494;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.j-circle-image-mini {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
background-color: black;
|
||||
border: 1px solid #949494;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Cirlce Stars */
|
||||
.circle-star-container {
|
||||
mask-image: url("../svg/star-fill.svg");
|
||||
mask-size: auto;
|
||||
align-self: center;
|
||||
mask-repeat: repeat-x;
|
||||
background: rgb(180,200, 214);
|
||||
height: 16px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.circle-star {
|
||||
mask-image: url("../svg/star-fill.svg");
|
||||
mask-size: auto;
|
||||
align-self: center;
|
||||
mask-repeat: repeat-x;
|
||||
height: 16px;
|
||||
background: linear-gradient(rgb(252,224,43), rgb(249,179,2));
|
||||
}
|
||||
|
||||
.circle-star-poor {
|
||||
width: 16px;
|
||||
width: 8px;
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
.circle-star-below-average {
|
||||
width: 16px;
|
||||
background-color: orange;
|
||||
}
|
||||
|
||||
.circle-star-average {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.circle-star-above-average {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.circle-star-popular {
|
||||
width: 64px;
|
||||
}
|
||||
|
||||
.circle-star-super-popular {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.circle-star-ultra-popular {
|
||||
width: 80px;
|
||||
background: linear-gradient(to bottom,#4dbdd4 0,#3398ac 100%);
|
||||
}
|
||||
|
||||
.circle-star-god-tier {
|
||||
width: 80px;
|
||||
background: linear-gradient(to bottom,#8963d3 0,#6640b3 100%);
|
||||
}
|
||||
|
||||
.star_rating.mini::before {
|
||||
height: 16px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.star_rating.mini::before {
|
||||
background: url(assets/images/web/common/icon_star_rating_02.png) no-repeat 0 0;
|
||||
background-position-x: 0px;
|
||||
background-position-y: 0px;
|
||||
background-size: auto;
|
||||
background-size: auto 32px;
|
||||
}
|
||||
|
||||
.star_rating.mini.star_45::before {
|
||||
background-position: 0 -16px;
|
||||
}
|
||||
22
JSMR.UI.Blazor/wwwroot/css/jsmr-mud-blazor.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.mud-input {
|
||||
background-color: var(--input-background-color);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.mud-input-control {
|
||||
border-color: var(--input-border-color);
|
||||
}
|
||||
|
||||
.mud-input > input.mud-input-root,
|
||||
div.mud-input-slot.mud-input-root {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.mud-input > input.mud-input-root-outlined.mud-input-root-margin-dense,
|
||||
div.mud-input-slot.mud-input-root-outlined.mud-input-root-margin-dense {
|
||||
padding: .25rem .5rem;
|
||||
}
|
||||
|
||||
.mud-typography {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
9
JSMR.UI.Blazor/wwwroot/css/theme-base.css
Normal file
@@ -0,0 +1,9 @@
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--primary-text-color);
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
24
JSMR.UI.Blazor/wwwroot/css/theme-frozen.css
Normal file
@@ -0,0 +1,24 @@
|
||||
:root {
|
||||
--font-family: 'Poppins';
|
||||
--background-color: rgb(16, 36, 50);
|
||||
--input-background-color: rgb(0,20,34);
|
||||
--input-border-color: #304562;
|
||||
--primary-text-color: rgb(180,200, 214);
|
||||
--product-container-background-color: rgb(39,59,73);
|
||||
--product-container-box-shadow: 1px 1px 4px rgba(0,0,0,.5);
|
||||
--product-title-text-color: rgb(200,220,234);
|
||||
--image-overlay-opacity: 0.9;
|
||||
--tag-background-color: #415664;
|
||||
--tag-text-color: rgb(210,220,230);
|
||||
--tag-border: none;
|
||||
--tag-border-radius: 0.8em;
|
||||
--tag-padding: 0.3em 1em;
|
||||
--tag-margin: 1em 0.5em 0 0;
|
||||
--tag-hover-background-color: #596f7e;
|
||||
--tag-hover-text-color: rgb(240,250,254);
|
||||
--tag-hover-border: none;
|
||||
--product-footer-background-color: rgb(0,20,34);
|
||||
--product-footer-text-color: rgb(220,230,234);
|
||||
--expected-date-text-color: #ffe073;
|
||||
--planned-date-text-color: #73bdff;
|
||||
}
|
||||
BIN
JSMR.UI.Blazor/wwwroot/images/web/common/download.png
Normal file
|
After Width: | Height: | Size: 234 B |
BIN
JSMR.UI.Blazor/wwwroot/images/web/common/icon_sale_status_01.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
JSMR.UI.Blazor/wwwroot/images/web/common/icon_star_rating_01.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
JSMR.UI.Blazor/wwwroot/images/web/common/icon_star_rating_02.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
JSMR.UI.Blazor/wwwroot/images/web/home/no_img_main.gif
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
JSMR.UI.Blazor/wwwroot/images/web/home/no_img_sam.gif
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
@@ -16,7 +16,13 @@
|
||||
<!-- Important: Increment the version parameter whenever you update MudBlazor to prevent caching issues -->
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js?v=1"></script>
|
||||
|
||||
<script src="js/site.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="css/jsmr-mud-blazor.css" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link rel="stylesheet" href="css/theme-base.css" />
|
||||
<link rel="stylesheet" href="css/theme-frozen.css" />
|
||||
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<link href="JSMR.UI.Blazor.styles.css" rel="stylesheet" />
|
||||
<link href="manifest.webmanifest" rel="manifest" />
|
||||
|
||||
5
JSMR.UI.Blazor/wwwroot/js/site.js
Normal file
@@ -0,0 +1,5 @@
|
||||
window.pageHelpers = {
|
||||
scrollToTop: () => {
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
1
JSMR.UI.Blazor/wwwroot/svg/flag-cn.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 30 20"><defs><path id="a" d="M0-1L.588.809-.952-.309H.952L-.588.809z" fill="#FF0"/></defs><path fill="#EE1C25" d="M0 0h30v20H0z"/><use xlink:href="#a" transform="matrix(3 0 0 3 5 5)"/><use xlink:href="#a" transform="rotate(23.036 .093 25.536)"/><use xlink:href="#a" transform="rotate(45.87 1.273 16.18)"/><use xlink:href="#a" transform="rotate(69.945 .996 12.078)"/><use xlink:href="#a" transform="rotate(20.66 -19.689 31.932)"/></svg>
|
||||
|
After Width: | Height: | Size: 531 B |
11
JSMR.UI.Blazor/wwwroot/svg/flag-jp.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-jp" viewBox="0 0 640 480">
|
||||
<defs>
|
||||
<clipPath id="a">
|
||||
<path fill-opacity=".7" d="M-88 32h640v480H-88z"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g fill-rule="evenodd" stroke-width="1pt" clip-path="url(#a)" transform="translate(88 -32)">
|
||||
<path fill="#fff" d="M-128 32h720v480h-720z"/>
|
||||
<circle cx="523.1" cy="344.1" r="194.9" fill="#bc002d" transform="translate(-168.4 8.6) scale(.76554)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 464 B |
1
JSMR.UI.Blazor/wwwroot/svg/flag-kr.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="-36 -24 72 48"><path fill="#fff" d="M-36-24h72v48h-72z"/><g transform="rotate(-56.31)"><g id="b"><path id="a" d="M-6-25H6m-12 3H6m-12 3H6" stroke="#000" stroke-width="2"/><use xlink:href="#a" y="44"/></g><path stroke="#fff" d="M0 17v10"/><circle fill="#cd2e3a" r="12"/><path fill="#0047a0" d="M0-12A6 6 0 000 0a6 6 0 010 12 12 12 0 010-24z"/></g><g transform="rotate(-123.69)"><use xlink:href="#b"/><path stroke="#fff" d="M0-23.5v3M0 17v3.5m0 3v3"/></g></svg>
|
||||
|
After Width: | Height: | Size: 551 B |
1
JSMR.UI.Blazor/wwwroot/svg/flag-us.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 7410 3900"><path fill="#b22234" d="M0 0h7410v3900H0z"/><path d="M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0" stroke="#fff" stroke-width="300"/><path fill="#3c3b6e" d="M0 0h2964v2100H0z"/><g fill="#fff"><g id="d"><g id="c"><g id="e"><g id="b"><path id="a" d="M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"/><use xlink:href="#a" y="420"/><use xlink:href="#a" y="840"/><use xlink:href="#a" y="1260"/></g><use xlink:href="#a" y="1680"/></g><use xlink:href="#b" x="247" y="210"/></g><use xlink:href="#c" x="494"/></g><use xlink:href="#d" x="988"/><use xlink:href="#c" x="1976"/><use xlink:href="#e" x="2470"/></g></svg>
|
||||
|
After Width: | Height: | Size: 741 B |
3
JSMR.UI.Blazor/wwwroot/svg/headphones.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-headphones" viewBox="0 0 16 16">
|
||||
<path d="M8 3a5 5 0 0 0-5 5v1h1a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V8a6 6 0 1 1 12 0v5a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-3a1 1 0 0 1 1-1h1V8a5 5 0 0 0-5-5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 304 B |
3
JSMR.UI.Blazor/wwwroot/svg/heart-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-heart-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M8 1.314C12.438-3.248 23.534 4.735 8 15-7.534 4.736 3.562-3.248 8 1.314z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 244 B |
3
JSMR.UI.Blazor/wwwroot/svg/heart.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-heart" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M8 2.748l-.717-.737C5.6.281 2.514.878 1.4 3.053c-.523 1.023-.641 2.5.314 4.385.92 1.815 2.834 3.989 6.286 6.357 3.452-2.368 5.365-4.542 6.286-6.357.955-1.886.838-3.362.314-4.385C13.486.878 10.4.28 8.717 2.01L8 2.748zM8 15C-7.333 4.868 3.279-3.04 7.824 1.143c.06.055.119.112.176.171a3.12 3.12 0 0 1 .176-.17C12.72-3.042 23.333 4.867 8 15z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
4
JSMR.UI.Blazor/wwwroot/svg/radioactive.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-radioactive" viewBox="0 0 16 16">
|
||||
<path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1ZM0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Z"/>
|
||||
<path d="M9.653 5.496A2.986 2.986 0 0 0 8 5c-.61 0-1.179.183-1.653.496L4.694 2.992A5.972 5.972 0 0 1 8 2c1.222 0 2.358.365 3.306.992L9.653 5.496Zm1.342 2.324a2.986 2.986 0 0 1-.884 2.312 3.01 3.01 0 0 1-.769.552l1.342 2.683c.57-.286 1.09-.66 1.538-1.103a5.986 5.986 0 0 0 1.767-4.624l-2.994.18Zm-5.679 5.548 1.342-2.684A3 3 0 0 1 5.005 7.82l-2.994-.18a6 6 0 0 0 3.306 5.728ZM10 8a2 2 0 1 1-4 0 2 2 0 0 1 4 0Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 633 B |
3
JSMR.UI.Blazor/wwwroot/svg/shield-fill-x.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-shield-fill-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M8 .5c-.662 0-1.77.249-2.813.525a61.11 61.11 0 0 0-2.772.815 1.454 1.454 0 0 0-1.003 1.184c-.573 4.197.756 7.307 2.368 9.365a11.192 11.192 0 0 0 2.417 2.3c.371.256.715.451 1.007.586.27.124.558.225.796.225s.527-.101.796-.225c.292-.135.636-.33 1.007-.586a11.191 11.191 0 0 0 2.418-2.3c1.611-2.058 2.94-5.168 2.367-9.365a1.454 1.454 0 0 0-1.003-1.184 61.09 61.09 0 0 0-2.772-.815C9.77.749 8.663.5 8 .5zM6.854 6.146a.5.5 0 1 0-.708.708L7.293 8 6.146 9.146a.5.5 0 1 0 .708.708L8 8.707l1.146 1.147a.5.5 0 0 0 .708-.708L8.707 8l1.147-1.146a.5.5 0 0 0-.708-.708L8 7.293 6.854 6.146z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 749 B |
4
JSMR.UI.Blazor/wwwroot/svg/shield-x.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-shield-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M5.443 1.991a60.17 60.17 0 0 0-2.725.802.454.454 0 0 0-.315.366C1.87 7.056 3.1 9.9 4.567 11.773c.736.94 1.533 1.636 2.197 2.093.333.228.626.394.857.5.116.053.21.089.282.11A.73.73 0 0 0 8 14.5c.007-.001.038-.005.097-.023.072-.022.166-.058.282-.111.23-.106.525-.272.857-.5a10.197 10.197 0 0 0 2.197-2.093C12.9 9.9 14.13 7.056 13.597 3.159a.454.454 0 0 0-.315-.366c-.626-.2-1.682-.526-2.725-.802C9.491 1.71 8.51 1.5 8 1.5c-.51 0-1.49.21-2.557.491zm-.256-.966C6.23.749 7.337.5 8 .5c.662 0 1.77.249 2.813.525a61.09 61.09 0 0 1 2.772.815c.528.168.926.623 1.003 1.184.573 4.197-.756 7.307-2.367 9.365a11.191 11.191 0 0 1-2.418 2.3 6.942 6.942 0 0 1-1.007.586c-.27.124-.558.225-.796.225s-.526-.101-.796-.225a6.908 6.908 0 0 1-1.007-.586 11.192 11.192 0 0 1-2.417-2.3C2.167 10.331.839 7.221 1.412 3.024A1.454 1.454 0 0 1 2.415 1.84a61.11 61.11 0 0 1 2.772-.815z"/>
|
||||
<path fill-rule="evenodd" d="M6.146 6.146a.5.5 0 0 1 .708 0L8 7.293l1.146-1.147a.5.5 0 1 1 .708.708L8.707 8l1.147 1.146a.5.5 0 0 1-.708.708L8 8.707 6.854 9.854a.5.5 0 0 1-.708-.708L7.293 8 6.146 6.854a.5.5 0 0 1 0-.708z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
JSMR.UI.Blazor/wwwroot/svg/star-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-star-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.283.95l-3.523 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 401 B |
3
JSMR.UI.Blazor/wwwroot/svg/star-half.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-star-half" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M5.354 5.119L7.538.792A.516.516 0 0 1 8 .5c.183 0 .366.097.465.292l2.184 4.327 4.898.696A.537.537 0 0 1 16 6.32a.55.55 0 0 1-.17.445l-3.523 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256a.519.519 0 0 1-.146.05c-.341.06-.668-.254-.6-.642l.83-4.73L.173 6.765a.55.55 0 0 1-.171-.403.59.59 0 0 1 .084-.302.513.513 0 0 1 .37-.245l4.898-.696zM8 12.027c.08 0 .16.018.232.056l3.686 1.894-.694-3.957a.564.564 0 0 1 .163-.505l2.906-2.77-4.052-.576a.525.525 0 0 1-.393-.288L8.002 2.223 8 2.226v9.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 672 B |
3
JSMR.UI.Blazor/wwwroot/svg/star.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-star" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.523-3.356c.329-.314.158-.888-.283-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767l-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288l1.847-3.658 1.846 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.564.564 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 664 B |
3
JSMR.UI.Blazor/wwwroot/svg/status-active.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-reception-4" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-8zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-11z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 484 B |
3
JSMR.UI.Blazor/wwwroot/svg/status-barely-active.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-reception-2" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4 5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 436 B |
3
JSMR.UI.Blazor/wwwroot/svg/status-dead.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-reception-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5zm4 0a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 414 B |
3
JSMR.UI.Blazor/wwwroot/svg/status-mildly-active.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-reception-3" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 11.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-5zm4-3a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-8zm4 8a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 458 B |
4
JSMR.UI.Blazor/wwwroot/svg/subtitles.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-badge-cc" viewBox="0 0 16 16">
|
||||
<path d="M3.708 7.755c0-1.111.488-1.753 1.319-1.753.681 0 1.138.47 1.186 1.107H7.36V7c-.052-1.186-1.024-2-2.342-2C3.414 5 2.5 6.05 2.5 7.751v.747c0 1.7.905 2.73 2.518 2.73 1.314 0 2.285-.792 2.342-1.939v-.114H6.213c-.048.615-.496 1.05-1.186 1.05-.84 0-1.319-.62-1.319-1.727v-.743zm6.14 0c0-1.111.488-1.753 1.318-1.753.682 0 1.139.47 1.187 1.107H13.5V7c-.053-1.186-1.024-2-2.342-2C9.554 5 8.64 6.05 8.64 7.751v.747c0 1.7.905 2.73 2.518 2.73 1.314 0 2.285-.792 2.342-1.939v-.114h-1.147c-.048.615-.497 1.05-1.187 1.05-.839 0-1.318-.62-1.318-1.727v-.743z"/>
|
||||
<path d="M14 3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h12zM2 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H2z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 845 B |
3
JSMR.UI.Blazor/wwwroot/svg/trash-fill.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-trash-fill" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1H2.5zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5zM8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5zm3 .5a.5.5 0 0 0-1 0v7a.5.5 0 0 0 1 0v-7z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
4
JSMR.UI.Blazor/wwwroot/svg/trash.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-trash" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4L4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 575 B |