feat: ajustes qrcode, estilos e cores.
Some checks failed
Deploy QR Rapido / test (push) Successful in 51s
Deploy QR Rapido / build-and-push (push) Failing after 6s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Has been skipped

This commit is contained in:
Ricardo Carneiro 2025-07-30 21:35:28 -03:00
parent a8faf0ef2f
commit 00d924ce3b
5 changed files with 689 additions and 34 deletions

View File

@ -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": []
} }

View File

@ -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)
{ {

View File

@ -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; }
}
} }
} }

View File

@ -158,19 +158,70 @@
@if (User.Identity.IsAuthenticated) @if (User.Identity.IsAuthenticated)
{ {
<div class="row"> <div class="row">
@{
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"> <div class="col-md-6 mb-3">
<label class="form-label">@Localizer["LogoIcon"]</label> <label class="form-label">
<input type="file" id="logo-upload" class="form-control" accept="image/*"> @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 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>
</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>
} }

View File

@ -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,20 +391,85 @@ 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);
// 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 { return {
data: {
type: document.getElementById('qr-type').value, type: document.getElementById('qr-type').value,
content: document.getElementById('qr-content').value, content: document.getElementById('qr-content').value,
quickStyle: quickStyle, quickStyle: quickStyle,
primaryColor: document.getElementById('primary-color').value, primaryColor: finalPrimaryColor,
backgroundColor: document.getElementById('bg-color').value, backgroundColor: finalBackgroundColor,
size: parseInt(document.getElementById('qr-size').value), size: parseInt(document.getElementById('qr-size').value),
margin: parseInt(document.getElementById('qr-margin').value), margin: parseInt(document.getElementById('qr-margin').value),
cornerStyle: document.getElementById('corner-style')?.value || 'square', cornerStyle: document.getElementById('corner-style')?.value || 'square',
optimizeForSpeed: true, optimizeForSpeed: true,
language: this.currentLang, language: this.currentLang
...styleSettings },
isMultipart: false,
endpoint: '/api/QR/GenerateRapid'
}; };
} }
}
getStyleSettings(style) { getStyleSettings(style) {
const styles = { const styles = {
@ -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;
} }