Add device detection and PDF generation features; refactor AuthDialog and update print utilities
This commit is contained in:
@@ -30,9 +30,16 @@ public static class ConfigureServices
|
||||
config.JsonSerializerOptions.WriteIndented = false;
|
||||
});
|
||||
|
||||
// Access HttpContext for User-Agent
|
||||
services.AddHttpContextAccessor();
|
||||
|
||||
// Register custom services
|
||||
services.AddScoped<AuthService>();
|
||||
services.AddScoped<AuthDialogService>();
|
||||
// Device detection: very light, dependency-free
|
||||
services.AddScoped<IDeviceDetector, DeviceDetector>();
|
||||
// PDF generation
|
||||
services.AddSingleton<FrontOffice.Main.Utilities.Pdf.IHtmlToPdfService, FrontOffice.Main.Utilities.Pdf.HtmlToPdfService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -19,10 +19,14 @@
|
||||
<PackageReference Include="Mapster" Version="7.4.0" />
|
||||
<PackageReference Include="Grpc.Net.Client.Web" Version="2.71.0" />
|
||||
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.71.0" />
|
||||
<!-- HTML to PDF for .NET Core using PdfSharpCore -->
|
||||
<PackageReference Include="PdfSharpCore" Version="1.3.56" />
|
||||
<PackageReference Include="HtmlRendererCore.PdfSharpCore" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Components\" />
|
||||
<Folder Include="Components" />
|
||||
<Folder Include="Utilities/Pdf" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PageTitle>ثبتنام سریع</PageTitle>
|
||||
|
||||
<section class="py-12 wizard-section">
|
||||
<MudContainer MaxWidth="MaxWidth.Large">
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="px-1">
|
||||
<MudGrid Spacing="4" Justify="Justify.Center">
|
||||
<MudItem xs="12" md="5" Class="d-none d-sm-block">
|
||||
<MudStack Spacing="2" Class="mb-6">
|
||||
@@ -68,7 +68,7 @@
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" />
|
||||
}
|
||||
|
||||
<MudStepper @bind-ActiveIndex="_activeStep" Elevation="0" DisableClick="true" Class="mb-4 " id="registration-stepper">
|
||||
<MudStepper @bind-ActiveIndex="_activeStep" @ref="_mudStepper" Elevation="0" DisableClick="true" Class="mb-4 " id="registration-stepper">
|
||||
<ChildContent>
|
||||
<MudStep Label="تأیید موبایل" Icon="@Icons.Material.Filled.Smartphone">
|
||||
@* Inline AuthDialog with captcha enabled *@
|
||||
@@ -110,10 +110,9 @@
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Download" Disabled="_isSubmitting"
|
||||
OnClick="DownloadContract">
|
||||
دانلود قرارداد نمونه
|
||||
دانلود/پرینت قرارداد نمونه
|
||||
</MudButton>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: فایل متنی
|
||||
</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mud-text-secondary">فرمت: PDF از طریق Print</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudCheckBox @bind-Checked="_model.AcceptTerms" Color="Color.Success"
|
||||
@@ -124,13 +123,23 @@
|
||||
</ChildContent>
|
||||
<ActionContent Context="stepper">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center">
|
||||
@if (_isAuthenticated && _activeStep == 1)
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Secondary"
|
||||
|
||||
OnClick="@AuthService.LogoutAsync">خروج از حساب</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
<MudButton Variant="Variant.Text" Color="Color.Secondary"
|
||||
Disabled="_activeStep == 0 || _isSubmitting"
|
||||
OnClick="@(async () => { await GoBack(stepper); })">مرحله قبل</MudButton>
|
||||
<MudSpacer />
|
||||
Disabled="_activeStep == 0 || _isSubmitting"
|
||||
OnClick="@(async () => { await GoBack(); })">مرحله قبل</MudButton>
|
||||
}
|
||||
<MudSpacer/>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary"
|
||||
Disabled="_isSubmitting"
|
||||
OnClick="@(async () => { await GoNextAsync(stepper); })">
|
||||
Disabled="_isSubmitting"
|
||||
OnClick="@(async () => { await GoNextAsync(); })">
|
||||
@_nextButtonText
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
|
||||
@@ -4,6 +4,7 @@ using FrontOffice.Main.Shared;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
|
||||
namespace FrontOffice.Main.Pages;
|
||||
@@ -19,7 +20,12 @@ public partial class RegisterWizard
|
||||
private bool _isSubmitting;
|
||||
private bool _completed;
|
||||
private AuthDialog _authDialog;
|
||||
private MudStepper _mudStepper;
|
||||
private UpdateUserRequest _updateUserRequest = new();
|
||||
private bool _isAuthenticated;
|
||||
|
||||
[Inject] private AuthService AuthService { get; set; } = default!;
|
||||
[Inject] private IJSRuntime Js { get; set; } = default!;
|
||||
|
||||
private string _nextButtonText => _activeStep switch
|
||||
{
|
||||
@@ -28,21 +34,56 @@ public partial class RegisterWizard
|
||||
_ => _isSubmitting ? "در حال ارسال..." : "ثبت نهایی"
|
||||
};
|
||||
|
||||
protected override void OnInitialized()
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
base.OnInitialized();
|
||||
_isAuthenticated = await AuthService.IsAuthenticatedAsync();
|
||||
if (_isAuthenticated && _activeStep == 0)
|
||||
{
|
||||
await _mudStepper.NextStepAsync();
|
||||
_activeStep = 1;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
else if (_isAuthenticated && _activeStep == 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
var existUser = await UserContract.GetUserAsync(new Empty());
|
||||
if (existUser != null && !string.IsNullOrEmpty(existUser.FirstName) && string.IsNullOrWhiteSpace(_model.FirstName))
|
||||
{
|
||||
_model.FirstName = existUser.FirstName;
|
||||
}
|
||||
|
||||
if (existUser != null && !string.IsNullOrEmpty(existUser.LastName) && string.IsNullOrWhiteSpace(_model.LastName))
|
||||
{
|
||||
_model.LastName = existUser.LastName;
|
||||
}
|
||||
|
||||
if (existUser != null && !string.IsNullOrEmpty(existUser.NationalCode) && string.IsNullOrWhiteSpace(_model.NationalCode))
|
||||
{
|
||||
_model.NationalCode = existUser.NationalCode;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
}
|
||||
|
||||
private async Task GoBack(MudStepper mudStepper)
|
||||
private async Task GoBack()
|
||||
{
|
||||
if (_activeStep == 0 || _isSubmitting)
|
||||
if (_activeStep == 0 || _isSubmitting || (_isAuthenticated && _activeStep == 1))
|
||||
return;
|
||||
|
||||
_activeStep--;
|
||||
await mudStepper.PreviousStepAsync();
|
||||
await _mudStepper.PreviousStepAsync();
|
||||
}
|
||||
|
||||
private async Task GoNextAsync(MudStepper mudStepper)
|
||||
private async Task GoNextAsync()
|
||||
{
|
||||
if (_isSubmitting)
|
||||
return;
|
||||
@@ -61,7 +102,6 @@ public partial class RegisterWizard
|
||||
if (!verifyOtp)
|
||||
return;
|
||||
|
||||
|
||||
_activeStep = 1;
|
||||
}
|
||||
|
||||
@@ -76,6 +116,7 @@ public partial class RegisterWizard
|
||||
return;
|
||||
|
||||
_activeStep = 2;
|
||||
await _mudStepper.NextStepAsync();
|
||||
break;
|
||||
case 2:
|
||||
if (!await ValidateStepAsync(_stepThreeForm))
|
||||
@@ -83,8 +124,6 @@ public partial class RegisterWizard
|
||||
await SubmitAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
await mudStepper.NextStepAsync();
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateStepAsync(MudForm? form)
|
||||
@@ -111,16 +150,29 @@ public partial class RegisterWizard
|
||||
}
|
||||
}
|
||||
|
||||
private void DownloadContract()
|
||||
private async void DownloadContract()
|
||||
{
|
||||
Navigation.NavigateTo("docs/sample-contract.txt", true);
|
||||
// Example dynamic HTML (could be constructed based on user data)
|
||||
var html = $"<h1 style='text-align:center'>قرارداد اولیه کاربر</h1><p>نام: {_model.FirstName ?? "-"} {_model.LastName ?? "-"}</p><p>کد ملی: {_model.NationalCode ?? "-"}</p><p>این یک نسخه نمونه است.</p>";
|
||||
|
||||
// Option A: trigger browser print dialog for PDF (client-side)
|
||||
try
|
||||
{
|
||||
await Js.InvokeVoidAsync("printUtils.printHtml", html, new { title = "قرارداد نمونه" });
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to server-side generation endpoint if JS fails
|
||||
var encoded = Uri.EscapeDataString(html);
|
||||
Navigation.NavigateTo($"contract/generate?html={encoded}&fileName=sample-contract", true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPhoneVerified()
|
||||
{
|
||||
// Move to next step after phone verification success
|
||||
// _activeStep = 1;
|
||||
// StateHasChanged();
|
||||
//StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task<bool> SavePersonalInfo()
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
<!-- Load MudBlazor JS before Blazor to avoid early JS interop calls failing; add cache-busting -->
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js" asp-append-version="true"></script>
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
<script src="/js/print-utils.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
// elementId: id نوار (مثلاً "top")
|
||||
// containerSelector: کانتینری که اسکرول میخوره؛ برای MudLayout معمولا ".mud-main-content"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using FluentValidation;
|
||||
using FrontOffice.Main.Utilities;
|
||||
using System.Net;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -54,6 +55,21 @@ app.UseRouting();
|
||||
app.MapBlazorHub();
|
||||
app.MapFallbackToPage("/_Host");
|
||||
|
||||
app.MapGet("/contract/sample", (FrontOffice.Main.Utilities.Pdf.IHtmlToPdfService pdfService) =>
|
||||
{
|
||||
var html = @"<h2 style='text-align:center'>قرارداد نمونه همکاری</h2><p>این یک متن نمونه برای قرارداد است که فقط جهت تست دانلود PDF قرار داده شده است.</p><ul><li>بند ۱: استفاده صرفا مجاز.</li><li>بند ۲: رعایت قوانین لازم است.</li><li>بند ۳: مسئولیت اطلاعات با کاربر است.</li></ul><p class='small'>تاریخ تولید: " + DateTime.Now.ToString("yyyy/MM/dd HH:mm") + "</p>";
|
||||
var bytes = pdfService.GeneratePdf(html, "sample-contract");
|
||||
return Results.File(bytes, "application/pdf", "sample-contract.pdf");
|
||||
});
|
||||
|
||||
app.MapGet("/contract/generate", (string html, string? fileName, FrontOffice.Main.Utilities.Pdf.IHtmlToPdfService pdfService) =>
|
||||
{
|
||||
var decoded = WebUtility.UrlDecode(html);
|
||||
var safeName = string.IsNullOrWhiteSpace(fileName) ? "contract" : fileName.Trim();
|
||||
var bytes = pdfService.GeneratePdf(decoded, safeName);
|
||||
return Results.File(bytes, "application/pdf", safeName + ".pdf");
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudDialog >
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h4" Align="Align.Center">@GetDialogTitle()</MudText>
|
||||
</TitleContent>
|
||||
@@ -20,7 +20,8 @@ else
|
||||
{
|
||||
<MudButton Variant="Variant.Text"
|
||||
OnClick="Cancel"
|
||||
Disabled="_isBusy">لغو</MudButton>
|
||||
Disabled="_isBusy">لغو
|
||||
</MudButton>
|
||||
}
|
||||
@if (_currentStep == AuthStep.Phone)
|
||||
{
|
||||
@@ -47,6 +48,7 @@ else
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
private RenderFragment PhoneOrVerifyContent() => __builder =>
|
||||
{
|
||||
if (_currentStep == AuthStep.Phone)
|
||||
@@ -70,37 +72,51 @@ else
|
||||
Required="true"
|
||||
RequiredError="وارد کردن شماره موبایل الزامی است."
|
||||
HelperText="مثال: 09121234567"
|
||||
Class="mb-2" />
|
||||
Class="mb-2"/>
|
||||
|
||||
@* @if (EnableCaptcha) *@
|
||||
@* { *@
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Class="mt-2 mb-2">
|
||||
<MudPaper Elevation="1" Class="captcha-box d-flex align-center justify-center" Style="min-width:100px;min-height:48px;">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Class="mt-2 mb-2">
|
||||
<MudItem xs="7" md="12">
|
||||
<MudTextField xs="4" Label="کد کپچا" Placeholder="کد نمایش داده شده" Immediate="true"
|
||||
Variant="Variant.Outlined" @bind-Value="_captchaInput" Required="true"
|
||||
RequiredError="لطفاً کد کپچا را وارد کنید."/>
|
||||
</MudItem>
|
||||
<MudItem xs="4" >
|
||||
<MudPaper Elevation="1" Class="captcha-box d-flex align-center justify-center"
|
||||
Style="min-width:100px;min-height:48px;">
|
||||
<MudText Typo="Typo.h5">@_captchaCode</MudText>
|
||||
</MudPaper>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh" Disabled="_isBusy" OnClick="GenerateCaptcha">
|
||||
تازهسازی کد
|
||||
</MudItem>
|
||||
<MudItem xs="1" >
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_isBusy"
|
||||
OnClick="GenerateCaptcha">
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudTextField Label="کد کپچا" Placeholder="کد نمایش داده شده" Immediate="true"
|
||||
Variant="Variant.Outlined" @bind-Value="_captchaInput" Required="true"
|
||||
RequiredError="لطفاً کد کپچا را وارد کنید." />
|
||||
@* } *@
|
||||
</MudItem>
|
||||
|
||||
<MudCheckBox T="bool"
|
||||
</MudStack>
|
||||
|
||||
@* } *@
|
||||
@* *@
|
||||
<MudCheckBox T="bool" Required="true"
|
||||
Label="شرایط و قوانین را میپذیرم"
|
||||
Class="mb-1" />
|
||||
RequiredError="برای ادامه باید شرایط و قوانین را بپذیرید."
|
||||
Class="mb-1"/>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_errorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0" Class="mb-2">@_errorMessage</MudAlert>
|
||||
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
|
||||
Class="mb-2">@_errorMessage</MudAlert>
|
||||
}
|
||||
</MudForm>
|
||||
}
|
||||
else if (_currentStep == AuthStep.Verify)
|
||||
{
|
||||
// Verify Step
|
||||
<MudText Typo="Typo.body2" Class="mb-4" Align="Align.Center">رمز پویا شش رقمی ارسال شده به @_phoneNumber را وارد کنید.</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-4" Align="Align.Center">رمز پویا شش رقمی ارسال شده به @_phoneNumber را
|
||||
وارد کنید.
|
||||
</MudText>
|
||||
|
||||
<MudForm @ref="_verifyForm" Model="_verifyRequest" Validation="@(_verifyRequestValidator.ValidateValue)">
|
||||
<MudTextField @bind-Value="_verifyRequest.Code"
|
||||
@@ -114,30 +130,39 @@ else
|
||||
RequiredError="وارد کردن رمز پویا الزامی است."
|
||||
HelperText="کد ۶ رقمی"
|
||||
Class="mb-2"
|
||||
MaxLength="6" />
|
||||
MaxLength="6"/>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(_errorMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0" Class="mb-2">@(_errorMessage)</MudAlert>
|
||||
<MudAlert Severity="Severity.Error" Dense="true" Elevation="0"
|
||||
Class="mb-2">@(_errorMessage)</MudAlert>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_infoMessage))
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Dense="true" Elevation="0" Class="mb-2">@(_infoMessage)</MudAlert>
|
||||
<MudAlert Severity="Severity.Success" Dense="true" Elevation="0"
|
||||
Class="mb-2">@(_infoMessage)</MudAlert>
|
||||
}
|
||||
|
||||
<MudStack Spacing="2">
|
||||
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="_isBusy" OnClick="ChangePhoneAsync">تغییر شماره</MudButton>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Secondary" Disabled="_isBusy"
|
||||
OnClick="ChangePhoneAsync">تغییر شماره
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudDivider Class="my-2" />
|
||||
<MudDivider Class="my-2"/>
|
||||
@if (_resendRemaining > 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">امکان ارسال مجدد تا @_resendRemaining ثانیه دیگر</MudText>
|
||||
<MudText Typo="Typo.body2" Align="Align.Center" Class="mud-text-secondary">امکان ارسال مجدد
|
||||
تا @_resendRemaining ثانیه دیگر
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Disabled="_isBusy" OnClick="ResendOtpAsync">ارسال مجدد رمز پویا</MudButton>
|
||||
<MudButton Variant="Variant.Text" Color="Color.Primary" Disabled="_isBusy" OnClick="ResendOtpAsync">
|
||||
ارسال مجدد رمز پویا
|
||||
</MudButton>
|
||||
}
|
||||
</MudForm>
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public partial class AuthDialog : IDisposable
|
||||
{
|
||||
_phoneRequest.Mobile = storedPhone;
|
||||
}
|
||||
await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
||||
// await LocalStorage.RemoveItemAsync(TokenStorageKey);
|
||||
}
|
||||
|
||||
private void GenerateCaptcha()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
@@ -6,17 +5,25 @@ namespace FrontOffice.Main.Utilities;
|
||||
public class AuthDialogService
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly DialogOptions _maxWidth = new() { MaxWidth = MaxWidth.Medium, FullWidth = true, };
|
||||
private readonly IDeviceDetector _deviceDetector;
|
||||
private readonly DialogOptions _normalWidth = new() { MaxWidth = MaxWidth.ExtraSmall, FullWidth = true };
|
||||
|
||||
public AuthDialogService(IDialogService dialogService)
|
||||
public AuthDialogService(IDialogService dialogService, IDeviceDetector deviceDetector)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
ArgumentNullException.ThrowIfNull(dialogService);
|
||||
ArgumentNullException.ThrowIfNull(deviceDetector);
|
||||
_dialogService = dialogService!;
|
||||
_deviceDetector = deviceDetector!;
|
||||
}
|
||||
|
||||
public async Task ShowAuthDialogAsync()
|
||||
{
|
||||
// Pick dialog size based on device type
|
||||
var options = _deviceDetector.IsMobile()
|
||||
? new DialogOptions() { FullScreen = true}
|
||||
: _normalWidth;
|
||||
|
||||
var dialog = await _dialogService.ShowAsync<Shared.AuthDialog>("ورود به حساب کاربری",_maxWidth);
|
||||
var dialog = await _dialogService.ShowAsync<Shared.AuthDialog>("ورود به حساب کاربری", options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled)
|
||||
|
||||
117
src/FrontOffice.Main/Utilities/DeviceDetector.cs
Normal file
117
src/FrontOffice.Main/Utilities/DeviceDetector.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace FrontOffice.Main.Utilities;
|
||||
|
||||
public interface IDeviceDetector
|
||||
{
|
||||
DeviceInfo Detect(string? userAgent = null);
|
||||
bool IsMobile(string? userAgent = null);
|
||||
bool IsTablet(string? userAgent = null);
|
||||
bool IsDesktop(string? userAgent = null);
|
||||
}
|
||||
|
||||
public sealed class DeviceDetector : IDeviceDetector
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public DeviceDetector(IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
|
||||
}
|
||||
|
||||
public DeviceInfo Detect(string? userAgent = null)
|
||||
{
|
||||
string? headerUa = null;
|
||||
var http = _httpContextAccessor.HttpContext;
|
||||
if (http != null)
|
||||
{
|
||||
headerUa = http.Request.Headers["User-Agent"].ToString();
|
||||
}
|
||||
|
||||
var ua = userAgent ?? headerUa ?? string.Empty;
|
||||
var uaLower = ua.ToLower(CultureInfo.InvariantCulture);
|
||||
|
||||
// Bots
|
||||
if (ContainsAny(uaLower, "bot", "spider", "crawler", "bingpreview", "facebookexternalhit"))
|
||||
{
|
||||
return new DeviceInfo(DeviceType.Bot, DetectOs(uaLower), DetectBrowser(uaLower), ua);
|
||||
}
|
||||
|
||||
var type = DetectDeviceType(uaLower);
|
||||
var os = DetectOs(uaLower);
|
||||
var browser = DetectBrowser(uaLower);
|
||||
|
||||
return new DeviceInfo(type, os, browser, ua);
|
||||
}
|
||||
|
||||
public bool IsMobile(string? userAgent = null) => Detect(userAgent).Type == DeviceType.Mobile;
|
||||
public bool IsTablet(string? userAgent = null) => Detect(userAgent).Type == DeviceType.Tablet;
|
||||
public bool IsDesktop(string? userAgent = null) => Detect(userAgent).Type == DeviceType.Desktop;
|
||||
|
||||
private static DeviceType DetectDeviceType(string uaLower)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uaLower)) return DeviceType.Unknown;
|
||||
|
||||
// Tablets
|
||||
if (ContainsAny(uaLower, "ipad", "tablet", "kindle", "silk", "playbook"))
|
||||
return DeviceType.Tablet;
|
||||
|
||||
// Mobiles (exclude tablets when possible)
|
||||
if (ContainsAny(uaLower, "mobi", "iphone", "ipod", "android") && !uaLower.Contains("tablet"))
|
||||
return DeviceType.Mobile;
|
||||
|
||||
// Smart TV
|
||||
if (ContainsAny(uaLower, "smart-tv", "smarttv", "hbbtv", "appletv"))
|
||||
return DeviceType.Desktop; // treat TVs as desktop-like
|
||||
|
||||
return DeviceType.Desktop;
|
||||
}
|
||||
|
||||
private static string DetectOs(string uaLower)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uaLower)) return "Unknown";
|
||||
if (uaLower.Contains("windows nt")) return "Windows";
|
||||
if (uaLower.Contains("android")) return "Android";
|
||||
if (uaLower.Contains("iphone") || uaLower.Contains("ipad") || uaLower.Contains("ipod") || uaLower.Contains("ios")) return "iOS";
|
||||
if (uaLower.Contains("mac os x") || uaLower.Contains("macintosh")) return "macOS";
|
||||
if (uaLower.Contains("linux")) return "Linux";
|
||||
if (uaLower.Contains("cros")) return "ChromeOS";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private static string DetectBrowser(string uaLower)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(uaLower)) return "Unknown";
|
||||
if (uaLower.Contains("edg/")) return "Edge"; // Chromium Edge
|
||||
if (uaLower.Contains("edge/")) return "Edge"; // Legacy Edge
|
||||
if (uaLower.Contains("opr/") || uaLower.Contains("opera")) return "Opera";
|
||||
if (uaLower.Contains("chrome/") && !uaLower.Contains("edg/") && !uaLower.Contains("opr/") && !uaLower.Contains("chromium")) return "Chrome";
|
||||
if (uaLower.Contains("safari") && !uaLower.Contains("chrome")) return "Safari";
|
||||
if (uaLower.Contains("firefox")) return "Firefox";
|
||||
if (uaLower.Contains("msie") || uaLower.Contains("trident/")) return "IE";
|
||||
if (uaLower.Contains("chromium")) return "Chromium";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string source, params string[] values)
|
||||
{
|
||||
foreach (var v in values)
|
||||
{
|
||||
if (source.Contains(v)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public enum DeviceType
|
||||
{
|
||||
Unknown = 0,
|
||||
Mobile = 1,
|
||||
Tablet = 2,
|
||||
Desktop = 3,
|
||||
Bot = 4
|
||||
}
|
||||
|
||||
public sealed record DeviceInfo(DeviceType Type, string Os, string Browser, string UserAgent);
|
||||
36
src/FrontOffice.Main/Utilities/Pdf/HtmlToPdfService.cs
Normal file
36
src/FrontOffice.Main/Utilities/Pdf/HtmlToPdfService.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using PdfSharpCore.Pdf;
|
||||
//using HtmlRendererCore.PdfSharpCore;
|
||||
|
||||
namespace FrontOffice.Main.Utilities.Pdf;
|
||||
|
||||
public interface IHtmlToPdfService
|
||||
{
|
||||
byte[] GeneratePdf(string html, string? fileName = null);
|
||||
}
|
||||
|
||||
public class HtmlToPdfService : IHtmlToPdfService
|
||||
{
|
||||
public byte[] GeneratePdf(string html, string? fileName = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
throw new ArgumentException("Html content is empty", nameof(html));
|
||||
|
||||
if (!html.Contains("<html", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
html = $"<html><head><meta charset='utf-8'><style>{GetDefaultStyles()}</style></head><body>{html}</body></html>";
|
||||
}
|
||||
else if (!html.Contains("<style", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Inject default styles if none provided
|
||||
html = html.Replace("<head>", "<head><style>" + GetDefaultStyles() + "</style>");
|
||||
}
|
||||
|
||||
using var document = new PdfDocument();
|
||||
//PdfGenerator.AddPdfPages(document, html, PdfSharpCore.PageSize.A4);
|
||||
using var ms = new MemoryStream();
|
||||
document.Save(ms);
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
private static string GetDefaultStyles() => @"body{font-family:'Vazir','Arial';direction:rtl;text-align:right;font-size:12px;line-height:1.6;margin:25px;} h1,h2,h3{margin:0 0 12px;} ul{margin:0 0 12px 0;padding:0 18px;} .small{font-size:10px;color:#555;}";
|
||||
}
|
||||
69
src/FrontOffice.Main/wwwroot/js/print-utils.js
Normal file
69
src/FrontOffice.Main/wwwroot/js/print-utils.js
Normal file
@@ -0,0 +1,69 @@
|
||||
(function(){
|
||||
function waitForImages(doc){
|
||||
var images = Array.from(doc.images || []);
|
||||
if(images.length === 0) return Promise.resolve();
|
||||
return Promise.all(images.map(function(img){
|
||||
return new Promise(function(resolve){
|
||||
if (img.complete) return resolve();
|
||||
img.addEventListener('load', function(){ resolve(); }, { once: true });
|
||||
img.addEventListener('error', function(){ resolve(); }, { once: true });
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
function writeAndPrint(html, options){
|
||||
options = options || {};
|
||||
var title = options.title || 'document';
|
||||
|
||||
var iframe = document.createElement('iframe');
|
||||
iframe.style.position = 'fixed';
|
||||
iframe.style.right = '0';
|
||||
iframe.style.bottom = '0';
|
||||
iframe.style.width = '0';
|
||||
iframe.style.height = '0';
|
||||
iframe.style.border = '0';
|
||||
iframe.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
var doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||
doc.open();
|
||||
doc.write("<!DOCTYPE html><html lang='fa'><head><meta charset='utf-8'>"+
|
||||
"<title>"+ title +"</title>"+
|
||||
"<link rel='stylesheet' href='/_content/MudBlazor/MudBlazor.min.css'>"+
|
||||
"<link rel='stylesheet' href='/css/site.css'>"+
|
||||
"<style>body{direction:rtl;text-align:right;padding:16px}</style>"+
|
||||
"</head><body>" + html + "</body></html>");
|
||||
doc.close();
|
||||
|
||||
var win = iframe.contentWindow;
|
||||
|
||||
var doPrint = function(){
|
||||
// Small delay to ensure layout
|
||||
setTimeout(function(){
|
||||
try { win.focus(); } catch(e) {}
|
||||
win.print();
|
||||
// Cleanup after some time
|
||||
setTimeout(function(){ document.body.removeChild(iframe); }, 1000);
|
||||
}, 150);
|
||||
};
|
||||
|
||||
if (doc.readyState === 'complete'){
|
||||
waitForImages(doc).then(doPrint);
|
||||
} else {
|
||||
doc.addEventListener('readystatechange', function(){
|
||||
if (doc.readyState === 'complete'){
|
||||
waitForImages(doc).then(doPrint);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.printUtils = {
|
||||
// html: string containing html fragment or full page
|
||||
// options: { title?: string }
|
||||
printHtml: function(html, options){
|
||||
writeAndPrint(html, options || {});
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user