1104 lines
38 KiB
Markdown
1104 lines
38 KiB
Markdown
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<IEnumerable<Contato>> 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<IEnumerable<Contato>> 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<Contato>(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
|
||
{
|
||
/// <summary>
|
||
/// Loads an image from disk.
|
||
/// </summary>
|
||
Task<BitmapImage> LoadAsync(string path);
|
||
|
||
/// <summary>
|
||
/// Renders the background + overlay and writes two PNGs:
|
||
/// • primaryPath: composite of background+overlay
|
||
/// • overlayPath (optional): overlay alone
|
||
/// Returns the actual paths written.
|
||
/// </summary>
|
||
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<BitmapImage> 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));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Renders a <paramref name="background"/> plus the <paramref name="mainGrid"/>
|
||
/// at bottom‐right offset by <paramref name="offset"/>. If background is null,
|
||
/// only the mainGrid is rendered at (0,0).
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Encodes the given bitmap as PNG and writes it to disk.
|
||
/// </summary>
|
||
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();
|
||
/// <summary>Returns true when the taskbar is horizontal, false when vertical.</summary>
|
||
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<APPBARDATA>() };
|
||
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<APPBARDATA>() };
|
||
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<object?> execute, Func<object?, bool>? canExecute = null) : ICommand
|
||
{
|
||
private readonly Action<object?> _execute = execute;
|
||
private readonly Func<object?, bool>? _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<Contato> RawContatos { get; } = [];
|
||
public ObservableCollection<Contato> Comando { get; } = [];
|
||
public ObservableCollection<Contato> ContatosSemCMD { get; } = [];
|
||
public ObservableCollection<Contato> Aniversarios { get; } = [];
|
||
|
||
|
||
private Contato? _selectedContato;
|
||
public Contato? SelectedContato{ get => _selectedContato; set { _selectedContato = value; OnPropertyChanged(); DeleteCommand.RaiseCanExecuteChanged(); } }
|
||
|
||
private ObservableCollection<Contato>? _selectedContatos;
|
||
public ObservableCollection<Contato> 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
|
||
<!--BackgroundBuilder\Views\MainWindow.xaml-->
|
||
<Window x:Class="BackgroundBuilder.Views.MainWindow"
|
||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||
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">
|
||
<Window.Resources>
|
||
<converters:DateNoDotConverter x:Key="DateNoDotConverter" />
|
||
</Window.Resources>
|
||
<Grid x:Name="RootGrid" Margin="20">
|
||
<Grid.RowDefinitions>
|
||
<RowDefinition Height="Auto"/>
|
||
<RowDefinition Height="Auto"/>
|
||
<RowDefinition Height="*"/>
|
||
<RowDefinition Height="Auto"/>
|
||
</Grid.RowDefinitions>
|
||
|
||
<!-- Row 0: Background selection -->
|
||
<Grid Grid.Row="0">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="*"/>
|
||
</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="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"
|
||
Grid.Column="2"
|
||
ItemsSource="{Binding RawContatos}"
|
||
SelectedItem="{Binding SelectedContato}"
|
||
AutoGenerateColumns="False"
|
||
IsReadOnly="False"
|
||
IsHitTestVisible ="True"
|
||
FontSize="11"
|
||
VerticalAlignment="Top"
|
||
HorizontalAlignment="Center"
|
||
Height="800"
|
||
AlternatingRowBackground="DarkGray">
|
||
<DataGrid.ColumnHeaderStyle>
|
||
<Style TargetType="DataGridColumnHeader">
|
||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||
<Setter Property="Background" Value="Gray"/>
|
||
<Setter Property="Foreground" Value="White"/>
|
||
<Setter Property="Padding" Value="5"/>
|
||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||
</Style>
|
||
</DataGrid.ColumnHeaderStyle>
|
||
<!-- your existing columns here -->
|
||
<DataGrid.Columns>
|
||
<DataGridTextColumn Header="Ramal" Binding="{Binding Ramal, UpdateSourceTrigger=PropertyChanged}" />
|
||
<DataGridTextColumn Header="Nome" Binding="{Binding Nome, UpdateSourceTrigger=PropertyChanged}" />
|
||
<DataGridTextColumn Header="Email" Binding="{Binding Email, UpdateSourceTrigger=PropertyChanged}" />
|
||
<DataGridTextColumn Header="Área" Binding="{Binding Area, UpdateSourceTrigger=PropertyChanged}" />
|
||
<DataGridTemplateColumn Header="Aniversário">
|
||
<DataGridTemplateColumn.CellTemplate>
|
||
<DataTemplate>
|
||
<DatePicker SelectedDate="{Binding Aniversario, UpdateSourceTrigger=PropertyChanged}"/>
|
||
</DataTemplate>
|
||
</DataGridTemplateColumn.CellTemplate>
|
||
</DataGridTemplateColumn>
|
||
<DataGridCheckBoxColumn Header="É Comando?" Binding="{Binding IsComando}" />
|
||
|
||
</DataGrid.Columns>
|
||
</DataGrid>
|
||
</Grid>
|
||
|
||
<!-- Row 3: Buttons for actions -->
|
||
<Grid Grid.Row="3"
|
||
Margin="5">
|
||
<Grid.ColumnDefinitions>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="Auto"/>
|
||
<ColumnDefinition Width="*"/>
|
||
</Grid.ColumnDefinitions>
|
||
<Button Content="Recarregar Contatos"
|
||
Command="{Binding RefreshCommand}"
|
||
Grid.Column="0"
|
||
FontWeight="Medium"
|
||
Padding="5"
|
||
Margin="5"/>
|
||
<Button Content="+ Nova linha"
|
||
Command="{Binding AddCommand}"
|
||
Grid.Column = "1"
|
||
Foreground="Green"
|
||
FontWeight="Medium"
|
||
Padding="5"
|
||
Margin="5"/>
|
||
<Button Content="- Deletar selecionada"
|
||
Command="{Binding DeleteCommand}"
|
||
Grid.Column = "2"
|
||
Foreground="Red"
|
||
FontWeight="Medium"
|
||
Padding="5"
|
||
Margin="5"/>
|
||
<Button Content="Salvar dados"
|
||
Command="{Binding UpdateCommand}"
|
||
CommandParameter="RawGrid"
|
||
Grid.Column = "3"
|
||
FontWeight="Medium"
|
||
Padding="5"
|
||
Margin="5"/>
|
||
<Button Content="Criar Imagem -->"
|
||
Command="{Binding ExportImageCommand}"
|
||
CommandParameter="MainGrid"
|
||
Grid.Column = "4"
|
||
Foreground="Blue"
|
||
FontWeight="Bold"
|
||
Padding="5"
|
||
Margin="5"/>
|
||
</Grid>
|
||
</Grid>
|
||
</Window>
|
||
```
|
||
]
|
||
|
||
MainWindow.xaml.cs:
|
||
[
|
||
```cs
|
||
//BackgroundBuilder\Views\MainWindow.xaml.cs
|
||
using System.ComponentModel;
|
||
using System.Windows;
|
||
using System.Windows.Controls;
|
||
using BackgroundBuilder.Models;
|
||
using BackgroundBuilder.ViewModels;
|
||
|
||
namespace BackgroundBuilder.Views
|
||
{
|
||
public partial class MainWindow : Window
|
||
{
|
||
private readonly MainWindowViewModel _vm;
|
||
|
||
public MainWindow(MainWindowViewModel vm)
|
||
{
|
||
InitializeComponent();
|
||
|
||
this.Language = System.Windows.Markup.XmlLanguage.GetLanguage(System.Globalization.CultureInfo.CurrentUICulture.IetfLanguageTag);
|
||
|
||
_vm = vm;
|
||
DataContext = vm;
|
||
|
||
// Point the VM’s OverlayElement to our DataGrid
|
||
_vm.OverlayElement = MainGrid;
|
||
|
||
// Load contatos on window load
|
||
Loaded += async (_, __) => await _vm.LoadRawAsync();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
]
|
||
|
||
App.xaml
|
||
[
|
||
```xml
|
||
<!--BackgroundBuilder\App.xaml-->
|
||
<Application x:Class="BackgroundBuilder.App"
|
||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||
Startup="OnStartup">
|
||
<Application.Resources>
|
||
<Style TargetType="DataGrid">
|
||
<Setter Property="AutoGenerateColumns" Value="True"/>
|
||
<Setter Property="CanUserAddRows" Value="False"/>
|
||
<Setter Property="CanUserDeleteRows" Value="False"/>
|
||
<Setter Property="CanUserReorderColumns" Value="False"/>
|
||
<Setter Property="CanUserResizeColumns" Value="False"/>
|
||
<Setter Property="CanUserSortColumns" Value="False"/>
|
||
<Setter Property="GridLinesVisibility" Value="None"/>
|
||
<Setter Property="BorderThickness" Value="0"/>
|
||
<Setter Property="BorderBrush" Value="Black"/>
|
||
<Setter Property="IsReadOnly" Value="True"/>
|
||
<Setter Property="FontSize" Value="16"/>
|
||
<Setter Property="RowBackground" Value="GhostWhite"/>
|
||
<Setter Property="AlternatingRowBackground" Value="#387f79"/>
|
||
<Setter Property="Margin" Value="5"/>
|
||
<Setter Property="FontStyle" Value="Italic" />
|
||
<Setter Property="FontWeight" Value="Medium" />
|
||
<Setter Property="HeadersVisibility" Value="Column" />
|
||
<Setter Property="TextBlock.TextAlignment" Value="Center"/>
|
||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||
<Setter Property="VerticalAlignment" Value="Bottom"/>
|
||
<Setter Property="IsHitTestVisible" Value="False"/>
|
||
</Style>
|
||
<Style TargetType="DataGridColumnHeader">
|
||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||
<Setter Property="Background" Value="#387f79"/>
|
||
<Setter Property="Foreground" Value="White"/>
|
||
<Setter Property="Padding" Value="5"/>
|
||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||
</Style>
|
||
<Style TargetType="DataGridCell">
|
||
<Setter Property="FontWeight" Value="Medium" />
|
||
<Setter Property="Margin" Value="10,0,10,0"/>
|
||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||
</Style>
|
||
<Style TargetType="DataGridRow">
|
||
<Setter Property="Foreground" Value="Black"/>
|
||
<Style.Triggers>
|
||
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
|
||
<Setter Property="Foreground" Value="White"/>
|
||
</Trigger>
|
||
</Style.Triggers>
|
||
</Style>
|
||
<Style TargetType="Button">
|
||
<Setter Property="FontSize" Value="16"/>
|
||
</Style>
|
||
</Application.Resources>
|
||
</Application>
|
||
```
|
||
]
|
||
|
||
App.xaml.cs:
|
||
[
|
||
```cs
|
||
//BackgroundBuilder\App.xaml.cs
|
||
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<DatabaseService>();
|
||
services.AddScoped<IContatoRepository, PostgresContatoRepository>();
|
||
|
||
// New image service
|
||
services.AddSingleton<IImageService, ImageService>();
|
||
|
||
// New taskbar service
|
||
services.AddSingleton<ITaskbarService, TaskbarService>();
|
||
|
||
// VM & View
|
||
services.AddTransient<MainWindowViewModel>();
|
||
services.AddTransient<MainWindow>();
|
||
})
|
||
.Build();
|
||
}
|
||
|
||
private async void OnStartup(object sender, StartupEventArgs e)
|
||
{
|
||
await _host.StartAsync();
|
||
var window = _host.Services.GetRequiredService<MainWindow>();
|
||
window.Show();
|
||
}
|
||
|
||
protected override async void OnExit(ExitEventArgs e)
|
||
{
|
||
using (_host) { await _host.StopAsync(); }
|
||
base.OnExit(e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
]
|
||
|
||
appsettings.json:
|
||
[
|
||
```json
|
||
//BackgroundBuilder\appsettings.json
|
||
{
|
||
"ConnectionStrings": {
|
||
"ContatosDb": "Host=192.168.10.248;Username=postgres;Password=gds21;Database=Smart Energia"
|
||
}
|
||
}
|
||
```
|
||
]
|
||
|
||
README.md:
|
||
[
|
||
```md
|
||
# 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.
|
||
---
|
||
```
|
||
] |