From 00d924ce3bdf79b52cc64187a8bc28c87f0dd836 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Wed, 30 Jul 2025 21:35:28 -0300 Subject: [PATCH] feat: ajustes qrcode, estilos e cores. --- .claude/settings.local.json | 4 +- Controllers/QRController.cs | 174 +++++++++++++++++++++++ Services/QRRapidoService.cs | 237 ++++++++++++++++++++++++++++++- Views/Home/Index.cshtml | 71 +++++++-- wwwroot/js/qr-speed-generator.js | 237 ++++++++++++++++++++++++++++--- 5 files changed, 689 insertions(+), 34 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 16797be..9895e44 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -5,7 +5,9 @@ "Bash(find:*)", "Bash(dotnet build:*)", "Bash(timeout:*)", - "Bash(rm:*)" + "Bash(rm:*)", + "Bash(dotnet run:*)", + "Bash(curl:*)" ], "deny": [] } diff --git a/Controllers/QRController.cs b/Controllers/QRController.cs index d109a52..df5c726 100644 --- a/Controllers/QRController.cs +++ b/Controllers/QRController.cs @@ -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 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 + { + ["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 GetHistory(int limit = 20) { diff --git a/Services/QRRapidoService.cs b/Services/QRRapidoService.cs index e95317d..4409990 100644 --- a/Services/QRRapidoService.cs +++ b/Services/QRRapidoService.cs @@ -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 ExtractQRModules(Bitmap image, int moduleSize) + { + var modules = new List(); + 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; } + } } } \ No newline at end of file diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 38f35d8..3e7ca44 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -158,19 +158,70 @@ @if (User.Identity.IsAuthenticated) {
-
- - -
@Localizer["PNGJPGUp2MB"]
-
+ @{ + var userService = Context.RequestServices.GetService(); + 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) + { +
+ + +
@Localizer["PNGJPGUp2MB"]
+
+ + + + +
+
+ } + else + { +
+
+
+ + Logo Personalizado - Premium +
+

Adicione sua marca aos QR Codes! Upgrade para Premium e personalize com seu logo.

+ + Fazer Upgrade + +
+
+ } +
- + + @if (isPremium) + { + + + + } + else + { + + + + } + @if (!isPremium) + { +
+ + Upgrade Premium para estilos personalizados +
+ }
} diff --git a/wwwroot/js/qr-speed-generator.js b/wwwroot/js/qr-speed-generator.js index b4693de..a657960 100644 --- a/wwwroot/js/qr-speed-generator.js +++ b/wwwroot/js/qr-speed-generator.js @@ -58,6 +58,18 @@ class QRRapidoGenerator { document.querySelectorAll('input[name="quick-style"]').forEach(radio => { 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'); @@ -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 = ` ${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; }