添加项目文件。

This commit is contained in:
2025-02-25 22:28:49 +08:00
parent 07e26cc93e
commit 1a7bdb585a
32 changed files with 1601 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
namespace MegghysAPI.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public class AutoInitAttribute : Attribute
{
public AutoInitAttribute()
{
}
public AutoInitAttribute(Action postInit)
{
PostInit = postInit;
}
public AutoInitAttribute(string log)
{
LogMessage = log;
}
/// <summary>
/// 越低越优先
/// </summary>
public int Order { get; set; } = 10;
public string LogMessage { get; set; }
public Action PostInit { get; set; }
public bool Async { get; set; } = false;
}
}

View File

@@ -0,0 +1,13 @@
namespace MegghysAPI.Attributes
{
[AttributeUsage(AttributeTargets.Method)]
public class AutoTimerAttribute : Attribute
{
/// <summary>
/// 单位为s
/// </summary>
public int Time { get; set; } = 30;
public bool CallOnRegister { get; set; } = false;
public bool Log { get; set; } = true;
}
}

24
Components/App.razor Normal file
View File

@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["MegghysAPI.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<HeadOutlet />
<script src="_content/Microsoft.FluentUI.AspNetCore.Components/js/loading-theme.js" type="text/javascript"></script>
<loading-theme storage-name="theme"></loading-theme>
</head>
<body>
<Routes />
<FluentDesignTheme StorageName="theme" />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
@inherits LayoutComponentBase
<FluentLayout>
<FluentHeader>
MegghysAPI
</FluentHeader>
<FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%">
<NavMenu />
<FluentBodyContent Class="body-content">
<div class="content">
@Body
</div>
</FluentBodyContent>
</FluentStack>
<FluentFooter>
<a href="https://www.fluentui-blazor.net" target="_blank">Documentation and demos</a>
<FluentSpacer />
<a href="https://learn.microsoft.com/en-us/aspnet/core/blazor" target="_blank">About Blazor</a>
</FluentFooter>
</FluentLayout>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>

View File

@@ -0,0 +1,20 @@
@rendermode InteractiveServer
<div class="navmenu">
<input type="checkbox" title="Menu expand/collapse toggle" id="navmenu-toggle" class="navmenu-icon" />
<label for="navmenu-toggle" class="navmenu-icon"><FluentIcon Value="@(new Icons.Regular.Size20.Navigation())" Color="Color.Fill" /></label>
<nav class="sitenav" aria-labelledby="main-menu">
<FluentNavMenu Id="main-menu" Collapsible="true" Width="250" Title="Navigation menu" @bind-Expanded="expanded" CustomToggle="true">
<FluentNavLink Href="/" Match="NavLinkMatch.All" Icon="@(new Icons.Regular.Size20.Home())" IconColor="Color.Accent">Home</FluentNavLink>
<FluentNavLink Href="pixiv" Icon="@(new Icons.Regular.Size20.NumberSymbolSquare())" IconColor="Color.Accent">Pixiv</FluentNavLink>
<FluentNavLink Href="weather" Icon="@(new Icons.Regular.Size20.WeatherPartlyCloudyDay())" IconColor="Color.Accent">Weather</FluentNavLink>
</FluentNavMenu>
</nav>
<FluentDesignTheme @bind-Mode="@Mode" @bind-OfficeColor="@OfficeColor" StorageName="theme" />
</div>
@code {
private bool expanded = true;
public DesignThemeModes Mode { get; set; }
public OfficeColor? OfficeColor { get; set; }
}

View File

@@ -0,0 +1,21 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<div role="status" style="padding-bottom: 1em;">
Current count: <FluentBadge Appearance="Appearance.Neutral">@currentCount</FluentBadge>
</div>
<FluentButton Appearance="Appearance.Accent" @onclick="IncrementCount">Click me</FluentButton>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new Fluent Blazor app.

View File

@@ -0,0 +1,43 @@
@page "/pixiv"
@rendermode InteractiveServer
<h3>Pixiv</h3>
<FluentButton OnClick="RandomGet">
随机获取
</FluentButton>
<FluentDivider />
<br/>
<FluentStack Orientation="Orientation.Vertical" HorizontalAlignment="HorizontalAlignment.Center">
@foreach (var (index, img) in CurrentImgs.S3URL.Index())
{
@if(index == 0)
{
<img src="@img.Large" loading="lazy" referrerpolicy="no-referrer" onload="" />
}
else
{
<img src="@img.Large" loading="lazy" referrerpolicy="no-referrer" />
}
}
</FluentStack>
@code {
public Modules.PixivFavoriteDownloader.Pixiv.PixivImgInfo CurrentImgs;
protected override void OnInitialized()
{
base.OnInitialized();
RandomGet();
}
public void FirstImgLoaded()
{
}
public void RandomGet()
{
CurrentImgs = Modules.PixivFavoriteDownloader.Favorites.OrderByRandom().First();
}
}

View File

@@ -0,0 +1,43 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
<!-- This page is rendered in SSR mode, so the FluentDataGrid component does not offer any interactivity (like sorting). -->
<FluentDataGrid Id="weathergrid" Items="@forecasts" GridTemplateColumns="1fr 1fr 1fr 2fr" Loading="@(forecasts == null)" Style="height:204px;" TGridItem="WeatherForecast">
<PropertyColumn Title="Date" Property="@(c => c!.Date)" Align="Align.Start"/>
<PropertyColumn Title="Temp. (C)" Property="@(c => c!.TemperatureC)" Align="Align.Center"/>
<PropertyColumn Title="Temp. (F)" Property="@(c => c!.TemperatureF)" Align="Align.Center"/>
<PropertyColumn Title="Summary" Property="@(c => c!.Summary)" Align="Align.End"/>
</FluentDataGrid>
@code {
private IQueryable<WeatherForecast>? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).AsQueryable();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

6
Components/Routes.razor Normal file
View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

15
Components/_Imports.razor Normal file
View File

@@ -0,0 +1,15 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.FluentUI.AspNetCore.Components
@using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons
@using Microsoft.JSInterop
@using MegghysAPI
@using MegghysAPI.Components
@using MegghysAPI.Entities
@using static MegghysAPI.Datas
@using Masuit.Tools

67
Config.cs Normal file
View File

@@ -0,0 +1,67 @@
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Unicode;
namespace MegghysAPI
{
public class Config
{
public static readonly JsonSerializerOptions DefaultSerializerOptions = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
};
private static Config _oldInstance = new();
private static Config? _instance;
public static Config Instance { get { _instance ??= Load(); return _instance; } }
public static string ConfigPath => Path.Combine(Datas.DataPath, "Config.json");
static bool _first = true;
public static Config Load()
{
if (File.Exists(ConfigPath))
{
try
{
var config = Newtonsoft.Json.JsonConvert.DeserializeObject<Config>(File.ReadAllText(ConfigPath))!;
_oldInstance = config;
if (_first)
config.Save();
return _oldInstance;
}
catch
{
Console.WriteLine($"配置文件读取失败");
return _oldInstance!;
}
}
else
{
var config = new Config();
config.Save();
return config;
}
}
public static void Reload()
{
_oldInstance = _instance;
_instance = null;
}
public void Save()
{
if (!Directory.Exists(Datas.DataPath))
Directory.CreateDirectory(Datas.DataPath);
File.WriteAllText(ConfigPath, JsonSerializer.Serialize(this, DefaultSerializerOptions));
}
public bool LogDanmakuError { get; set; } = false;
public string ListenHost { get; set; } = "*";
public int ListenPort { get; set; } = 4050;
public string DBConnectString { get; set; } = $"Host=localhost;Port=5432;Username=postgres;Password=!Hzy05152484; Database=MegghysAPI;";
public string MinIOEndpoint { get; set; } = "eternalland.top:9000";
public string MinIOAccessKey { get; set; } = "RBzbElm21lf7sy7wK7wG";
public string MinIOSecretKey { get; set; } = "Nko5azOSUiYgOUeLsj8hLxGz4cKC8XOcH0VS7lWq";
public string MinIORegion { get; set; } = "cn-main";
public string MinIOBucket { get; set; } = "general";
}
}

