diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..66bf335 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build + +on: + push: + branches: + - main + - dev + pull_request: + branches: + - main + - dev + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + build: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '9.0.x' + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Restore + run: msbuild "Simple QR Code Maker.sln" /t:Restore /p:Configuration=Release /p:Platform=x64 + + - name: Build + run: msbuild "Simple QR Code Maker.sln" /p:Configuration=Release /p:Platform=x64 \ No newline at end of file diff --git a/Simple QR Code Maker.Core/Simple QR Code Maker.Core.csproj b/Simple QR Code Maker.Core/Simple QR Code Maker.Core.csproj index 4af9d37..8c1cb13 100644 --- a/Simple QR Code Maker.Core/Simple QR Code Maker.Core.csproj +++ b/Simple QR Code Maker.Core/Simple QR Code Maker.Core.csproj @@ -1,6 +1,6 @@  - net8.0 + net9.0 Simple_QR_Code_Maker.Core x86;x64;arm64;AnyCPU enable @@ -11,6 +11,6 @@ - + diff --git a/Simple QR Code Maker/Controls/IconAndTextContent.xaml b/Simple QR Code Maker/Controls/IconAndTextContent.xaml new file mode 100644 index 0000000..4583fe8 --- /dev/null +++ b/Simple QR Code Maker/Controls/IconAndTextContent.xaml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/Simple QR Code Maker/Controls/IconAndTextContent.xaml.cs b/Simple QR Code Maker/Controls/IconAndTextContent.xaml.cs new file mode 100644 index 0000000..3f6f92e --- /dev/null +++ b/Simple QR Code Maker/Controls/IconAndTextContent.xaml.cs @@ -0,0 +1,26 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Simple_QR_Code_Maker.Controls; + +public sealed partial class IconAndTextContent : StackPanel +{ + public IconAndTextContent() + { + InitializeComponent(); + } + + public Symbol Icon + { get => (Symbol)GetValue(IconProperty); set => SetValue(IconProperty, value); + } + + public static readonly DependencyProperty IconProperty = + DependencyProperty.Register(nameof(Icon), typeof(Symbol), typeof(IconAndTextContent), new PropertyMetadata(Symbol.Placeholder)); + + public string ContentText + { get => (string)GetValue(ContentTextProperty); set => SetValue(ContentTextProperty, value); + } + + public static readonly DependencyProperty ContentTextProperty = + DependencyProperty.Register(nameof(ContentText), typeof(string), typeof(IconAndTextContent), new PropertyMetadata(string.Empty)); +} diff --git a/Simple QR Code Maker/Helpers/BarcodeHelpers.cs b/Simple QR Code Maker/Helpers/BarcodeHelpers.cs index 51ff1eb..53f4bf6 100644 --- a/Simple QR Code Maker/Helpers/BarcodeHelpers.cs +++ b/Simple QR Code Maker/Helpers/BarcodeHelpers.cs @@ -14,7 +14,28 @@ namespace Simple_QR_Code_Maker.Helpers; public static class BarcodeHelpers { - public static WriteableBitmap GetQrCodeBitmapFromText(string text, ErrorCorrectionLevel correctionLevel, System.Drawing.Color foreground, System.Drawing.Color background) + /// + /// Calculate the maximum safe logo size percentage based on QR code error correction level and version + /// + /// The text to encode in the QR code + /// The error correction level + /// Maximum safe logo size as a percentage (0-100) + // public static int GetMaxLogoSizePercentage(string text, ErrorCorrectionLevel correctionLevel) + public static int GetMaxLogoSizePercentage(ErrorCorrectionLevel correctionLevel) + { + // Error correction capacity by level (percentage of code that can be damaged and still readable) + // These are the theoretical maximum recovery percentages + return correctionLevel.ToString() switch + { + "L" => 20, // Low: ~7% + "M" => 23, // Medium: ~15% + "Q" => 35, // Quartile: ~25% + "H" => 40, // High: ~30% + _ => 20 + }; + } + + public static WriteableBitmap GetQrCodeBitmapFromText(string text, ErrorCorrectionLevel correctionLevel, System.Drawing.Color foreground, System.Drawing.Color background, Bitmap? logoImage = null, double logoSizePercentage = 20.0, double logoPaddingPixels = 8.0) { BitmapRenderer bitmapRenderer = new() { @@ -38,6 +59,16 @@ public static WriteableBitmap GetQrCodeBitmapFromText(string text, ErrorCorrecti barcodeWriter.Options = encodingOptions; using Bitmap bitmap = barcodeWriter.Write(text); + + // If a logo is provided, overlay it on the center of the QR code + if (logoImage != null) + { + // Get the QR code details to calculate module size + QRCode qrCode = ZXing.QrCode.Internal.Encoder.encode(text, correctionLevel); + int moduleCount = qrCode.Version.DimensionForVersion; + OverlayLogoOnQrCode(bitmap, logoImage, logoSizePercentage, moduleCount, encodingOptions.Margin, logoPaddingPixels, background); + } + using MemoryStream ms = new(); bitmap.Save(ms, ImageFormat.Png); WriteableBitmap bitmapImage = new(encodingOptions.Width, encodingOptions.Height); @@ -47,6 +78,81 @@ public static WriteableBitmap GetQrCodeBitmapFromText(string text, ErrorCorrecti return bitmapImage; } + private static void OverlayLogoOnQrCode(Bitmap qrCodeBitmap, Bitmap logo, double sizePercentage, int moduleCount, int margin, double logoPaddingPixels, System.Drawing.Color backgroundColor) + { + // Calculate the pixel size of each QR code module + // The total size includes the margin on both sides + int totalModules = moduleCount + (margin * 2); + double modulePixelSize = (double)qrCodeBitmap.Width / totalModules; + + // Calculate the punchout space size based on the size percentage + // This is the area that will be covered (blocking QR code modules) + int punchoutSizePixels = (int)(Math.Min(qrCodeBitmap.Width, qrCodeBitmap.Height) * (sizePercentage / 100.0)); + + // Round the punchout size to the nearest module boundary + int punchoutSizeModules = (int)Math.Round(punchoutSizePixels / modulePixelSize); + // Ensure it's at least 1 module and odd number for better centering + if (punchoutSizeModules < 1) punchoutSizeModules = 1; + if (punchoutSizeModules % 2 == 0) punchoutSizeModules++; // Make it odd for symmetry + + // Convert back to pixels, aligned to module boundaries + int punchoutSize = (int)(punchoutSizeModules * modulePixelSize); + + // Calculate the position to center the punchout area + int punchoutX = (qrCodeBitmap.Width - punchoutSize) / 2; + int punchoutY = (qrCodeBitmap.Height - punchoutSize) / 2; + + // Convert padding to actual pixels + // Positive padding = logo smaller than punchout (adds white space) + // Negative padding = logo larger than punchout (logo extends beyond white background) + int paddingPixels = (int)Math.Round(logoPaddingPixels); + + // Calculate the actual logo display size + // Logo fills the punchout area minus the padding on all sides + int logoDisplayWidth = Math.Max(1, punchoutSize - (Math.Abs(paddingPixels) * 2)); + int logoDisplayHeight = Math.Max(1, punchoutSize - (Math.Abs(paddingPixels) * 2)); + + // If padding is negative, logo is larger than punchout + if (paddingPixels < 0) + { + logoDisplayWidth = punchoutSize + (Math.Abs(paddingPixels) * 2); + logoDisplayHeight = punchoutSize + (Math.Abs(paddingPixels) * 2); + } + + // Calculate logo dimensions preserving aspect ratio + float aspectRatio = (float)logo.Width / logo.Height; + int logoWidth, logoHeight; + + if (aspectRatio > 1) // Wider than tall + { + logoWidth = logoDisplayWidth; + logoHeight = (int)(logoDisplayWidth / aspectRatio); + } + else // Taller than wide or square + { + logoHeight = logoDisplayHeight; + logoWidth = (int)(logoDisplayHeight * aspectRatio); + } + + // Center the logo within the punchout area (or offset if larger) + int logoX = punchoutX + (punchoutSize - logoWidth) / 2; + int logoY = punchoutY + (punchoutSize - logoHeight) / 2; + + using Graphics g = Graphics.FromImage(qrCodeBitmap); + // Set high quality rendering + g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; + g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality; + g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; + + // Draw the punchout background with the same color as the QR code background + using SolidBrush backgroundBrush = new(backgroundColor); + g.FillRectangle(backgroundBrush, punchoutX, punchoutY, punchoutSize, punchoutSize); + + // Draw the logo scaled to fit within or extend beyond the punchout + g.DrawImage(logo, logoX, logoY, logoWidth, logoHeight); + } + /// /// Calculate the smallest side of a QR code based on the distance between the camera and the QR code /// @@ -106,7 +212,7 @@ public static string SmallestSideWithUnits(double distance, int numberOfBlocks, return $"{smallestSideCm:F2} x {smallestSideCm:F2} cm"; } - public static SvgImage GetSvgQrCodeForText(string text, ErrorCorrectionLevel correctionLevel, System.Drawing.Color foreground, System.Drawing.Color background) + public static SvgImage GetSvgQrCodeForText(string text, ErrorCorrectionLevel correctionLevel, System.Drawing.Color foreground, System.Drawing.Color background, Bitmap? logoImage = null, double logoSizePercentage = 20.0, double logoPaddingPixels = 8.0) { SvgRenderer svgRenderer = new() { @@ -131,9 +237,114 @@ public static SvgImage GetSvgQrCodeForText(string text, ErrorCorrectionLevel cor SvgImage svg = barcodeWriter.Write(text); + // If a logo is provided, embed it in the SVG + if (logoImage != null) + { + // Get the QR code details to calculate module size + QRCode qrCode = ZXing.QrCode.Internal.Encoder.encode(text, correctionLevel); + int moduleCount = qrCode.Version.DimensionForVersion; + svg = EmbedLogoInSvg(svg, logoImage, logoSizePercentage, moduleCount, encodingOptions.Margin, logoPaddingPixels, background); + } + return svg; } + private static SvgImage EmbedLogoInSvg(SvgImage svg, Bitmap logo, double sizePercentage, int moduleCount, int margin, double logoPaddingPixels, System.Drawing.Color backgroundColor) + { + const int svgSize = 1024; // Should match the encoding options Width/Height + + // Calculate the pixel size of each QR code module + int totalModules = moduleCount + (margin * 2); + double modulePixelSize = (double)svgSize / totalModules; + + // Calculate the punchout space size based on the size percentage + // This is the area that will be covered (blocking QR code modules) + int punchoutSizePixels = (int)(svgSize * (sizePercentage / 100.0)); + + // Round the punchout size to the nearest module boundary + int punchoutSizeModules = (int)Math.Round(punchoutSizePixels / modulePixelSize); + if (punchoutSizeModules < 1) punchoutSizeModules = 1; + if (punchoutSizeModules % 2 == 0) punchoutSizeModules++; + + int punchoutSize = (int)(punchoutSizeModules * modulePixelSize); + + // Calculate the position to center the punchout area + int punchoutX = (svgSize - punchoutSize) / 2; + int punchoutY = (svgSize - punchoutSize) / 2; + + // Convert padding to actual pixels + // Positive padding = logo smaller than punchout (adds white space) + // Negative padding = logo larger than punchout (logo extends beyond white background) + int paddingPixels = (int)Math.Round(logoPaddingPixels); + + // Calculate the actual logo display size + // Logo fills the punchout area minus the padding on all sides + int logoDisplayWidth = Math.Max(1, punchoutSize - (Math.Abs(paddingPixels) * 2)); + int logoDisplayHeight = Math.Max(1, punchoutSize - (Math.Abs(paddingPixels) * 2)); + + // If padding is negative, logo is larger than punchout + if (paddingPixels < 0) + { + logoDisplayWidth = punchoutSize + (Math.Abs(paddingPixels) * 2); + logoDisplayHeight = punchoutSize + (Math.Abs(paddingPixels) * 2); + } + + // Calculate logo dimensions preserving aspect ratio + float aspectRatio = (float)logo.Width / logo.Height; + int logoWidth, logoHeight; + + if (aspectRatio > 1) // Wider than tall + { + logoWidth = logoDisplayWidth; + logoHeight = (int)(logoDisplayWidth / aspectRatio); + } + else // Taller than wide or square + { + logoHeight = logoDisplayHeight; + logoWidth = (int)(logoDisplayHeight * aspectRatio); + } + + // Center the logo within the punchout area (or offset if larger) + int logoX = punchoutX + (punchoutSize - logoWidth) / 2; + int logoY = punchoutY + (punchoutSize - logoHeight) / 2; + + // Resize the logo to the display size before encoding to reduce SVG file size + Bitmap resizedLogo = new(logoWidth, logoHeight); + using (Graphics g = Graphics.FromImage(resizedLogo)) + { + g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; + g.PixelOffsetMode = System.Drawing.Drawing2D.PixelOffsetMode.HighQuality; + g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; + g.DrawImage(logo, 0, 0, logoWidth, logoHeight); + } + + // Convert resized logo to base64 for embedding + string base64Logo; + using (MemoryStream ms = new()) + { + resizedLogo.Save(ms, ImageFormat.Png); + byte[] imageBytes = ms.ToArray(); + base64Logo = Convert.ToBase64String(imageBytes); + } + resizedLogo.Dispose(); + + // Build the SVG logo element with punchout background and logo + // Convert the background color to RGB format for SVG + string backgroundColorHex = $"rgb({backgroundColor.R},{backgroundColor.G},{backgroundColor.B})"; + + string logoSvgElement = $@" + + + + "; + + // Find the closing tag and insert the logo before it + string modifiedContent = svg.Content.Replace("", logoSvgElement + "\n"); + + return new SvgImage(modifiedContent); + } + public static IEnumerable<(string, Result)> GetStringsFromImageFile(StorageFile storageFile) { Bitmap bitmap = new(storageFile.Path); @@ -148,14 +359,14 @@ public static SvgImage GetSvgQrCodeForText(string text, ErrorCorrectionLevel cor AutoRotate = true, Options = { - TryHarder = true, - TryInverted = true, - } + TryHarder = true, + TryInverted = true, +} }; Result[] results = barcodeReader.DecodeMultiple(bitmap); - List<(string, Result)> strings = new(); + List<(string, Result)> strings = []; if (results == null || results.Length == 0) return strings; diff --git a/Simple QR Code Maker/Models/BarcodeImageItem.cs b/Simple QR Code Maker/Models/BarcodeImageItem.cs index b943b94..f0dc4c0 100644 --- a/Simple QR Code Maker/Models/BarcodeImageItem.cs +++ b/Simple QR Code Maker/Models/BarcodeImageItem.cs @@ -34,6 +34,12 @@ public partial class BarcodeImageItem : ObservableRecipient public Windows.UI.Color BackgroundColor { get; set; } + public System.Drawing.Bitmap? LogoImage { get; set; } + + public double LogoSizePercentage { get; set; } = 20.0; + + public double LogoPaddingPixels { get; set; } = 8.0; + public QRCode QRCodeDetails => Encoder.encode(CodeAsText, ErrorCorrection); public string ToolTipText => $"Smallest recommended size {SmallestSide}, {CodeAsText}"; @@ -63,7 +69,7 @@ public async Task SaveCodeAsSvgFile(StorageFile file, System.Drawing.Color { try { - SvgImage svgImage = BarcodeHelpers.GetSvgQrCodeForText(CodeAsText, correctionLevel, foreground, background); + SvgImage svgImage = BarcodeHelpers.GetSvgQrCodeForText(CodeAsText, correctionLevel, foreground, background, LogoImage, LogoSizePercentage, LogoPaddingPixels); using IRandomAccessStream randomAccessStream = await file.OpenAsync(FileAccessMode.ReadWrite); DataWriter dataWriter = new(randomAccessStream); dataWriter.WriteString(svgImage.Content); @@ -81,7 +87,7 @@ public string GetCodeAsSvgText(System.Drawing.Color foreground, System.Drawing.C { try { - SvgImage svgImage = BarcodeHelpers.GetSvgQrCodeForText(CodeAsText, correctionLevel, foreground, background); + SvgImage svgImage = BarcodeHelpers.GetSvgQrCodeForText(CodeAsText, correctionLevel, foreground, background, LogoImage, LogoSizePercentage, LogoPaddingPixels); return svgImage.Content; } catch @@ -126,6 +132,12 @@ private async Task SaveCodeSvgContext() [RelayCommand] private async Task CopyCodePngContext() { + if (CodeAsBitmap is null) + { + WeakReferenceMessenger.Default.Send(new RequestShowMessage("Failed to copy QR Code to the clipboard", "No QR Code to copy to the clipboard", InfoBarSeverity.Error)); + return; + } + StorageFolder folder = ApplicationData.Current.LocalCacheFolder; List files = []; diff --git a/Simple QR Code Maker/Models/FaqItem.cs b/Simple QR Code Maker/Models/FaqItem.cs index 5af5d7a..f7e7152 100644 --- a/Simple QR Code Maker/Models/FaqItem.cs +++ b/Simple QR Code Maker/Models/FaqItem.cs @@ -1,5 +1,4 @@ - -namespace Simple_QR_Code_Maker.Models; +namespace Simple_QR_Code_Maker.Models; public class FaqItem { @@ -15,6 +14,8 @@ public class FaqItem new FaqItem { Title = "What is a two-dimensional barcode", Content="Barcodes are content which has been translated into a format which can be easily scanned by a machine. The barcodes you see on most products which are a row of lines with different widths are a form of one-dimensional barcodes because they are only one row of data. Two-dimensional barcodes encode data into a pattern which is multiple rows. The data is not always encoded left-to-right or top-to-bottom. Frequently two-dimensional barcodes wrap or pack the data in unique ways depending on the type of code."}, new FaqItem { Title = "How to read a QR Code?", Content = "To read a QR Code, you need a scanner which can read 2-dimensional (2D) barcodes. Android, iOS, and Windows all support QR Code scanning in the native default camera apps. To scan the code, simply open the app and hold the camera in front of it. The app will automatically recognize the code and act accordingly. NOTE: this does not mean the webpage you are sent to is exactly what is encoded into the QR Code. To read the content of a QR Code use the Read Code page of this app and open an image or paste one from your clipboard." }, new FaqItem { Title = "What is the Error Correction Level?", Content = "All QR Codes can still be readable even if some of the QR Code is hidden due to damage or challenging conditions. The amount of the QR Code which can be hidden and the code still work is called the Error Correction Level. A higher error correction level will be more durable however the minimum size will be larger due to more data being encoded in the QR Code." }, + new FaqItem { Title = "How does adding an image affect the QR Code?", Content = "When you add an image like a logo to a QR Code using this app, the logo is placed directly over the center of the QR Code, covering up some of the encoded data. This is why error correction is so important - the QR Code's error correction capability allows it to remain scannable even though part of it is obscured. The higher the error correction level, the more of the QR Code can be covered by your logo while still remaining functional. Always use a higher error correction level when adding logos, and test your QR Codes thoroughly to ensure they scan reliably." }, + new FaqItem { Title = "How is the center image stored in SVG files?", Content = "When you save a QR Code with a center image as an SVG file, the image is embedded directly into the SVG using Base64 encoding. This means the entire image data is converted to text and included within the SVG file itself, making the SVG completely self-contained. You don't need to keep the original image file separate - everything is embedded in the single SVG file. This makes it easy to share and use the QR Code without worrying about missing image references. Note that the center image is stored as a raster image (PNG), not as a vector, so it may appear pixelated when zoomed in significantly, even though the QR Code itself remains crisp as a vector." }, new FaqItem { Title = "How to put an image inside of the center of a QR Code?", Content = "Take the QR Code from this app and place it in any image editing software and overlay the image you want over the center. Test to make sure the image you place is not too large. QR Codes are can accept some error (how much depends on the error correction level used when making the QR Code) which means you can put an image in the middle of a QR Code and cover up some of the QR Code. Always test QR Codes in application when hiding any part of the QR Code to make sure it still scans easily." }, new FaqItem { Title = "How is minimum size calculated?", Content = "The minimum size of a QR Code is calculated based experimental data. QR Codes at several sizes were printed using a 600dpi laser printer on white paper. The codes were scanned and a relationship between code size and scan distance was developed. The contrast between the QR Code's foreground and background is also factored into the minimum size." }, new FaqItem { Title = "How can I change the maximum scan distance?", Content = "You can the maximum estimated scan distance of the minimum QR Code size in the settings. As always test in the real world before committing to a costly order or placement."}, diff --git a/Simple QR Code Maker/Models/HistoryItem.cs b/Simple QR Code Maker/Models/HistoryItem.cs index f413320..729c77f 100644 --- a/Simple QR Code Maker/Models/HistoryItem.cs +++ b/Simple QR Code Maker/Models/HistoryItem.cs @@ -39,6 +39,12 @@ public ErrorCorrectionLevel ErrorCorrection [JsonConverter(typeof(JsonStringEnumConverter))] public BarcodeFormat Format { get; set; } = BarcodeFormat.QR_CODE; + public string? LogoImagePath { get; set; } + + public double LogoSizePercentage { get; set; } = 15; + + public double LogoPaddingPixels { get; set; } = 4.0; + public HistoryItem() { diff --git a/Simple QR Code Maker/Package.appxmanifest b/Simple QR Code Maker/Package.appxmanifest index bcafe21..be99a78 100644 --- a/Simple QR Code Maker/Package.appxmanifest +++ b/Simple QR Code Maker/Package.appxmanifest @@ -13,7 +13,7 @@ + Version="1.9.0.0" /> diff --git a/Simple QR Code Maker/Simple QR Code Maker.csproj b/Simple QR Code Maker/Simple QR Code Maker.csproj index 80bba17..3369b18 100644 --- a/Simple QR Code Maker/Simple QR Code Maker.csproj +++ b/Simple QR Code Maker/Simple QR Code Maker.csproj @@ -1,38 +1,26 @@  WinExe - net8.0-windows10.0.22621.0 + net9.0-windows10.0.19041.0 10.0.19041.0 Simple_QR_Code_Maker Assets/WindowIcon.ico app.manifest - x86;x64;arm64 - win-x86;win-x64;win-arm64 - Properties\PublishProfiles\win-$(Platform).pubxml - enable - enable - true - true - False - SHA256 - False - False - True - True - Never - x86|x64|arm64 - True - - AppPackages\ - False - en-US - True - True + true + app.manifest + x64;ARM64 + win-x64;win-arm64 + win-$(Platform).pubxml + true + true + enable + true + @@ -46,15 +34,16 @@ - - - - + + + + + - - - - + + + + @@ -64,6 +53,9 @@ Always + + MSBuild:Compile + MSBuild:Compile @@ -81,19 +73,26 @@ - - - - - - true - + + true + - - - - - + + + False + True + False + True + False + False + SHA256 + False + False + Always + 0 + x64|arm64 + + diff --git a/Simple QR Code Maker/ViewModels/DecodingViewModel.cs b/Simple QR Code Maker/ViewModels/DecodingViewModel.cs index 2058538..50ba22f 100644 --- a/Simple QR Code Maker/ViewModels/DecodingViewModel.cs +++ b/Simple QR Code Maker/ViewModels/DecodingViewModel.cs @@ -36,7 +36,7 @@ public partial class DecodingViewModel : ObservableRecipient, INavigationAware [ObservableProperty] private ObservableCollection decodingImageItems = []; - private string passedUrl = string.Empty; + private HistoryItem? navigationHistoryItem = null; private INavigationService NavigationService { get; } @@ -78,8 +78,16 @@ private void CheckIfCanPaste() public void OnNavigatedTo(object parameter) { - if (parameter is string url) - passedUrl = url; + // Store the HistoryItem to pass back when returning to main page + if (parameter is HistoryItem historyItem) + { + navigationHistoryItem = historyItem; + } + // For backward compatibility, also handle string parameter + else if (parameter is string url) + { + navigationHistoryItem = new HistoryItem { CodesContent = url }; + } } [RelayCommand] @@ -127,7 +135,7 @@ private async Task TryLaunchLink() [RelayCommand] private void GoBack() { - NavigationService.NavigateTo(typeof(MainViewModel).FullName!, passedUrl); + NavigationService.NavigateTo(typeof(MainViewModel).FullName!, navigationHistoryItem); } [RelayCommand] @@ -174,7 +182,21 @@ private void EditQrCode(object parameter) if (string.IsNullOrWhiteSpace(InfoBarMessage)) return; - NavigationService.NavigateTo(typeof(MainViewModel).FullName!, InfoBarMessage); + // Create a new HistoryItem with the decoded text, preserving other state if available + var editHistoryItem = navigationHistoryItem != null + ? new HistoryItem + { + CodesContent = InfoBarMessage, + Foreground = navigationHistoryItem.Foreground, + Background = navigationHistoryItem.Background, + ErrorCorrection = navigationHistoryItem.ErrorCorrection, + LogoImagePath = navigationHistoryItem.LogoImagePath, + LogoSizePercentage = navigationHistoryItem.LogoSizePercentage, + LogoPaddingPixels = navigationHistoryItem.LogoPaddingPixels, + } + : new HistoryItem { CodesContent = InfoBarMessage }; + + NavigationService.NavigateTo(typeof(MainViewModel).FullName!, editHistoryItem); } private void OpenAndDecodeBitmap(IRandomAccessStreamWithContentType streamWithContentType) diff --git a/Simple QR Code Maker/ViewModels/MainViewModel.cs b/Simple QR Code Maker/ViewModels/MainViewModel.cs index dc55f19..8f1c6bf 100644 --- a/Simple QR Code Maker/ViewModels/MainViewModel.cs +++ b/Simple QR Code Maker/ViewModels/MainViewModel.cs @@ -10,9 +10,11 @@ using Simple_QR_Code_Maker.Helpers; using Simple_QR_Code_Maker.Models; using System.Collections.ObjectModel; +using System.IO; using Windows.ApplicationModel.DataTransfer; using Windows.Storage; using Windows.Storage.Pickers; +using Windows.Storage.Streams; using WinRT.Interop; using ZXing.QrCode.Internal; @@ -86,6 +88,23 @@ public partial class MainViewModel : ObservableRecipient, INavigationAware [ObservableProperty] private bool copySharePopupOpen = false; + [ObservableProperty] + private System.Drawing.Bitmap? logoImage = null; + + [ObservableProperty] + private bool hasLogo = false; + + [ObservableProperty] + private double logoPaddingPixels = 4.0; + + [ObservableProperty] + private double logoSizePercentage = 15; + + [ObservableProperty] + private double logoSizeMaxPercentage = 20; + + private string? currentLogoPath = null; + private double MinSizeScanDistanceScaleFactor = 1; private readonly DispatcherTimer copyInfoBarTimer = new(); @@ -100,11 +119,52 @@ partial void OnSelectedHistoryItemChanged(HistoryItem? value) ForegroundColor = value.Foreground; BackgroundColor = value.Background; SelectedOption = ErrorCorrectionLevels.First(x => x.ErrorCorrectionLevel == value.ErrorCorrection); + + // Restore logo image and size if available + if (!string.IsNullOrEmpty(value.LogoImagePath)) + { + _ = LoadLogoFromHistory(value.LogoImagePath); + } + else + { + // Clear logo if history item has no logo + LogoImage?.Dispose(); + LogoImage = null; + } + + LogoSizePercentage = value.LogoSizePercentage; + LogoPaddingPixels = value.LogoPaddingPixels; SelectedHistoryItem = null; } + + private async Task LoadLogoFromHistory(string logoPath) + { + try + { + if (File.Exists(logoPath)) + { + // Dispose old logo first + LogoImage?.Dispose(); + + StorageFile file = await StorageFile.GetFileFromPathAsync(logoPath); + using var stream = await file.OpenReadAsync(); + LogoImage = new System.Drawing.Bitmap(stream.AsStreamForRead()); + currentLogoPath = logoPath; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to load logo from history: {ex.Message}"); + LogoImage?.Dispose(); + LogoImage = null; + currentLogoPath = null; + } + } - public List ErrorCorrectionLevels { get; } = + public ObservableCollection ErrorCorrectionLevels { get; set; } = new(allCorrectionLevels); + + private static readonly List allCorrectionLevels = [ new("Low 7%", ErrorCorrectionLevel.L), new("Medium 15%", ErrorCorrectionLevel.M), @@ -114,6 +174,15 @@ partial void OnSelectedHistoryItemChanged(HistoryItem? value) partial void OnSelectedOptionChanged(ErrorCorrectionOptions value) { + // Ensure logo size doesn't exceed the new error correction level's maximum + if (logoSizePercentage > logoSizeMaxPercentage) + { + logoSizePercentage = logoSizeMaxPercentage; + } + OnPropertyChanged(nameof(logoSizeMaxPercentage)); + + LogoSizeMaxPercentage = BarcodeHelpers.GetMaxLogoSizePercentage(value.ErrorCorrectionLevel); + debounceTimer.Stop(); debounceTimer.Start(); } @@ -130,10 +199,58 @@ partial void OnForegroundColorChanged(Windows.UI.Color value) debounceTimer.Start(); } - public bool CanSaveImage { get => !string.IsNullOrWhiteSpace(UrlText); } + partial void OnLogoImageChanged(System.Drawing.Bitmap? value) + { + HasLogo = value != null; + + + if (HasLogo) + { + if (SelectedOption.ErrorCorrectionLevel == ErrorCorrectionLevel.L && ErrorCorrectionLevels.Count > 1) + { + SelectedOption = ErrorCorrectionLevels[1]; + } + + if (ErrorCorrectionLevels.Count > 0 + && ErrorCorrectionLevels[0].ErrorCorrectionLevel == ErrorCorrectionLevel.L) + ErrorCorrectionLevels.RemoveAt(0); + } + else + { + if (ErrorCorrectionLevels.Count == 3) + ErrorCorrectionLevels.Insert(0,new("Low 7%", ErrorCorrectionLevel.L)); + } + + + debounceTimer.Stop(); + debounceTimer.Start(); + } + + partial void OnLogoSizePercentageChanged(double value) + { + debounceTimer.Stop(); + debounceTimer.Start(); + } + + partial void OnLogoPaddingPixelsChanged(double value) + { + debounceTimer.Stop(); + debounceTimer.Start(); + } + + public bool CanSaveImage => !string.IsNullOrWhiteSpace(UrlText); partial void OnUrlTextChanged(string value) { + // Update max logo size when text changes (affects QR version/density) + OnPropertyChanged(nameof(logoSizeMaxPercentage)); + + // Ensure current logo size doesn't exceed the new maximum + if (logoSizePercentage > logoSizeMaxPercentage) + { + logoSizePercentage = logoSizeMaxPercentage; + } + debounceTimer.Stop(); debounceTimer.Start(); } @@ -207,10 +324,7 @@ private void OnRequestPaneChange(object recipient, RequestPaneChange message) } } - private void OnSaveHistoryMessage(object recipient, SaveHistoryMessage message) - { - SaveCurrentStateToHistory(); - } + private async void OnSaveHistoryMessage(object recipient, SaveHistoryMessage message) => await SaveCurrentStateToHistory(); private void OnRequestShowMessage(object recipient, RequestShowMessage rsm) { @@ -249,6 +363,9 @@ private void CheckCanPasteText() debounceTimer.Tick -= DebounceTimer_Tick; Clipboard.ContentChanged -= Clipboard_ContentChanged; + + // Dispose of the logo image + LogoImage?.Dispose(); } private void PlaceholderTextTimer_Tick(object? sender, object e) @@ -289,17 +406,23 @@ private void GenerateQrCodeFromOneLine(string text) textToUse, SelectedOption.ErrorCorrectionLevel, ForegroundColor.ToSystemDrawingColor(), - BackgroundColor.ToSystemDrawingColor()); + BackgroundColor.ToSystemDrawingColor(), + LogoImage, + LogoSizePercentage, + LogoPaddingPixels); BarcodeImageItem barcodeImageItem = new() { CodeAsBitmap = bitmap, CodeAsText = textToUse, IsAppShowingUrlWarnings = WarnWhenNotUrl, - SizeTextVisible = HideMinimumSizeText ? Visibility.Collapsed : Visibility.Visible, + SizeTextVisible = (HideMinimumSizeText || LogoImage != null) ? Visibility.Collapsed : Visibility.Visible, ErrorCorrection = SelectedOption.ErrorCorrectionLevel, ForegroundColor = ForegroundColor, BackgroundColor = BackgroundColor, MaxSizeScaleFactor = MinSizeScanDistanceScaleFactor, + LogoImage = LogoImage, + LogoSizePercentage = LogoSizePercentage, + LogoPaddingPixels = LogoPaddingPixels, }; double ratio = barcodeImageItem.ColorContrastRatio; @@ -345,7 +468,7 @@ private async Task CopyPngToClipboard() if (QrCodeBitmaps.Count == 0) return; - SaveCurrentStateToHistory(); + await SaveCurrentStateToHistory(); StorageFolder folder = ApplicationData.Current.LocalCacheFolder; List files = []; @@ -394,7 +517,7 @@ private async Task CopySvgToClipboard() if (QrCodeBitmaps.Count == 0) return; - SaveCurrentStateToHistory(); + await SaveCurrentStateToHistory(); StorageFolder folder = ApplicationData.Current.LocalCacheFolder; List files = []; @@ -438,12 +561,12 @@ private async Task CopySvgToClipboard() } [RelayCommand] - private void CopySvgTextToClipboard() + private async Task CopySvgTextToClipboard() { if (QrCodeBitmaps.Count == 0) return; - SaveCurrentStateToHistory(); + await SaveCurrentStateToHistory(); List textStrings = []; foreach (BarcodeImageItem qrCodeItem in QrCodeBitmaps) @@ -492,35 +615,40 @@ private void CopyInfoBarTimer_Tick(object? sender, object e) } [RelayCommand] - private void ToggleFaqPaneOpen() - { - IsFaqPaneOpen = !IsFaqPaneOpen; - } + private void ToggleFaqPaneOpen() => IsFaqPaneOpen = !IsFaqPaneOpen; [RelayCommand] - private void ToggleHistoryPaneOpen() - { - IsHistoryPaneOpen = !IsHistoryPaneOpen; - } + private void ToggleHistoryPaneOpen() => IsHistoryPaneOpen = !IsHistoryPaneOpen; [RelayCommand] - private void ShareApp() - { - CopySharePopupOpen = !CopySharePopupOpen; - } + private void ShareApp() => CopySharePopupOpen = !CopySharePopupOpen; [RelayCommand] - private void OpenFile() - { - NavigationService.NavigateTo(typeof(DecodingViewModel).FullName!, UrlText); - } + private void OpenFile() => NavigationService.NavigateTo(typeof(DecodingViewModel).FullName!, CreateCurrentStateHistoryItem()); [RelayCommand] - private void GoToSettings() + private void GoToSettings() => + // pass the current state as a HistoryItem to the settings page + // so when coming back it can be fully restored + NavigationService.NavigateTo(typeof(SettingsViewModel).FullName!, CreateCurrentStateHistoryItem()); + + private HistoryItem CreateCurrentStateHistoryItem() + { + return new HistoryItem + { + CodesContent = UrlText, + Foreground = ForegroundColor, + Background = BackgroundColor, + ErrorCorrection = SelectedOption.ErrorCorrectionLevel, + LogoImagePath = LogoImage != null ? GetCurrentLogoPath() : null, + LogoSizePercentage = LogoSizePercentage, + LogoPaddingPixels = LogoPaddingPixels, + }; + } + + private string? GetCurrentLogoPath() { - // pass the contents of the UrlText to the settings page - // so when coming back it can be rehydrated - NavigationService.NavigateTo(typeof(SettingsViewModel).FullName!, UrlText); + return currentLogoPath; } [RelayCommand] @@ -529,7 +657,7 @@ private async Task SavePng() if (QrCodeBitmaps.Count == 0) return; - SaveCurrentStateToHistory(); + await SaveCurrentStateToHistory(); await SaveAllFiles(FileKind.PNG); @@ -548,7 +676,7 @@ private async Task SaveSvg() if (QrCodeBitmaps.Count == 0) return; - SaveCurrentStateToHistory(); + await SaveCurrentStateToHistory(); await SaveAllFiles(FileKind.SVG); @@ -574,6 +702,55 @@ private void AddNewLine() UrlText += $"\r{stringToAdd}"; } + [RelayCommand] + private async Task SelectLogo() + { + FileOpenPicker openPicker = new() + { + SuggestedStartLocation = PickerLocationId.PicturesLibrary, + }; + openPicker.FileTypeFilter.Add(".png"); + openPicker.FileTypeFilter.Add(".jpg"); + openPicker.FileTypeFilter.Add(".jpeg"); + openPicker.FileTypeFilter.Add(".bmp"); + openPicker.FileTypeFilter.Add(".gif"); + + Window window = new(); + IntPtr windowHandle = WindowNative.GetWindowHandle(window); + InitializeWithWindow.Initialize(openPicker, windowHandle); + + StorageFile file = await openPicker.PickSingleFileAsync(); + + if (file is null) + return; + + try + { + // Dispose of the old logo if it exists + LogoImage?.Dispose(); + + // Use stream to access file instead of direct path for better compatibility + using IRandomAccessStreamWithContentType stream = await file.OpenReadAsync(); + LogoImage = new System.Drawing.Bitmap(stream.AsStreamForRead()); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to load logo image: {ex.Message}"); + CodeInfoBarMessage = "Failed to load the selected image"; + ShowCodeInfoBar = true; + CodeInfoBarSeverity = InfoBarSeverity.Error; + CodeInfoBarTitle = "Error loading logo"; + } + } + + [RelayCommand] + private void RemoveLogo() + { + LogoImage?.Dispose(); + LogoImage = null; + currentLogoPath = null; + } + private async Task WriteImageToFile(BarcodeImageItem imageItem, StorageFile file, FileKind kindOfFile) { switch (kindOfFile) @@ -632,6 +809,7 @@ public async Task SaveAllFiles(FileKind kindOfFile) public async void OnNavigatedTo(object parameter) { await LoadHistory(); + CheckCanPasteText(); MultiLineCodeMode = await LocalSettingsService.ReadSettingAsync(nameof(MultiLineCodeMode)); BaseText = await LocalSettingsService.ReadSettingAsync(nameof(BaseText)) ?? string.Empty; UrlText = BaseText; @@ -645,47 +823,175 @@ public async void OnNavigatedTo(object parameter) await LocalSettingsService.SaveSettingAsync(nameof(MinSizeScanDistanceScaleFactor), MinSizeScanDistanceScaleFactor); } - // check on text rehydration, could be coming from Reading or Settings - if (parameter is string textParam && !string.IsNullOrWhiteSpace(textParam)) + // Check if parameter is a HistoryItem with full state restoration + if (parameter is HistoryItem historyItem) + { + RestoreFromHistoryItem(historyItem); + } + // Otherwise check for text rehydration from other pages + else if (parameter is string textParam && !string.IsNullOrWhiteSpace(textParam)) + { UrlText = textParam; + } + } + + private void RestoreFromHistoryItem(HistoryItem historyItem) + { + UrlText = historyItem.CodesContent; + ForegroundColor = historyItem.Foreground; + BackgroundColor = historyItem.Background; + SelectedOption = ErrorCorrectionLevels.First(x => x.ErrorCorrectionLevel == historyItem.ErrorCorrection); + + // Restore logo image and settings if available + if (!string.IsNullOrEmpty(historyItem.LogoImagePath)) + { + _ = LoadLogoFromHistory(historyItem.LogoImagePath); + } + else + { + // Clear logo if history item has no logo + LogoImage?.Dispose(); + LogoImage = null; + currentLogoPath = null; + } + + LogoSizePercentage = historyItem.LogoSizePercentage; + LogoPaddingPixels = historyItem.LogoPaddingPixels; } public void OnNavigatedFrom() { if (!string.IsNullOrWhiteSpace(UrlText)) - SaveCurrentStateToHistory(); + _ = SaveCurrentStateToHistory(); } - public void SaveCurrentStateToHistory() + public async Task SaveCurrentStateToHistory() { + string? logoImagePath = null; + + // Save logo image to local app storage if present + if (LogoImage != null) + { + try + { + StorageFolder logoFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync("LogoImages", CreationCollisionOption.OpenIfExists); + string fileName = $"logo_{DateTime.Now:yyyyMMddHHmmss}_{Guid.NewGuid():N}.png"; + StorageFile logoFile = await logoFolder.CreateFileAsync(fileName, CreationCollisionOption.ReplaceExisting); + + using (var stream = await logoFile.OpenAsync(FileAccessMode.ReadWrite)) + { + using (var outputStream = stream.GetOutputStreamAt(0)) + { + using (var dataWriter = new DataWriter(outputStream)) + { + using (MemoryStream ms = new()) + { + LogoImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png); + byte[] bytes = ms.ToArray(); + dataWriter.WriteBytes(bytes); + await dataWriter.StoreAsync(); + } + } + } + } + + logoImagePath = logoFile.Path; + currentLogoPath = logoImagePath; // Update current logo path + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to save logo image: {ex.Message}"); + } + } + HistoryItem historyItem = new() { CodesContent = UrlText, Foreground = ForegroundColor, Background = BackgroundColor, ErrorCorrection = SelectedOption.ErrorCorrectionLevel, + LogoImagePath = logoImagePath ?? currentLogoPath, // Use saved path or current path + LogoSizePercentage = LogoSizePercentage, + LogoPaddingPixels = LogoPaddingPixels, }; HistoryItems.Remove(historyItem); HistoryItems.Insert(0, historyItem); - LocalSettingsService.SaveSettingAsync(nameof(HistoryItems), HistoryItems); + await SaveHistoryToFile(); } - private async Task LoadHistory() + private async Task SaveHistoryToFile() { - ObservableCollection? prevHistory = null; + try + { + StorageFolder localFolder = ApplicationData.Current.LocalFolder; + StorageFile historyFile = await localFolder.CreateFileAsync("History.json", CreationCollisionOption.ReplaceExisting); + + string json = System.Text.Json.JsonSerializer.Serialize(HistoryItems, new System.Text.Json.JsonSerializerOptions + { + WriteIndented = true + }); + + await FileIO.WriteTextAsync(historyFile, json); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to save history to file: {ex.Message}"); + } + } + private async Task LoadHistory() + { + ObservableCollection? historyFromFile = null; + + // First, try to load from file (new location) try { - prevHistory = await LocalSettingsService.ReadSettingAsync>(nameof(HistoryItems)); + StorageFolder localFolder = ApplicationData.Current.LocalFolder; + StorageFile historyFile = await localFolder.GetFileAsync("History.json"); + string json = await FileIO.ReadTextAsync(historyFile); + historyFromFile = System.Text.Json.JsonSerializer.Deserialize>(json); + } + catch + { + // File doesn't exist yet, that's okay } - catch { } - if (prevHistory is null || prevHistory.Count == 0) + // If we found history in the file, use it + if (historyFromFile != null && historyFromFile.Count > 0) + { + foreach (HistoryItem hisItem in historyFromFile) + HistoryItems.Add(hisItem); return; + } + + // Otherwise, try to migrate from old settings location + ObservableCollection? historyFromSettings = null; + try + { + historyFromSettings = await LocalSettingsService.ReadSettingAsync>(nameof(HistoryItems)); + } + catch { } - foreach (HistoryItem hisItem in prevHistory) - HistoryItems.Add(hisItem); + if (historyFromSettings != null && historyFromSettings.Count > 0) + { + // Migrate: add to collection and save to file + foreach (HistoryItem hisItem in historyFromSettings) + HistoryItems.Add(hisItem); + + // Save to new file location + await SaveHistoryToFile(); + + // Clear from old settings location to free up space + try + { + await LocalSettingsService.SaveSettingAsync(nameof(HistoryItems), new ObservableCollection()); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to clear old history from settings: {ex.Message}"); + } + } } } diff --git a/Simple QR Code Maker/ViewModels/SettingsViewModel.cs b/Simple QR Code Maker/ViewModels/SettingsViewModel.cs index 8f67bc4..1e6b8c3 100644 --- a/Simple QR Code Maker/ViewModels/SettingsViewModel.cs +++ b/Simple QR Code Maker/ViewModels/SettingsViewModel.cs @@ -4,6 +4,7 @@ using Simple_QR_Code_Maker.Contracts.Services; using Simple_QR_Code_Maker.Contracts.ViewModels; using Simple_QR_Code_Maker.Helpers; +using Simple_QR_Code_Maker.Models; using System.Globalization; using System.Reflection; using System.Windows.Input; @@ -41,7 +42,7 @@ public partial class SettingsViewModel : ObservableRecipient, INavigationAware private readonly DispatcherTimer settingChangedDebounceTimer = new(); - private string navigationText = string.Empty; + private HistoryItem? navigationHistoryItem = null; private INavigationService NavigationService { get; } public ILocalSettingsService LocalSettingsService { get; } @@ -142,7 +143,7 @@ partial void OnMinSizeScanDistanceScaleFactorChanged(double value) [RelayCommand] private void GoHome() { - NavigationService.NavigateTo(typeof(MainViewModel).FullName!, navigationText); + NavigationService.NavigateTo(typeof(MainViewModel).FullName!, navigationHistoryItem); } [RelayCommand] @@ -183,9 +184,15 @@ public async void OnNavigatedTo(object parameter) HideMinimumSizeText = await LocalSettingsService.ReadSettingAsync(nameof(HideMinimumSizeText)); MinSizeScanDistanceScaleFactor = await LocalSettingsService.ReadSettingAsync(nameof(MinSizeScanDistanceScaleFactor)); - if (parameter is string urlText && !string.IsNullOrWhiteSpace(urlText)) + // Store the HistoryItem to pass back when returning to main page + if (parameter is HistoryItem historyItem) { - navigationText = urlText; + navigationHistoryItem = historyItem; + } + // For backward compatibility, also handle string parameter + else if (parameter is string urlText && !string.IsNullOrWhiteSpace(urlText)) + { + navigationHistoryItem = new HistoryItem { CodesContent = urlText }; } } diff --git a/Simple QR Code Maker/Views/MainPage.xaml b/Simple QR Code Maker/Views/MainPage.xaml index d950138..76a7d73 100644 --- a/Simple QR Code Maker/Views/MainPage.xaml +++ b/Simple QR Code Maker/Views/MainPage.xaml @@ -24,9 +24,7 @@ @@ -35,9 +33,7 @@ @@ -58,12 +54,9 @@ MinWidth="258" MaxWidth="800" AcceptsReturn="True" - PlaceholderText="{x:Bind ViewModel.PlaceholderText, - Mode=OneWay}" + PlaceholderText="{x:Bind ViewModel.PlaceholderText, Mode=OneWay}" ScrollViewer.HorizontalScrollBarVisibility="Auto" - Text="{x:Bind ViewModel.UrlText, - Mode=TwoWay, - UpdateSourceTrigger=PropertyChanged}" + Text="{x:Bind ViewModel.UrlText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="UrlTextBox_TextChanged" TextWrapping="NoWrap" /> @@ -75,9 +68,7 @@ VerticalAlignment="Bottom" Command="{x:Bind ViewModel.AddNewLineCommand}" ToolTipService.ToolTip="Add a return and start making another QR Code" - Visibility="{x:Bind ViewModel.CanSaveImage, - Converter={StaticResource BoolToVisibility}, - Mode=OneWay}"> + Visibility="{x:Bind ViewModel.CanSaveImage, Converter={StaticResource BoolToVisibility}, Mode=OneWay}"> @@ -89,13 +80,9 @@ Padding="0" VerticalAlignment="Bottom" Command="{x:Bind ViewModel.PasteTextIntoUrlTextCommand}" - IsEnabled="{x:Bind ViewModel.CanPasteText, - Mode=OneWay}" + IsEnabled="{x:Bind ViewModel.CanPasteText, Mode=OneWay}" ToolTipService.ToolTip="Paste text from clipboard" - Visibility="{x:Bind ViewModel.CanSaveImage, - Converter={StaticResource BoolToVisibility}, - ConverterParameter=True, - Mode=OneWay}"> + Visibility="{x:Bind ViewModel.CanSaveImage, Converter={StaticResource BoolToVisibility}, ConverterParameter=True, Mode=OneWay}"> @@ -107,11 +94,9 @@ x:Name="QrCodeOptions" HorizontalAlignment="Center" Orientation="Horizontal" - Spacing="12" - Visibility="{x:Bind ViewModel.CanSaveImage, - Mode=OneWay, - Converter={StaticResource BoolToVisibility}}"> - - @@ -164,6 +143,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -196,15 +233,16 @@ + + + + + + + IsOpen="{x:Bind ViewModel.CopySharePopupOpen, Mode=TwoWay}"> + IsOpen="{x:Bind ViewModel.ShowCodeInfoBar, Mode=TwoWay}" + Message="{x:Bind ViewModel.CodeInfoBarMessage, Mode=OneWay}" + Severity="{x:Bind ViewModel.CodeInfoBarSeverity, Mode=OneWay}" /> @@ -354,9 +388,7 @@ + SelectedItem="{x:Bind ViewModel.SelectedHistoryItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">