feat: ajustes qrcode, estilos e cores.
This commit is contained in:
parent
a8faf0ef2f
commit
00d924ce3b
@ -5,7 +5,9 @@
|
||||
"Bash(find:*)",
|
||||
"Bash(dotnet build:*)",
|
||||
"Bash(timeout:*)",
|
||||
"Bash(rm:*)"
|
||||
"Bash(rm:*)",
|
||||
"Bash(dotnet run:*)",
|
||||
"Bash(curl:*)"
|
||||
],
|
||||
"deny": []
|
||||
}
|
||||
|
||||
@ -63,6 +63,19 @@ namespace QRRapidoApp.Controllers
|
||||
// Check user status
|
||||
var user = await _userService.GetUserAsync(userId);
|
||||
|
||||
// Validate premium features
|
||||
if (!string.IsNullOrEmpty(request.CornerStyle) && request.CornerStyle != "square" && user?.IsPremium != true)
|
||||
{
|
||||
_logger.LogWarning("Custom corner style attempted by non-premium user - UserId: {UserId}, CornerStyle: {CornerStyle}",
|
||||
userId ?? "anonymous", request.CornerStyle);
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "Estilos de borda personalizados são exclusivos do plano Premium. Faça upgrade para usar esta funcionalidade.",
|
||||
requiresPremium = true,
|
||||
success = false
|
||||
});
|
||||
}
|
||||
|
||||
// Rate limiting for free users
|
||||
var rateLimitPassed = await CheckRateLimitAsync(userId, user);
|
||||
if (!rateLimitPassed)
|
||||
@ -272,6 +285,167 @@ namespace QRRapidoApp.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("GenerateRapidWithLogo")]
|
||||
public async Task<IActionResult> GenerateRapidWithLogo([FromForm] QRGenerationRequest request, IFormFile? logo)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var requestId = Guid.NewGuid().ToString("N")[..8];
|
||||
var userId = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||||
var isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
using (_logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["RequestId"] = requestId,
|
||||
["UserId"] = userId ?? "anonymous",
|
||||
["IsAuthenticated"] = isAuthenticated,
|
||||
["QRType"] = request.Type ?? "unknown",
|
||||
["ContentLength"] = request.Content?.Length ?? 0,
|
||||
["QRGeneration"] = true,
|
||||
["HasLogo"] = logo != null
|
||||
}))
|
||||
{
|
||||
_logger.LogInformation("QR generation with logo request started - Type: {QRType}, ContentLength: {ContentLength}, HasLogo: {HasLogo}",
|
||||
request.Type, request.Content?.Length ?? 0, logo != null);
|
||||
|
||||
try
|
||||
{
|
||||
// Quick validations
|
||||
if (string.IsNullOrWhiteSpace(request.Content))
|
||||
{
|
||||
_logger.LogWarning("QR generation failed - empty content provided");
|
||||
return BadRequest(new { error = "Conteúdo é obrigatório", success = false });
|
||||
}
|
||||
|
||||
if (request.Content.Length > 4000)
|
||||
{
|
||||
_logger.LogWarning("QR generation failed - content too long: {ContentLength} characters", request.Content.Length);
|
||||
return BadRequest(new { error = "Conteúdo muito longo. Máximo 4000 caracteres.", success = false });
|
||||
}
|
||||
|
||||
// Check user status
|
||||
var user = await _userService.GetUserAsync(userId);
|
||||
|
||||
// Validate premium status for logo feature
|
||||
if (user?.IsPremium != true)
|
||||
{
|
||||
_logger.LogWarning("Logo upload attempted by non-premium user - UserId: {UserId}", userId ?? "anonymous");
|
||||
return BadRequest(new
|
||||
{
|
||||
error = "Logo personalizado é exclusivo do plano Premium. Faça upgrade para usar esta funcionalidade.",
|
||||
requiresPremium = true,
|
||||
success = false
|
||||
});
|
||||
}
|
||||
|
||||
// Validate premium corner styles
|
||||
if (!string.IsNullOrEmpty(request.CornerStyle) && request.CornerStyle != "square")
|
||||
{
|
||||
_logger.LogInformation("Premium user using custom corner style - UserId: {UserId}, CornerStyle: {CornerStyle}",
|
||||
userId, request.CornerStyle);
|
||||
}
|
||||
|
||||
// Process logo upload if provided
|
||||
if (logo != null && logo.Length > 0)
|
||||
{
|
||||
// Validate file size (2MB max)
|
||||
if (logo.Length > 2 * 1024 * 1024)
|
||||
{
|
||||
_logger.LogWarning("Logo upload failed - file too large: {FileSize} bytes", logo.Length);
|
||||
return BadRequest(new { error = "Logo muito grande. Máximo 2MB.", success = false });
|
||||
}
|
||||
|
||||
// Validate file format
|
||||
var allowedTypes = new[] { "image/png", "image/jpeg", "image/jpg" };
|
||||
if (!allowedTypes.Contains(logo.ContentType?.ToLower()))
|
||||
{
|
||||
_logger.LogWarning("Logo upload failed - invalid format: {ContentType}", logo.ContentType);
|
||||
return BadRequest(new { error = "Formato inválido. Use PNG ou JPG.", success = false });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Convert file to byte array
|
||||
using var memoryStream = new MemoryStream();
|
||||
await logo.CopyToAsync(memoryStream);
|
||||
request.Logo = memoryStream.ToArray();
|
||||
request.HasLogo = true;
|
||||
|
||||
_logger.LogInformation("Logo processed successfully - Size: {LogoSize} bytes, Format: {ContentType}",
|
||||
logo.Length, logo.ContentType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing logo file");
|
||||
return BadRequest(new { error = "Erro ao processar logo.", success = false });
|
||||
}
|
||||
}
|
||||
|
||||
// Rate limiting for free users (premium users get unlimited)
|
||||
var rateLimitPassed = await CheckRateLimitAsync(userId, user);
|
||||
if (!rateLimitPassed)
|
||||
{
|
||||
_logger.LogWarning("QR generation rate limited - User: {UserId}, IsPremium: {IsPremium}",
|
||||
userId ?? "anonymous", user?.IsPremium ?? false);
|
||||
return StatusCode(429, new
|
||||
{
|
||||
error = "Limite de QR codes atingido",
|
||||
upgradeUrl = "/Premium/Upgrade",
|
||||
success = false
|
||||
});
|
||||
}
|
||||
|
||||
// Configure optimizations based on user
|
||||
request.IsPremium = user?.IsPremium == true;
|
||||
request.OptimizeForSpeed = true;
|
||||
|
||||
_logger.LogDebug("Generating QR code with logo - IsPremium: {IsPremium}, HasLogo: {HasLogo}",
|
||||
request.IsPremium, request.HasLogo);
|
||||
|
||||
// Generate QR code
|
||||
var generationStopwatch = Stopwatch.StartNew();
|
||||
var result = await _qrService.GenerateRapidAsync(request);
|
||||
generationStopwatch.Stop();
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
_logger.LogError("QR generation failed - Error: {ErrorMessage}, GenerationTime: {GenerationTimeMs}ms",
|
||||
result.ErrorMessage, generationStopwatch.ElapsedMilliseconds);
|
||||
return StatusCode(500, new { error = result.ErrorMessage, success = false });
|
||||
}
|
||||
|
||||
_logger.LogInformation("QR code with logo generated successfully - GenerationTime: {GenerationTimeMs}ms, FromCache: {FromCache}, HasLogo: {HasLogo}",
|
||||
generationStopwatch.ElapsedMilliseconds, result.FromCache, request.HasLogo);
|
||||
|
||||
// Save to history if user is logged in (fire and forget)
|
||||
if (userId != null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _userService.SaveQRToHistoryAsync(userId, result);
|
||||
_logger.LogDebug("QR code saved to history successfully for user {UserId}", userId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving QR to history for user {UserId}", userId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stopwatch.Stop();
|
||||
return Ok(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
_logger.LogError(ex, "QR generation with logo failed with exception - RequestTime: {RequestTimeMs}ms, UserId: {UserId}",
|
||||
stopwatch.ElapsedMilliseconds, userId ?? "anonymous");
|
||||
return StatusCode(500, new { error = "Erro interno do servidor", success = false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("History")]
|
||||
public async Task<IActionResult> GetHistory(int limit = 20)
|
||||
{
|
||||
|
||||
@ -3,6 +3,7 @@ using QRCoder;
|
||||
using QRRapidoApp.Models.ViewModels;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
@ -114,7 +115,21 @@ namespace QRRapidoApp.Services
|
||||
var primaryColorBytes = ColorToBytes(ParseHtmlColor(request.PrimaryColor));
|
||||
var backgroundColorBytes = ColorToBytes(ParseHtmlColor(request.BackgroundColor));
|
||||
|
||||
return qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes);
|
||||
var qrBytes = qrCode.GetGraphic(pixelsPerModule, primaryColorBytes, backgroundColorBytes);
|
||||
|
||||
// Apply custom corner styles for premium users
|
||||
if (request.IsPremium && !string.IsNullOrEmpty(request.CornerStyle) && request.CornerStyle != "square")
|
||||
{
|
||||
qrBytes = ApplyCornerStyle(qrBytes, request.CornerStyle, request.Size);
|
||||
}
|
||||
|
||||
// Apply logo overlay if provided
|
||||
if (request.HasLogo && request.Logo != null)
|
||||
{
|
||||
return ApplyLogoOverlay(qrBytes, request.Logo, request.Size);
|
||||
}
|
||||
|
||||
return qrBytes;
|
||||
});
|
||||
}
|
||||
|
||||
@ -253,5 +268,225 @@ namespace QRRapidoApp.Services
|
||||
{
|
||||
return new byte[] { color.R, color.G, color.B };
|
||||
}
|
||||
|
||||
private byte[] ApplyLogoOverlay(byte[] qrBytes, byte[] logoBytes, int qrSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var qrStream = new MemoryStream(qrBytes);
|
||||
using var logoStream = new MemoryStream(logoBytes);
|
||||
using var qrImage = new Bitmap(qrStream);
|
||||
using var logoImage = new Bitmap(logoStream);
|
||||
|
||||
// Create a new bitmap to draw on (to avoid modifying the original)
|
||||
using var finalImage = new Bitmap(qrImage.Width, qrImage.Height);
|
||||
using var graphics = Graphics.FromImage(finalImage);
|
||||
|
||||
// Set high quality rendering
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
|
||||
|
||||
// Draw the QR code as base
|
||||
graphics.DrawImage(qrImage, 0, 0, qrImage.Width, qrImage.Height);
|
||||
|
||||
// Calculate logo size (20% of QR code size)
|
||||
var logoSize = Math.Min(qrImage.Width, qrImage.Height) / 5;
|
||||
|
||||
// Calculate center position
|
||||
var logoX = (qrImage.Width - logoSize) / 2;
|
||||
var logoY = (qrImage.Height - logoSize) / 2;
|
||||
|
||||
// Create a white background circle for better contrast
|
||||
var backgroundSize = logoSize + 10; // Slightly larger than logo
|
||||
var backgroundX = (qrImage.Width - backgroundSize) / 2;
|
||||
var backgroundY = (qrImage.Height - backgroundSize) / 2;
|
||||
|
||||
using var whiteBrush = new SolidBrush(System.Drawing.Color.White);
|
||||
graphics.FillEllipse(whiteBrush, backgroundX, backgroundY, backgroundSize, backgroundSize);
|
||||
|
||||
// Draw the logo
|
||||
graphics.DrawImage(logoImage, logoX, logoY, logoSize, logoSize);
|
||||
|
||||
// Convert back to byte array
|
||||
using var outputStream = new MemoryStream();
|
||||
finalImage.Save(outputStream, ImageFormat.Png);
|
||||
return outputStream.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error applying logo overlay, returning original QR code");
|
||||
// Return original QR code if logo overlay fails
|
||||
return qrBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] ApplyCornerStyle(byte[] qrBytes, string cornerStyle, int targetSize)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var originalStream = new MemoryStream(qrBytes);
|
||||
using var originalImage = new Bitmap(originalStream);
|
||||
using var styledImage = new Bitmap(originalImage.Width, originalImage.Height);
|
||||
using var graphics = Graphics.FromImage(styledImage);
|
||||
|
||||
// Set high quality rendering
|
||||
graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
|
||||
graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
|
||||
graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
|
||||
|
||||
// Fill with background color first
|
||||
graphics.Clear(System.Drawing.Color.White);
|
||||
|
||||
// Analyze the QR code to identify modules
|
||||
var moduleSize = DetectModuleSize(originalImage);
|
||||
var modules = ExtractQRModules(originalImage, moduleSize);
|
||||
|
||||
// Draw modules with custom style
|
||||
foreach (var module in modules)
|
||||
{
|
||||
if (module.IsBlack)
|
||||
{
|
||||
DrawStyledModule(graphics, module, cornerStyle, moduleSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to byte array
|
||||
using var outputStream = new MemoryStream();
|
||||
styledImage.Save(outputStream, ImageFormat.Png);
|
||||
return outputStream.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error applying corner style {CornerStyle}, returning original QR code", cornerStyle);
|
||||
// Return original QR code if styling fails
|
||||
return qrBytes;
|
||||
}
|
||||
}
|
||||
|
||||
private int DetectModuleSize(Bitmap qrImage)
|
||||
{
|
||||
// Simple detection: scan from top-left to find first module boundaries
|
||||
// QR codes typically have quiet zones, so we look for the first black pixel pattern
|
||||
for (int size = 4; size <= 20; size++)
|
||||
{
|
||||
if (IsValidModuleSize(qrImage, size))
|
||||
{
|
||||
return size;
|
||||
}
|
||||
}
|
||||
return 8; // Fallback
|
||||
}
|
||||
|
||||
private bool IsValidModuleSize(Bitmap image, int moduleSize)
|
||||
{
|
||||
// Simple validation: check if the suspected module size creates a reasonable grid
|
||||
var expectedModules = image.Width / moduleSize;
|
||||
return expectedModules >= 21 && expectedModules <= 177 && (expectedModules % 4 == 1);
|
||||
}
|
||||
|
||||
private List<QRModule> ExtractQRModules(Bitmap image, int moduleSize)
|
||||
{
|
||||
var modules = new List<QRModule>();
|
||||
var modulesPerRow = image.Width / moduleSize;
|
||||
|
||||
for (int row = 0; row < modulesPerRow; row++)
|
||||
{
|
||||
for (int col = 0; col < modulesPerRow; col++)
|
||||
{
|
||||
var x = col * moduleSize + moduleSize / 2;
|
||||
var y = row * moduleSize + moduleSize / 2;
|
||||
|
||||
if (x < image.Width && y < image.Height)
|
||||
{
|
||||
var pixel = image.GetPixel(x, y);
|
||||
var isBlack = pixel.R < 128; // Simple threshold
|
||||
|
||||
modules.Add(new QRModule
|
||||
{
|
||||
X = col * moduleSize,
|
||||
Y = row * moduleSize,
|
||||
Size = moduleSize,
|
||||
IsBlack = isBlack
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return modules;
|
||||
}
|
||||
|
||||
private void DrawStyledModule(Graphics graphics, QRModule module, string style, int moduleSize)
|
||||
{
|
||||
using var brush = new SolidBrush(System.Drawing.Color.Black);
|
||||
|
||||
switch (style.ToLower())
|
||||
{
|
||||
case "rounded":
|
||||
// Draw rounded rectangles
|
||||
var roundedRect = new Rectangle(module.X, module.Y, moduleSize, moduleSize);
|
||||
using (var path = CreateRoundedRectPath(roundedRect, moduleSize / 4))
|
||||
{
|
||||
graphics.FillPath(brush, path);
|
||||
}
|
||||
break;
|
||||
|
||||
case "circle":
|
||||
case "dots":
|
||||
// Draw circles/dots
|
||||
var margin = moduleSize / 6;
|
||||
graphics.FillEllipse(brush,
|
||||
module.X + margin, module.Y + margin,
|
||||
moduleSize - 2 * margin, moduleSize - 2 * margin);
|
||||
break;
|
||||
|
||||
case "leaf":
|
||||
// Draw leaf-shaped modules (rounded on one side)
|
||||
DrawLeafModule(graphics, brush, module, moduleSize);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Square (fallback)
|
||||
graphics.FillRectangle(brush, module.X, module.Y, moduleSize, moduleSize);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private System.Drawing.Drawing2D.GraphicsPath CreateRoundedRectPath(Rectangle rect, int radius)
|
||||
{
|
||||
var path = new System.Drawing.Drawing2D.GraphicsPath();
|
||||
path.AddArc(rect.X, rect.Y, radius * 2, radius * 2, 180, 90);
|
||||
path.AddArc(rect.Right - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90);
|
||||
path.AddArc(rect.Right - radius * 2, rect.Bottom - radius * 2, radius * 2, radius * 2, 0, 90);
|
||||
path.AddArc(rect.X, rect.Bottom - radius * 2, radius * 2, radius * 2, 90, 90);
|
||||
path.CloseFigure();
|
||||
return path;
|
||||
}
|
||||
|
||||
private void DrawLeafModule(Graphics graphics, SolidBrush brush, QRModule module, int moduleSize)
|
||||
{
|
||||
// Create a path that looks like a leaf (rounded on top-right, square elsewhere)
|
||||
using var path = new System.Drawing.Drawing2D.GraphicsPath();
|
||||
var rect = new Rectangle(module.X, module.Y, moduleSize, moduleSize);
|
||||
var radius = moduleSize / 3;
|
||||
|
||||
// Start from top-left, go clockwise
|
||||
path.AddLine(rect.X, rect.Y, rect.Right - radius, rect.Y);
|
||||
path.AddArc(rect.Right - radius * 2, rect.Y, radius * 2, radius * 2, 270, 90);
|
||||
path.AddLine(rect.Right, rect.Y + radius, rect.Right, rect.Bottom);
|
||||
path.AddLine(rect.Right, rect.Bottom, rect.X, rect.Bottom);
|
||||
path.AddLine(rect.X, rect.Bottom, rect.X, rect.Y);
|
||||
path.CloseFigure();
|
||||
|
||||
graphics.FillPath(brush, path);
|
||||
}
|
||||
|
||||
private class QRModule
|
||||
{
|
||||
public int X { get; set; }
|
||||
public int Y { get; set; }
|
||||
public int Size { get; set; }
|
||||
public bool IsBlack { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -158,19 +158,70 @@
|
||||
@if (User.Identity.IsAuthenticated)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">@Localizer["LogoIcon"]</label>
|
||||
<input type="file" id="logo-upload" class="form-control" accept="image/*">
|
||||
<div class="form-text">@Localizer["PNGJPGUp2MB"]</div>
|
||||
</div>
|
||||
@{
|
||||
var userService = Context.RequestServices.GetService<QRRapidoApp.Services.IUserService>();
|
||||
var currentUserId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
var currentUser = currentUserId != null ? await userService.GetUserAsync(currentUserId) : null;
|
||||
var isPremium = currentUser?.IsPremium == true;
|
||||
}
|
||||
|
||||
@if (isPremium)
|
||||
{
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">
|
||||
@Localizer["LogoIcon"]
|
||||
<span class="badge bg-warning text-dark ms-1">Premium</span>
|
||||
</label>
|
||||
<input type="file" id="logo-upload" class="form-control" accept="image/png,image/jpeg,image/jpg">
|
||||
<div class="form-text">@Localizer["PNGJPGUp2MB"]</div>
|
||||
<div id="logo-preview" class="mt-2 d-none">
|
||||
<small class="text-success">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span id="logo-filename"></span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="col-md-6 mb-3">
|
||||
<div class="premium-upgrade-box p-3 border rounded bg-light">
|
||||
<h6 class="fw-bold mb-2">
|
||||
<i class="fas fa-crown text-warning"></i>
|
||||
Logo Personalizado - Premium
|
||||
</h6>
|
||||
<p class="mb-2 small">Adicione sua marca aos QR Codes! Upgrade para Premium e personalize com seu logo.</p>
|
||||
<a href="/Pagamento/SelecaoPlano" class="btn btn-warning btn-sm">
|
||||
<i class="fas fa-arrow-up"></i> Fazer Upgrade
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">@Localizer["BorderStyle"]</label>
|
||||
<select id="corner-style" class="form-select">
|
||||
<option value="square">@Localizer["Square"]</option>
|
||||
<option value="rounded">@Localizer["Rounded"]</option>
|
||||
<option value="circle">@Localizer["Circular"]</option>
|
||||
<option value="leaf">@Localizer["Leaf"]</option>
|
||||
<select id="corner-style" class="form-select @(isPremium ? "" : "border-warning")">
|
||||
<option value="square">@Localizer["Square"] (Grátis)</option>
|
||||
@if (isPremium)
|
||||
{
|
||||
<option value="rounded">@Localizer["Rounded"] 👑</option>
|
||||
<option value="circle">Círculos 👑</option>
|
||||
<option value="leaf">Folha 👑</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="rounded" disabled>@Localizer["Rounded"] - Premium 👑</option>
|
||||
<option value="circle" disabled>Círculos - Premium 👑</option>
|
||||
<option value="leaf" disabled>Folha - Premium 👑</option>
|
||||
}
|
||||
</select>
|
||||
@if (!isPremium)
|
||||
{
|
||||
<div class="form-text text-warning">
|
||||
<i class="fas fa-crown"></i>
|
||||
<a href="/Pagamento/SelecaoPlano" class="text-warning">Upgrade Premium</a> para estilos personalizados
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -59,6 +59,18 @@ class QRRapidoGenerator {
|
||||
radio.addEventListener('change', this.applyQuickStyle.bind(this));
|
||||
});
|
||||
|
||||
// Logo upload feedback
|
||||
const logoUpload = document.getElementById('logo-upload');
|
||||
if (logoUpload) {
|
||||
logoUpload.addEventListener('change', this.handleLogoSelection.bind(this));
|
||||
}
|
||||
|
||||
// Corner style validation for non-premium users
|
||||
const cornerStyle = document.getElementById('corner-style');
|
||||
if (cornerStyle) {
|
||||
cornerStyle.addEventListener('change', this.handleCornerStyleChange.bind(this));
|
||||
}
|
||||
|
||||
// QR type change with hints
|
||||
const qrType = document.getElementById('qr-type');
|
||||
if (qrType) {
|
||||
@ -295,28 +307,47 @@ class QRRapidoGenerator {
|
||||
this.startTime = performance.now();
|
||||
this.showGenerationStarted();
|
||||
|
||||
const formData = this.collectFormData();
|
||||
const requestData = this.collectFormData();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/QR/GenerateRapid', {
|
||||
// Build fetch options based on request type
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
body: requestData.isMultipart ? requestData.data : JSON.stringify(requestData.data)
|
||||
};
|
||||
|
||||
// Add Content-Type header only for JSON requests (FormData sets its own)
|
||||
if (!requestData.isMultipart) {
|
||||
fetchOptions.headers = {
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(requestData.endpoint, fetchOptions);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
|
||||
if (response.status === 429) {
|
||||
this.showUpgradeModal('Limite de QR codes atingido! Upgrade para QR Rapido Premium e gere códigos ilimitados.');
|
||||
return;
|
||||
}
|
||||
throw new Error('Erro na geração');
|
||||
|
||||
if (response.status === 400 && errorData.requiresPremium) {
|
||||
this.showUpgradeModal(errorData.error || 'Logo personalizado é exclusivo do plano Premium.');
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(errorData.error || 'Erro na geração');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
if (result.requiresPremium) {
|
||||
this.showUpgradeModal(result.error || 'Logo personalizado é exclusivo do plano Premium.');
|
||||
return;
|
||||
}
|
||||
throw new Error(result.error || 'Erro desconhecido');
|
||||
}
|
||||
|
||||
@ -324,7 +355,7 @@ class QRRapidoGenerator {
|
||||
|
||||
this.displayQRResult(result, generationTime);
|
||||
this.updateSpeedStats(generationTime);
|
||||
this.trackGenerationEvent(formData.type, generationTime);
|
||||
this.trackGenerationEvent(requestData.data.type || requestData.data.get('type'), generationTime);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar QR:', error);
|
||||
@ -360,19 +391,84 @@ class QRRapidoGenerator {
|
||||
const quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic';
|
||||
const styleSettings = this.getStyleSettings(quickStyle);
|
||||
|
||||
return {
|
||||
type: document.getElementById('qr-type').value,
|
||||
content: document.getElementById('qr-content').value,
|
||||
quickStyle: quickStyle,
|
||||
primaryColor: document.getElementById('primary-color').value,
|
||||
backgroundColor: document.getElementById('bg-color').value,
|
||||
size: parseInt(document.getElementById('qr-size').value),
|
||||
margin: parseInt(document.getElementById('qr-margin').value),
|
||||
cornerStyle: document.getElementById('corner-style')?.value || 'square',
|
||||
optimizeForSpeed: true,
|
||||
language: this.currentLang,
|
||||
...styleSettings
|
||||
};
|
||||
// Check if logo is selected for premium users
|
||||
const logoUpload = document.getElementById('logo-upload');
|
||||
const hasLogo = logoUpload && logoUpload.files && logoUpload.files[0];
|
||||
|
||||
if (hasLogo) {
|
||||
// Use FormData for premium users with logo
|
||||
const formData = new FormData();
|
||||
|
||||
// Get user-selected colors with proper priority
|
||||
const userPrimaryColor = document.getElementById('primary-color').value;
|
||||
const userBackgroundColor = document.getElementById('bg-color').value;
|
||||
|
||||
// Priority: User selection > Style defaults > Fallback
|
||||
// Always use user selection if it exists, regardless of what color it is
|
||||
const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000');
|
||||
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
|
||||
|
||||
// Debug logging for color selection
|
||||
console.log('🎨 Color Selection Debug (FormData):');
|
||||
console.log(' Style:', quickStyle);
|
||||
console.log(' Style Default Primary:', styleSettings.primaryColor);
|
||||
console.log(' User Selected Primary:', userPrimaryColor);
|
||||
console.log(' Final Primary Color:', finalPrimaryColor);
|
||||
console.log(' Final Background Color:', finalBackgroundColor);
|
||||
|
||||
// Add basic form fields
|
||||
formData.append('type', document.getElementById('qr-type').value);
|
||||
formData.append('content', document.getElementById('qr-content').value);
|
||||
formData.append('quickStyle', quickStyle);
|
||||
formData.append('primaryColor', finalPrimaryColor);
|
||||
formData.append('backgroundColor', finalBackgroundColor);
|
||||
formData.append('size', parseInt(document.getElementById('qr-size').value));
|
||||
formData.append('margin', parseInt(document.getElementById('qr-margin').value));
|
||||
formData.append('cornerStyle', document.getElementById('corner-style')?.value || 'square');
|
||||
formData.append('optimizeForSpeed', 'true');
|
||||
formData.append('language', this.currentLang);
|
||||
|
||||
// Add logo file
|
||||
formData.append('logo', logoUpload.files[0]);
|
||||
console.log('Logo file added to form data:', logoUpload.files[0].name, logoUpload.files[0].size + ' bytes');
|
||||
|
||||
return { data: formData, isMultipart: true, endpoint: '/api/QR/GenerateRapidWithLogo' };
|
||||
} else {
|
||||
// Use JSON for basic QR generation (original working method)
|
||||
// Get user-selected colors
|
||||
const userPrimaryColor = document.getElementById('primary-color').value;
|
||||
const userBackgroundColor = document.getElementById('bg-color').value;
|
||||
|
||||
// Priority: User selection > Style defaults > Fallback
|
||||
// Always use user selection if it exists, regardless of what color it is
|
||||
const finalPrimaryColor = userPrimaryColor || (styleSettings.primaryColor || '#000000');
|
||||
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
|
||||
|
||||
// Debug logging for color selection
|
||||
console.log('🎨 Color Selection Debug (JSON):');
|
||||
console.log(' Style:', quickStyle);
|
||||
console.log(' Style Default Primary:', styleSettings.primaryColor);
|
||||
console.log(' User Selected Primary:', userPrimaryColor);
|
||||
console.log(' Final Primary Color:', finalPrimaryColor);
|
||||
console.log(' Final Background Color:', finalBackgroundColor);
|
||||
|
||||
return {
|
||||
data: {
|
||||
type: document.getElementById('qr-type').value,
|
||||
content: document.getElementById('qr-content').value,
|
||||
quickStyle: quickStyle,
|
||||
primaryColor: finalPrimaryColor,
|
||||
backgroundColor: finalBackgroundColor,
|
||||
size: parseInt(document.getElementById('qr-size').value),
|
||||
margin: parseInt(document.getElementById('qr-margin').value),
|
||||
cornerStyle: document.getElementById('corner-style')?.value || 'square',
|
||||
optimizeForSpeed: true,
|
||||
language: this.currentLang
|
||||
},
|
||||
isMultipart: false,
|
||||
endpoint: '/api/QR/GenerateRapid'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
getStyleSettings(style) {
|
||||
@ -562,6 +658,61 @@ class QRRapidoGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
handleLogoSelection(e) {
|
||||
const file = e.target.files[0];
|
||||
const logoPreview = document.getElementById('logo-preview');
|
||||
const logoFilename = document.getElementById('logo-filename');
|
||||
|
||||
if (file) {
|
||||
// Validate file size (2MB max)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
this.showError('Logo muito grande. Máximo 2MB.');
|
||||
e.target.value = ''; // Clear the input
|
||||
logoPreview?.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const allowedTypes = ['image/png', 'image/jpeg', 'image/jpg'];
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
this.showError('Formato inválido. Use PNG ou JPG.');
|
||||
e.target.value = ''; // Clear the input
|
||||
logoPreview?.classList.add('d-none');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show success feedback
|
||||
if (logoFilename) {
|
||||
const fileSizeKB = Math.round(file.size / 1024);
|
||||
logoFilename.textContent = `${file.name} (${fileSizeKB}KB)`;
|
||||
}
|
||||
logoPreview?.classList.remove('d-none');
|
||||
|
||||
console.log('Logo selected:', file.name, file.size + ' bytes', file.type);
|
||||
} else {
|
||||
// Hide preview when no file selected
|
||||
logoPreview?.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
handleCornerStyleChange(e) {
|
||||
const selectedStyle = e.target.value;
|
||||
const premiumStyles = ['rounded', 'circle', 'leaf'];
|
||||
|
||||
if (premiumStyles.includes(selectedStyle)) {
|
||||
// Check if user is premium (we can detect this by checking if the option is disabled)
|
||||
const option = e.target.options[e.target.selectedIndex];
|
||||
if (option.disabled) {
|
||||
// Reset to square
|
||||
e.target.value = 'square';
|
||||
this.showUpgradeModal('Estilos de borda personalizados são exclusivos do plano Premium. Faça upgrade para usar esta funcionalidade.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Corner style selected:', selectedStyle);
|
||||
}
|
||||
|
||||
applyQuickStyle(e) {
|
||||
const style = e.target.value;
|
||||
const settings = this.getStyleSettings(style);
|
||||
@ -617,6 +768,48 @@ class QRRapidoGenerator {
|
||||
window.qrRapidoStats.times.push(parseFloat(time));
|
||||
}
|
||||
|
||||
updateSpeedStats(generationTime) {
|
||||
// Update the live statistics display
|
||||
const timeFloat = parseFloat(generationTime);
|
||||
|
||||
// Update average time display in the stats cards
|
||||
const avgElement = document.querySelector('.card-body h5:contains("1.2s")');
|
||||
if (avgElement) {
|
||||
const avgTime = window.qrRapidoStats ? window.qrRapidoStats.getAverageTime() : generationTime;
|
||||
avgElement.innerHTML = `<i class="fas fa-stopwatch"></i> ${avgTime}s`;
|
||||
}
|
||||
|
||||
// Update the generation timer in the header
|
||||
const timerElement = document.querySelector('.generation-timer span');
|
||||
if (timerElement) {
|
||||
timerElement.textContent = `${generationTime}s`;
|
||||
}
|
||||
|
||||
// Update performance statistics
|
||||
if (!window.qrRapidoPerformance) {
|
||||
window.qrRapidoPerformance = {
|
||||
totalGenerations: 0,
|
||||
totalTime: 0,
|
||||
bestTime: Infinity,
|
||||
worstTime: 0
|
||||
};
|
||||
}
|
||||
|
||||
const perf = window.qrRapidoPerformance;
|
||||
perf.totalGenerations++;
|
||||
perf.totalTime += timeFloat;
|
||||
perf.bestTime = Math.min(perf.bestTime, timeFloat);
|
||||
perf.worstTime = Math.max(perf.worstTime, timeFloat);
|
||||
|
||||
// Log performance statistics for debugging
|
||||
console.log('📊 Performance Update:', {
|
||||
currentTime: `${generationTime}s`,
|
||||
averageTime: `${(perf.totalTime / perf.totalGenerations).toFixed(1)}s`,
|
||||
bestTime: `${perf.bestTime.toFixed(1)}s`,
|
||||
totalGenerations: perf.totalGenerations
|
||||
});
|
||||
}
|
||||
|
||||
isPremiumUser() {
|
||||
return document.querySelector('.text-success')?.textContent.includes('Premium Ativo') || false;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user