fix: Melhorar a leitura mesmo com o logotipo maior.
This commit is contained in:
parent
5f0d3dbf66
commit
8ab0b913e2
@ -21,7 +21,9 @@
|
|||||||
"Bash(convert:*)",
|
"Bash(convert:*)",
|
||||||
"Bash(dotnet add package:*)",
|
"Bash(dotnet add package:*)",
|
||||||
"Bash(dotnet remove package:*)",
|
"Bash(dotnet remove package:*)",
|
||||||
"Bash(dotnet restore:*)"
|
"Bash(dotnet restore:*)",
|
||||||
|
"Bash(rg:*)",
|
||||||
|
"Bash(dotnet test:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
|||||||
34
Models/LogoReadability.cs
Normal file
34
Models/LogoReadability.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
namespace QRRapidoApp.Models
|
||||||
|
{
|
||||||
|
public class LogoReadabilityInfo
|
||||||
|
{
|
||||||
|
public bool HasLogo { get; set; }
|
||||||
|
public int LogoSizePercent { get; set; }
|
||||||
|
public int ReadabilityScore { get; set; } // 0-100
|
||||||
|
public string DifficultyLevel { get; set; } = string.Empty; // VeryEasy, Easy, Medium, Hard, VeryHard
|
||||||
|
public string UserMessage { get; set; } = string.Empty;
|
||||||
|
public List<string> Tips { get; set; } = new();
|
||||||
|
public LogoComplexity LogoComplexity { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LogoComplexity
|
||||||
|
{
|
||||||
|
public int Width { get; set; }
|
||||||
|
public int Height { get; set; }
|
||||||
|
public bool IsSquare { get; set; }
|
||||||
|
public bool HasTransparency { get; set; }
|
||||||
|
public int DominantColorCount { get; set; }
|
||||||
|
public double ContrastRatio { get; set; }
|
||||||
|
public string FileFormat { get; set; } = string.Empty;
|
||||||
|
public int FileSizeBytes { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ReadabilityLevel
|
||||||
|
{
|
||||||
|
VeryEasy = 85, // 85-100
|
||||||
|
Easy = 70, // 70-84
|
||||||
|
Medium = 55, // 55-69
|
||||||
|
Hard = 40, // 40-54
|
||||||
|
VeryHard = 0 // 0-39
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
using QRRapidoApp.Models;
|
||||||
|
|
||||||
namespace QRRapidoApp.Models.ViewModels
|
namespace QRRapidoApp.Models.ViewModels
|
||||||
{
|
{
|
||||||
public class QRGenerationRequest
|
public class QRGenerationRequest
|
||||||
@ -39,5 +41,6 @@ namespace QRRapidoApp.Models.ViewModels
|
|||||||
public int? RemainingQRs { get; set; } // For free users
|
public int? RemainingQRs { get; set; } // For free users
|
||||||
public bool Success { get; set; } = true;
|
public bool Success { get; set; } = true;
|
||||||
public string? ErrorMessage { get; set; }
|
public string? ErrorMessage { get; set; }
|
||||||
|
public LogoReadabilityInfo? ReadabilityInfo { get; set; } // Nova propriedade para análise de legibilidade
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -181,6 +181,7 @@ builder.Services.AddScoped<IUserService, UserService>();
|
|||||||
builder.Services.AddScoped<IPlanService, QRRapidoApp.Services.PlanService>();
|
builder.Services.AddScoped<IPlanService, QRRapidoApp.Services.PlanService>();
|
||||||
builder.Services.AddScoped<AdDisplayService>();
|
builder.Services.AddScoped<AdDisplayService>();
|
||||||
builder.Services.AddScoped<StripeService>();
|
builder.Services.AddScoped<StripeService>();
|
||||||
|
builder.Services.AddScoped<LogoReadabilityAnalyzer>();
|
||||||
|
|
||||||
// Background Services
|
// Background Services
|
||||||
builder.Services.AddHostedService<HistoryCleanupService>();
|
builder.Services.AddHostedService<HistoryCleanupService>();
|
||||||
|
|||||||
348
Services/LogoReadabilityAnalyzer.cs
Normal file
348
Services/LogoReadabilityAnalyzer.cs
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
using QRRapidoApp.Models;
|
||||||
|
using QRRapidoApp.Models.ViewModels;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace QRRapidoApp.Services
|
||||||
|
{
|
||||||
|
public class LogoReadabilityAnalyzer
|
||||||
|
{
|
||||||
|
private readonly ILogger<LogoReadabilityAnalyzer> _logger;
|
||||||
|
private readonly IStringLocalizer<LogoReadabilityAnalyzer> _localizer;
|
||||||
|
|
||||||
|
public LogoReadabilityAnalyzer(ILogger<LogoReadabilityAnalyzer> logger, IStringLocalizer<LogoReadabilityAnalyzer> localizer)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_localizer = localizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LogoReadabilityInfo> AnalyzeAsync(QRGenerationRequest request, int qrSize = 300)
|
||||||
|
{
|
||||||
|
var result = new LogoReadabilityInfo();
|
||||||
|
|
||||||
|
if (!request.HasLogo || request.Logo == null || request.Logo.Length == 0)
|
||||||
|
{
|
||||||
|
result.HasLogo = false;
|
||||||
|
result.ReadabilityScore = 100;
|
||||||
|
result.DifficultyLevel = ReadabilityLevel.VeryEasy.ToString();
|
||||||
|
result.UserMessage = "✅ Perfeita legibilidade! QR code sem logo é sempre facilmente lido.";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.HasLogo = true;
|
||||||
|
result.LogoComplexity = await AnalyzeLogoComplexityAsync(request.Logo);
|
||||||
|
result.LogoSizePercent = request.LogoSizePercent ?? 20;
|
||||||
|
|
||||||
|
// Calcular score baseado em múltiplos fatores
|
||||||
|
result.ReadabilityScore = CalculateReadabilityScore(result.LogoComplexity, result.LogoSizePercent, qrSize);
|
||||||
|
result.DifficultyLevel = GetDifficultyLevel(result.ReadabilityScore).ToString();
|
||||||
|
result.UserMessage = GenerateUserMessage(result.ReadabilityScore, result.LogoSizePercent);
|
||||||
|
result.Tips = GeneratePersonalizedTips(result.LogoComplexity, result.LogoSizePercent, result.ReadabilityScore);
|
||||||
|
|
||||||
|
_logger.LogInformation("Logo readability analysis completed - Score: {Score}, Size: {Size}%, Level: {Level}",
|
||||||
|
result.ReadabilityScore, result.LogoSizePercent, result.DifficultyLevel);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error analyzing logo readability");
|
||||||
|
|
||||||
|
// Fallback para análise básica baseada apenas no tamanho
|
||||||
|
result.HasLogo = true;
|
||||||
|
result.LogoSizePercent = request.LogoSizePercent ?? 20;
|
||||||
|
result.ReadabilityScore = Math.Max(40, 100 - (result.LogoSizePercent - 15) * 3);
|
||||||
|
result.DifficultyLevel = GetDifficultyLevel(result.ReadabilityScore).ToString();
|
||||||
|
result.UserMessage = GenerateUserMessage(result.ReadabilityScore, result.LogoSizePercent);
|
||||||
|
result.Tips = new List<string> { "⚠️ Análise limitada devido a erro técnico. Teste sempre em vários dispositivos." };
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<LogoComplexity> AnalyzeLogoComplexityAsync(byte[] logoBytes)
|
||||||
|
{
|
||||||
|
return await Task.Run(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(logoBytes);
|
||||||
|
using var image = Image.Load<Rgba32>(stream);
|
||||||
|
|
||||||
|
var complexity = new LogoComplexity
|
||||||
|
{
|
||||||
|
Width = image.Width,
|
||||||
|
Height = image.Height,
|
||||||
|
IsSquare = Math.Abs(image.Width - image.Height) <= Math.Min(image.Width, image.Height) * 0.1,
|
||||||
|
FileSizeBytes = logoBytes.Length,
|
||||||
|
FileFormat = DetectImageFormat(logoBytes)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Analisar transparência
|
||||||
|
complexity.HasTransparency = HasTransparency(image);
|
||||||
|
|
||||||
|
// Analisar complexidade de cores
|
||||||
|
var colorAnalysis = AnalyzeColors(image);
|
||||||
|
complexity.DominantColorCount = colorAnalysis.dominantColors;
|
||||||
|
complexity.ContrastRatio = colorAnalysis.contrastRatio;
|
||||||
|
|
||||||
|
_logger.LogDebug("Logo complexity analysis - {Width}x{Height}, Square: {IsSquare}, Transparency: {HasTransparency}, Colors: {Colors}",
|
||||||
|
complexity.Width, complexity.Height, complexity.IsSquare, complexity.HasTransparency, complexity.DominantColorCount);
|
||||||
|
|
||||||
|
return complexity;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error analyzing logo complexity");
|
||||||
|
return new LogoComplexity
|
||||||
|
{
|
||||||
|
Width = 100,
|
||||||
|
Height = 100,
|
||||||
|
IsSquare = true,
|
||||||
|
HasTransparency = false,
|
||||||
|
DominantColorCount = 5,
|
||||||
|
ContrastRatio = 0.5,
|
||||||
|
FileFormat = "unknown",
|
||||||
|
FileSizeBytes = logoBytes.Length
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private int CalculateReadabilityScore(LogoComplexity complexity, int logoSizePercent, int qrSize)
|
||||||
|
{
|
||||||
|
int score = 100; // Base score
|
||||||
|
|
||||||
|
// Penalização por tamanho (fator mais importante)
|
||||||
|
if (logoSizePercent > 20)
|
||||||
|
{
|
||||||
|
score -= (logoSizePercent - 20) * 2; // -2 pontos por % acima de 20%
|
||||||
|
}
|
||||||
|
|
||||||
|
// Penalização por complexidade de cores
|
||||||
|
if (complexity.DominantColorCount > 5)
|
||||||
|
{
|
||||||
|
score -= (complexity.DominantColorCount - 5) * 3; // -3 pontos por cor extra
|
||||||
|
}
|
||||||
|
|
||||||
|
// Penalização por baixo contraste
|
||||||
|
if (complexity.ContrastRatio < 0.3)
|
||||||
|
{
|
||||||
|
score -= 10; // Contraste muito baixo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonificações
|
||||||
|
if (complexity.HasTransparency)
|
||||||
|
{
|
||||||
|
score += 5; // PNG com transparência é melhor
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complexity.IsSquare)
|
||||||
|
{
|
||||||
|
score += 5; // Logo quadrado funciona melhor
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qrSize >= 500)
|
||||||
|
{
|
||||||
|
score += 5; // QR codes maiores facilitam leitura
|
||||||
|
}
|
||||||
|
|
||||||
|
// Penalização por logo muito grande em pixels (pode ser mais difícil de processar)
|
||||||
|
if (complexity.Width > 800 || complexity.Height > 800)
|
||||||
|
{
|
||||||
|
score -= 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Garantir que o score fique entre 0-100
|
||||||
|
return Math.Max(0, Math.Min(100, score));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReadabilityLevel GetDifficultyLevel(int score)
|
||||||
|
{
|
||||||
|
return score switch
|
||||||
|
{
|
||||||
|
>= 85 => ReadabilityLevel.VeryEasy,
|
||||||
|
>= 70 => ReadabilityLevel.Easy,
|
||||||
|
>= 55 => ReadabilityLevel.Medium,
|
||||||
|
>= 40 => ReadabilityLevel.Hard,
|
||||||
|
_ => ReadabilityLevel.VeryHard
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GenerateUserMessage(int score, int logoSizePercent)
|
||||||
|
{
|
||||||
|
return score switch
|
||||||
|
{
|
||||||
|
>= 85 => $"✅ Excelente legibilidade! Logo de {logoSizePercent}% será facilmente lido por qualquer celular.",
|
||||||
|
>= 70 => $"🟢 Boa legibilidade! Logo de {logoSizePercent}% deve funcionar bem na maioria dos dispositivos.",
|
||||||
|
>= 55 => $"⚠️ Legibilidade moderada. Logo de {logoSizePercent}% pode exigir melhor iluminação ou posicionamento.",
|
||||||
|
>= 40 => $"🟡 Legibilidade baixa. Logo de {logoSizePercent}% pode ser difícil de ler. Considere reduzir para melhor resultado.",
|
||||||
|
_ => $"🔴 Legibilidade muito baixa. Logo de {logoSizePercent}% provavelmente será difícil de escanear. Recomendamos reduzir significativamente."
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> GeneratePersonalizedTips(LogoComplexity complexity, int logoSizePercent, int score)
|
||||||
|
{
|
||||||
|
var tips = new List<string>();
|
||||||
|
|
||||||
|
// Dicas básicas sempre presentes
|
||||||
|
tips.Add("📱 Teste sempre em vários apps de QR Code diferentes");
|
||||||
|
|
||||||
|
// Dicas baseadas no score
|
||||||
|
if (score < 70)
|
||||||
|
{
|
||||||
|
tips.Add("💡 Use boa iluminação ao escanear para melhor resultado");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dicas baseadas no tamanho
|
||||||
|
if (logoSizePercent > 22)
|
||||||
|
{
|
||||||
|
tips.Add($"📏 Considere reduzir logo para 18-20% (atual: {logoSizePercent}%)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dicas baseadas na complexidade
|
||||||
|
if (!complexity.HasTransparency)
|
||||||
|
{
|
||||||
|
tips.Add("🎨 PNG com fundo transparente melhora a legibilidade");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!complexity.IsSquare)
|
||||||
|
{
|
||||||
|
tips.Add("🔲 Logos quadrados funcionam melhor que retangulares");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complexity.DominantColorCount > 6)
|
||||||
|
{
|
||||||
|
tips.Add("🌈 Logos com menos cores (2-4) são mais fáceis de ler");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (complexity.ContrastRatio < 0.4)
|
||||||
|
{
|
||||||
|
tips.Add("⚡ Aumente o contraste do logo para melhor visibilidade");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dica sobre tamanho do QR
|
||||||
|
tips.Add("📐 QR Codes maiores (500px+) facilitam leitura com logo");
|
||||||
|
|
||||||
|
// Dica final baseada no nível de dificuldade
|
||||||
|
if (score < 55)
|
||||||
|
{
|
||||||
|
tips.Add("🔧 Para máxima compatibilidade, considere versão sem logo para impressões pequenas");
|
||||||
|
}
|
||||||
|
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool HasTransparency(Image<Rgba32> image)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Verificar se há pixels com alpha < 255 (transparência)
|
||||||
|
var hasTransparency = false;
|
||||||
|
image.ProcessPixelRows(accessor =>
|
||||||
|
{
|
||||||
|
for (int y = 0; y < accessor.Height && !hasTransparency; y++)
|
||||||
|
{
|
||||||
|
var pixelRow = accessor.GetRowSpan(y);
|
||||||
|
for (int x = 0; x < pixelRow.Length && !hasTransparency; x++)
|
||||||
|
{
|
||||||
|
if (pixelRow[x].A < 255)
|
||||||
|
{
|
||||||
|
hasTransparency = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return hasTransparency;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false; // Assume não transparente se der erro
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private (int dominantColors, double contrastRatio) AnalyzeColors(Image<Rgba32> image)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var colorCounts = new Dictionary<uint, int>();
|
||||||
|
var totalPixels = 0;
|
||||||
|
var brightPixels = 0;
|
||||||
|
|
||||||
|
// Amostragem para performance (analisa a cada 4 pixels)
|
||||||
|
image.ProcessPixelRows(accessor =>
|
||||||
|
{
|
||||||
|
for (int y = 0; y < accessor.Height; y += 4)
|
||||||
|
{
|
||||||
|
var pixelRow = accessor.GetRowSpan(y);
|
||||||
|
for (int x = 0; x < pixelRow.Length; x += 4)
|
||||||
|
{
|
||||||
|
var pixel = pixelRow[x];
|
||||||
|
|
||||||
|
// Ignorar pixels totalmente transparentes
|
||||||
|
if (pixel.A < 128) continue;
|
||||||
|
|
||||||
|
// Quantizar cores para reduzir variações
|
||||||
|
var quantizedColor = QuantizeColor(pixel);
|
||||||
|
colorCounts[quantizedColor] = colorCounts.GetValueOrDefault(quantizedColor, 0) + 1;
|
||||||
|
|
||||||
|
// Calcular brilho para contraste
|
||||||
|
var luminance = (pixel.R * 0.299 + pixel.G * 0.587 + pixel.B * 0.114) / 255.0;
|
||||||
|
if (luminance > 0.5) brightPixels++;
|
||||||
|
totalPixels++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var dominantColors = Math.Min(colorCounts.Count, 10); // Limitar a 10 cores dominantes
|
||||||
|
var contrastRatio = totalPixels > 0 ? (double)brightPixels / totalPixels : 0.5;
|
||||||
|
|
||||||
|
return (dominantColors, contrastRatio);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return (5, 0.5); // Valores padrão em caso de erro
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint QuantizeColor(Rgba32 color)
|
||||||
|
{
|
||||||
|
// Quantizar cores para 32 níveis (reduz variações mínimas)
|
||||||
|
var r = (byte)((color.R / 8) * 8);
|
||||||
|
var g = (byte)((color.G / 8) * 8);
|
||||||
|
var b = (byte)((color.B / 8) * 8);
|
||||||
|
|
||||||
|
return (uint)(r << 16 | g << 8 | b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string DetectImageFormat(byte[] imageBytes)
|
||||||
|
{
|
||||||
|
if (imageBytes.Length < 4) return "unknown";
|
||||||
|
|
||||||
|
// PNG signature
|
||||||
|
if (imageBytes[0] == 0x89 && imageBytes[1] == 0x50 && imageBytes[2] == 0x4E && imageBytes[3] == 0x47)
|
||||||
|
return "PNG";
|
||||||
|
|
||||||
|
// JPEG signature
|
||||||
|
if (imageBytes[0] == 0xFF && imageBytes[1] == 0xD8)
|
||||||
|
return "JPEG";
|
||||||
|
|
||||||
|
// GIF signature
|
||||||
|
if (imageBytes[0] == 0x47 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46)
|
||||||
|
return "GIF";
|
||||||
|
|
||||||
|
// WebP signature
|
||||||
|
if (imageBytes.Length > 12 &&
|
||||||
|
imageBytes[0] == 0x52 && imageBytes[1] == 0x49 && imageBytes[2] == 0x46 && imageBytes[3] == 0x46 &&
|
||||||
|
imageBytes[8] == 0x57 && imageBytes[9] == 0x45 && imageBytes[10] == 0x42 && imageBytes[11] == 0x50)
|
||||||
|
return "WebP";
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,12 +20,14 @@ namespace QRRapidoApp.Services
|
|||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
private readonly ILogger<QRRapidoService> _logger;
|
private readonly ILogger<QRRapidoService> _logger;
|
||||||
private readonly SemaphoreSlim _semaphore;
|
private readonly SemaphoreSlim _semaphore;
|
||||||
|
private readonly LogoReadabilityAnalyzer _readabilityAnalyzer;
|
||||||
|
|
||||||
public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger<QRRapidoService> logger)
|
public QRRapidoService(IDistributedCache cache, IConfiguration config, ILogger<QRRapidoService> logger, LogoReadabilityAnalyzer readabilityAnalyzer)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_config = config;
|
_config = config;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_readabilityAnalyzer = readabilityAnalyzer;
|
||||||
|
|
||||||
// Limit simultaneous generations to maintain performance
|
// Limit simultaneous generations to maintain performance
|
||||||
var maxConcurrent = _config.GetValue<int>("Performance:MaxConcurrentGenerations", 100);
|
var maxConcurrent = _config.GetValue<int>("Performance:MaxConcurrentGenerations", 100);
|
||||||
@ -54,6 +56,13 @@ namespace QRRapidoApp.Services
|
|||||||
{
|
{
|
||||||
cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
|
cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
cachedResult.FromCache = true;
|
cachedResult.FromCache = true;
|
||||||
|
|
||||||
|
// Se não tem análise de legibilidade no cache, gerar agora
|
||||||
|
if (cachedResult.ReadabilityInfo == null)
|
||||||
|
{
|
||||||
|
cachedResult.ReadabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size);
|
||||||
|
}
|
||||||
|
|
||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -62,6 +71,9 @@ namespace QRRapidoApp.Services
|
|||||||
var qrCode = await GenerateQRCodeOptimizedAsync(request);
|
var qrCode = await GenerateQRCodeOptimizedAsync(request);
|
||||||
var base64 = Convert.ToBase64String(qrCode);
|
var base64 = Convert.ToBase64String(qrCode);
|
||||||
|
|
||||||
|
// Análise de legibilidade do logo
|
||||||
|
var readabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size);
|
||||||
|
|
||||||
var result = new QRGenerationResult
|
var result = new QRGenerationResult
|
||||||
{
|
{
|
||||||
QRCodeBase64 = base64,
|
QRCodeBase64 = base64,
|
||||||
@ -70,7 +82,8 @@ namespace QRRapidoApp.Services
|
|||||||
FromCache = false,
|
FromCache = false,
|
||||||
Size = qrCode.Length,
|
Size = qrCode.Length,
|
||||||
RequestSettings = request,
|
RequestSettings = request,
|
||||||
Success = true
|
Success = true,
|
||||||
|
ReadabilityInfo = readabilityInfo
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cache for configurable time
|
// Cache for configurable time
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
using Moq;
|
using Moq;
|
||||||
using QRRapidoApp.Models.ViewModels;
|
using QRRapidoApp.Models.ViewModels;
|
||||||
using QRRapidoApp.Services;
|
using QRRapidoApp.Services;
|
||||||
@ -14,6 +15,7 @@ namespace QRRapidoApp.Tests.Services
|
|||||||
private readonly Mock<IDistributedCache> _cacheMock;
|
private readonly Mock<IDistributedCache> _cacheMock;
|
||||||
private readonly Mock<IConfiguration> _configMock;
|
private readonly Mock<IConfiguration> _configMock;
|
||||||
private readonly Mock<ILogger<QRRapidoService>> _loggerMock;
|
private readonly Mock<ILogger<QRRapidoService>> _loggerMock;
|
||||||
|
private readonly Mock<LogoReadabilityAnalyzer> _readabilityAnalyzerMock;
|
||||||
private readonly QRRapidoService _service;
|
private readonly QRRapidoService _service;
|
||||||
|
|
||||||
public QRRapidoServiceTests()
|
public QRRapidoServiceTests()
|
||||||
@ -21,9 +23,10 @@ namespace QRRapidoApp.Tests.Services
|
|||||||
_cacheMock = new Mock<IDistributedCache>();
|
_cacheMock = new Mock<IDistributedCache>();
|
||||||
_configMock = new Mock<IConfiguration>();
|
_configMock = new Mock<IConfiguration>();
|
||||||
_loggerMock = new Mock<ILogger<QRRapidoService>>();
|
_loggerMock = new Mock<ILogger<QRRapidoService>>();
|
||||||
|
_readabilityAnalyzerMock = new Mock<LogoReadabilityAnalyzer>(Mock.Of<ILogger<LogoReadabilityAnalyzer>>(), Mock.Of<Microsoft.Extensions.Localization.IStringLocalizer<LogoReadabilityAnalyzer>>());
|
||||||
|
|
||||||
SetupDefaultConfiguration();
|
SetupDefaultConfiguration();
|
||||||
_service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object);
|
_service = new QRRapidoService(_cacheMock.Object, _configMock.Object, _loggerMock.Object, _readabilityAnalyzerMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetupDefaultConfiguration()
|
private void SetupDefaultConfiguration()
|
||||||
|
|||||||
@ -78,6 +78,18 @@ class QRRapidoGenerator {
|
|||||||
logoUpload.addEventListener('change', this.handleLogoSelection.bind(this));
|
logoUpload.addEventListener('change', this.handleLogoSelection.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logo size slider preview em tempo real
|
||||||
|
const logoSizeSlider = document.getElementById('logo-size-slider');
|
||||||
|
if (logoSizeSlider && typeof this.updateLogoReadabilityPreview === 'function') {
|
||||||
|
logoSizeSlider.addEventListener('input', this.updateLogoReadabilityPreview.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logo colorize toggle preview em tempo real
|
||||||
|
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
|
||||||
|
if (logoColorizeToggle && typeof this.updateLogoReadabilityPreview === 'function') {
|
||||||
|
logoColorizeToggle.addEventListener('change', this.updateLogoReadabilityPreview.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
// Corner style validation for non-premium users
|
// Corner style validation for non-premium users
|
||||||
const cornerStyle = document.getElementById('corner-style');
|
const cornerStyle = document.getElementById('corner-style');
|
||||||
if (cornerStyle) {
|
if (cornerStyle) {
|
||||||
@ -506,7 +518,20 @@ class QRRapidoGenerator {
|
|||||||
let content, actualType;
|
let content, actualType;
|
||||||
|
|
||||||
if (type === 'url') {
|
if (type === 'url') {
|
||||||
const dynamicData = window.dynamicQRManager.getDynamicQRData();
|
let dynamicData;
|
||||||
|
if (typeof this.getDynamicQRData === 'function') {
|
||||||
|
dynamicData = this.getDynamicQRData();
|
||||||
|
} else {
|
||||||
|
// Fallback se a função não existir
|
||||||
|
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
|
||||||
|
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
|
||||||
|
dynamicData = {
|
||||||
|
isDynamic: isDynamic,
|
||||||
|
originalUrl: document.getElementById('qr-content').value,
|
||||||
|
requiresPremium: !this.isPremium && isDynamic
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (dynamicData.requiresPremium) {
|
if (dynamicData.requiresPremium) {
|
||||||
throw new Error('QR Dinâmico é exclusivo para usuários Premium. Faça upgrade para usar analytics.');
|
throw new Error('QR Dinâmico é exclusivo para usuários Premium. Faça upgrade para usar analytics.');
|
||||||
}
|
}
|
||||||
@ -558,7 +583,19 @@ class QRRapidoGenerator {
|
|||||||
|
|
||||||
// Add dynamic QR data if it's a URL type
|
// Add dynamic QR data if it's a URL type
|
||||||
if (type === 'url') {
|
if (type === 'url') {
|
||||||
const dynamicData = window.dynamicQRManager.getDynamicQRData();
|
let dynamicData;
|
||||||
|
if (typeof this.getDynamicQRData === 'function') {
|
||||||
|
dynamicData = this.getDynamicQRData();
|
||||||
|
} else {
|
||||||
|
// Fallback se a função não existir
|
||||||
|
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
|
||||||
|
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
|
||||||
|
dynamicData = {
|
||||||
|
isDynamic: isDynamic,
|
||||||
|
originalUrl: document.getElementById('qr-content').value,
|
||||||
|
requiresPremium: !this.isPremium && isDynamic
|
||||||
|
};
|
||||||
|
}
|
||||||
commonData.isDynamic = dynamicData.isDynamic;
|
commonData.isDynamic = dynamicData.isDynamic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -623,7 +660,19 @@ class QRRapidoGenerator {
|
|||||||
// Função auxiliar para obter conteúdo baseado no tipo
|
// Função auxiliar para obter conteúdo baseado no tipo
|
||||||
getContentForType(type) {
|
getContentForType(type) {
|
||||||
if (type === 'url') {
|
if (type === 'url') {
|
||||||
const dynamicData = window.dynamicQRManager?.getDynamicQRData();
|
let dynamicData;
|
||||||
|
if (typeof this.getDynamicQRData === 'function') {
|
||||||
|
dynamicData = this.getDynamicQRData();
|
||||||
|
} else {
|
||||||
|
// Fallback se a função não existir
|
||||||
|
const dynamicToggle = document.getElementById('qr-dynamic-toggle');
|
||||||
|
const isDynamic = dynamicToggle && dynamicToggle.checked && this.isPremium;
|
||||||
|
dynamicData = {
|
||||||
|
isDynamic: isDynamic,
|
||||||
|
originalUrl: document.getElementById('qr-content').value,
|
||||||
|
requiresPremium: !this.isPremium && isDynamic
|
||||||
|
};
|
||||||
|
}
|
||||||
return dynamicData?.originalUrl || document.getElementById('qr-content').value;
|
return dynamicData?.originalUrl || document.getElementById('qr-content').value;
|
||||||
} else if (type === 'wifi') {
|
} else if (type === 'wifi') {
|
||||||
return window.wifiGenerator?.generateWiFiString() || '';
|
return window.wifiGenerator?.generateWiFiString() || '';
|
||||||
@ -661,6 +710,11 @@ class QRRapidoGenerator {
|
|||||||
style="image-rendering: crisp-edges;">
|
style="image-rendering: crisp-edges;">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Exibir análise de legibilidade do logo se disponível
|
||||||
|
if (result.readabilityInfo && typeof this.displayReadabilityAnalysis === 'function') {
|
||||||
|
this.displayReadabilityAnalysis(result.readabilityInfo);
|
||||||
|
}
|
||||||
|
|
||||||
// CORREÇÃO: Log para debug - verificar se QR code tem logo
|
// CORREÇÃO: Log para debug - verificar se QR code tem logo
|
||||||
const logoUpload = document.getElementById('logo-upload');
|
const logoUpload = document.getElementById('logo-upload');
|
||||||
const hasLogo = logoUpload && logoUpload.files && logoUpload.files.length > 0;
|
const hasLogo = logoUpload && logoUpload.files && logoUpload.files.length > 0;
|
||||||
@ -668,7 +722,8 @@ class QRRapidoGenerator {
|
|||||||
hasLogo: hasLogo,
|
hasLogo: hasLogo,
|
||||||
logoFile: hasLogo ? logoUpload.files[0].name : 'nenhum',
|
logoFile: hasLogo ? logoUpload.files[0].name : 'nenhum',
|
||||||
generationTime: generationTime + 's',
|
generationTime: generationTime + 's',
|
||||||
imageSize: result.qrCodeBase64.length + ' chars'
|
imageSize: result.qrCodeBase64.length + ' chars',
|
||||||
|
readabilityScore: result.readabilityInfo?.readabilityScore
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show generation statistics
|
// Show generation statistics
|
||||||
@ -685,7 +740,8 @@ class QRRapidoGenerator {
|
|||||||
base64: result.qrCodeBase64,
|
base64: result.qrCodeBase64,
|
||||||
qrCodeBase64: result.qrCodeBase64, // Both properties for compatibility
|
qrCodeBase64: result.qrCodeBase64, // Both properties for compatibility
|
||||||
id: result.qrId,
|
id: result.qrId,
|
||||||
generationTime: generationTime
|
generationTime: generationTime,
|
||||||
|
readabilityInfo: result.readabilityInfo
|
||||||
};
|
};
|
||||||
|
|
||||||
// Increment rate limit counter after successful generation
|
// Increment rate limit counter after successful generation
|
||||||
@ -916,6 +972,11 @@ class QRRapidoGenerator {
|
|||||||
type: file.type,
|
type: file.type,
|
||||||
timestamp: new Date().toLocaleTimeString()
|
timestamp: new Date().toLocaleTimeString()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Atualizar preview de legibilidade
|
||||||
|
if (typeof this.updateLogoReadabilityPreview === 'function') {
|
||||||
|
this.updateLogoReadabilityPreview();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
} else {
|
} else {
|
||||||
@ -925,6 +986,11 @@ class QRRapidoGenerator {
|
|||||||
if (logoVisualPreview) logoVisualPreview.style.display = 'none';
|
if (logoVisualPreview) logoVisualPreview.style.display = 'none';
|
||||||
if (logoPreviewImage) logoPreviewImage.src = '';
|
if (logoPreviewImage) logoPreviewImage.src = '';
|
||||||
|
|
||||||
|
// Limpar preview de legibilidade
|
||||||
|
if (typeof this.clearReadabilityPreview === 'function') {
|
||||||
|
this.clearReadabilityPreview();
|
||||||
|
}
|
||||||
|
|
||||||
console.log('🗑️ Logo removido');
|
console.log('🗑️ Logo removido');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2424,6 +2490,203 @@ class DynamicQRManager {
|
|||||||
requiresPremium: !this.isPremium && isDynamic
|
requiresPremium: !this.isPremium && isDynamic
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
displayReadabilityAnalysis(readabilityInfo) {
|
||||||
|
if (!readabilityInfo || !readabilityInfo.hasLogo) return;
|
||||||
|
|
||||||
|
// Criar ou encontrar container para análise de legibilidade
|
||||||
|
let analysisContainer = document.getElementById('readability-analysis');
|
||||||
|
if (!analysisContainer) {
|
||||||
|
// Criar container se não existir
|
||||||
|
analysisContainer = document.createElement('div');
|
||||||
|
analysisContainer.id = 'readability-analysis';
|
||||||
|
analysisContainer.className = 'mt-3 p-3 border rounded bg-light';
|
||||||
|
|
||||||
|
// Inserir após o preview do QR code
|
||||||
|
const previewDiv = document.getElementById('qr-preview');
|
||||||
|
if (previewDiv && previewDiv.parentNode) {
|
||||||
|
previewDiv.parentNode.insertBefore(analysisContainer, previewDiv.nextSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determinar classe de cor baseada no score
|
||||||
|
const getScoreClass = (score) => {
|
||||||
|
if (score >= 85) return 'text-success';
|
||||||
|
if (score >= 70) return 'text-info';
|
||||||
|
if (score >= 55) return 'text-warning';
|
||||||
|
return 'text-danger';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determinar ícone baseado no nível de dificuldade
|
||||||
|
const getIconClass = (score) => {
|
||||||
|
if (score >= 85) return 'fas fa-check-circle';
|
||||||
|
if (score >= 70) return 'fas fa-thumbs-up';
|
||||||
|
if (score >= 55) return 'fas fa-exclamation-triangle';
|
||||||
|
return 'fas fa-exclamation-circle';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gerar lista de dicas
|
||||||
|
const tipsHtml = readabilityInfo.tips && readabilityInfo.tips.length > 0
|
||||||
|
? `<div class="collapse mt-2" id="readabilityTips">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
${readabilityInfo.tips.map(tip => `<li class="mb-1"><small>${tip}</small></li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Criar HTML da análise
|
||||||
|
const scoreClass = getScoreClass(readabilityInfo.readabilityScore);
|
||||||
|
const iconClass = getIconClass(readabilityInfo.readabilityScore);
|
||||||
|
|
||||||
|
analysisContainer.innerHTML = `
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="${iconClass} ${scoreClass} me-2"></i>
|
||||||
|
<div>
|
||||||
|
<h6 class="mb-1 ${scoreClass}">Análise de Legibilidade</h6>
|
||||||
|
<p class="mb-0 small">${readabilityInfo.userMessage}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="badge bg-secondary">${readabilityInfo.readabilityScore}/100</div>
|
||||||
|
${readabilityInfo.tips && readabilityInfo.tips.length > 0
|
||||||
|
? `<button class="btn btn-sm btn-outline-secondary ms-2" type="button"
|
||||||
|
data-bs-toggle="collapse" data-bs-target="#readabilityTips">
|
||||||
|
<i class="fas fa-lightbulb"></i> Dicas
|
||||||
|
</button>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${tipsHtml}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Animação de entrada suave
|
||||||
|
analysisContainer.style.opacity = '0';
|
||||||
|
analysisContainer.style.transform = 'translateY(-10px)';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
analysisContainer.style.transition = 'all 0.3s ease';
|
||||||
|
analysisContainer.style.opacity = '1';
|
||||||
|
analysisContainer.style.transform = 'translateY(0)';
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
console.log('📊 Análise de legibilidade exibida:', {
|
||||||
|
score: readabilityInfo.readabilityScore,
|
||||||
|
level: readabilityInfo.difficultyLevel,
|
||||||
|
logoSize: readabilityInfo.logoSizePercent + '%',
|
||||||
|
tipsCount: readabilityInfo.tips?.length || 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLogoReadabilityPreview() {
|
||||||
|
const logoUpload = document.getElementById('logo-upload');
|
||||||
|
const logoSizeSlider = document.getElementById('logo-size-slider');
|
||||||
|
|
||||||
|
// Só mostrar preview se há logo selecionado
|
||||||
|
if (!logoUpload || !logoUpload.files || logoUpload.files.length === 0) {
|
||||||
|
this.clearReadabilityPreview();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoSize = parseInt(logoSizeSlider?.value || '20');
|
||||||
|
|
||||||
|
// Calcular score estimado baseado apenas no tamanho (simplificado para preview)
|
||||||
|
let estimatedScore = 100;
|
||||||
|
if (logoSize > 20) {
|
||||||
|
estimatedScore -= (logoSize - 20) * 2;
|
||||||
|
}
|
||||||
|
estimatedScore = Math.max(40, estimatedScore); // Mínimo de 40
|
||||||
|
|
||||||
|
// Simular análise básica para preview
|
||||||
|
const previewInfo = {
|
||||||
|
hasLogo: true,
|
||||||
|
logoSizePercent: logoSize,
|
||||||
|
readabilityScore: estimatedScore,
|
||||||
|
difficultyLevel: this.getDifficultyLevelFromScore(estimatedScore),
|
||||||
|
userMessage: this.generatePreviewMessage(estimatedScore, logoSize),
|
||||||
|
tips: this.generatePreviewTips(logoSize, estimatedScore)
|
||||||
|
};
|
||||||
|
|
||||||
|
this.displayReadabilityPreview(previewInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDifficultyLevelFromScore(score) {
|
||||||
|
if (score >= 85) return 'VeryEasy';
|
||||||
|
if (score >= 70) return 'Easy';
|
||||||
|
if (score >= 55) return 'Medium';
|
||||||
|
if (score >= 40) return 'Hard';
|
||||||
|
return 'VeryHard';
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePreviewMessage(score, logoSize) {
|
||||||
|
if (score >= 85) {
|
||||||
|
return `✅ Estimativa: Excelente legibilidade com logo de ${logoSize}%`;
|
||||||
|
} else if (score >= 70) {
|
||||||
|
return `🟢 Estimativa: Boa legibilidade com logo de ${logoSize}%`;
|
||||||
|
} else if (score >= 55) {
|
||||||
|
return `⚠️ Estimativa: Legibilidade moderada com logo de ${logoSize}%`;
|
||||||
|
} else {
|
||||||
|
return `🟡 Estimativa: Legibilidade baixa com logo de ${logoSize}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generatePreviewTips(logoSize, score) {
|
||||||
|
const tips = [];
|
||||||
|
|
||||||
|
if (logoSize > 22) {
|
||||||
|
tips.push(`📏 Considere reduzir para 18-20% (atual: ${logoSize}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (score < 70) {
|
||||||
|
tips.push('💡 Use boa iluminação ao escanear');
|
||||||
|
}
|
||||||
|
|
||||||
|
tips.push('🎨 PNG transparente melhora a legibilidade');
|
||||||
|
tips.push('📱 Teste em vários apps de QR Code');
|
||||||
|
|
||||||
|
return tips;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayReadabilityPreview(previewInfo) {
|
||||||
|
// Usar a mesma função de display, mas com ID diferente para preview
|
||||||
|
let previewContainer = document.getElementById('readability-preview');
|
||||||
|
if (!previewContainer) {
|
||||||
|
previewContainer = document.createElement('div');
|
||||||
|
previewContainer.id = 'readability-preview';
|
||||||
|
previewContainer.className = 'mt-2 p-2 border rounded bg-info bg-opacity-10 border-info border-opacity-25';
|
||||||
|
|
||||||
|
// Inserir após o slider de tamanho do logo
|
||||||
|
const logoSizeSlider = document.getElementById('logo-size-slider');
|
||||||
|
if (logoSizeSlider && logoSizeSlider.parentNode) {
|
||||||
|
logoSizeSlider.parentNode.insertBefore(previewContainer, logoSizeSlider.nextSibling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scoreClass = previewInfo.readabilityScore >= 85 ? 'text-success' :
|
||||||
|
previewInfo.readabilityScore >= 70 ? 'text-info' :
|
||||||
|
previewInfo.readabilityScore >= 55 ? 'text-warning' : 'text-danger';
|
||||||
|
|
||||||
|
const iconClass = previewInfo.readabilityScore >= 85 ? 'fas fa-check-circle' :
|
||||||
|
previewInfo.readabilityScore >= 70 ? 'fas fa-thumbs-up' :
|
||||||
|
previewInfo.readabilityScore >= 55 ? 'fas fa-exclamation-triangle' : 'fas fa-exclamation-circle';
|
||||||
|
|
||||||
|
previewContainer.innerHTML = `
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="${iconClass} ${scoreClass} me-2"></i>
|
||||||
|
<small class="${scoreClass}">${previewInfo.userMessage}</small>
|
||||||
|
</div>
|
||||||
|
<div class="badge bg-secondary">${previewInfo.readabilityScore}/100</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearReadabilityPreview() {
|
||||||
|
const previewContainer = document.getElementById('readability-preview');
|
||||||
|
if (previewContainer) {
|
||||||
|
previewContainer.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SMSQRGenerator {
|
class SMSQRGenerator {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user