更换为wpf

This commit is contained in:
ww-rm
2025-05-27 15:55:10 +08:00
parent d0f629d9ba
commit 17257a0ffe
316 changed files with 19206 additions and 76537 deletions

View File

@@ -0,0 +1,140 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using NLog;
using SFMLRenderer;
using Spine;
using Spine.Exporters;
using SpineViewer.Extensions;
using SpineViewer.Resources;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace SpineViewer.ViewModels.Exporters
{
public abstract class BaseExporterViewModel: ObservableObject
{
protected static readonly Logger _logger = LogManager.GetCurrentClassLogger();
protected readonly MainWindowViewModel _vmMain;
protected readonly ISFMLRenderer _renderer;
public BaseExporterViewModel(MainWindowViewModel vmMain)
{
_vmMain = vmMain;
_renderer = _vmMain.SFMLRenderer;
}
public uint ResolutionX => _vmMain.SFMLRendererViewModel.ResolutionX;
public uint ResolutionY => _vmMain.SFMLRendererViewModel.ResolutionY;
/// <summary>
/// 是否导出成单个
/// </summary>
public bool ExportSingle { get => _exportSingle; set => SetProperty(ref _exportSingle, value); }
protected bool _exportSingle = false;
/// <summary>
/// 输出文件夹, 如果指定则将输出内容输出到该文件夹, 如果没指定则输出到各自目录下, 导出单个时必须指定
/// </summary>
public string? OutputDir { get => _outputDir; set => SetProperty(ref _outputDir, value); }
protected string? _outputDir;
/// <summary>
/// 背景颜色
/// </summary>
public Color BackgroundColor { get => _backgroundColor; set => SetProperty(ref _backgroundColor, value); }
protected Color _backgroundColor = Color.FromArgb(0, 0, 0, 0);
/// <summary>
/// 四周边缘距离
/// </summary>
public uint Margin { get => _margin; set => SetProperty(ref _margin, value); }
protected uint _margin = 0;
/// <summary>
/// 使用自动分辨率
/// </summary>
public bool AutoResolution { get => _autoResolution; set => SetProperty(ref _autoResolution, value); }
protected bool _autoResolution = false;
/// <summary>
/// 最大分辨率
/// </summary>
public uint MaxResolution { get => _maxResolution; set => SetProperty(ref _maxResolution, value); }
protected uint _maxResolution = 2048;
/// <summary>
/// 使用提供的包围盒设置自动分辨率
/// </summary>
private void SetAutoResolution(BaseExporter exporter, Rect bounds)
{
if (!_autoResolution) return;
var resolution = bounds.Size.ToVector2u();
if (resolution.X >= _maxResolution || resolution.Y >= _maxResolution)
{
// 缩小到最大像素限制
var scale = Math.Min(_maxResolution / bounds.Width, _maxResolution / bounds.Height);
resolution.X = (uint)(bounds.Width * scale);
resolution.Y = (uint)(bounds.Height * scale);
}
exporter.Resolution = new(resolution.X + _margin * 2, resolution.Y + _margin * 2);
var viewBounds = bounds.ToFloatRect().GetCanvasBounds(resolution, _margin);
exporter.Size = new(viewBounds.Width, -viewBounds.Height);
exporter.Center = viewBounds.Position + viewBounds.Size / 2;
exporter.Rotation = 0;
}
/// <summary>
/// 使用提供的模型设置导出器的自动分辨率和视区参数, 静态画面
/// </summary>
protected void SetAutoResolutionStatic(BaseExporter exporter, params SpineObject[] spines)
{
var bounds = spines[0].GetAnimationBounds();
foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetAnimationBounds());
SetAutoResolution(exporter, bounds);
}
/// <summary>
/// 使用提供的模型设置导出器的自动分辨率和视区参数, 动画画面
/// </summary>
protected void SetAutoResolutionAnimated(BaseExporter exporter, params SpineObject[] spines)
{
var bounds = spines[0].GetAnimationBounds();
foreach (var sp in spines.Skip(1)) bounds.Union(sp.GetAnimationBounds());
SetAutoResolution(exporter, bounds);
}
/// <summary>
/// 检查参数是否合法并规范化参数值, 否则返回用户错误原因
/// </summary>
public virtual string? Validate()
{
if (!string.IsNullOrWhiteSpace(_outputDir) && File.Exists(_outputDir))
return AppResource.Str_InvalidOutputDir;
if (!string.IsNullOrWhiteSpace(_outputDir) && !Directory.Exists(_outputDir))
return AppResource.Str_OutputDirNotFound;
if (_exportSingle && string.IsNullOrWhiteSpace(_outputDir))
return AppResource.Str_OutputDirRequired;
if (_autoResolution && _maxResolution <= 0)
return AppResource.Str_InvalidMaxResolution;
OutputDir = string.IsNullOrWhiteSpace(_outputDir) ? null : Path.GetFullPath(_outputDir);
return null;
}
public RelayCommand<IList?> Cmd_Export => _cmd_Export ??= new(Export_Execute, args => args is not null && args.Count > 0);
private RelayCommand<IList?>? _cmd_Export;
protected abstract void Export_Execute(IList? args);
}
}

