Files
Minint/Minint/ViewModels/MainWindowViewModel.cs
2026-03-29 18:19:39 +03:00

280 lines
8.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Minint.Core.Models;
using Minint.Core.Services;
using Minint.Core.Services.Impl;
using Minint.Infrastructure.Export;
using Minint.Infrastructure.Serialization;
using Minint.Views;
namespace Minint.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private readonly MinintSerializer _serializer = new();
private readonly IImageEffectsService _effects = new ImageEffectsService();
private readonly IPatternGenerator _patternGen = new PatternGenerator();
private readonly IBmpExporter _bmpExporter = new BmpExporter();
private readonly IGifExporter _gifExporter = new GifExporter();
private readonly ICompositor _compositor = new Compositor();
private static readonly FilePickerFileType MinintFileType = new("Minint Files")
{
Patterns = ["*.minint"],
};
private static readonly FilePickerFileType BmpFileType = new("BMP Image")
{
Patterns = ["*.bmp"],
};
private static readonly FilePickerFileType GifFileType = new("GIF Animation")
{
Patterns = ["*.gif"],
};
[ObservableProperty]
private EditorViewModel _editor = new();
[ObservableProperty]
private string _statusText = "Ready";
public TopLevel? Owner { get; set; }
#region File commands
[RelayCommand]
private void NewFile()
{
Editor.NewContainer(64, 64);
StatusText = "New 64×64 container created.";
}
[RelayCommand]
private async Task OpenFileAsync()
{
if (Owner?.StorageProvider is not { } sp) return;
var files = await sp.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Open .minint file",
FileTypeFilter = [MinintFileType],
AllowMultiple = false,
});
if (files.Count == 0) return;
var file = files[0];
try
{
await using var stream = await file.OpenReadAsync();
var container = _serializer.Read(stream);
var path = file.TryGetLocalPath();
Editor.LoadContainer(container, path);
StatusText = $"Opened {file.Name}";
}
catch (Exception ex)
{
StatusText = $"Error opening file: {ex.Message}";
}
}
[RelayCommand]
private async Task SaveFileAsync()
{
if (Editor.Container is null) return;
if (Editor.FilePath is not null)
await SaveToPathAsync(Editor.FilePath);
else
await SaveFileAsAsync();
}
[RelayCommand]
private async Task SaveFileAsAsync()
{
if (Owner?.StorageProvider is not { } sp || Editor.Container is null) return;
var file = await sp.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Save .minint file",
DefaultExtension = "minint",
FileTypeChoices = [MinintFileType],
SuggestedFileName = Editor.FilePath is not null
? Path.GetFileName(Editor.FilePath) : "untitled.minint",
});
if (file is null) return;
var path = file.TryGetLocalPath();
if (path is null)
{
StatusText = "Error: could not resolve file path.";
return;
}
await SaveToPathAsync(path);
}
private async Task SaveToPathAsync(string path)
{
try
{
await using var fs = File.Create(path);
_serializer.Write(fs, Editor.Container!);
Editor.FilePath = path;
StatusText = $"Saved {Path.GetFileName(path)}";
}
catch (Exception ex)
{
StatusText = $"Error saving file: {ex.Message}";
}
}
#endregion
#region Effects (A1, A2)
[RelayCommand]
private async Task ApplyContrastAsync()
{
if (Editor.ActiveDocument is null || Owner is not Window window) return;
var dialog = new ContrastDialog();
var result = await dialog.ShowDialog<bool?>(window);
if (result != true) return;
_effects.ApplyContrast(Editor.ActiveDocument, dialog.Factor);
Editor.RefreshCanvas();
StatusText = $"Contrast ×{dialog.Factor:F1} applied.";
}
[RelayCommand]
private void ApplyGrayscale()
{
if (Editor.ActiveDocument is null) return;
_effects.ApplyGrayscale(Editor.ActiveDocument);
Editor.RefreshCanvas();
StatusText = "Grayscale applied.";
}
#endregion
#region Pattern generation (Б4)
[RelayCommand]
private async Task GeneratePatternAsync()
{
if (Editor.Container is null || Owner is not Window window) return;
var dialog = new PatternDialog();
var result = await dialog.ShowDialog<bool?>(window);
if (result != true) return;
try
{
var doc = _patternGen.Generate(
dialog.SelectedPattern,
Editor.Container.Width,
Editor.Container.Height,
[dialog.PatternColor1, dialog.PatternColor2],
dialog.PatternParam1,
dialog.PatternParam2);
Editor.Container.Documents.Add(doc);
Editor.SyncAfterExternalChange();
Editor.SelectDocument(doc);
StatusText = $"Pattern '{dialog.SelectedPattern}' generated.";
}
catch (Exception ex)
{
StatusText = $"Pattern generation failed: {ex.Message}";
}
}
#endregion
#region Export (BMP / GIF)
[RelayCommand]
private async Task ExportBmpAsync()
{
if (Editor.Container is null || Editor.ActiveDocument is null) return;
if (Owner?.StorageProvider is not { } sp) return;
var file = await sp.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Export document as BMP",
DefaultExtension = "bmp",
FileTypeChoices = [BmpFileType],
SuggestedFileName = $"{Editor.ActiveDocument.Name}.bmp",
});
if (file is null) return;
var path = file.TryGetLocalPath();
if (path is null) { StatusText = "Error: could not resolve file path."; return; }
try
{
int w = Editor.Container.Width, h = Editor.Container.Height;
uint[] argb = _compositor.Composite(Editor.ActiveDocument, w, h);
await using var fs = File.Create(path);
_bmpExporter.Export(fs, argb, w, h);
StatusText = $"Exported BMP: {Path.GetFileName(path)}";
}
catch (Exception ex)
{
StatusText = $"BMP export failed: {ex.Message}";
}
}
[RelayCommand]
private async Task ExportGifAsync()
{
if (Editor.Container is null || Editor.Container.Documents.Count == 0) return;
if (Owner?.StorageProvider is not { } sp) return;
var file = await sp.SaveFilePickerAsync(new FilePickerSaveOptions
{
Title = "Export animation as GIF",
DefaultExtension = "gif",
FileTypeChoices = [GifFileType],
SuggestedFileName = Editor.FilePath is not null
? Path.GetFileNameWithoutExtension(Editor.FilePath) + ".gif"
: "animation.gif",
});
if (file is null) return;
var path = file.TryGetLocalPath();
if (path is null) { StatusText = "Error: could not resolve file path."; return; }
try
{
int w = Editor.Container.Width, h = Editor.Container.Height;
var frames = new List<(uint[] Pixels, uint DelayMs)>();
foreach (var doc in Editor.Container.Documents)
{
uint[] argb = _compositor.Composite(doc, w, h);
frames.Add((argb, doc.FrameDelayMs));
}
await using var fs = File.Create(path);
_gifExporter.Export(fs, frames, w, h);
StatusText = $"Exported GIF ({frames.Count} frames): {Path.GetFileName(path)}";
}
catch (Exception ex)
{
StatusText = $"GIF export failed: {ex.Message}";
}
}
#endregion
}