diff --git a/.gitignore b/.gitignore index 9491a2f..1753ed8 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,5 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +.history/ \ No newline at end of file diff --git a/App.xaml b/App.xaml index 919c746..25fa19c 100644 --- a/App.xaml +++ b/App.xaml @@ -14,7 +14,7 @@ - + diff --git a/App.xaml.cs b/App.xaml.cs index ed4a219..f85cdb4 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -17,8 +17,6 @@ namespace BackgroundBuilder public App() { _host = Host.CreateDefaultBuilder() - .ConfigureAppConfiguration(cfg => - cfg.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)) .ConfigureServices((_, services) => { // Database & repository diff --git a/BackgroundBuilder.csproj b/BackgroundBuilder.csproj index f70ee4d..a438ff2 100644 --- a/BackgroundBuilder.csproj +++ b/BackgroundBuilder.csproj @@ -1,29 +1,44 @@  - - WinExe - net9.0-windows7.0 - true - BackgroundBuilder - BackgroundBuilder - enable - true - - - - - - - - - - - - - - - Always - - + + WinExe + net9.0-windows7.0 + true + BackgroundBuilder + BackgroundBuilder + enable + true + smart.ico + smart.ico + + + + + + + + + + + + + + + + True + True + Settings.settings + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + \ + + diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs new file mode 100644 index 0000000..700b5d6 --- /dev/null +++ b/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// O código foi gerado por uma ferramenta. +// Versão de Tempo de Execução:4.0.30319.42000 +// +// As alterações ao arquivo poderão causar comportamento incorreto e serão perdidas se +// o código for gerado novamente. +// +//------------------------------------------------------------------------------ + +namespace BackgroundBuilder.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.12.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/Properties/Settings.settings b/Properties/Settings.settings new file mode 100644 index 0000000..049245f --- /dev/null +++ b/Properties/Settings.settings @@ -0,0 +1,6 @@ + + + + + + diff --git a/README.md b/README.md index b3c668e..99167f1 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,65 @@ # BackgroundBuilder -An MVVM WPF application (.NET 6) providing an Excel-like editor for the `contatos` table in PostgreSQL. +[![.NET](https://img.shields.io/badge/.NET-8.0-blueviolet?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) [![WPF](https://img.shields.io/badge/WPF-%23C8C8C8?logo=windows&logoColor=blue)](https://learn.microsoft.com/dotnet/desktop/wpf/) [![XAML](https://img.shields.io/badge/XAML-0C54C2?logo=xaml&logoColor=white)](https://learn.microsoft.com/dotnet/desktop/wpf/xaml/) [![C#](https://img.shields.io/badge/C%23-239120?logo=c-sharp&logoColor=white)](https://learn.microsoft.com/dotnet/csharp/) [![MVVM](https://img.shields.io/badge/Pattern-MVVM-ff69b4)](https://learn.microsoft.com/pt-br/dotnet/architecture/maui/mvvm) [![DI](https://img.shields.io/badge/DI-Microsoft.Extensions.Hosting-0078D7?logo=azure-devops&logoColor=white)](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) [![PostgreSQL](https://img.shields.io/badge/DB-PostgreSQL-4169E1?logo=postgresql&logoColor=white)](https://www.postgresql.org/) [![Npgsql](https://img.shields.io/badge/Driver-Npgsql-008bb9?logo=postgresql&logoColor=white)](https://www.npgsql.org/) [![Dapper](https://img.shields.io/badge/ORM-Dapper-0089D6)](https://github.com/DapperLib/Dapper) [![Windows](https://img.shields.io/badge/Platform-Windows-0078D6?logo=windows&logoColor=white)](https://www.microsoft.com/windows) -## Prerequisites +## 📝 Project description -- .NET 6 SDK -- PostgreSQL database with table: +> 🖥️ An MVVM WPF application (.NET 8) providing an Excel-like editor for the `contatos` table in PostgreSQL - ```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 +## 📑 Prerequisites -- **Select Background…** +- [![.NET](https://img.shields.io/badge/.NET_8.0-blueviolet?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) +- [![PostgreSQL](https://img.shields.io/badge/PostgreSQL-4169E1?logo=postgresql&logoColor=white)](https://www.postgresql.org/) **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 `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 + +--- + +## 🚀 Releases + +### 📆 21/05/2025: 🆕 Background & Export Features + +- 🎨 **Select Background…** Opens a file picker—choose any image (PNG, JPG, BMP). That image becomes your canvas. -- **Create Image…** +- 🖼️ **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/Services/ContactService.cs b/Services/ContactService.cs new file mode 100644 index 0000000..688989a --- /dev/null +++ b/Services/ContactService.cs @@ -0,0 +1,36 @@ +// BackgroundBuilder\Services\ContactService.cs +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using BackgroundBuilder.Models; +using BackgroundBuilder.Repositories; + +namespace BackgroundBuilder.Services +{ + public class ContactService(IContatoRepository repo) : IContactService + { + private readonly IContatoRepository _repo = repo; + + public Task> GetAllAsync() + => _repo.GetAllAsync(); + + public Task InsertUpdateAsync(Contato contato) + => _repo.InsertUpdateAsync(contato); + + public Task AddAsync(Contato contato) + => _repo.InsertUpdateAsync(contato); + + public Task DeleteAsync(string ramal) + => _repo.DeleteAsync(ramal); + + public IEnumerable GetComando(IEnumerable all) + => all.Where(c => c.IsComando); + + public IEnumerable GetSemComando(IEnumerable all) + => all.Where(c => !c.IsComando).OrderBy(x => x.Nome); + + public IEnumerable GetAniversarios(IEnumerable all) + => all.Where(c => c.Aniversario.HasValue).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Day).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Month); + } +} diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 20ad871..179a036 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -4,10 +4,9 @@ using Npgsql; namespace BackgroundBuilder.Services { - public class DatabaseService(IConfiguration config) + public class DatabaseService() { - private readonly string _connString = config.GetConnectionString("ContatosDb") - ?? throw new InvalidOperationException("Missing connection string 'ContatosDb'."); + private readonly string _connString = "Host=192.168.10.248;Username=postgres;Password=gds21;Database=Smart Energia"; public NpgsqlConnection CreateConnection() { diff --git a/Services/IContactService.cs b/Services/IContactService.cs new file mode 100644 index 0000000..4bbb4a6 --- /dev/null +++ b/Services/IContactService.cs @@ -0,0 +1,18 @@ +// BackgroundBuilder\Services\IContactService.cs +using System.Collections.Generic; +using System.Threading.Tasks; +using BackgroundBuilder.Models; + +namespace BackgroundBuilder.Services +{ + public interface IContactService + { + Task InsertUpdateAsync(Contato contato); + Task DeleteAsync(string ramal); + + Task> GetAllAsync(); + IEnumerable GetComando(IEnumerable all); + IEnumerable GetSemComando(IEnumerable all); + IEnumerable GetAniversarios(IEnumerable all); + } +} diff --git a/Services/IImageService.cs b/Services/IImageService.cs index 56f9144..b4b8e5f 100644 --- a/Services/IImageService.cs +++ b/Services/IImageService.cs @@ -17,7 +17,7 @@ namespace BackgroundBuilder.Services /// • overlayPath (optional): overlay alone /// Returns the actual paths written. /// - Task<(string primaryPath, string? overlayPath)> SaveAsync( + Task SaveAsync( FrameworkElement overlay, BitmapImage background, string primaryPath, diff --git a/Services/IMessageService.cs b/Services/IMessageService.cs new file mode 100644 index 0000000..3fee25e --- /dev/null +++ b/Services/IMessageService.cs @@ -0,0 +1,10 @@ +using System.Windows; + +namespace BackgroundBuilder.Services +{ + public interface IMessageService + { + void Show(string message, string caption, MessageBoxButton buttons, MessageBoxImage icon); + MessageBoxResult ShowConfirm(string message, string caption, MessageBoxButton buttons, MessageBoxImage icon); + } +} \ No newline at end of file diff --git a/Services/ImageService.cs b/Services/ImageService.cs index bae1c37..0274814 100644 --- a/Services/ImageService.cs +++ b/Services/ImageService.cs @@ -26,14 +26,12 @@ namespace BackgroundBuilder.Services return await Task.FromResult(bmp); } - public async Task<(string primaryPath, string? overlayPath)> SaveAsync( + public async Task 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)) @@ -43,7 +41,10 @@ namespace BackgroundBuilder.Services savedOverlayPath = overlayPath; } - return await Task.FromResult((primaryPath, savedOverlayPath)); + var compositeBmp = RenderComposite(overlay, background, OverlayOffset); + SaveBitmap(compositeBmp, primaryPath); + + await Task.FromResult((primaryPath, savedOverlayPath)); } /// @@ -54,11 +55,79 @@ namespace BackgroundBuilder.Services private static RenderTargetBitmap RenderComposite( FrameworkElement mainGrid, BitmapImage? background, - Thickness offset) + Thickness offset, + int taskbarHeight = 0) { - // Determine canvas size - int width = background?.PixelWidth ?? (int)mainGrid.ActualWidth; - int height = background?.PixelHeight ?? (int)mainGrid.ActualHeight; + double width = background?.PixelWidth ?? mainGrid.ActualWidth; + double height = background?.PixelHeight ?? mainGrid.ActualHeight; + + // 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 gridWidth = mainGrid.ActualWidth > 0 ? mainGrid.ActualWidth : mainGrid.DesiredSize.Width; + double gridHeight = mainGrid.ActualHeight > 0 ? mainGrid.ActualHeight : mainGrid.DesiredSize.Height; + + double x; + double y; + double scale; + + if (background != null) + { + // Calculate max allowed size (half background, minus taskbar for height) + double maxWidth = width; + double maxHeight = (height - taskbarHeight); + + // Compute scale factor to fit within maxWidth/maxHeight, preserving aspect ratio + scale = Math.Min(1.0, Math.Min(maxWidth / gridWidth, maxHeight / gridHeight)); + + double scaledWidth = gridWidth * scale; + double scaledHeight = gridHeight * scale; + + // Place at lower right, offset + x = width - scaledWidth - offset.Right; + y = height - scaledHeight - offset.Bottom; + + gridWidth = scaledWidth + offset.Right; + gridHeight = scaledHeight + offset.Bottom; + } + else + { + // When rendering without background, use the mainGrid's own size and render at (0,0) + // --- Fix: Ensure full layout and hide scrollbars before rendering --- + mainGrid.UpdateLayout(); + + // If mainGrid is a ScrollViewer or contains one, hide scrollbars + if (mainGrid is System.Windows.Controls.ScrollViewer sv) + { + sv.HorizontalScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility.Hidden; + sv.VerticalScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility.Hidden; + } + // If mainGrid contains a DataGrid or similar, try to hide scrollbars + if (mainGrid is System.Windows.Controls.Panel panel) + { + foreach (var child in panel.Children) + { + if (child is System.Windows.Controls.DataGrid dg) + { + dg.HorizontalScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility.Hidden; + dg.VerticalScrollBarVisibility = System.Windows.Controls.ScrollBarVisibility.Hidden; + } + } + } + + // Re-measure after hiding scrollbars + mainGrid.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + mainGrid.Arrange(new Rect(0, 0, mainGrid.DesiredSize.Width, mainGrid.DesiredSize.Height)); + mainGrid.UpdateLayout(); + + gridWidth = mainGrid.ActualWidth > 0 ? mainGrid.ActualWidth : mainGrid.DesiredSize.Width; + gridHeight = mainGrid.ActualHeight > 0 ? mainGrid.ActualHeight : mainGrid.DesiredSize.Height; + width = (int)Math.Ceiling(gridWidth); + height = (int)Math.Ceiling(gridHeight); + x = 0; + y = 0; + } var dv = new DrawingVisual(); using (var ctx = dv.RenderOpen()) @@ -66,30 +135,20 @@ namespace BackgroundBuilder.Services // Draw background if provided if (background != null) { - ctx.DrawImage( - background, - new Rect(0, 0, width, height)); + 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)); + // Draw mainGrid at calculated position, with scaling if needed + var brush = new VisualBrush(mainGrid) + { + Stretch = Stretch.Uniform, + }; + ctx.DrawRectangle(brush, null, new Rect(x, y, gridWidth, gridHeight)); } var rtb = new RenderTargetBitmap( - width, - height, + (int)width, + (int)height, 96, 96, PixelFormats.Pbgra32); @@ -103,8 +162,11 @@ namespace BackgroundBuilder.Services /// private static void SaveBitmap(RenderTargetBitmap bitmap, string path) { - // Ensure directory exists - Directory.CreateDirectory(Path.GetDirectoryName(path)!); + var directory = Path.GetDirectoryName(path); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory!); + } using var fs = new FileStream(path, FileMode.Create, FileAccess.Write); var encoder = new PngBitmapEncoder(); diff --git a/Services/MessageService.cs b/Services/MessageService.cs new file mode 100644 index 0000000..b88d37f --- /dev/null +++ b/Services/MessageService.cs @@ -0,0 +1,13 @@ +using System.Windows; + +namespace BackgroundBuilder.Services +{ + public class MessageService : IMessageService + { + public void Show(string message, string caption, MessageBoxButton buttons, MessageBoxImage icon) + => MessageBox.Show(message, caption, buttons, icon); + + public MessageBoxResult ShowConfirm(string message, string caption, MessageBoxButton buttons, MessageBoxImage icon) + => MessageBox.Show(message, caption, buttons, icon); + } +} \ No newline at end of file diff --git a/Thumbs.db b/Thumbs.db new file mode 100644 index 0000000..da31ada Binary files /dev/null and b/Thumbs.db differ diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 8adef02..9893804 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -27,9 +27,11 @@ namespace BackgroundBuilder.ViewModels public ObservableCollection RawContatos { get; } = []; public ObservableCollection Comando { get; } = []; - public ObservableCollection ContatosSemCMD { get; } = []; public ObservableCollection Aniversarios { get; } = []; + public ObservableCollection ContatosSemCMDFirstHalf { get; } = new(); + public ObservableCollection ContatosSemCMDSecondHalf { get; } = new(); + private Contato? _selectedContato; public Contato? SelectedContato{ get => _selectedContato; set { _selectedContato = value; OnPropertyChanged(); DeleteCommand.RaiseCanExecuteChanged(); } } @@ -212,18 +214,30 @@ namespace BackgroundBuilder.ViewModels 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); + LoadContatosSemCMD(RawContatos.Where(x => !x.IsComando).OrderBy(x => x.Nome)); + } + + private void LoadContatosSemCMD(IEnumerable contatos) + { + ContatosSemCMDFirstHalf.Clear(); + ContatosSemCMDSecondHalf.Clear(); + var list = contatos.ToList(); + int half = (list.Count + 1) / 2; + for (int i = 0; i < list.Count; i++) + { + if (i < half) + ContatosSemCMDFirstHalf.Add(list[i]); + else + ContatosSemCMDSecondHalf.Add(list[i]); + } } } } \ No newline at end of file diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml index 41ef816..419ed06 100644 --- a/Views/MainWindow.xaml +++ b/Views/MainWindow.xaml @@ -1,110 +1,39 @@  + Title="BackgroundBuilder" WindowStartupLocation="CenterScreen" WindowState="Maximized" MinHeight="950" MinWidth="1290" MaxHeight="1048" MaxWidth="2250"> - + - - - - - + + + + - - - - - - -