View File

@@ -0,0 +1,173 @@
using Spine.Exporters;
using Spine;
using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.Services;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
namespace SpineViewer.ViewModels.Exporters
{
public class CustomFFmpegExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
{
public string Format { get => _format; set => SetProperty(ref _format, value); }
protected string _format = "mp4";
public string? Codec { get => _codec; set => SetProperty(ref _codec, value); }
protected string? _codec;
public string? PixelFormat { get => _pixelFormat; set => SetProperty(ref _pixelFormat, value); }
protected string? _pixelFormat;
public string? Bitrate { get => _bitrate; set => SetProperty(ref _bitrate, value); }
protected string? _bitrate;
public string? Filter { get => _filter; set => SetProperty(ref _filter, value); }
protected string? _filter;
public string? CustomArgs { get => _customArgs; set => SetProperty(ref _customArgs, value); }
protected string? _customArgs;
private string FormatSuffix => $".{_format.ToString().ToLower()}";
public override string? Validate()
{
if (base.Validate() is string err)
return err;
if (string.IsNullOrWhiteSpace(_format))
return AppResource.Str_FFmpegFormatRequired;
return null;
}
protected override void Export_Execute(IList? args)
{
if (args is null || args.Count <= 0) return;
if (!ExporterDialogService.ShowCustomFFmpegExporterDialog(this)) return;
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_CustomFFmpegExporterTitle);
foreach (var sp in spines) sp.Dispose();
}
private void ExportTask(SpineObject[] spines, IProgressReporter pr, CancellationToken ct)
{
if (spines.Length <= 0) return;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
using var exporter = new CustomFFmpegExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2)
{
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
Duration = _duration,
Fps = _fps,
KeepLast = _keepLast,
Format = _format,
Codec = _codec,
PixelFormat = _pixelFormat,
Bitrate = _bitrate,
Filter = _filter,
CustomArgs = _customArgs
};
// 非自动分辨率则直接用预览画面的视区参数
if (!_autoResolution)
{
using var view = _renderer.GetView();
var bounds = view.GetBounds().GetCanvasBounds(_renderer.Resolution, _margin);
exporter.Size = bounds.Size;
exporter.Center = bounds.Position + bounds.Size / 2;
exporter.Rotation = view.Rotation;
}
if (_exportSingle)
{
var filename = $"ffmpeg_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
var output = Path.Combine(_outputDir!, filename);
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
exporter.ProgressReporter = (total, done, text) =>
{
pr.Total = total;
pr.Done = done;
pr.ProgressText = text;
_vmMain.ProgressValue = pr.Done / pr.Total;
};
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
try
{
exporter.Export(output, ct, spines);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
}
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
}
else
{
// 统计总帧数
int totalFrameCount = 0;
if (_duration > 0)
{
exporter.Duration = _duration;
totalFrameCount = exporter.GetFrameCount() * spines.Length;
}
else
{
foreach (var sp in spines)
{
exporter.Duration = sp.GetAnimationMaxDuration();
totalFrameCount += exporter.GetFrameCount();
}
}
pr.Total = totalFrameCount;
pr.Done = 0;
exporter.ProgressReporter = (total, done, text) =>
{
pr.Done++;
pr.ProgressText = text;
_vmMain.ProgressValue = pr.Done / pr.Total;
};
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
foreach (var sp in spines)
{
if (ct.IsCancellationRequested)
{
_logger.Info("Export cancelled");
break;
}
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
if (_duration <= 0) exporter.Duration = sp.GetAnimationMaxDuration();
var filename = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
var output = Path.Combine(_outputDir ?? sp.AssetsDir, filename);
try
{
exporter.Export(output, ct, sp);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
}
}
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
}
}
}
}

