Import inicial: migração de arquivos da rede

This commit is contained in:
Giuliano Paschoalino 2025-07-14 11:44:57 -03:00
parent b979de68c5
commit 1badf9db58
21 changed files with 513 additions and 273 deletions

3
.gitignore vendored
View File

@ -360,4 +360,5 @@ MigrationBackup/
.ionide/ .ionide/
# Fody - auto-generated XML schema # Fody - auto-generated XML schema
FodyWeavers.xsd FodyWeavers.xsd
.history/

View File

@ -14,7 +14,7 @@
<Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderThickness" Value="0"/>
<Setter Property="BorderBrush" Value="Black"/> <Setter Property="BorderBrush" Value="Black"/>
<Setter Property="IsReadOnly" Value="True"/> <Setter Property="IsReadOnly" Value="True"/>
<Setter Property="FontSize" Value="16"/> <Setter Property="FontSize" Value="14"/>
<Setter Property="RowBackground" Value="GhostWhite"/> <Setter Property="RowBackground" Value="GhostWhite"/>
<Setter Property="AlternatingRowBackground" Value="#387f79"/> <Setter Property="AlternatingRowBackground" Value="#387f79"/>
<Setter Property="Margin" Value="5"/> <Setter Property="Margin" Value="5"/>

View File

@ -17,8 +17,6 @@ namespace BackgroundBuilder
public App() public App()
{ {
_host = Host.CreateDefaultBuilder() _host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration(cfg =>
cfg.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true))
.ConfigureServices((_, services) => .ConfigureServices((_, services) =>
{ {
// Database & repository // Database & repository

View File

@ -1,29 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows7.0</TargetFramework> <TargetFramework>net9.0-windows7.0</TargetFramework>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<RootNamespace>BackgroundBuilder</RootNamespace> <RootNamespace>BackgroundBuilder</RootNamespace>
<AssemblyName>BackgroundBuilder</AssemblyName> <AssemblyName>BackgroundBuilder</AssemblyName>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> <PackageIcon>smart.ico</PackageIcon>
<ApplicationIcon>smart.ico</ApplicationIcon>
<ItemGroup> </PropertyGroup>
<PackageReference Include="Dapper" Version="2.1.66" /> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-preview.3.25171.5" /> <Content Include="smart.ico" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-preview.3.25171.5" /> </ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-preview.3.25171.5" /> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.3.25171.5" /> <PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Npgsql" Version="9.0.3" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-preview.3.25171.5" />
<PackageReference Include="System.Drawing.Common" Version="10.0.0-preview.3.25173.2" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-preview.3.25171.5" />
</ItemGroup> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0-preview.3.25171.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.3.25171.5" />
<ItemGroup> <PackageReference Include="Npgsql" Version="9.0.3" />
<None Update="appsettings.json"> <PackageReference Include="System.Drawing.Common" Version="10.0.0-preview.3.25173.2" />
<CopyToOutputDirectory>Always</CopyToOutputDirectory> </ItemGroup>
</None> <ItemGroup>
</ItemGroup> <Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
<None Update="smart.ico">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project> </Project>

26
Properties/Settings.Designer.cs generated Normal file
View File

@ -0,0 +1,26 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
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;
}
}
}
}

View File

@ -0,0 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)">
<Profiles>
<Profile Name="(Default)" />
</Profiles>
</SettingsFile>

View File

@ -1,46 +1,65 @@
# BackgroundBuilder # 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 > 🖥️ An MVVM WPF application (.NET 8) providing an Excel-like editor for the `contatos` table in PostgreSQL
- 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 ## 📑 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. 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. Saves the current DataGrid overlaid on the background as a single PNG.
Uses WPFs `RenderTargetBitmap` and `PngBitmapEncoder` under the hood. Uses WPFs `RenderTargetBitmap` and `PngBitmapEncoder` under the hood.
---
---

View File

@ -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<IEnumerable<Contato>> 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<Contato> GetComando(IEnumerable<Contato> all)
=> all.Where(c => c.IsComando);
public IEnumerable<Contato> GetSemComando(IEnumerable<Contato> all)
=> all.Where(c => !c.IsComando).OrderBy(x => x.Nome);
public IEnumerable<Contato> GetAniversarios(IEnumerable<Contato> all)
=> all.Where(c => c.Aniversario.HasValue).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Day).OrderBy(x => (x.Aniversario ?? DateTime.MinValue).Month);
}
}

View File

@ -4,10 +4,9 @@ using Npgsql;
namespace BackgroundBuilder.Services namespace BackgroundBuilder.Services
{ {
public class DatabaseService(IConfiguration config) public class DatabaseService()
{ {
private readonly string _connString = config.GetConnectionString("ContatosDb") private readonly string _connString = "Host=192.168.10.248;Username=postgres;Password=gds21;Database=Smart Energia";
?? throw new InvalidOperationException("Missing connection string 'ContatosDb'.");
public NpgsqlConnection CreateConnection() public NpgsqlConnection CreateConnection()
{ {

View File

@ -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<IEnumerable<Contato>> GetAllAsync();
IEnumerable<Contato> GetComando(IEnumerable<Contato> all);
IEnumerable<Contato> GetSemComando(IEnumerable<Contato> all);
IEnumerable<Contato> GetAniversarios(IEnumerable<Contato> all);
}
}

View File

@ -17,7 +17,7 @@ namespace BackgroundBuilder.Services
/// • overlayPath (optional): overlay alone /// • overlayPath (optional): overlay alone
/// Returns the actual paths written. /// Returns the actual paths written.
/// </summary> /// </summary>
Task<(string primaryPath, string? overlayPath)> SaveAsync( Task SaveAsync(
FrameworkElement overlay, FrameworkElement overlay,
BitmapImage background, BitmapImage background,
string primaryPath, string primaryPath,

View File

@ -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);
}
}

View File

@ -26,14 +26,12 @@ namespace BackgroundBuilder.Services
return await Task.FromResult(bmp); return await Task.FromResult(bmp);
} }
public async Task<(string primaryPath, string? overlayPath)> SaveAsync( public async Task SaveAsync(
FrameworkElement overlay, FrameworkElement overlay,
BitmapImage background, BitmapImage background,
string primaryPath, string primaryPath,
string? overlayPath = null) string? overlayPath = null)
{ {
var compositeBmp = RenderComposite(overlay, background, OverlayOffset);
SaveBitmap(compositeBmp, primaryPath);
string? savedOverlayPath = null; string? savedOverlayPath = null;
if (!string.IsNullOrWhiteSpace(overlayPath)) if (!string.IsNullOrWhiteSpace(overlayPath))
@ -43,7 +41,10 @@ namespace BackgroundBuilder.Services
savedOverlayPath = overlayPath; savedOverlayPath = overlayPath;
} }
return await Task.FromResult((primaryPath, savedOverlayPath)); var compositeBmp = RenderComposite(overlay, background, OverlayOffset);
SaveBitmap(compositeBmp, primaryPath);
await Task.FromResult((primaryPath, savedOverlayPath));
} }
/// <summary> /// <summary>
@ -54,11 +55,79 @@ namespace BackgroundBuilder.Services
private static RenderTargetBitmap RenderComposite( private static RenderTargetBitmap RenderComposite(
FrameworkElement mainGrid, FrameworkElement mainGrid,
BitmapImage? background, BitmapImage? background,
Thickness offset) Thickness offset,
int taskbarHeight = 0)
{ {
// Determine canvas size double width = background?.PixelWidth ?? mainGrid.ActualWidth;
int width = background?.PixelWidth ?? (int)mainGrid.ActualWidth; double height = background?.PixelHeight ?? mainGrid.ActualHeight;
int height = background?.PixelHeight ?? (int)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(); var dv = new DrawingVisual();
using (var ctx = dv.RenderOpen()) using (var ctx = dv.RenderOpen())
@ -66,30 +135,20 @@ namespace BackgroundBuilder.Services
// Draw background if provided // Draw background if provided
if (background != null) if (background != null)
{ {
ctx.DrawImage( ctx.DrawImage(background, new Rect(0, 0, width, height));
background,
new Rect(0, 0, width, height));
} }
// Measure & arrange the mainGrid so ActualWidth/Height are valid // Draw mainGrid at calculated position, with scaling if needed
mainGrid.Measure(new Size(width, height)); var brush = new VisualBrush(mainGrid)
mainGrid.Arrange(new Rect(0, 0, mainGrid.DesiredSize.Width, mainGrid.DesiredSize.Height)); {
Stretch = Stretch.Uniform,
double ow = mainGrid.ActualWidth > 0 ? mainGrid.ActualWidth : mainGrid.DesiredSize.Width; };
double oh = mainGrid.ActualHeight > 0 ? mainGrid.ActualHeight : mainGrid.DesiredSize.Height; ctx.DrawRectangle(brush, null, new Rect(x, y, gridWidth, gridHeight));
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( var rtb = new RenderTargetBitmap(
width, (int)width,
height, (int)height,
96, 96,
96, 96,
PixelFormats.Pbgra32); PixelFormats.Pbgra32);
@ -103,8 +162,11 @@ namespace BackgroundBuilder.Services
/// </summary> /// </summary>
private static void SaveBitmap(RenderTargetBitmap bitmap, string path) private static void SaveBitmap(RenderTargetBitmap bitmap, string path)
{ {
// Ensure directory exists var directory = Path.GetDirectoryName(path);
Directory.CreateDirectory(Path.GetDirectoryName(path)!); if (!Directory.Exists(directory))
{
Directory.CreateDirectory(directory!);
}
using var fs = new FileStream(path, FileMode.Create, FileAccess.Write); using var fs = new FileStream(path, FileMode.Create, FileAccess.Write);
var encoder = new PngBitmapEncoder(); var encoder = new PngBitmapEncoder();

View File

@ -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);
}
}

BIN
Thumbs.db Normal file

Binary file not shown.

View File

@ -27,9 +27,11 @@ namespace BackgroundBuilder.ViewModels
public ObservableCollection<Contato> RawContatos { get; } = []; public ObservableCollection<Contato> RawContatos { get; } = [];
public ObservableCollection<Contato> Comando { get; } = []; public ObservableCollection<Contato> Comando { get; } = [];
public ObservableCollection<Contato> ContatosSemCMD { get; } = [];
public ObservableCollection<Contato> Aniversarios { get; } = []; public ObservableCollection<Contato> Aniversarios { get; } = [];
public ObservableCollection<Contato> ContatosSemCMDFirstHalf { get; } = new();
public ObservableCollection<Contato> ContatosSemCMDSecondHalf { get; } = new();
private Contato? _selectedContato; private Contato? _selectedContato;
public Contato? SelectedContato{ get => _selectedContato; set { _selectedContato = value; OnPropertyChanged(); DeleteCommand.RaiseCanExecuteChanged(); } } public Contato? SelectedContato{ get => _selectedContato; set { _selectedContato = value; OnPropertyChanged(); DeleteCommand.RaiseCanExecuteChanged(); } }
@ -212,18 +214,30 @@ namespace BackgroundBuilder.ViewModels
private void ApplyFilters() private void ApplyFilters()
{ {
Comando.Clear(); Comando.Clear();
ContatosSemCMD.Clear();
Aniversarios.Clear(); Aniversarios.Clear();
foreach (var item in RawContatos.Where(x => x.IsComando)) foreach (var item in RawContatos.Where(x => x.IsComando))
Comando.Add(item); 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)) 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); Aniversarios.Add(item);
LoadContatosSemCMD(RawContatos.Where(x => !x.IsComando).OrderBy(x => x.Nome));
}
private void LoadContatosSemCMD(IEnumerable<Contato> 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]);
}
} }
} }
} }

