feat: ajustes qrcode, estilos e cores.
This commit is contained in:
parent
a8faf0ef2f
commit
00d924ce3b
@ -5,7 +5,9 @@
|
|||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(dotnet build:*)",
|
"Bash(dotnet build:*)",
|
||||||
"Bash(timeout:*)",
|
"Bash(timeout:*)",
|
||||||
"Bash(rm:*)"
|
"Bash(rm:*)",
|
||||||
|
"Bash(dotnet run:*)",
|
||||||
|
"Bash(curl:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,6 +63,19 @@ namespace QRRapidoApp.Controllers
|
|||||||
// Check user status
|
// Check user status
|
||||||
var user = await _userService.GetUserAsync(userId);
|
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
|
// Rate limiting for free users
|
||||||
var rateLimitPassed = await CheckRateLimitAsync(userId, user);
|
var rateLimitPassed = await CheckRateLimitAsync(userId, user);
|
||||||
if (!rateLimitPassed)
|
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")]
|
[HttpGet("History")]
|
||||||
public async Task<IActionResult> GetHistory(int limit = 20)
|
public async Task<IActionResult> GetHistory(int limit = 20)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using QRCoder;
|
|||||||
using QRRapidoApp.Models.ViewModels;
|
using QRRapidoApp.Models.ViewModels;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@ -114,7 +115,21 @@ namespace QRRapidoApp.Services
|
|||||||
var primaryColorBytes = ColorToBytes(ParseHtmlColor(request.PrimaryColor));
|
var primaryColorBytes = ColorToBytes(ParseHtmlColor(request.PrimaryColor));
|
||||||
var backgroundColorBytes = ColorToBytes(ParseHtmlColor(request.BackgroundColor));
|
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 };
|
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)
|
@if (User.Identity.IsAuthenticated)
|
||||||
{
|
{
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 mb-3">
|
@{
|
||||||
<label class="form-label">@Localizer["LogoIcon"]</label>
|
var userService = Context.RequestServices.GetService<QRRapidoApp.Services.IUserService>();
|
||||||
<input type="file" id="logo-upload" class="form-control" accept="image/*">
|
var currentUserId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||||
<div class="form-text">@Localizer["PNGJPGUp2MB"]</div>
|
var currentUser = currentUserId != null ? await userService.GetUserAsync(currentUserId) : null;
|
||||||
</div>
|
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">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label">@Localizer["BorderStyle"]</label>
|
<label class="form-label">@Localizer["BorderStyle"]</label>
|
||||||
<select id="corner-style" class="form-select">
|
<select id="corner-style" class="form-select @(isPremium ? "" : "border-warning")">
|
||||||
<option value="square">@Localizer["Square"]</option>
|
<option value="square">@Localizer["Square"] (Grátis)</option>
|
||||||
<option value="rounded">@Localizer["Rounded"]</option>
|
@if (isPremium)
|
||||||
<option value="circle">@Localizer["Circular"]</option>
|
{
|
||||||
<option value="leaf">@Localizer["Leaf"]</option>
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,6 +59,18 @@ class QRRapidoGenerator {
|
|||||||
radio.addEventListener('change', this.applyQuickStyle.bind(this));
|
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
|
// QR type change with hints
|
||||||
const qrType = document.getElementById('qr-type');
|
const qrType = document.getElementById('qr-type');
|
||||||
if (qrType) {
|
if (qrType) {
|
||||||
@ -295,28 +307,47 @@ class QRRapidoGenerator {
|
|||||||
this.startTime = performance.now();
|
this.startTime = performance.now();
|
||||||
this.showGenerationStarted();
|
this.showGenerationStarted();
|
||||||
|
|
||||||
const formData = this.collectFormData();
|
const requestData = this.collectFormData();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/QR/GenerateRapid', {
|
// Build fetch options based on request type
|
||||||
|
const fetchOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
body: requestData.isMultipart ? requestData.data : JSON.stringify(requestData.data)
|
||||||
'Content-Type': 'application/json',
|
};
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
// 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) {
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
|
||||||
if (response.status === 429) {
|
if (response.status === 429) {
|
||||||
this.showUpgradeModal('Limite de QR codes atingido! Upgrade para QR Rapido Premium e gere códigos ilimitados.');
|
this.showUpgradeModal('Limite de QR codes atingido! Upgrade para QR Rapido Premium e gere códigos ilimitados.');
|
||||||
return;
|
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();
|
const result = await response.json();
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
|
if (result.requiresPremium) {
|
||||||
|
this.showUpgradeModal(result.error || 'Logo personalizado é exclusivo do plano Premium.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
throw new Error(result.error || 'Erro desconhecido');
|
throw new Error(result.error || 'Erro desconhecido');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +355,7 @@ class QRRapidoGenerator {
|
|||||||
|
|
||||||
this.displayQRResult(result, generationTime);
|
this.displayQRResult(result, generationTime);
|
||||||
this.updateSpeedStats(generationTime);
|
this.updateSpeedStats(generationTime);
|
||||||
this.trackGenerationEvent(formData.type, generationTime);
|
this.trackGenerationEvent(requestData.data.type || requestData.data.get('type'), generationTime);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Erro ao gerar QR:', 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 quickStyle = document.querySelector('input[name="quick-style"]:checked')?.value || 'classic';
|
||||||
const styleSettings = this.getStyleSettings(quickStyle);
|
const styleSettings = this.getStyleSettings(quickStyle);
|
||||||
|
|
||||||
return {
|
// Check if logo is selected for premium users
|
||||||
type: document.getElementById('qr-type').value,
|
const logoUpload = document.getElementById('logo-upload');
|
||||||
content: document.getElementById('qr-content').value,
|
const hasLogo = logoUpload && logoUpload.files && logoUpload.files[0];
|
||||||
quickStyle: quickStyle,
|
|
||||||
primaryColor: document.getElementById('primary-color').value,
|
if (hasLogo) {
|
||||||
backgroundColor: document.getElementById('bg-color').value,
|
// Use FormData for premium users with logo
|
||||||
size: parseInt(document.getElementById('qr-size').value),
|
const formData = new FormData();
|
||||||
margin: parseInt(document.getElementById('qr-margin').value),
|
|
||||||
cornerStyle: document.getElementById('corner-style')?.value || 'square',
|
// Get user-selected colors with proper priority
|
||||||
optimizeForSpeed: true,
|
const userPrimaryColor = document.getElementById('primary-color').value;
|
||||||
language: this.currentLang,
|
const userBackgroundColor = document.getElementById('bg-color').value;
|
||||||
...styleSettings
|
|
||||||
};
|
// 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) {
|
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) {
|
applyQuickStyle(e) {
|
||||||
const style = e.target.value;
|
const style = e.target.value;
|
||||||
const settings = this.getStyleSettings(style);
|
const settings = this.getStyleSettings(style);
|
||||||
@ -617,6 +768,48 @@ class QRRapidoGenerator {
|
|||||||
window.qrRapidoStats.times.push(parseFloat(time));
|
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() {
|
isPremiumUser() {
|
||||||
return document.querySelector('.text-success')?.textContent.includes('Premium Ativo') || false;
|
return document.querySelector('.text-success')?.textContent.includes('Premium Ativo') || false;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user