View File

@@ -0,0 +1,91 @@
using Microsoft.AspNetCore.Mvc;
using Minio.DataModel.Result;
namespace MegghysAPI.Controllers
{
public interface IResponseResult
{
public string Message { get; set; }
public int Code { get; set; }
public object? Data { get; set; }
}
public class ResponseResult : IResponseResult
{
public ResponseResult()
{
}
public ResponseResult(object? data, int code = 200, string msg = "成功")
{
Message = msg;
Code = code;
Data = data;
}
public string Message { get; set; } = "";
public int Code { get; set; } = 0;
public object? Data { get; set; } = new();
}
public class ResponseResult<T>
{
public ResponseResult()
{
}
public ResponseResult(T? data, int code = 200, string msg = "成功")
{
Message = msg;
Code = code;
Data = data;
}
public string Message { get; set; } = "";
public int Code { get; set; } = 0;
public T? Data { get; set; } = default;
public static implicit operator ResponseResult<T>(T value)
{
return new(value);
}
public static implicit operator ResponseResult(ResponseResult<T> value)
{
return new(value.Data, value.Code, value.Message);
}
public static implicit operator ResponseResult<T>(ResponseResult result)
=> new(result.Data is null ? default : (T)result.Data, result.Code, result.Message);
}
public class PaginationResponse<T> : ResponseResult<T>
{
public PaginationResponse(T data, int pn, int ps, long total)
{
Code = 200;
Message = "成功";
Data = data;
Ps = ps;
Pn = pn;
Total = total;
More = ps * pn < total;
}
public int Ps { get; set; }
public int Pn { get; set; }
public long Total { get; set; }
public bool More { get; set; }
public static implicit operator PaginationResponse<T>(ResponseResult result)
=> new(result.Data is null ? default : (T)result.Data, -1, -1, -1);
}
public class MControllerBase : Controller
{
protected HttpContext context => ControllerContext.HttpContext;
protected ResponseResult<T> ResponseOK<T>(T? data, string msg = "成功")
=> new(data, 200, msg);
protected ResponseResult ResponseOKWithoutData(string msg = "成功")
=> new(null, 200, msg);
protected ResponseResult ResponseUnauthorized(string msg = "未认证")
=> new(null, 401, msg);
protected ResponseResult ResponseForbidden(string msg = "无权限")
=> new(null, 403, msg);
protected ResponseResult ResponseNotFound(string msg = "未找到")
=> new(null, 404, msg);
protected ResponseResult ResponseInternalError(string msg = "内部错误")
=> new(null, 500, msg);
protected ResponseResult ResponseBadRequest(string msg = "无效操作")
=> new(null, 400, msg);
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.AspNetCore.Mvc;
namespace MegghysAPI.Controllers
{
[Route("api/public")]
[ApiController]
public class PublicController : MControllerBase
{
[Route("header")]
public IActionResult Header()
{
return Ok(string.Join(",", context.Request.Headers.Keys));
}
}
}

141
Core/FileManager.cs Normal file
View File

@@ -0,0 +1,141 @@
using MegghysAPI.Attributes;
using Minio;
using Minio.DataModel.Args;
namespace MegghysAPI.Core
{
internal class FileManager
{
private static IMinioClient minio = new MinioClient()
.WithEndpoint(Config.Instance.MinIOEndpoint)
.WithRegion(Config.Instance.MinIORegion)
.WithCredentials(Config.Instance.MinIOAccessKey, Config.Instance.MinIOSecretKey)
.WithSSL(false)
.Build();
public const string FavoritePath = "/Files/Favorite/";
[AutoInit("初始化文件服务")]
public static void Init()
{
}
public static async Task<string> DownloadFileAsync(string fileName, string path)
{
try
{
var temp = Path.Combine(Datas.TempFilePath, fileName);
if (File.Exists(temp))
return temp;
var result = await minio.GetObjectAsync(new Minio.DataModel.Args.GetObjectArgs()
.WithBucket(Config.Instance.MinIOBucket)
.WithObject(fileName)
.WithCallbackStream(stream =>
{
// 将下载流保存为本地文件
using var fileStream = File.Create(temp);
stream.CopyTo(fileStream);
}));
return temp;
}
catch (Exception ex)
{
Logs.Warn($"未能从储存空间下载文件: {path}{fileName}\r\n{ex}");
return null;
}
}
public static bool UploadBytes(byte[] data, string remotePath, string contentType = "application/octet-stream", bool overwrite = false)
{
using var stream = new MemoryStream(data);
return UploadStream(stream, remotePath, contentType, overwrite: overwrite);
}
public static async Task<bool> UploadStreamAsync(Stream stream, string remotePath, string contentType = "application/octet-stream", bool overwrite = false)
{
try
{
if (!overwrite && await ContainsFileAsync(remotePath))
return true;
// Upload a file to bucket.
var putObjectArgs = new PutObjectArgs()
.WithBucket(Config.Instance.MinIOBucket)
.WithStreamData(stream)
.WithObjectSize(stream.Length)
.WithObject(remotePath)
.WithContentType(contentType);
await minio.PutObjectAsync(putObjectArgs);
return true;
}
catch (Exception ex)
{
Logs.Warn($"未能向储存空间路径上传文件流 {remotePath}\r\n{ex}");
return false;
}
}
public static bool UploadStream(Stream stream, string remotePath, string contentType = "application/octet-stream", bool overwrite = false)
=> UploadStreamAsync(stream, remotePath, contentType, overwrite).Result;
/*public static async Task<FtpStatus> UploadFileAsync(string localFilePath, string remotePath = SetuPath, bool overwrite = false)
{
{
try
{
if (!File.Exists(localFilePath))
return false;
return await FTPConnection.UploadFile(localFilePath, remotePath, overwrite ? FtpRemoteExists.Overwrite : FtpRemoteExists.Skip);
}
catch (Exception ex)
{
Logs.Warn($"未能向储存空间路径上传文件 {localFilePath} => {remotePath}\r\n{ex}");
return FtpStatus.Failed;
}
}
}
public static FtpStatus UploadFile(string localFilePath, string remotePath = SetuPath, bool overwrite = false)
=> UploadFileAsync(localFilePath, remotePath, overwrite).Result;*/
public static async Task<string[]> ListFilesNameAsync(string path)
{
return (await ListFilesAsync(path)).Select(x => x.Name).ToArray();
}
public class MItem(Minio.DataModel.Item item) : Minio.DataModel.Item
{
public string Name { get; set; } = item.Key.Contains('/') ? item.Key[(item.Key.LastIndexOf('/') + 1)..] : item.Key;
}
public static async Task<MItem[]> ListFilesAsync(string path, bool recursive = false, bool includeUserMetadata = false)
{
try
{
var result = minio.ListObjectsEnumAsync(new ListObjectsArgs()
.WithBucket(Config.Instance.MinIOBucket)
.WithRecursive(recursive)
.WithIncludeUserMetadata(includeUserMetadata));
var fileList = new List<Minio.DataModel.Item>();
await foreach (var item in result)
{
fileList.Add(item);
}
return [.. fileList.Select(x => new MItem(x))];
}
catch (Exception ex)
{
Logs.Warn($"未能列出储存空间中 {path} 的文件\r\n{ex}");
return null;
}
}
public static async Task<bool> ContainsFileAsync(string path)
{
try
{
var stat = await minio.StatObjectAsync(
new StatObjectArgs()
.WithBucket(Config.Instance.MinIOBucket)
.WithObject(path)
);
return true;
}
catch
{
return false;
}
}
}
}

50
Core/TimerManager.cs Normal file
View File

@@ -0,0 +1,50 @@
using System.Diagnostics;
using System.Reflection;
using MegghysAPI.Attributes;
namespace MegghysAPI.Core
{
public static class TimerManager
{
private static Dictionary<MethodInfo, AutoTimerAttribute> timers = new();
private static long time = 0;
[AutoInit(Order = 100)]
public static void RegisterAll()
{
System.Timers.Timer temp = new()
{
Interval = 1000,
AutoReset = true,
};
temp.Elapsed += (_, _) =>
{
if (time != 0)
timers.Where(timer => time % timer.Value.Time == 0)
.ForEach(timer =>
{
Task.Run(() =>
{
var sw = new Stopwatch();
sw.Start();
timer.Key.Invoke(null, null);
sw.Stop();
if (timer.Value.Log)
Logs.Text($"[{sw.ElapsedMilliseconds} ms] [{timer.Key.DeclaringType.Name}.{timer.Key.Name}] 计时器触发 [{timer.Value.Time}-{time}]");
});
});
time++;
};
AppDomain.CurrentDomain.GetAssemblies()
.ForEach(a => a.GetTypes().Where(t => t.IsClass).ForEach(t => t.GetMethods().Where(m => m.GetCustomAttributes(true).FirstOrDefault(a => a is AutoTimerAttribute) != null)
.ForEach(m =>
{
var attr = m.GetCustomAttributes(true).FirstOrDefault(a => a is AutoTimerAttribute) as AutoTimerAttribute;
timers.Add(m, attr);
Logs.Info($"注册自动计时器 {m.DeclaringType.Name}.{m.Name} <{attr.Time} s>");
})));
temp.Start();
timers.Where(timer => timer.Value.CallOnRegister).ForEach(timer => Task.Run(() => timer.Key.Invoke(null, null)));
}
}
}

19
DB.cs Normal file
View File

@@ -0,0 +1,19 @@
using FreeSql;
using MegghysAPI.Attributes;
namespace MegghysAPI
{
public static class DB
{
public static IFreeSql SQL { get; private set; }
[AutoInit(Order = 0)]
internal static void InitDB()
{
SQL = new FreeSqlBuilder()
.UseConnectionString(DataType.PostgreSQL, Config.Instance.DBConnectString)
.UseAutoSyncStructure(true)
.Build();
SQL.UseJsonMap();
}
}
}

11
Data/Config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"LogDanmakuError": false,
"ListenHost": "*",
"ListenPort": 4050,
"DBConnectString": "Host=42.193.20.136;Port=5432;Username=postgres;Password=!Hzy05152484; Database=MegghysAPI;",
"MinIOEndpoint": "eternalland.top:9000",
"MinIOAccessKey": "RBzbElm21lf7sy7wK7wG",
"MinIOSecretKey": "Nko5azOSUiYgOUeLsj8hLxGz4cKC8XOcH0VS7lWq",
"MinIORegion": "cn-main",
"MinIOBucket": "general"
}

26
Datas.cs Normal file
View File

@@ -0,0 +1,26 @@
using MegghysAPI.Attributes;
namespace MegghysAPI
{
public static class Datas
{
#if DEBUG
public const bool IsDebug = true;
#else
public const bool IsDebug = false;
#endif
public const string HIBI_URL = "https://hibi.suki.club/";
public static string DataPath => Path.Combine(Environment.CurrentDirectory, "Data");
public static string TempFilePath => Path.Combine(DataPath, "Temp");
[AutoInit]
public static void Init()
{
if (!Directory.Exists(DataPath))
Directory.CreateDirectory(DataPath);
if (!Directory.Exists(TempFilePath))
Directory.CreateDirectory(TempFilePath);
}
}
}

19
Entities/LogString.cs Normal file
View File

@@ -0,0 +1,19 @@

namespace MegghysAPI.Entities
{
public struct LogString(object text, ConsoleColor? color = null)
{
public string Text { get; set; } = text?.ToString() ?? string.Empty;
public ConsoleColor? Color { get; set; } = color;
public static implicit operator string(LogString d) => d.Text;
public static implicit operator LogString(string d) => new(d);
public static implicit operator LogString(int d) => new(d.ToString());
public static implicit operator LogString(long d) => new(d.ToString());
public static implicit operator LogString((string? text, ConsoleColor color) d) => new(d.text, d.color);
public static implicit operator LogString((object? text, ConsoleColor color) d) => new(d.text, d.color);
public static implicit operator (string text, ConsoleColor color)(LogString d) => (d.Text, d.Color ?? ConsoleColor.Gray);
public static implicit operator (object text, ConsoleColor color)(LogString d) => (d.Text, d.Color ?? ConsoleColor.Gray);
}
}

173
Logs.cs Normal file
View File

@@ -0,0 +1,173 @@
using System.Collections.Concurrent;
using System.Text;
using MegghysAPI.Attributes;
using MegghysAPI.Entities;
namespace MegghysAPI
{
public class Logs
{
public readonly static string SavePath = Path.Combine(Environment.CurrentDirectory, "Logs");
public static string LogPath => Path.Combine(Environment.CurrentDirectory, "Logs");
public static string LogName => Path.Combine(SavePath, DateTime.Now.ToString("yyyy-MM-dd") + ".log");
public const ConsoleColor DefaultColor = ConsoleColor.Gray;
public static void Text(object text)
{
LogAndSave("[Normal]", ConsoleColor.Gray, true, text.ToString());
}
public static void Info(object text)
{
LogAndSave("[Info]", ConsoleColor.Yellow, true, text.ToString());
}
public static void Error(object text)
{
LogAndSave("[Error]", ConsoleColor.Red, true, text.ToString());
}
public static void Warn(object text)
{
LogAndSave("[Warn]", ConsoleColor.DarkYellow, true, text.ToString());
}
public static void Success(object text)
{
LogAndSave("[Success]", ConsoleColor.Green, true, text.ToString());
}
public static void Text(params LogString[] text)
{
LogAndSave("[Normal]", ConsoleColor.Gray, true, text);
}
public static void Info(params LogString[] text)
{
LogAndSave("[Info]", ConsoleColor.Yellow, true, text);
}
public static void Error(params LogString[] text)
{
LogAndSave("[Error]", ConsoleColor.Red, true, text);
}
public static void Warn(params LogString[] text)
{
LogAndSave("[Warn]", ConsoleColor.DarkYellow, true, text);
}
public static void Success(params LogString[] text)
{
LogAndSave("[Success]", ConsoleColor.Green, true, text);
}
public static void TextCondition(Func<bool> condition, params LogString[] text)
{
if (condition?.Invoke() ?? true)
LogAndSave("[Normal]", ConsoleColor.Gray, true, text);
}
public static void InfoCondition(Func<bool> condition, params LogString[] text)
{
if (condition?.Invoke() ?? true)
LogAndSave("[Info]", ConsoleColor.Yellow, true, text);
}
public static void ErrorCondition(Func<bool> condition, params LogString[] text)
{
if (condition?.Invoke() ?? true)
LogAndSave("[Error]", ConsoleColor.Red, true, text);
}
public static void WarnCondition(Func<bool> condition, params LogString[] text)
{
if (condition?.Invoke() ?? true)
LogAndSave("[Warn]", ConsoleColor.DarkYellow, true, text);
}
public static void SuccesCondition(Func<bool> condition, params LogString[] text)
{
if (condition?.Invoke() ?? true)
LogAndSave("[Success]", ConsoleColor.Green, true, text);
}
public static void Info(params (object message, ConsoleColor color)[] text)
{
LogAndSave("[Info]", ConsoleColor.Yellow, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
}
public static void Text(params (object message, ConsoleColor color)[] text)
{
LogAndSave("[Normal]", ConsoleColor.Gray, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
}
public static void Error(params (object message, ConsoleColor color)[] text)
{
LogAndSave("[Error]", ConsoleColor.Red, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
}
public static void Warn(params (object message, ConsoleColor color)[] text)
{
LogAndSave("[Warn]", ConsoleColor.DarkYellow, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
}
public static void Success(params (object message, ConsoleColor color)[] text)
{
LogAndSave("[Success]", ConsoleColor.Green, true, text.Select(t => new LogString(t.message, t.color)).ToArray());
}
internal static void Init()
{
if (!Directory.Exists(LogPath))
Directory.CreateDirectory(LogPath);
_ = Task.Run(LogLoop);
}
public static void LogNotDisplay(object message, string prefix = "[NotDisplay]")
{
File.AppendAllText(LogName, $"{DateTime.Now:yyyy-MM-dd-HH:mm:ss} - {prefix} {message}{Environment.NewLine}", Encoding.UTF8);
}
public static BlockingCollection<(string prefix, ConsoleColor color, bool save, LogString[] msg)> logQueue = new();
[AutoInit(Async = true)]
public static void LogLoop()
{
if (!Directory.Exists(LogPath))
{
Directory.CreateDirectory(LogPath);
}
while (true)
{
try
{
if (logQueue.TryTake(out var log))
{
var (prefix, color, save, message) = log;
if (save)
{
File.AppendAllText(LogName, $"{DateTime.Now:yyyy-MM-dd-HH:mm:ss} - {prefix} {string.Join("", message.Select(m => m.Text))}{Environment.NewLine}", Encoding.UTF8);
}
Console.ForegroundColor = ConsoleColor.DarkGray;
Console.Write($"{DateTime.Now:HH:mm:ss} ");
lastColor = color;
Console.ForegroundColor = color;
Console.Write($"{prefix} ");
foreach (var item in message)
{
if (item.Color.HasValue)
{
if (lastColor != item.Color)
{
lastColor = item.Color;
Console.ForegroundColor = item.Color.Value;
}
}
else if (lastColor != color)
{
lastColor = color;
Console.ForegroundColor = color;
}
Console.Write($"{item.Text}");
}
Console.WriteLine();
}
else
Thread.Sleep(1);
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
}
static ConsoleColor? lastColor = ConsoleColor.Gray;
public static void LogAndSave(string prefix = "[Log]", ConsoleColor color = DefaultColor, bool save = true, params LogString[] message)
{
try
{
logQueue.TryAdd((prefix, color, save, message));
}
catch (Exception ex) { Console.WriteLine(ex); }
}
}
}

23
MegghysAPI.csproj Normal file
View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FreeSql.Extensions.JsonMap" Version="3.5.104" />
<PackageReference Include="FreeSql.Provider.PostgreSQL" Version="3.5.104" />
<PackageReference Include="Masuit.Tools.Core" Version="2025.1.0" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.11.3" />
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.11.3" />
<PackageReference Include="Minio" Version="6.0.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Masuit.Tools" />
<Using Include="MegghysAPI" />
<Using Include="MegghysAPI.Core" />
</ItemGroup>
</Project>

24
MegghysAPI.sln Normal file
View File

@@ -0,0 +1,24 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35507.96
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MegghysAPI", "MegghysAPI.csproj", "{A01A05D9-A1FF-632A-4B5F-149B79FADB39}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A01A05D9-A1FF-632A-4B5F-149B79FADB39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A01A05D9-A1FF-632A-4B5F-149B79FADB39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A01A05D9-A1FF-632A-4B5F-149B79FADB39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A01A05D9-A1FF-632A-4B5F-149B79FADB39}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4923B96A-4C69-48C8-8F8F-0372B9E8FE4D}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,246 @@
using System.Text.Json.Nodes;
using System.Web;
using FreeSql.DataAnnotations;
using MegghysAPI.Attributes;
using static MegghysAPI.Modules.PixivFavoriteDownloader.Pixiv;
namespace MegghysAPI.Modules
{
public static class PixivFavoriteDownloader
{
[AutoInit]
public static void Init()
{
Favorites = DB.SQL.Select<PixivImgInfo>().ToList();
Favorites.ForEach(f =>
{
f.S3URL = f.URL.Select(u => new PixivURLInfo()
{
Origin = $"http://eternalland.top:9000/{Config.Instance.MinIOBucket}{FileManager.FavoritePath}{f.Id}_{f.URL.IndexOf(u)}.{u.Extension}",
Large = $"http://194.104.147.128:880/{u.Large?.Replace("i.pixiv.re", "i.pximg.net")}?referer=https://www.pixiv.net/",
Medium = $"http://194.104.147.128:880/{u.Medium?.Replace("i.pixiv.re", "i.pximg.net")}?referer=https://www.pixiv.net/",
Small = $"http://194.104.147.128:880/{u.Small?.Replace("i.pixiv.re", "i.pximg.net")}?referer=https://www.pixiv.net/",
}).ToList();
});
Logs.Success($"Pixiv 收藏图片总数: {Favorites.Count}");
}
public static readonly string PIXIV_FAVORITE_URL = Datas.HIBI_URL + "api/pixiv/favorite?id=19045447";
public const string PixivFavoritePath = "/files/favorite/";
public static List<PixivImgInfo> Favorites = [];
[AutoTimer(Time = 5 * 60, CallOnRegister = true)]
public static void UpdateMyFavorite()
{
Logs.Info("正在更新收藏图片");
var url = PIXIV_FAVORITE_URL;
var num = 0;
while (num < 5)
{
try
{
var result = Utils.RequestString(url);
var json = JsonNode.Parse(result);
if (json is null)
return;
json["illusts"]?.AsArray().ForEach(i =>
{
if (!Favorites.Any(f => f.Id == (long)i["id"]))
{
var img = new PixivImgInfo(i, PixivImgType.Favorite);
Favorites.Add(img);
DB.SQL.InsertOrUpdate<PixivImgInfo>()
.SetSource(img)
.ExecuteAffrows();
Logs.Success($"[{Favorites.Count}] 发现新增收藏: {Favorites.Last().Title}, 类型: {Favorites.Last().Restrict}");
}
});
var next = (string)json["next_url"];
if (next is null)
break;
var list = next.Split("max_bookmark_id=", StringSplitOptions.RemoveEmptyEntries);
if (list.Length != 0)
url = PIXIV_FAVORITE_URL + $"&max_bookmark_id={list[1]}";
else
break;
}
catch (Exception ex)
{
Logs.Error(ex);
num++;
}
}
Logs.Success($"收藏图片更新完成. 当前共 {Favorites.Count} 个");
}
static bool uploading = false;
[AutoTimer(Time = 60 * 5, CallOnRegister = true)]
public static async Task DownloadFavoriteToBucket()
{
if (uploading)
{
Logs.Info($"正在下载收藏, 忽略");
return;
}
uploading = true;
var files = await FileManager.ListFilesAsync(FileManager.FavoritePath, true);
if (files is null)
return;
Logs.Info($"FTP 总收藏数: {files.Length}");
int num = 0;
for (int i = 0; i < Favorites.Count; i++)
{
var s = Favorites[i];
try
{
num = 0;
foreach (var u in s?.URL)
{
var url = u.Origin.Replace("i.pixiv.re", "i.pximg.net");
url = $"http://194.104.147.128:880/{url}?referer=https://www.pixiv.net/";
var fileName = $"{s.Id}_{num}.{u.Extension}";
if (!files.Any(f => f.Name == fileName))
{
var data = await Utils.DownloadBytesAsync(url);
if (data.code == System.Net.HttpStatusCode.OK)
{
using var stream = new MemoryStream(data.data);
if (await FileManager.UploadStreamAsync(stream, FileManager.FavoritePath + fileName))
Logs.Success($"已上传收藏图片 {s.Title}({s.Id})[{num + 1}-{s.URL.Count}]:{Favorites.IndexOf(s)} 至储存空间 [{(double)data.data.Length / 1024 / 1024:0.00} Mb].");
else
Logs.Warn($"{s.Title} 上传失败");
}
num++;
}
}
}
catch (Exception ex) { Logs.Error(ex); }
}
uploading = false;
}
public enum PixivImgType
{
None,
Setu,
Favorite
}
public partial class Pixiv
{
public class PixivImgInfo
{
public PixivImgInfo() { Type = PixivImgType.None; }
public PixivImgInfo(PixivImgType t) { Type = t; }
public PixivImgInfo(JsonNode node, PixivImgType t)
{
Type = t;
ArgumentNullException.ThrowIfNull(node);
var tag = new List<PixivTagInfo>();
node["tags"].AsArray().ForEach(t => tag.Add(new((string)t["name"], (string)t["translated_name"])));
Id = (long)node["id"];
Author = new()
{
UID = ((long)node["user"]["id"]).ToString(),
Name = (string)node["user"]["name"],
Account = (string)node["user"]["account"],
HeadURL = (string)node["user"]["profile_image_urls"]["medium"]
};
Title = (string)node["title"];
Restrict = (PixivRestrictInfo)(int)node["x_restrict"];
Description = (string)node["caption"];
Tags = tag;
var url = new List<PixivURLInfo>();
if ((int)node["page_count"] == 1)
url.Add(new() { Origin = ((string)node["meta_single_page"]["original_image_url"]).Replace("i.pximg.net", "i.pixiv.cat") });
else
node["meta_pages"].AsArray().ForEach(p =>
{
var imgs = p["image_urls"];
url.Add(new()
{
Origin = ((string)imgs["original"] ?? "")
.Replace("i.pximg.net", "i.pixiv.re"),
Large = ((string)imgs["large"] ?? "")
.Replace("i.pximg.net", "i.pixiv.re"),
Medium = ((string)imgs["medium"] ?? "")
.Replace("i.pximg.net", "i.pixiv.re"),
Small = ((string)imgs["square_medium"] ?? "")
.Replace("i.pximg.net", "i.pixiv.re")
});
});
URL = url;
Width = (int)node["width"];
Height = (int)node["height"];
UploadDate = DateTime.TryParse((string)node["create_date"], out var date) ? date.Ticks : 0;
ViewCount = (long)node["total_view"];
FavoriteCount = (long)node["total_bookmarks"];
}
public long Id { get; set; }
public PixivImgType Type { get; set; }
public string Title { get; set; }
[JsonMap]
public PixivRestrictInfo Restrict { get; set; }
[Column(DbType = "text")]
public string Description { get; set; }
[JsonMap]
public PixivAuthorInfo Author { get; set; }
public bool R18 => Restrict != PixivRestrictInfo.Normal;
[JsonMap]
public List<PixivTagInfo> Tags { get; set; } = [];
[JsonMap]
public List<PixivURLInfo> URL { get; set; } = [];
[JsonMap]
public List<PixivURLInfo> S3URL { get; set; } = [];
public int Width { get; set; }
public int Height { get; set; }
public long UploadDate { set; get; }
public long ViewCount { get; set; }
public long FavoriteCount { get; set; }
}
public enum PixivRestrictInfo
{
Normal,
R18,
R18G
}
public record PixivAuthorInfo
{
public string Name { get; set; }
public string UID { get; set; }
public string Account { get; set; }
public string HeadURL { get; set; }
}
public record PixivURLInfo
{
public override string ToString()
{
return Origin;
}
public string Extension => Path.GetExtension(HttpUtility.UrlDecode(new Uri(Origin).Segments.Last()))?.Replace(".", "");
public string Name => HttpUtility.UrlDecode(new Uri(Origin).Segments.Last()) is { } url ? $"{url}" : "";
public string Origin { get; set; }
public string Large { get; set; }
public string Medium { get; set; }
public string Small { get; set; }
}
public record PixivTagInfo
{
public override string ToString() => $"{Name} {(TranslateName is null ? "" : $"<{TranslateName}>")}";
public PixivTagInfo(string name, string translateName)
{
Name = name;
TranslateName = translateName;
}
public bool Contains(string tag)
{
return Name.ToLower().Contains(tag.ToLower()) || (TranslateName != null && TranslateName.ToLower().Contains(tag.ToLower()));
}
public string Name { get; set; }
public string TranslateName { get; set; }
}
}
}
}

68
Program.cs Normal file
View File

@@ -0,0 +1,68 @@
using System.Diagnostics;
using System.Reflection;
using MegghysAPI.Attributes;
using MegghysAPI.Components;
using Microsoft.FluentUI.AspNetCore.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddFluentUIComponents();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> AutoInitAttribute <20><><EFBFBD><EFBFBD>
// <20><>ȡ<EFBFBD><C8A1>ǰAssembly
var inits = new List<MethodInfo>();
Assembly.GetExecutingAssembly()
.GetTypes()
.ForEach(t => t.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)
.Where(m => m.GetCustomAttribute<AutoInitAttribute>() is { }).ForEach(m => inits.Add(m)));
inits = [.. inits.OrderBy(m => m.GetCustomAttribute<AutoInitAttribute>().Order)];
inits.ForEach(m =>
{
var sw = new Stopwatch();
sw.Start();
var attr = m.GetCustomAttribute<AutoInitAttribute>();
if (attr.LogMessage is not null)
Logs.Info(attr.LogMessage);
if (attr.Async)
{
Task.Run(() =>
{
m.Invoke(null, null);
attr.PostInit?.Invoke();
Logs.Info($"[{sw.ElapsedMilliseconds} ms] Async <{m.DeclaringType.Name}.{m.Name}> => Inited.");
});
}
else
{
m.Invoke(null, null);
attr.PostInit?.Invoke();
Logs.Info($"[{sw.ElapsedMilliseconds} ms] <{m.DeclaringType.Name}.{m.Name}> => Inited.");
}
});
app.Run();

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5024",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7009;http://localhost:5024",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

106
Utils.cs Normal file
View File

@@ -0,0 +1,106 @@
using System.Net;
namespace MegghysAPI
{
public static class Utils
{
public static readonly HttpClient _client = new(new HttpClientHandler()
{
AutomaticDecompression = DecompressionMethods.All,
})
{
//Timeout = new TimeSpan(0, 0, 15),
};
public static HttpResponseMessage Request(string address) => RequestAsync(address).Result;
public static async Task<HttpResponseMessage?> RequestAsync(string address, Dictionary<string, string> header = null)
{
try
{
var uri = new Uri(address);
var request = new HttpRequestMessage(HttpMethod.Get, uri);
if (header?.Count > 0)
{
foreach (var h in header)
{
request.Headers.TryAddWithoutValidation(h.Key, h.Value);
}
}
return await _client.SendAsync(request);
}
catch (Exception ex)
{
Logs.Error((ex.InnerException ?? ex).Message);
return null;
}
}
public static async Task<string?> RequestStringAsync(string address, Dictionary<string, string> header = null)
{
try
{
var uri = new Uri(address);
if (uri.Host.EndsWith("bilibili.com"))
{
header ??= [];
header.TryAdd("Referer", "https://" + uri.Host);
}
var result = await RequestAsync(address, header);
var content = await result?.Content.ReadAsStringAsync();
if (result?.IsSuccessStatusCode != true)
{
Logs.Warn($"请求失败: {address} {result?.StatusCode}: {content[..100]}");
}
return content;
}
catch (Exception ex)
{
Logs.Error(ex);
return null;
}
}
public static string RequestString(string address, Dictionary<string, string> header = null) => RequestStringAsync(address, header).Result;
public static async Task<(HttpStatusCode code, byte[] data)> DownloadBytesAsync(string uri)
{
try
{
return (HttpStatusCode.OK, await _client.GetByteArrayAsync(uri));
}
catch (HttpRequestException ex)
{
return (ex.StatusCode ?? HttpStatusCode.BadRequest, null);
}
catch (Exception ex)
{
Logs.Error(ex); // 返回false下载失败
return (HttpStatusCode.BadRequest, null);
}
}
public static async Task<(HttpStatusCode code, string path)> DownloadFileAsync(string url, string path = null, bool resume = true)
{
path ??= Datas.TempFilePath;
var dire = Path.GetDirectoryName(path);
if (!Directory.Exists(dire))
Directory.CreateDirectory(dire);
try
{
var result = await DownloadBytesAsync(url);
if (result.code == HttpStatusCode.OK)
{
await File.WriteAllBytesAsync(path, result.data);
return (HttpStatusCode.OK, path);
}
else
return (result.code, null);
}
catch (HttpRequestException webex)
{
return (webex.StatusCode ?? HttpStatusCode.BadRequest, null);
}
catch (Exception ex)
{
Logs.Error(ex); // 返回false下载失败
return (HttpStatusCode.BadRequest, null);
}
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
appsettings.json Normal file
View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

203
wwwroot/app.css Normal file
View File

@@ -0,0 +1,203 @@
@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css';
body {
--body-font: "Segoe UI Variable", "Segoe UI", sans-serif;
font-family: var(--body-font);
font-size: var(--type-ramp-base-font-size);
line-height: var(--type-ramp-base-line-height);
margin: 0;
}
body {
margin: 0;
padding: 0;
height: 100vh;
font-family: var(--body-font);
font-size: var(--type-ramp-base-font-size);
line-height: var(--type-ramp-base-line-height);
font-weight: var(--font-weight);
color: var(--neutral-foreground-rest);
background: var(--neutral-fill-layer-rest);
}
.navmenu-icon {
display: none;
}
.main {
min-height: calc(100dvh - 86px);
color: var(--neutral-foreground-rest);
align-items: stretch !important;
}
.body-content {
align-self: stretch;
height: calc(100dvh - 86px) !important;
display: flex;
}
.content {
padding: 0.5rem 1.5rem;
align-self: stretch !important;
width: 100%;
}
.manage {
width: 100dvw;
}
footer {
background: var(--neutral-layer-4);
color: var(--neutral-foreground-rest);
align-items: center;
padding: 10px 10px;
}
footer a {
color: var(--neutral-foreground-rest);
text-decoration: none;
}
footer a:focus {
outline: 1px dashed;
outline-offset: 3px;
}
footer a:hover {
text-decoration: underline;
}
.alert {
border: 1px dashed var(--accent-fill-rest);
padding: 5px;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
margin: 20px 0;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::before {
content: "An error has occurred. "
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}
@media (max-width: 600px) {
.header-gutters {
margin: 0.5rem 3rem 0.5rem 1.5rem !important;
}
[dir="rtl"] .header-gutters {
margin: 0.5rem 1.5rem 0.5rem 3rem !important;
}
.main {
flex-direction: column !important;
row-gap: 0 !important;
}
nav.sitenav {
width: 100%;
height: 100%;
}
#main-menu {
width: 100% !important;
}
#main-menu > div:first-child:is(.expander) {
display: none;
}
.navmenu {
width: 100%;
}
#navmenu-toggle {
appearance: none;
}
#navmenu-toggle ~ nav {
display: none;
}
#navmenu-toggle:checked ~ nav {
display: block;
}
.navmenu-icon {
cursor: pointer;
z-index: 10;
display: block;
position: absolute;
top: 15px;
left: unset;
right: 20px;
width: 20px;
height: 20px;
border: none;
}
[dir="rtl"] .navmenu-icon {
left: 20px;
right: unset;
}
}

BIN
wwwroot/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB