diff --git a/App.xaml b/App.xaml new file mode 100644 index 0000000..919c746 --- /dev/null +++ b/App.xaml @@ -0,0 +1,53 @@ + + + + + + + + + \ No newline at end of file diff --git a/App.xaml.cs b/App.xaml.cs new file mode 100644 index 0000000..ed4a219 --- /dev/null +++ b/App.xaml.cs @@ -0,0 +1,54 @@ +using System; +using System.Windows; +using BackgroundBuilder.Repositories; +using BackgroundBuilder.Services; +using BackgroundBuilder.ViewModels; +using BackgroundBuilder.Views; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace BackgroundBuilder +{ + public partial class App : Application + { + private readonly IHost _host; + + public App() + { + _host = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration(cfg => + cfg.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)) + .ConfigureServices((_, services) => + { + // Database & repository + services.AddSingleton(); + services.AddScoped(); + + // New image service + services.AddSingleton(); + + // New taskbar service + services.AddSingleton(); + + // VM & View + services.AddTransient(); + services.AddTransient(); + }) + .Build(); + } + + private async void OnStartup(object sender, StartupEventArgs e) + { + await _host.StartAsync(); + var window = _host.Services.GetRequiredService(); + window.Show(); + } + + protected override async void OnExit(ExitEventArgs e) + { + using (_host) { await _host.StopAsync(); } + base.OnExit(e); + } + } +} \ No newline at end of file diff --git a/AssemblyInfo.cs b/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/BackgroundBuilder.csproj b/BackgroundBuilder.csproj new file mode 100644 index 0000000..f70ee4d --- /dev/null +++ b/BackgroundBuilder.csproj @@ -0,0 +1,29 @@ + + + + WinExe + net9.0-windows7.0 + true + BackgroundBuilder + BackgroundBuilder + enable + true + + + + + + + + + + + + + + + Always + + + + diff --git a/BackgroundBuilder.sln b/BackgroundBuilder.sln new file mode 100644 index 0000000..d8ea512 --- /dev/null +++ b/BackgroundBuilder.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35527.113 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackgroundBuilder", "BackgroundBuilder.csproj", "{4AC89EE0-00B5-47D2-924B-FD65DD09E67B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4AC89EE0-00B5-47D2-924B-FD65DD09E67B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4AC89EE0-00B5-47D2-924B-FD65DD09E67B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4AC89EE0-00B5-47D2-924B-FD65DD09E67B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4AC89EE0-00B5-47D2-924B-FD65DD09E67B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Converters/DateNoDotConverter.cs b/Converters/DateNoDotConverter.cs new file mode 100644 index 0000000..34b458f --- /dev/null +++ b/Converters/DateNoDotConverter.cs @@ -0,0 +1,26 @@ +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(); + } +} diff --git a/Models/Contato.cs b/Models/Contato.cs new file mode 100644 index 0000000..56a857c --- /dev/null +++ b/Models/Contato.cs @@ -0,0 +1,14 @@ +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; } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3c668e --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# BackgroundBuilder + +An MVVM WPF application (.NET 6) providing an Excel-like editor for the `contatos` table in PostgreSQL. + +## Prerequisites + +- .NET 6 SDK +- PostgreSQL database with table: + + ```sql + CREATE TABLE public.contatos ( + ramal text PRIMARY KEY NOT NULL, + nome text NOT NULL, + email text, + area text, + aniversario date, + "isComando" boolean NOT NULL + ); + ``` +## Setup +1. Edit appsettings.json, set your ConnectionStrings:ContatosDb. +2. In a terminal: + ```bash + dotnet restore + dotnet build + dotnet run --project BackgroundBuilder.csproj + ``` +3. The main window will appear; on load it fetches and displays all contatos. + +## Architecture +- MVVM with MVVM with ObservableObject, RelayCommand. +- DI via Microsoft.Extensions.Hosting. +- Repositories (PostgresContatoRepository) handle all DB I/O with Dapper. +- Services (DatabaseService) manage the Npgsql connection. +- ViewModels free of data-access logic: only orchestration. +--- + +### New Background & Export Features + +- **Select Background…** + Opens a file picker—choose any image (PNG, JPG, BMP). That image becomes your canvas. + +- **Create Image…** + Saves the current DataGrid overlaid on the background as a single PNG. + Uses WPF’s `RenderTargetBitmap` and `PngBitmapEncoder` under the hood. +--- \ No newline at end of file diff --git a/Repositories/IContatoRepository.cs b/Repositories/IContatoRepository.cs new file mode 100644 index 0000000..8d07791 --- /dev/null +++ b/Repositories/IContatoRepository.cs @@ -0,0 +1,13 @@ +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); + } +} diff --git a/Repositories/PostgresContatoRepository.cs b/Repositories/PostgresContatoRepository.cs new file mode 100644 index 0000000..a448efc --- /dev/null +++ b/Repositories/PostgresContatoRepository.cs @@ -0,0 +1,71 @@ +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); + } + } +} diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs new file mode 100644 index 0000000..20ad871 --- /dev/null +++ b/Services/DatabaseService.cs @@ -0,0 +1,19 @@ +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; + } + } +} diff --git a/Services/IImageService.cs b/Services/IImageService.cs new file mode 100644 index 0000000..56f9144 --- /dev/null +++ b/Services/IImageService.cs @@ -0,0 +1,26 @@ +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); + } +} diff --git a/Services/ITaskbarService.cs b/Services/ITaskbarService.cs new file mode 100644 index 0000000..c3305a7 --- /dev/null +++ b/Services/ITaskbarService.cs @@ -0,0 +1,13 @@ +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(); + } +} \ No newline at end of file diff --git a/Services/ImageService.cs b/Services/ImageService.cs new file mode 100644 index 0000000..bae1c37 --- /dev/null +++ b/Services/ImageService.cs @@ -0,0 +1,115 @@ +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); + } + } +} diff --git a/Services/TaskbarService.cs b/Services/TaskbarService.cs new file mode 100644 index 0000000..b6bff08 --- /dev/null +++ b/Services/TaskbarService.cs @@ -0,0 +1,54 @@ +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; + } + } +} \ No newline at end of file diff --git a/Utils/ObservableObject.cs b/Utils/ObservableObject.cs new file mode 100644 index 0000000..f391471 --- /dev/null +++ b/Utils/ObservableObject.cs @@ -0,0 +1,12 @@ +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)); + } +} diff --git a/Utils/RelayCommand.cs b/Utils/RelayCommand.cs new file mode 100644 index 0000000..309b4c8 --- /dev/null +++ b/Utils/RelayCommand.cs @@ -0,0 +1,18 @@ +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); + } +} diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..8adef02 --- /dev/null +++ b/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,229 @@ +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); + + } + } +} \ No newline at end of file diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml new file mode 100644 index 0000000..41ef816 --- /dev/null +++ b/Views/MainWindow.xaml @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + +