mirror of
https://github.com/Megghy/MegghysAPI.git
synced 2025-12-06 14:16:56 +08:00
添加项目文件。
This commit is contained in:
25
Attributes/AutoInitAttribute.cs
Normal file
25
Attributes/AutoInitAttribute.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
13
Attributes/AutoTimerAttribute.cs
Normal file
13
Attributes/AutoTimerAttribute.cs
Normal 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
24
Components/App.razor
Normal 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>
|
||||
26
Components/Layout/MainLayout.razor
Normal file
26
Components/Layout/MainLayout.razor
Normal 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>
|
||||
20
Components/Layout/NavMenu.razor
Normal file
20
Components/Layout/NavMenu.razor
Normal 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; }
|
||||
}
|
||||
21
Components/Pages/Counter.razor
Normal file
21
Components/Pages/Counter.razor
Normal 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++;
|
||||
}
|
||||
}
|
||||
36
Components/Pages/Error.razor
Normal file
36
Components/Pages/Error.razor
Normal 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;
|
||||
}
|
||||
7
Components/Pages/Home.razor
Normal file
7
Components/Pages/Home.razor
Normal file
@@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
|
||||
Welcome to your new Fluent Blazor app.
|
||||
43
Components/Pages/Pixiv.razor
Normal file
43
Components/Pages/Pixiv.razor
Normal 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();
|
||||
}
|
||||
}
|
||||
43
Components/Pages/Weather.razor
Normal file
43
Components/Pages/Weather.razor
Normal 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
6
Components/Routes.razor
Normal 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
15
Components/_Imports.razor
Normal 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
67
Config.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
91
Controllers/MControllerBase.cs
Normal file
91
Controllers/MControllerBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
15
Controllers/PublicController.cs
Normal file
15
Controllers/PublicController.cs
Normal 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
141
Core/FileManager.cs
Normal 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
50
Core/TimerManager.cs
Normal 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
19
DB.cs
Normal 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
11
Data/Config.json
Normal 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
26
Datas.cs
Normal 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
19
Entities/LogString.cs
Normal 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
173
Logs.cs
Normal 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
23
MegghysAPI.csproj
Normal 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
24
MegghysAPI.sln
Normal 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
|
||||
246
Modules/PixivFavoriteDownloader.cs
Normal file
246
Modules/PixivFavoriteDownloader.cs
Normal 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
68
Program.cs
Normal 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();
|
||||
23
Properties/launchSettings.json
Normal file
23
Properties/launchSettings.json
Normal 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
106
Utils.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
appsettings.Development.json
Normal file
8
appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
9
appsettings.json
Normal file
9
appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
203
wwwroot/app.css
Normal file
203
wwwroot/app.css
Normal 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
BIN
wwwroot/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Reference in New Issue
Block a user