View File

@ -1,110 +1,39 @@
<Window x:Class="BackgroundBuilder.Views.MainWindow" <Window x:Class="BackgroundBuilder.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:BackgroundBuilder.Converters" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:BackgroundBuilder.Converters"
Title="BackgroundBuilder" WindowStartupLocation="CenterScreen" WindowState="Maximized" MinHeight="950" MinWidth="1230"> Title="BackgroundBuilder" WindowStartupLocation="CenterScreen" WindowState="Maximized" MinHeight="950" MinWidth="1290" MaxHeight="1048" MaxWidth="2250">
<Window.Resources> <Window.Resources>
<converters:DateNoDotConverter x:Key="DateNoDotConverter" /> <converters:DateNoDotConverter x:Key="DateNoDotConverter"/>
</Window.Resources> </Window.Resources>
<Grid x:Name="RootGrid" Margin="20"> <Grid x:Name="RootGrid" Margin="20">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border Grid.Row="0" BorderBrush="Black" BorderThickness="1"/>
<!-- Row 0: Background selection --> <Border Grid.Row="1" BorderBrush="Black" BorderThickness="1,0,1,1"/>
<Grid Grid.Row="0"> <!-- Row 0: three side-by-side DataGrids -->
<Grid Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Row 2: Image--> <Border Grid.Column="0" BorderBrush="Black" BorderThickness="1"/>
<Rectangle Grid.Column="0" <!-- Column 0: DataGrid for RawContatos -->
Fill="Gainsboro"
Height="90"
Width="160"
>
</Rectangle>
<Image Source="{Binding BackgroundImage}" Height="90" Stretch="Uniform" Grid.Column="0"/>
<Button Content="Selecionar Imagem de Fundo…"
Command="{Binding SelectImageCommand}"
Grid.Column="1"
Padding="20,3,20,3"
Height="30"
Margin="5"/>
<TextBox Text="{Binding CaminhoBGImage}"
Grid.Column="2"
IsReadOnly="True"
Padding="20,3,20,3"
Height="30"
Margin="5"/>
</Grid>
<!-- Row 1: three side-by-side DataGrids -->
<Grid Grid.Row="1" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" x:Name="MainGrid" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Comando -->
<DataGrid Grid.Column="0"
ItemsSource="{Binding Comando}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Ação" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal}"/>
</DataGrid.Columns>
</DataGrid>
<!-- ContatosSemCMD -->
<DataGrid Grid.Column="1"
ItemsSource="{Binding ContatosSemCMD}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Nome" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Email" Binding="{Binding Email}"/>
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal}"/>
<DataGridTextColumn Header="Área" Binding="{Binding Area}"/>
</DataGrid.Columns>
</DataGrid>
<!-- Aniversarios -->
<DataGrid Grid.Column="2"
ItemsSource="{Binding Aniversarios}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Nome" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Aniversário"
Binding="{Binding Aniversario,Converter={StaticResource DateNoDotConverter}}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
<!-- Row 3: DataGrid for RawContatos -->
<DataGrid x:Name="RawGrid" <DataGrid x:Name="RawGrid"
Grid.Column="2" Grid.Column="0"
ItemsSource="{Binding RawContatos}" ItemsSource="{Binding RawContatos}"
SelectedItem="{Binding SelectedContato}" SelectedItem="{Binding SelectedContato}"
AutoGenerateColumns="False" AutoGenerateColumns="False"
IsReadOnly="False" IsReadOnly="False"
IsHitTestVisible ="True" IsHitTestVisible ="True"
FontSize="11" FontSize="11"
BorderBrush="Black"
BorderThickness="1"
VerticalAlignment="Top" VerticalAlignment="Top"
HorizontalAlignment="Center" HorizontalAlignment="Left"
Height="800" Height="800"
Panel.ZIndex="1"
SelectionChanged="ListView_SelectionChanged"
AlternatingRowBackground="DarkGray"> AlternatingRowBackground="DarkGray">
<DataGrid.ColumnHeaderStyle> <DataGrid.ColumnHeaderStyle>
<Style TargetType="DataGridColumnHeader"> <Style TargetType="DataGridColumnHeader">
@ -115,6 +44,7 @@
<Setter Property="HorizontalAlignment" Value="Stretch"/> <Setter Property="HorizontalAlignment" Value="Stretch"/>
</Style> </Style>
</DataGrid.ColumnHeaderStyle> </DataGrid.ColumnHeaderStyle>
<!-- your existing columns here --> <!-- your existing columns here -->
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal, UpdateSourceTrigger=PropertyChanged}" /> <DataGridTextColumn Header="Ramal" Binding="{Binding Ramal, UpdateSourceTrigger=PropertyChanged}" />
@ -132,52 +62,139 @@
</DataGrid.Columns> </DataGrid.Columns>
</DataGrid> </DataGrid>
<!-- Column 1: DataGrid for MainGrid -->
<Grid Grid.Column="0" x:Name="MainGrid" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- Comando -->
<DataGrid Grid.Column="0"
ItemsSource="{Binding Comando}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Ação" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal}"/>
</DataGrid.Columns>
</DataGrid>
<!-- ContatosSemCMD (First Half) -->
<DataGrid Grid.Column="1"
x:Name="ContatosSemCMDGrid1"
ItemsSource="{Binding ContatosSemCMDFirstHalf}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Nome" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Email" Binding="{Binding Email}"/>
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal}"/>
<DataGridTextColumn Header="Área" Binding="{Binding Area}"/>
</DataGrid.Columns>
</DataGrid>
<!-- ContatosSemCMD (Second Half) -->
<DataGrid Grid.Column="2"
x:Name="ContatosSemCMDGrid2"
ItemsSource="{Binding ContatosSemCMDSecondHalf}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Nome" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Email" Binding="{Binding Email}"/>
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal}"/>
<DataGridTextColumn Header="Área" Binding="{Binding Area}"/>
</DataGrid.Columns>
</DataGrid>
<!-- Aniversarios -->
<DataGrid Grid.Column="3"
ItemsSource="{Binding Aniversarios}"
AutoGenerateColumns="False"
CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Nome" Binding="{Binding Nome}"/>
<DataGridTextColumn Header="Aniversário"
Binding="{Binding Aniversario,Converter={StaticResource DateNoDotConverter}}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Grid> </Grid>
<!-- Row 3: Buttons for actions --> <!-- Row 1: Buttons for actions -->
<Grid Grid.Row="3" <Grid Grid.Row="1"
Margin="5"> Margin="5">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Row 2: Image-->
<Rectangle Grid.Column="0"
Fill="Gainsboro"
Height="90"
Width="160"
>
</Rectangle>
<Image Source="{Binding BackgroundImage}" Height="90" Stretch="Uniform" Grid.Column="0"/>
<Button Content="Selecionar Imagem de Fundo…"
Command="{Binding SelectImageCommand}"
Grid.Column="1"
Padding="20,3,20,3"
Height="40"
Margin="5"/>
<TextBox Text="{Binding CaminhoBGImage}"
Grid.Column="2"
IsReadOnly="True"
Padding="20,3,20,3"
Height="40"
Margin="5"/>
<Button Content="Recarregar Contatos" <Button Content="Recarregar Contatos"
Command="{Binding RefreshCommand}" Command="{Binding RefreshCommand}"
Grid.Column="0" Grid.Column="3"
FontWeight="Medium" FontWeight="Medium"
Padding="5" Padding="5"
Height="40"
Margin="5"/> Margin="5"/>
<Button Content="+ Nova linha" <Button Content="+ Nova linha"
Command="{Binding AddCommand}" Command="{Binding AddCommand}"
Grid.Column = "1" Grid.Column = "4"
Foreground="Green" Foreground="Green"
FontWeight="Medium" FontWeight="Medium"
Padding="5" Padding="5"
Height="40"
Margin="5"/> Margin="5"/>
<Button Content="- Deletar selecionada" <Button Content="- Deletar selecionada"
Command="{Binding DeleteCommand}" Command="{Binding DeleteCommand}"
Grid.Column = "2" Grid.Column = "5"
Foreground="Red" Foreground="Red"
FontWeight="Medium" FontWeight="Medium"
Padding="5" Padding="5"
Height="40"
Margin="5"/> Margin="5"/>
<Button Content="Salvar dados" <Button Content="Salvar dados"
Command="{Binding UpdateCommand}" Command="{Binding UpdateCommand}"
CommandParameter="RawGrid" CommandParameter="RawGrid"
Grid.Column = "3" Grid.Column = "6"
FontWeight="Medium" FontWeight="Medium"
Padding="5" Padding="5"
Height="40"
Margin="5"/> Margin="5"/>
<Button Content="Criar Imagem -->" <Button Content="Criar Imagem -->"
Command="{Binding ExportImageCommand}" Command="{Binding ExportImageCommand}"
CommandParameter="MainGrid" CommandParameter="MainGrid"
Grid.Column = "4" Grid.Column = "7"
Foreground="Blue" Foreground="Blue"
FontWeight="Bold" FontWeight="Bold"
Padding="5" Padding="5"
Height="40"
Margin="5"/> Margin="5"/>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -25,5 +25,13 @@ namespace BackgroundBuilder.Views
// Load contatos on window load // Load contatos on window load
Loaded += async (_, __) => await _vm.LoadRawAsync(); Loaded += async (_, __) => await _vm.LoadRawAsync();
} }
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is DataGrid gridView && gridView.SelectedItem != null)
{
gridView.ScrollIntoView(gridView.SelectedItem);
}
}
} }
} }

