280 lines
8.4 KiB
C#
280 lines
8.4 KiB
C#
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
|
||
}
|