View File

@@ -0,0 +1,158 @@
using Spine.Exporters;
using Spine;
using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.Services;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Collections.Immutable;
namespace SpineViewer.ViewModels.Exporters
{
public class FFmpegVideoExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
{
public ImmutableArray<FFmpegVideoExporter.VideoFormat> VideoFormats { get; } = Enum.GetValues<FFmpegVideoExporter.VideoFormat>().ToImmutableArray();
public FFmpegVideoExporter.VideoFormat Format { get => _format; set => SetProperty(ref _format, value); }
protected FFmpegVideoExporter.VideoFormat _format = FFmpegVideoExporter.VideoFormat.Mp4;
public bool Loop { get => _loop; set => SetProperty(ref _loop, value); }
protected bool _loop = true;
public int Quality { get => _quality; set => SetProperty(ref _quality, Math.Clamp(value, 0, 100)); }
protected int _quality = 75;
public int Crf { get => _crf; set => SetProperty(ref _crf, Math.Clamp(value, 0, 63)); }
protected int _crf = 23;
private string FormatSuffix => $".{_format.ToString().ToLower()}";
protected override void Export_Execute(IList? args)
{
if (args is null || args.Count <= 0) return;
if (!ExporterDialogService.ShowFFmpegVideoExporterDialog(this)) return;
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FFmpegVideoExporterTitle);
foreach (var sp in spines) sp.Dispose();
}
private void ExportTask(SpineObject[] spines, IProgressReporter pr, CancellationToken ct)
{
if (spines.Length <= 0) return;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
using var exporter = new FFmpegVideoExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2)
{
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
Duration = _duration,
Fps = _fps,
KeepLast = _keepLast,
Format = _format,
Loop = _loop,
Quality = _quality,
Crf = _crf
};
// 非自动分辨率则直接用预览画面的视区参数
if (!_autoResolution)
{
using var view = _renderer.GetView();
var bounds = view.GetBounds().GetCanvasBounds(_renderer.Resolution, _margin);
exporter.Size = bounds.Size;
exporter.Center = bounds.Position + bounds.Size / 2;
exporter.Rotation = view.Rotation;
}
if (_exportSingle)
{
var filename = $"video_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
var output = Path.Combine(_outputDir!, filename);
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
exporter.ProgressReporter = (total, done, text) =>
{
pr.Total = total;
pr.Done = done;
pr.ProgressText = text;
_vmMain.ProgressValue = pr.Done / pr.Total;
};
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
try
{
exporter.Export(output, ct, spines);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
}
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
}
else
{
// 统计总帧数
int totalFrameCount = 0;
if (_duration > 0)
{
exporter.Duration = _duration;
totalFrameCount = exporter.GetFrameCount() * spines.Length;
}
else
{
foreach (var sp in spines)
{
exporter.Duration = sp.GetAnimationMaxDuration();
totalFrameCount += exporter.GetFrameCount();
}
}
pr.Total = totalFrameCount;
pr.Done = 0;
exporter.ProgressReporter = (total, done, text) =>
{
pr.Done++;
pr.ProgressText = text;
_vmMain.ProgressValue = pr.Done / pr.Total;
};
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
foreach (var sp in spines)
{
if (ct.IsCancellationRequested)
{
_logger.Info("Export cancelled");
break;
}
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
if (_duration <= 0) exporter.Duration = sp.GetAnimationMaxDuration();
var filename = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}{FormatSuffix}";
var output = Path.Combine(_outputDir ?? sp.AssetsDir, filename);
try
{
exporter.Export(output, ct, sp);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
}
}
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
}
}
}
}

View File

@@ -0,0 +1,129 @@
using CommunityToolkit.Mvvm.ComponentModel;
using SkiaSharp;
using Spine;
using Spine.Exporters;
using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.Services;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.ViewModels.Exporters
{
public class FrameExporterViewModel(MainWindowViewModel vmMain) : BaseExporterViewModel(vmMain)
{
public ImmutableArray<SKEncodedImageFormat> FrameFormats { get; } = Enum.GetValues<SKEncodedImageFormat>().ToImmutableArray();
public SKEncodedImageFormat Format { get => _format; set => SetProperty(ref _format, value); }
protected SKEncodedImageFormat _format = SKEncodedImageFormat.Png;
public int Quality { get => _quality; set => SetProperty(ref _quality, Math.Clamp(value, 0, 100)); }
protected int _quality = 80;
private string FormatSuffix
{
get
{
if (_format == SKEncodedImageFormat.Heif) return ".jpeg";
else if (_format == SKEncodedImageFormat.Jpegxl) return ".jpeg";
else return $".{_format.ToString().ToLower()}";
}
}
protected override void Export_Execute(IList? args)
{
if (args is null || args.Count <= 0) return;
if (!ExporterDialogService.ShowFrameExporterDialog(this)) return;
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject(true)).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameExporterTitle);
foreach (var sp in spines) sp.Dispose();
}
private void ExportTask(SpineObject[] spines, IProgressReporter pr, CancellationToken ct)
{
if (spines.Length <= 0) return;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
using var exporter = new FrameExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2)
{
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
Format = _format,
Quality = _quality
};
// 非自动分辨率则直接用预览画面的视区参数
if (!_autoResolution)
{
using var view = _renderer.GetView();
var bounds = view.GetBounds().GetCanvasBounds(_renderer.Resolution, _margin);
exporter.Size = bounds.Size;
exporter.Center = bounds.Position + bounds.Size / 2;
exporter.Rotation = view.Rotation;
}
if (_exportSingle)
{
var filename = $"frame_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_quality}{FormatSuffix}";
var output = Path.Combine(_outputDir!, filename);
if (_autoResolution) SetAutoResolutionStatic(exporter, spines);
try
{
exporter.Export(output, spines);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
}
}
else
{
int total = spines.Length;
int done = 1;
pr.Total = total;
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
foreach (var sp in spines)
{
if (ct.IsCancellationRequested)
{
_logger.Info("Export cancelled");
break;
}
var filename = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_quality}{FormatSuffix}";
var output = Path.Combine(_outputDir ?? sp.AssetsDir, filename);
pr.Done = done;
pr.ProgressText = $"[{done}/{total}] {output}";
_vmMain.ProgressValue = pr.Done / pr.Total;
if (_autoResolution) SetAutoResolutionStatic(exporter, sp);
try
{
exporter.Export(output, sp);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
}
done++;
}
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
}
}
}
}

View File

@@ -0,0 +1,137 @@
using Spine;
using Spine.Exporters;
using SpineViewer.Extensions;
using SpineViewer.Models;
using SpineViewer.Resources;
using SpineViewer.Services;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.ViewModels.Exporters
{
public class FrameSequenceExporterViewModel(MainWindowViewModel vmMain) : VideoExporterViewModel(vmMain)
{
protected override void Export_Execute(IList? args)
{
if (args is null || args.Count <= 0) return;
if (!ExporterDialogService.ShowFrameSequenceExporterDialog(this)) return;
SpineObject[] spines = args.Cast<SpineObjectModel>().Select(m => m.GetSpineObject()).ToArray();
ProgressService.RunAsync((pr, ct) => ExportTask(spines, pr, ct), AppResource.Str_FrameSequenceExporterTitle);
foreach (var sp in spines) sp.Dispose();
}
private void ExportTask(SpineObject[] spines, IProgressReporter pr, CancellationToken ct)
{
if (spines.Length <= 0) return;
var timestamp = DateTime.Now.ToString("yyMMddHHmmss");
using var exporter = new FrameSequenceExporter(_renderer.Resolution.X + _margin * 2, _renderer.Resolution.Y + _margin * 2)
{
BackgroundColor = new(_backgroundColor.R, _backgroundColor.G, _backgroundColor.B, _backgroundColor.A),
Duration = _duration,
Fps = _fps,
KeepLast = _keepLast
};
// 非自动分辨率则直接用预览画面的视区参数
if (!_autoResolution)
{
using var view = _renderer.GetView();
var bounds = view.GetBounds().GetCanvasBounds(_renderer.Resolution, _margin);
exporter.Size = bounds.Size;
exporter.Center = bounds.Position + bounds.Size / 2;
exporter.Rotation = view.Rotation;
}
if (_exportSingle)
{
var folderName = $"frames_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}";
var output = Path.Combine(_outputDir!, folderName);
if (_autoResolution) SetAutoResolutionAnimated(exporter, spines);
exporter.ProgressReporter = (total, done, text) =>
{
pr.Total = total;
pr.Done = done;
pr.ProgressText = text;
_vmMain.ProgressValue = pr.Done / pr.Total;
};
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
try
{
exporter.Export(output, ct, spines);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
}
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
}
else
{
// 统计总帧数
int totalFrameCount = 0;
if (_duration > 0)
{
exporter.Duration = _duration;
totalFrameCount = exporter.GetFrameCount() * spines.Length;
}
else
{
foreach (var sp in spines)
{
exporter.Duration = sp.GetAnimationMaxDuration();
totalFrameCount += exporter.GetFrameCount();
}
}
pr.Total = totalFrameCount;
pr.Done = 0;
exporter.ProgressReporter = (total, done, text) =>
{
pr.Done++;
pr.ProgressText = text;
_vmMain.ProgressValue = pr.Done / pr.Total;
};
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.Normal;
_vmMain.ProgressValue = 0;
foreach (var sp in spines)
{
if (ct.IsCancellationRequested)
{
_logger.Info("Export cancelled");
break;
}
if (_autoResolution) SetAutoResolutionAnimated(exporter, sp);
if (_duration <= 0) exporter.Duration = sp.GetAnimationMaxDuration();
var folderName = $"{sp.Name}_{timestamp}_{Guid.NewGuid().ToString()[..6]}_{_fps}";
var output = Path.Combine(_outputDir ?? sp.AssetsDir, folderName);
try
{
exporter.Export(output, ct, sp);
}
catch (Exception ex)
{
_logger.Trace(ex.ToString());
_logger.Error("Failed to export {0}, {1}", output, ex.Message);
}
}
_vmMain.ProgressState = System.Windows.Shell.TaskbarItemProgressState.None;
}
}
}
}

View File

@@ -0,0 +1,30 @@
using SpineViewer.Resources;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SpineViewer.ViewModels.Exporters
{
public abstract class VideoExporterViewModel(MainWindowViewModel vmMain) : BaseExporterViewModel(vmMain)
{
public float Duration { get => _duration; set => SetProperty(ref _duration, value); }
protected float _duration = -1;
public uint Fps { get => _fps; set => SetProperty(ref _fps, Math.Max(1, value)); }
protected uint _fps = 30;
public bool KeepLast { get => _keepLast; set => SetProperty(ref _keepLast, value); }
protected bool _keepLast = true;
public override string? Validate()
{
if (base.Validate() is string err)
return err;
if (_exportSingle && _duration <= 0)
return AppResource.Str_InvalidDuration;
return null;
}
}
}