DateNoDotConverter.cs: [ ```cs //BackgroundBuilder\Converters\DateNoDotConverter.cs using System; using System.Globalization; using System.Windows.Data; namespace BackgroundBuilder.Converters { public class DateNoDotConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is DateTime dt) { // get "MMM" then TrimEnd('.'): string month = culture .DateTimeFormat .GetAbbreviatedMonthName(dt.Month) .TrimEnd('.'); return $"{dt:dd}/{month}"; } return value; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); } } ``` ] Contato.cs: [ ```cs //BackgroundBuilder\Models\Contato.cs using System; namespace BackgroundBuilder.Models { public class Contato { public string Ramal { get; set; } = string.Empty; public string? Nome { get; set; } = string.Empty; public string? Email { get; set; } public string? Area { get; set; } public DateTime? Aniversario { get; set; } public bool IsComando { get; set; } } } ``` ] IContatoRepository.cs: [ ```cs //BackgroundBuilder\Repositories\IContatoRepository.cs using System.Collections.Generic; using System.Threading.Tasks; using BackgroundBuilder.Models; namespace BackgroundBuilder.Repositories { public interface IContatoRepository { Task> GetAllAsync(); Task InsertUpdateAsync(Contato c); Task DeleteAsync(string ramal); } } ``` ] PostgresContatoRepository.cs: [ ```cs //BackgroundBuilder\Repositories\PostgresContatoRepository.cs using System.Collections.Generic; using System.Threading.Tasks; using Dapper; using Npgsql; using BackgroundBuilder.Models; using BackgroundBuilder.Services; namespace BackgroundBuilder.Repositories { public class PostgresContatoRepository(DatabaseService db) : IContatoRepository { private readonly DatabaseService _db = db; public async Task> GetAllAsync() { using var conn = _db.CreateConnection(); const string sql = @" SELECT ramal AS Ramal, nome AS Nome, email AS Email, area AS Area, aniversario AS Aniversario, ""isComando"" AS IsComando FROM contatos;"; return await conn.QueryAsync(sql); } public async Task AddAsync(Contato c) { using var conn = _db.CreateConnection(); const string sql = @" INSERT INTO contatos (ramal, nome, email, area, aniversario, ""isComando"") VALUES (@Ramal, @Nome, @Email, @Area, @Aniversario, @IsComando);"; await conn.ExecuteAsync(sql, c); } public async Task DeleteAsync(string ramal) { using var conn = _db.CreateConnection(); const string sql = "DELETE FROM contatos WHERE ramal=@Ramal;"; await conn.ExecuteAsync(sql, new { Ramal = ramal }); } public async Task InsertUpdateAsync(Contato c) { using var conn = _db.CreateConnection(); string Ramal = $"'{c.Ramal}'"; string Nome = c.Nome is null ? "null" : $"'{c.Nome}'"; string Email = c.Email is null ? "null" : $"'{c.Email}'"; string Area = c.Area == "" ? "null" : $"'{c.Area}'"; string Aniversario = c.Aniversario is null ? "null" : $"'{c.Aniversario:yyyy-MM-dd}'"; string IsComando = c.IsComando ? "true" : "false"; string sql = @$" INSERT INTO contatos (ramal, nome, email, area, aniversario, ""isComando"") VALUES ({Ramal}, {Nome}, {Email}, {Area}, {Aniversario:YYYY-MM-DD}, {IsComando}) ON CONFLICT (ramal) DO UPDATE SET nome = EXCLUDED.nome, email = EXCLUDED.email, area = EXCLUDED.area, aniversario = EXCLUDED.aniversario, ""isComando"" = EXCLUDED.""isComando"";"; await conn.ExecuteAsync(sql); } } } ``` ] DatabaseService.cs: [ ```cs //BackgroundBuilder\Services\DatabaseService.cs using System; using Microsoft.Extensions.Configuration; using Npgsql; namespace BackgroundBuilder.Services { public class DatabaseService(IConfiguration config) { private readonly string _connString = config.GetConnectionString("ContatosDb") ?? throw new InvalidOperationException("Missing connection string 'ContatosDb'."); public NpgsqlConnection CreateConnection() { var conn = new NpgsqlConnection(_connString); conn.Open(); return conn; } } } ``` ] IImageService.cs: [ ```cs //BackgroundBuilder\Services\IImageService.cs using System.Threading.Tasks; using System.Windows; using System.Windows.Media.Imaging; namespace BackgroundBuilder.Services { public interface IImageService { /// /// Loads an image from disk. /// Task LoadAsync(string path); /// /// Renders the background + overlay and writes two PNGs: /// • primaryPath: composite of background+overlay /// • overlayPath (optional): overlay alone /// Returns the actual paths written. /// Task<(string primaryPath, string? overlayPath)> SaveAsync( FrameworkElement overlay, BitmapImage background, string primaryPath, string? overlayPath = null); } } ``` ] ImageService.cs: [ ```cs //BackgroundBuilder\Services\ImageService.cs using System; using System.IO; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; namespace BackgroundBuilder.Services { public class ImageService : IImageService { // no longer injected — use a private constant private static readonly Thickness OverlayOffset = new(10, 58, 10, 58); public async Task LoadAsync(string path) { // Load image from disk into BitmapImage var bmp = new BitmapImage(); using var stream = File.OpenRead(path); bmp.BeginInit(); bmp.UriSource = new Uri(path, UriKind.Absolute); bmp.CacheOption = BitmapCacheOption.OnLoad; bmp.EndInit(); // no real async work—return completed task return await Task.FromResult(bmp); } public async Task<(string primaryPath, string? overlayPath)> SaveAsync( FrameworkElement overlay, BitmapImage background, string primaryPath, string? overlayPath = null) { var compositeBmp = RenderComposite(overlay, background, OverlayOffset); SaveBitmap(compositeBmp, primaryPath); string? savedOverlayPath = null; if (!string.IsNullOrWhiteSpace(overlayPath)) { var overlayBmp = RenderComposite(overlay, null, new Thickness(0)); SaveBitmap(overlayBmp, overlayPath!); savedOverlayPath = overlayPath; } return await Task.FromResult((primaryPath, savedOverlayPath)); } /// /// Renders a plus the /// at bottom‐right offset by . If background is null, /// only the mainGrid is rendered at (0,0). /// private static RenderTargetBitmap RenderComposite( FrameworkElement mainGrid, BitmapImage? background, Thickness offset) { // Determine canvas size int width = background?.PixelWidth ?? (int)mainGrid.ActualWidth; int height = background?.PixelHeight ?? (int)mainGrid.ActualHeight; var dv = new DrawingVisual(); using (var ctx = dv.RenderOpen()) { // Draw background if provided if (background != null) { ctx.DrawImage( background, new Rect(0, 0, width, height)); } // Measure & arrange the mainGrid so ActualWidth/Height are valid mainGrid.Measure(new Size(width, height)); mainGrid.Arrange(new Rect(0, 0, mainGrid.DesiredSize.Width, mainGrid.DesiredSize.Height)); double ow = mainGrid.ActualWidth > 0 ? mainGrid.ActualWidth : mainGrid.DesiredSize.Width; double oh = mainGrid.ActualHeight > 0 ? mainGrid.ActualHeight : mainGrid.DesiredSize.Height; double x = width - ow - offset.Left; double y = height - oh - offset.Right; // Draw mainGrid at either origin or bottom-right with offset var brush = new VisualBrush(mainGrid); ctx.DrawRectangle(brush, null, new Rect(x, y, ow, oh)); } var rtb = new RenderTargetBitmap( width, height, 96, 96, PixelFormats.Pbgra32); rtb.Render(dv); return rtb; } /// /// Encodes the given bitmap as PNG and writes it to disk. /// private static void SaveBitmap(RenderTargetBitmap bitmap, string path) { // Ensure directory exists Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var fs = new FileStream(path, FileMode.Create, FileAccess.Write); var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(bitmap)); encoder.Save(fs); } } } ``` ] ITaskbarService.cs: [ ```cs //BackgroundBuilder\Services\ITaskbarService.cs namespace BackgroundBuilder.Services { /// /// Defines methods for querying the Windows taskbar size and orientation. /// public interface ITaskbarService { /// Gets the current thickness (height if horizontal, width if vertical) of the taskbar in pixels. int GetTaskbarSize(); /// Returns true when the taskbar is horizontal, false when vertical. bool IsHorizontal(); } } ``` ] TaskbarService.cs: [ ```cs //BackgroundBuilder\Services\TaskbarService.cs using System; using System.Runtime.InteropServices; namespace BackgroundBuilder.Services { /// /// Implementation of using P/Invoke (SHAppBarMessage). /// public class TaskbarService : ITaskbarService { #region WinAPI [DllImport("shell32.dll")] private static extern IntPtr SHAppBarMessage(uint msg, ref APPBARDATA data); [StructLayout(LayoutKind.Sequential)] private struct APPBARDATA { public uint cbSize; public IntPtr hWnd; public uint uCallbackMessage; public uint uEdge; public RECT rc; public IntPtr lParam; } [StructLayout(LayoutKind.Sequential)] private struct RECT { public int left, top, right, bottom; } private const uint ABM_GETTASKBARPOS = 5; #endregion public int GetTaskbarSize() { var data = new APPBARDATA { cbSize = (uint)Marshal.SizeOf() }; if (SHAppBarMessage(ABM_GETTASKBARPOS, ref data) == IntPtr.Zero) throw new InvalidOperationException("Failed to retrieve taskbar position."); int thickness = IsHorizontal() ? Math.Abs(data.rc.bottom - data.rc.top) : Math.Abs(data.rc.right - data.rc.left); return thickness; } public bool IsHorizontal() { var data = new APPBARDATA { cbSize = (uint)Marshal.SizeOf() }; SHAppBarMessage(ABM_GETTASKBARPOS, ref data); int height = Math.Abs(data.rc.bottom - data.rc.top); int width = Math.Abs(data.rc.right - data.rc.left); return width > height; } } } ``` ] ObservableObject.cs: [ ```cs //BackgroundBuilder\Utils\ObservableObject.cs using System.ComponentModel; using System.Runtime.CompilerServices; namespace BackgroundBuilder.Utils { public abstract class ObservableObject : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? propName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName)); } } ``` ] RelayCommand.cs: [ ```cs //BackgroundBuilder\Utils\RelayCommand.cs using System; using System.Windows.Input; namespace BackgroundBuilder.Utils { public class RelayCommand(Action execute, Func? canExecute = null) : ICommand { private readonly Action _execute = execute; private readonly Func? _canExecute = canExecute; public bool CanExecute(object? parameter) => _canExecute?.Invoke(parameter) ?? true; public void Execute(object? parameter) => _execute(parameter); public event EventHandler? CanExecuteChanged; public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); } } ``` ] MainWindowViewModel.cs: [ ```cs //BackgroundBuilder\ViewModels\MainWindowViewModel.cs using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media.Imaging; using BackgroundBuilder.Models; using BackgroundBuilder.Repositories; using BackgroundBuilder.Services; using BackgroundBuilder.Utils; using Microsoft.Win32; namespace BackgroundBuilder.ViewModels { public class MainWindowViewModel : ObservableObject { private readonly IContatoRepository _repo; private readonly IImageService _imageService; private readonly ITaskbarService _taskbarService; public ObservableCollection RawContatos { get; } = []; public ObservableCollection Comando { get; } = []; public ObservableCollection ContatosSemCMD { get; } = []; public ObservableCollection Aniversarios { get; } = []; private Contato? _selectedContato; public Contato? SelectedContato{ get => _selectedContato; set { _selectedContato = value; OnPropertyChanged(); DeleteCommand.RaiseCanExecuteChanged(); } } private ObservableCollection? _selectedContatos; public ObservableCollection SelectedContatos { get => _selectedContatos ?? RawContatos; set { _selectedContatos = value; OnPropertyChanged(); UpdateCommand.RaiseCanExecuteChanged(); } } private string? _caminhoBGImage; public string? CaminhoBGImage{ get => _caminhoBGImage; set { _caminhoBGImage = value; OnPropertyChanged(); }} private BitmapImage? _backgroundImage; public BitmapImage? BackgroundImage{ get => _backgroundImage; private set { _backgroundImage = value; OnPropertyChanged(); }} private int _taskbarSize; public int TaskbarSize{ get => _taskbarSize; private set { _taskbarSize = value; OnPropertyChanged(); }} private string _orientation = "Horizontal"; public string Orientation{ get => _orientation; private set { _orientation = value; OnPropertyChanged(); }} public FrameworkElement? OverlayElement { get; set; } // Commands public RelayCommand DeleteCommand { get; } public RelayCommand AddCommand { get; } public RelayCommand SelectImageCommand { get; } public ICommand RefreshCommand { get; } public RelayCommand UpdateCommand { get; } public RelayCommand ExportImageCommand { get; } public MainWindowViewModel( IContatoRepository repo, IImageService imageService, ITaskbarService taskbarService) { _repo = repo; _imageService = imageService; _taskbarService = taskbarService; RefreshTaskbarInfo(); DeleteCommand = new RelayCommand(async _ => await DeleteAsync(), _ => SelectedContato != null); AddCommand = new RelayCommand(async _ => await AddContactAsync()); SelectImageCommand = new RelayCommand(async _ => await SelectBackground()); RefreshCommand = new RelayCommand(async _ => await LoadRawAsync()); UpdateCommand = new RelayCommand(async _ => await UpdateAsync(), _ => SelectedContatos != null); ExportImageCommand = new RelayCommand(async _ => await RenderImageAsync(), _ => BackgroundImage != null && OverlayElement != null); } public void RefreshTaskbarInfo() { TaskbarSize = _taskbarService.GetTaskbarSize(); Orientation = _taskbarService.IsHorizontal() ? "Horizontal" : "Vertical"; } private async Task DeleteAsync() { if (SelectedContato == null) return; var result = MessageBox.Show( "Delete selected record?", "Confirm Delete", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) return; try { await _repo.DeleteAsync(SelectedContato.Ramal); RawContatos.Remove(SelectedContato); ApplyFilters(); } catch { MessageBox.Show( "Error deleting record.", "Delete Failed", MessageBoxButton.OK, MessageBoxImage.Error); } } private async Task AddContactAsync() { var novo = new Contato { Ramal = "", Nome = "" }; RawContatos.Add(novo); SelectedContato = novo; // Set the newly added contact as the selected one await Task.CompletedTask; // preserve async signature } private async Task SelectBackground() { var dlg = new OpenFileDialog { Filter = "Image Files|*.png;*.jpg;*.jpeg;*.bmp" }; if (dlg.ShowDialog() == true) { CaminhoBGImage = dlg.FileName; BackgroundImage = await _imageService.LoadAsync(dlg.FileName); if (!string.IsNullOrEmpty(CaminhoBGImage)) { ExportImageCommand.RaiseCanExecuteChanged(); } } } public async Task LoadRawAsync() { var all = await _repo.GetAllAsync(); RawContatos.Clear(); foreach (var c in all.Where(x => x.IsComando)) RawContatos.Add(c); foreach (var c in all.Where(x => !x.IsComando).OrderBy(x => x.Nome)) RawContatos.Add(c); ApplyFilters(); } private async Task RenderImageAsync() { var dlg = new SaveFileDialog { Filter = "PNG Image|*.png" }; if (dlg.ShowDialog() == true && OverlayElement is FrameworkElement overlay && BackgroundImage is BitmapImage bg) { await _imageService.SaveAsync(overlay, bg, dlg.FileName, dlg.FileName.Replace(".", "_1.")); } } public async Task UpdateAsync() { if (SelectedContatos == null) return; var result = MessageBox.Show( "Save selected records?", "Confirm Save", MessageBoxButton.YesNo, MessageBoxImage.Question); if (result != MessageBoxResult.Yes) return; foreach (var contato in SelectedContatos) { if (string.IsNullOrWhiteSpace(contato.Ramal) || string.IsNullOrWhiteSpace(contato.Nome)) { MessageBox.Show( "Ramal and Nome are required.", "Validation Error", MessageBoxButton.OK, MessageBoxImage.Warning); return; } try { await _repo.InsertUpdateAsync(contato); } catch { MessageBox.Show( "Error saving record.", "Save Failed", MessageBoxButton.OK, MessageBoxImage.Error); } } await LoadRawAsync(); MessageBox.Show( "Concluído!", "Confirm Save", MessageBoxButton.OK, MessageBoxImage.Information); } private void ApplyFilters() { Comando.Clear(); ContatosSemCMD.Clear(); Aniversarios.Clear(); foreach (var item in RawContatos.Where(x => x.IsComando)) Comando.Add(item); foreach (var item in RawContatos.Where(x => !x.IsComando).OrderBy(x => x.Nome)) ContatosSemCMD.Add(item); foreach (var item in RawContatos.Where(x => !x.IsComando).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Day).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Month)) Aniversarios.Add(item); } } } ``` ] MainWindow.xaml [ ```xml