View File

@ -1,5 +0,0 @@
{
"ConnectionStrings": {
"ContatosDb": "Host=192.168.10.248;Username=postgres;Password=gds21;Database=Smart Energia"
}
}

149
resume.md
View File

@ -1,5 +1,7 @@
DateNoDotConverter.cs: # Resume
[
DateNoDotConverter.cs:
```cs ```cs
//BackgroundBuilder\Converters\DateNoDotConverter.cs //BackgroundBuilder\Converters\DateNoDotConverter.cs
using System; using System;
@ -29,9 +31,9 @@ namespace BackgroundBuilder.Converters
} }
} }
``` ```
]
Contato.cs: Contato.cs:
[
```cs ```cs
//BackgroundBuilder\Models\Contato.cs //BackgroundBuilder\Models\Contato.cs
using System; using System;
@ -49,10 +51,9 @@ namespace BackgroundBuilder.Models
} }
} }
``` ```
]
IContatoRepository.cs: IContatoRepository.cs:
[
```cs ```cs
//BackgroundBuilder\Repositories\IContatoRepository.cs //BackgroundBuilder\Repositories\IContatoRepository.cs
using System.Collections.Generic; using System.Collections.Generic;
@ -69,10 +70,9 @@ namespace BackgroundBuilder.Repositories
} }
} }
``` ```
]
PostgresContatoRepository.cs: PostgresContatoRepository.cs:
[
```cs ```cs
//BackgroundBuilder\Repositories\PostgresContatoRepository.cs //BackgroundBuilder\Repositories\PostgresContatoRepository.cs
using System.Collections.Generic; using System.Collections.Generic;
@ -147,10 +147,9 @@ namespace BackgroundBuilder.Repositories
} }
} }
``` ```
]
DatabaseService.cs: DatabaseService.cs:
[
```cs ```cs
//BackgroundBuilder\Services\DatabaseService.cs //BackgroundBuilder\Services\DatabaseService.cs
using System; using System;
@ -173,10 +172,9 @@ namespace BackgroundBuilder.Services
} }
} }
``` ```
]
IImageService.cs: IImageService.cs:
[
```cs ```cs
//BackgroundBuilder\Services\IImageService.cs //BackgroundBuilder\Services\IImageService.cs
using System.Threading.Tasks; using System.Threading.Tasks;
@ -206,10 +204,9 @@ namespace BackgroundBuilder.Services
} }
} }
``` ```
]
ImageService.cs: ImageService.cs:
[
```cs ```cs
//BackgroundBuilder\Services\ImageService.cs //BackgroundBuilder\Services\ImageService.cs
using System; using System;
@ -328,10 +325,9 @@ namespace BackgroundBuilder.Services
} }
} }
``` ```
]
ITaskbarService.cs: ITaskbarService.cs:
[
```cs ```cs
//BackgroundBuilder\Services\ITaskbarService.cs //BackgroundBuilder\Services\ITaskbarService.cs
namespace BackgroundBuilder.Services namespace BackgroundBuilder.Services
@ -348,10 +344,9 @@ namespace BackgroundBuilder.Services
} }
} }
``` ```
]
TaskbarService.cs: TaskbarService.cs:
[
```cs ```cs
//BackgroundBuilder\Services\TaskbarService.cs //BackgroundBuilder\Services\TaskbarService.cs
using System; using System;
@ -409,10 +404,9 @@ namespace BackgroundBuilder.Services
} }
} }
``` ```
]
ObservableObject.cs: ObservableObject.cs:
[
```cs ```cs
//BackgroundBuilder\Utils\ObservableObject.cs //BackgroundBuilder\Utils\ObservableObject.cs
using System.ComponentModel; using System.ComponentModel;
@ -428,10 +422,9 @@ namespace BackgroundBuilder.Utils
} }
} }
``` ```
]
RelayCommand.cs: RelayCommand.cs:
[
```cs ```cs
//BackgroundBuilder\Utils\RelayCommand.cs //BackgroundBuilder\Utils\RelayCommand.cs
using System; using System;
@ -453,10 +446,9 @@ namespace BackgroundBuilder.Utils
} }
} }
``` ```
]
MainWindowViewModel.cs: MainWindowViewModel.cs:
[
```cs ```cs
//BackgroundBuilder\ViewModels\MainWindowViewModel.cs //BackgroundBuilder\ViewModels\MainWindowViewModel.cs
using System; using System;
@ -689,15 +681,14 @@ namespace BackgroundBuilder.ViewModels
} }
} }
``` ```
]
MainWindow.xaml MainWindow.xaml
[
```xml ```xml
<!--BackgroundBuilder\Views\MainWindow.xaml--> <!--BackgroundBuilder\Views\MainWindow.xaml-->
<Window x:Class="BackgroundBuilder.Views.MainWindow" <Window x:Class="BackgroundBuilder.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:BackgroundBuilder.Converters" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:BackgroundBuilder.Converters"
Title="BackgroundBuilder" WindowStartupLocation="CenterScreen" WindowState="Maximized" MinHeight="950" MinWidth="1230"> Title="BackgroundBuilder" WindowStartupLocation="CenterScreen" WindowState="Maximized" MinHeight="950" MinWidth="1230">
<Window.Resources> <Window.Resources>
<converters:DateNoDotConverter x:Key="DateNoDotConverter" /> <converters:DateNoDotConverter x:Key="DateNoDotConverter" />
@ -880,10 +871,9 @@ MainWindow.xaml
</Grid> </Grid>
</Window> </Window>
``` ```
]
MainWindow.xaml.cs: MainWindow.xaml.cs:
[
```cs ```cs
//BackgroundBuilder\Views\MainWindow.xaml.cs //BackgroundBuilder\Views\MainWindow.xaml.cs
using System.ComponentModel; using System.ComponentModel;
@ -916,10 +906,9 @@ namespace BackgroundBuilder.Views
} }
} }
``` ```
]
App.xaml App.xaml
[
```xml ```xml
<!--BackgroundBuilder\App.xaml--> <!--BackgroundBuilder\App.xaml-->
<Application x:Class="BackgroundBuilder.App" <Application x:Class="BackgroundBuilder.App"
@ -976,10 +965,9 @@ App.xaml
</Application.Resources> </Application.Resources>
</Application> </Application>
``` ```
]
App.xaml.cs: App.xaml.cs:
[
```cs ```cs
//BackgroundBuilder\App.xaml.cs //BackgroundBuilder\App.xaml.cs
using System; using System;
@ -1037,10 +1025,9 @@ namespace BackgroundBuilder
} }
} }
``` ```
]
appsettings.json: appsettings.json:
[
```json ```json
//BackgroundBuilder\appsettings.json //BackgroundBuilder\appsettings.json
{ {
@ -1049,56 +1036,72 @@ appsettings.json:
} }
} }
``` ```
]
README.md: README.md:
[
```md ```md
# BackgroundBuilder # 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 > 🖥️ An MVVM WPF application (.NET 8) providing an Excel-like editor for the `contatos` table in PostgreSQL
- 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 ## 📑 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. 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. Saves the current DataGrid overlaid on the background as a single PNG.
Uses WPFs `RenderTargetBitmap` and `PngBitmapEncoder` under the hood. Uses WPFs `RenderTargetBitmap` and `PngBitmapEncoder` under the hood.
--- ---
```
]

BIN
smart.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB