diff --git a/src/Beutl.Extensibility/OutputExtension.cs b/src/Beutl.Extensibility/OutputExtension.cs index 8ef5d1636..16bc6c766 100644 --- a/src/Beutl.Extensibility/OutputExtension.cs +++ b/src/Beutl.Extensibility/OutputExtension.cs @@ -15,6 +15,8 @@ public interface IOutputContext : IDisposable, IJsonSerializable string TargetFile { get; } + IReactiveProperty Name { get; } + IReadOnlyReactiveProperty IsIndeterminate { get; } IReadOnlyReactiveProperty IsEncoding { get; } diff --git a/src/Beutl/Pages/AddOutputQueueDialog.axaml b/src/Beutl/Pages/AddOutputQueueDialog.axaml deleted file mode 100644 index 2e0404367..000000000 --- a/src/Beutl/Pages/AddOutputQueueDialog.axaml +++ /dev/null @@ -1,75 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Beutl/Pages/AddOutputQueueDialog.axaml.cs b/src/Beutl/Pages/AddOutputQueueDialog.axaml.cs deleted file mode 100644 index 562f59d72..000000000 --- a/src/Beutl/Pages/AddOutputQueueDialog.axaml.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.Platform.Storage; - -using Beutl.ViewModels.Dialogs; - -using FluentAvalonia.UI.Controls; - -namespace Beutl.Pages; - -public partial class AddOutputQueueDialog : ContentDialog -{ - private IDisposable? _sBtnBinding; - - public AddOutputQueueDialog() - { - InitializeComponent(); - } - - protected override Type StyleKeyOverride => typeof(ContentDialog); - - protected override void OnPrimaryButtonClick(ContentDialogButtonClickEventArgs args) - { - base.OnPrimaryButtonClick(args); - if (carousel.SelectedIndex == 1) - { - args.Cancel = true; - IsPrimaryButtonEnabled = false; - _sBtnBinding?.Dispose(); - SecondaryButtonText = Strings.Next; - IsSecondaryButtonEnabled = true; - carousel.Previous(); - } - } - - protected override void OnSecondaryButtonClick(ContentDialogButtonClickEventArgs args) - { - base.OnSecondaryButtonClick(args); - if (DataContext is not AddOutputQueueViewModel vm) return; - - if (carousel.SelectedIndex == 1) - { - vm.Add(); - } - else - { - args.Cancel = true; - - IsPrimaryButtonEnabled = true; - _sBtnBinding = Bind(IsSecondaryButtonEnabledProperty, vm.CanAdd); - SecondaryButtonText = Strings.Add; - carousel.Next(); - } - } - - // 場所を選択 - private async void OpenFileClick(object? sender, RoutedEventArgs e) - { - if (DataContext is AddOutputQueueViewModel vm && VisualRoot is TopLevel parent) - { - var options = new FilePickerOpenOptions() - { - FileTypeFilter = vm.GetFilePickerFileTypes() - }; - IReadOnlyList result = await parent.StorageProvider.OpenFilePickerAsync(options); - - if (result.Count > 0 - && result[0].TryGetLocalPath() is string localPath) - { - vm.SelectedFile.Value = localPath; - } - } - } -} diff --git a/src/Beutl/Pages/OutputDialog.axaml b/src/Beutl/Pages/OutputDialog.axaml deleted file mode 100644 index a49df9f7b..000000000 --- a/src/Beutl/Pages/OutputDialog.axaml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Beutl/Pages/OutputDialog.axaml.cs b/src/Beutl/Pages/OutputDialog.axaml.cs deleted file mode 100644 index ad642745b..000000000 --- a/src/Beutl/Pages/OutputDialog.axaml.cs +++ /dev/null @@ -1,108 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using Avalonia.Interactivity; -using Avalonia.Platform; -using Beutl.Services; -using Beutl.ViewModels; -using Beutl.ViewModels.Dialogs; -using FluentAvalonia.UI.Windowing; - -namespace Beutl.Pages; - -public partial class OutputDialog : AppWindow -{ - private readonly IDataTemplate _sharedDataTemplate = new _DataTemplate(); - - public OutputDialog() - { - InitializeComponent(); - if (OperatingSystem.IsWindows()) - { - TitleBar.ExtendsContentIntoTitleBar = true; - TitleBar.Height = 40; - } - else if (OperatingSystem.IsMacOS()) - { - Grid.Margin = new Thickness(8, 30, 0, 0); - ExtendClientAreaToDecorationsHint = true; - ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome; - } - - contentControl.ContentTemplate = _sharedDataTemplate; -#if DEBUG - this.AttachDevTools(); -#endif - } - - protected override void OnOpened(EventArgs e) - { - base.OnOpened(e); - if (DataContext is OutputPageViewModel viewModel) - { - viewModel.Restore(); - } - } - - protected override void OnClosed(EventArgs e) - { - base.OnClosed(e); - if (DataContext is OutputPageViewModel viewModel) - { - viewModel.Save(); - } - } - - private async void OnAddClick(object? sender, RoutedEventArgs e) - { - if (DataContext is OutputPageViewModel viewModel) - { - var dialogViewModel = new AddOutputQueueViewModel(); - var dialog = new AddOutputQueueDialog { DataContext = dialogViewModel }; - - await dialog.ShowAsync(); - dialogViewModel.Dispose(); - - viewModel.Save(); - } - } - - private void OnRemoveClick(object? sender, RoutedEventArgs e) - { - if (DataContext is OutputPageViewModel viewModel) - { - viewModel.RemoveSelected(); - viewModel.Save(); - } - } - - private sealed class _DataTemplate : IDataTemplate - { - private readonly Dictionary _contextToViewType = []; - - public Control? Build(object? param) - { - if (param is OutputQueueItem item) - { - if (_contextToViewType.TryGetValue(item.Context.Extension, out Control? control)) - { - control.DataContext = item.Context; - return control; - } - else if (item.Context.Extension.TryCreateControl(item.Context.TargetFile, out control)) - { - _contextToViewType[item.Context.Extension] = control; - control.DataContext = item.Context; - return control; - } - } - - return null; - } - - public bool Match(object? data) - { - return data is OutputQueueItem; - } - } -} diff --git a/src/Beutl/Services/EditorService.cs b/src/Beutl/Services/EditorService.cs index e546e0eb5..bcea3418e 100644 --- a/src/Beutl/Services/EditorService.cs +++ b/src/Beutl/Services/EditorService.cs @@ -113,7 +113,6 @@ public void ActivateTabItem(string? file) if (ext?.TryCreateContext(file, out IEditorContext? context) == true) { - context.IsEnabled.Value = !OutputService.Current.Items.Any(x => x.Context.TargetFile == file && x.Context.IsEncoding.Value); var tabItem2 = new EditorTabItem(context) { IsSelected = diff --git a/src/Beutl/Services/OutputService.cs b/src/Beutl/Services/OutputService.cs index d8291d639..89917edb8 100644 --- a/src/Beutl/Services/OutputService.cs +++ b/src/Beutl/Services/OutputService.cs @@ -1,21 +1,19 @@ using System.Text.Json; using System.Text.Json.Nodes; - using Beutl.Api.Services; using Beutl.Logging; - +using Beutl.Models; +using Beutl.ViewModels; using Microsoft.Extensions.Logging; - using Reactive.Bindings; namespace Beutl.Services; -public sealed class OutputQueueItem : IDisposable +public sealed class OutputProfileItem : IDisposable { - public OutputQueueItem(IOutputContext context) + public OutputProfileItem(IOutputContext context) { Context = context; - Name = Path.GetFileName(context.TargetFile); Context.Started += OnStarted; Context.Finished += OnFinished; @@ -23,8 +21,6 @@ public OutputQueueItem(IOutputContext context) public IOutputContext Context { get; } - public string Name { get; } - private void OnStarted(object? sender, EventArgs e) { if (EditorService.Current.TryGetTabItem(Context.TargetFile, out EditorTabItem? tabItem)) @@ -48,7 +44,7 @@ public void Dispose() Context.Dispose(); } - public static JsonNode ToJson(OutputQueueItem item) + public static JsonNode ToJson(OutputProfileItem item) { var ctxJson = new JsonObject(); item.Context.WriteToJson(ctxJson); @@ -60,7 +56,7 @@ public static JsonNode ToJson(OutputQueueItem item) }; } - public static OutputQueueItem? FromJson(JsonNode json, ILogger logger) + public static OutputProfileItem? FromJson(JsonNode json, ILogger logger) { try { @@ -70,7 +66,8 @@ public static JsonNode ToJson(OutputQueueItem item) string extensionStr = obj["Extension"]!.AsValue().GetValue(); Type? extensionType = TypeFormat.ToType(extensionStr); ExtensionProvider provider = ExtensionProvider.Current; - OutputExtension? extension = Array.Find(provider.GetExtensions(), x => x.GetType() == extensionType); + OutputExtension? extension = Array.Find(provider.GetExtensions(), + x => x.GetType() == extensionType); string file = obj["File"]!.AsValue().GetValue(); @@ -80,7 +77,7 @@ public static JsonNode ToJson(OutputQueueItem item) && extension.TryCreateContext(file, out IOutputContext? context)) { context.ReadFromJson(contextJson.AsObject()); - return new OutputQueueItem(context); + return new OutputProfileItem(context); } else { @@ -95,19 +92,20 @@ public static JsonNode ToJson(OutputQueueItem item) } } -public sealed class OutputService +public sealed class OutputService(EditViewModel editViewModel) { - private readonly CoreList _items = []; - private readonly ReactivePropertySlim _selectedItem = new(); - private readonly string _filePath = Path.Combine(BeutlEnvironment.GetHomeDirectoryPath(), "outputlist.json"); + private readonly CoreList _items = []; + private readonly ReactivePropertySlim _selectedItem = new(); + + private readonly string _filePath = Path.Combine( + Path.GetDirectoryName(editViewModel.Scene.FileName)!, Constants.BeutlFolder, "output-profile.json"); + private readonly ILogger _logger = Log.CreateLogger(); private bool _isRestored; - public static OutputService Current { get; } = new(); - - public ICoreList Items => _items; + public ICoreList Items => _items; - public IReactiveProperty SelectedItem => _selectedItem; + public IReactiveProperty SelectedItem => _selectedItem; public void AddItem(string file, OutputExtension extension) { @@ -115,17 +113,19 @@ public void AddItem(string file, OutputExtension extension) { throw new Exception("Already added"); } + if (!extension.TryCreateContext(file, out IOutputContext? context)) { throw new Exception("Failed to create context"); } - var item = new OutputQueueItem(context); + context.Name.Value = Items.Count == 0 ? "Default" : $"Profile {Items.Count}"; + var item = new OutputProfileItem(context); Items.Add(item); SelectedItem.Value = item; } - public OutputExtension[] GetExtensions(string file) + public static OutputExtension[] GetExtensions(string file) { return ExtensionProvider.Current .GetExtensions() @@ -134,44 +134,40 @@ public OutputExtension[] GetExtensions(string file) public void SaveItems() { - if (_isRestored) - { - var array = new JsonArray(); - foreach (OutputQueueItem item in _items.GetMarshal().Value) - { - JsonNode json = OutputQueueItem.ToJson(item); - array.Add(json); - } + if (!_isRestored) return; - using FileStream stream = File.Create(_filePath); - using var writer = new Utf8JsonWriter(stream); - array.WriteTo(writer); + var array = new JsonArray(); + foreach (OutputProfileItem item in _items.GetMarshal().Value) + { + JsonNode json = OutputProfileItem.ToJson(item); + array.Add(json); } + + using FileStream stream = File.Create(_filePath); + using var writer = new Utf8JsonWriter(stream); + array.WriteTo(writer); } public void RestoreItems() { _isRestored = true; - if (File.Exists(_filePath)) + if (!File.Exists(_filePath)) return; + + using FileStream stream = File.Open(_filePath, FileMode.Open); + var jsonNode = JsonNode.Parse(stream); + if (jsonNode is not JsonArray jsonArray) return; + + _items.Clear(); + _items.EnsureCapacity(jsonArray.Count); + + foreach (JsonNode? jsonItem in jsonArray) { - using FileStream stream = File.Open(_filePath, FileMode.Open); - var jsonNode = JsonNode.Parse(stream); - if (jsonNode is JsonArray jsonArray) + if (jsonItem == null) continue; + + var item = OutputProfileItem.FromJson(jsonItem, _logger); + if (item != null) { - _items.Clear(); - _items.EnsureCapacity(jsonArray.Count); - - foreach (JsonNode? jsonItem in jsonArray) - { - if (jsonItem != null) - { - var item = OutputQueueItem.FromJson(jsonItem, _logger); - if (item != null) - { - _items.Add(item); - } - } - } + _items.Add(item); } } } diff --git a/src/Beutl/Services/PrimitiveImpls/OutputPageExtension.cs b/src/Beutl/Services/PrimitiveImpls/OutputPageExtension.cs deleted file mode 100644 index f1a92699b..000000000 --- a/src/Beutl/Services/PrimitiveImpls/OutputPageExtension.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Avalonia.Controls; -using Beutl.Pages; -using Beutl.ViewModels; -using FluentAvalonia.UI.Controls; -using Symbol = FluentIcons.Common.Symbol; -using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; - -namespace Beutl.Services.PrimitiveImpls; - -[PrimitiveImpl] -public sealed class OutputPageExtension : PageExtension -{ - public static readonly OutputPageExtension Instance = new(); - - public override string Name => "OutputPage"; - - public override string DisplayName => Strings.Output; - - public override IPageContext CreateContext() - { - return new OutputPageViewModel(); - } - - public override Control CreateControl() - { - return new OutputDialog(); - } - - [Obsolete] - public override IconSource GetFilledIcon() - { - return new SymbolIconSource() { Symbol = Symbol.ArrowExportLtr, IsFilled = true }; - } - - public override IconSource GetRegularIcon() - { - return new SymbolIconSource() { Symbol = Symbol.ArrowExportLtr }; - } -} diff --git a/src/Beutl/Services/PrimitiveImpls/OutputTabExtension.cs b/src/Beutl/Services/PrimitiveImpls/OutputTabExtension.cs new file mode 100644 index 000000000..23fa3416b --- /dev/null +++ b/src/Beutl/Services/PrimitiveImpls/OutputTabExtension.cs @@ -0,0 +1,57 @@ +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; +using Beutl.ViewModels; +using Beutl.ViewModels.Tools; +using Beutl.Views.Tools; +using FluentAvalonia.UI.Controls; +using Symbol = FluentIcons.Common.Symbol; +using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; + +namespace Beutl.Services.PrimitiveImpls; + +[PrimitiveImpl] +public sealed class OutputTabExtension : ToolTabExtension +{ + public static readonly OutputTabExtension Instance = new(); + + public override string Name => "Output"; + + public override string DisplayName => Strings.Output; + + public override string? Header => Strings.Output; + + public override bool CanMultiple => false; + + public override IconSource GetIcon() + { + return new SymbolIconSource { Symbol = Symbol.ArrowExportLtr }; + } + + public override bool TryCreateContent(IEditorContext editorContext, [NotNullWhen(true)] out Control? control) + { + if (editorContext is EditViewModel) + { + control = new OutputTab(); + return true; + } + else + { + control = null; + return false; + } + } + + public override bool TryCreateContext(IEditorContext editorContext, [NotNullWhen(true)] out IToolContext? context) + { + if (editorContext is EditViewModel editViewModel) + { + context = new OutputTabViewModel(editViewModel); + return true; + } + else + { + context = null; + return false; + } + } +} diff --git a/src/Beutl/Services/PrimitiveImpls/SceneOutputExtension.cs b/src/Beutl/Services/PrimitiveImpls/SceneOutputExtension.cs index d021afc3c..f4b6ca6ce 100644 --- a/src/Beutl/Services/PrimitiveImpls/SceneOutputExtension.cs +++ b/src/Beutl/Services/PrimitiveImpls/SceneOutputExtension.cs @@ -4,9 +4,8 @@ using Avalonia.Platform.Storage; using Beutl.ProjectSystem; -using Beutl.ViewModels; -using Beutl.Views; - +using Beutl.ViewModels.Tools; +using Beutl.Views.Tools; using FluentAvalonia.UI.Controls; namespace Beutl.Services.PrimitiveImpls; diff --git a/src/Beutl/Services/StartupTasks/LoadPrimitiveExtensionTask.cs b/src/Beutl/Services/StartupTasks/LoadPrimitiveExtensionTask.cs index 2187a42e0..0e5e6c343 100644 --- a/src/Beutl/Services/StartupTasks/LoadPrimitiveExtensionTask.cs +++ b/src/Beutl/Services/StartupTasks/LoadPrimitiveExtensionTask.cs @@ -10,7 +10,7 @@ public sealed class LoadPrimitiveExtensionTask : StartupTask public static readonly Extension[] PrimitiveExtensions = [ ExtensionsPageExtension.Instance, - OutputPageExtension.Instance, + OutputTabExtension.Instance, SceneEditorExtension.Instance, SceneOutputExtension.Instance, SceneProjectItemExtension.Instance, diff --git a/src/Beutl/ViewModels/Dialogs/AddOutputProfileViewModel.cs b/src/Beutl/ViewModels/Dialogs/AddOutputProfileViewModel.cs new file mode 100644 index 000000000..2d85dbb7a --- /dev/null +++ b/src/Beutl/ViewModels/Dialogs/AddOutputProfileViewModel.cs @@ -0,0 +1,33 @@ +using Beutl.Services; +using Beutl.ViewModels.Tools; +using Reactive.Bindings; + +namespace Beutl.ViewModels.Dialogs; + +public sealed class AddOutputProfileViewModel +{ + private readonly OutputTabViewModel _outputTabViewModel; + + public AddOutputProfileViewModel(OutputTabViewModel outputTabViewModel) + { + _outputTabViewModel = outputTabViewModel; + AvailableExtensions = OutputService.GetExtensions(outputTabViewModel.EditViewModel.Scene.FileName); + + CanAdd = SelectedExtension.Select(x => x != null) + .ToReadOnlyReactivePropertySlim(); + } + + public ReadOnlyReactivePropertySlim CanAdd { get; } + + public ReactiveProperty SelectedExtension { get; } = new(); + + public OutputExtension[] AvailableExtensions { get; } + + public void Add() + { + if (SelectedExtension.Value != null) + { + _outputTabViewModel.AddItem(SelectedExtension.Value); + } + } +} diff --git a/src/Beutl/ViewModels/Dialogs/AddOutputQueueViewModel.cs b/src/Beutl/ViewModels/Dialogs/AddOutputQueueViewModel.cs deleted file mode 100644 index 20d1aea0a..000000000 --- a/src/Beutl/ViewModels/Dialogs/AddOutputQueueViewModel.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Collections.ObjectModel; - -using Avalonia.Platform.Storage; - -using Beutl.Api.Services; -using Beutl.Services; - -using DynamicData; -using DynamicData.Binding; - -using Reactive.Bindings; - -namespace Beutl.ViewModels.Dialogs; - -public sealed class AddOutputQueueViewModel : IDisposable -{ - private readonly OutputExtension[] _extensions; - private readonly ReadOnlyObservableCollection _suggestion; - private readonly IDisposable _disposable1; - private readonly IDisposable _disposable2; - private readonly ReadOnlyObservableCollection _availableExtensions; - private FilePickerFileType[]? _cachedFileTypes; - - public AddOutputQueueViewModel() - { - _extensions = ExtensionProvider.Current.GetExtensions(); - - _disposable1 = EditorService.Current.TabItems - .ToObservableChangeSet, EditorTabItem>() - .Filter(x => !OutputService.Current.Items.Any(y => y.Context.TargetFile == x.FilePath.Value)) - .Transform(x => x.FilePath.Value) - .Bind(out _suggestion) - .Subscribe(); - - _disposable2 = _extensions.AsObservableChangeSet() - .Filter(SelectedFile.Select>( - f => f == null - ? _ => false - : ext => ext.IsSupported(f))) - .Bind(out _availableExtensions) - .Subscribe(); - - CanAdd = SelectedFile.Select(File.Exists) - .CombineLatest(SelectedExtension.Select(x => x != null)) - .Select(x => x.First && x.Second) - .ToReadOnlyReactivePropertySlim(); - } - - public ReactiveProperty SelectedFile { get; } = new(); - - public ReadOnlyObservableCollection Suggestion => _suggestion; - - public ReadOnlyReactivePropertySlim CanAdd { get; } - - public ReactiveProperty SelectedExtension { get; } = new(); - - public ReadOnlyObservableCollection AvailableExtensions => _availableExtensions; - - public void Add() - { - if (SelectedFile.Value != null - && SelectedExtension.Value != null) - { - OutputService.Current.AddItem(SelectedFile.Value, SelectedExtension.Value); - } - } - - public FilePickerFileType[] GetFilePickerFileTypes() - { - return _cachedFileTypes ??= _extensions - .Select(x => x.GetFilePickerFileType()) - .Where(x => x != null) - .ToArray(); - } - - public void Dispose() - { - _disposable1.Dispose(); - _disposable2.Dispose(); - } -} diff --git a/src/Beutl/ViewModels/DockHostViewModel.cs b/src/Beutl/ViewModels/DockHostViewModel.cs index 135663e54..81b837ac9 100644 --- a/src/Beutl/ViewModels/DockHostViewModel.cs +++ b/src/Beutl/ViewModels/DockHostViewModel.cs @@ -374,7 +374,10 @@ public void OpenDefaultTabs() { var tabs = new ToolTabExtension[] { - TimelineTabExtension.Instance, SourceOperatorsTabExtension.Instance, LibraryTabExtension.Instance + TimelineTabExtension.Instance, + OutputTabExtension.Instance, + SourceOperatorsTabExtension.Instance, + LibraryTabExtension.Instance, }; foreach (var ext in tabs) { diff --git a/src/Beutl/ViewModels/EncoderSettingsViewModel.cs b/src/Beutl/ViewModels/EncoderSettingsViewModel.cs index f64aff1f7..bfd3cc77c 100644 --- a/src/Beutl/ViewModels/EncoderSettingsViewModel.cs +++ b/src/Beutl/ViewModels/EncoderSettingsViewModel.cs @@ -59,7 +59,7 @@ private void InitializeCoreObject(MediaEncoderSettings obj, (foundItems, extension) = PropertyEditorService.MatchProperty(props); if (foundItems != null && extension != null) { - if (extension.TryCreateContextForSettings(foundItems, out IPropertyEditorContext? context)) + if (extension.TryCreateContext(foundItems, out IPropertyEditorContext? context)) { tempItems.Add(context); context.Accept(this); diff --git a/src/Beutl/ViewModels/OutputPageViewModel.cs b/src/Beutl/ViewModels/OutputPageViewModel.cs deleted file mode 100644 index 0f1bc56a6..000000000 --- a/src/Beutl/ViewModels/OutputPageViewModel.cs +++ /dev/null @@ -1,75 +0,0 @@ -using Beutl.Logging; -using Beutl.Services; -using Beutl.Services.PrimitiveImpls; -using Beutl.ViewModels.ExtensionsPages; - -using Microsoft.Extensions.Logging; - -using Reactive.Bindings; - -namespace Beutl.ViewModels; - -public sealed class OutputPageViewModel : BasePageViewModel, IPageContext -{ - private readonly OutputService _outputService; - private readonly ILogger _logger = Log.CreateLogger(); - - public OutputPageViewModel() - { - _outputService = OutputService.Current; - CanRemove = SelectedItem - .SelectMany(x => x?.Context?.IsEncoding?.Not() ?? Observable.Return(false)) - .ToReadOnlyReactivePropertySlim(); - } - - public PageExtension Extension => OutputPageExtension.Instance; - - public string Header => Strings.Output; - - public ICoreList Items => _outputService.Items; - - public IReactiveProperty SelectedItem => _outputService.SelectedItem; - - public ReadOnlyReactivePropertySlim CanRemove { get; } - - public void AddItem(string file, OutputExtension extension) - { - try - { - _outputService.AddItem(file, extension); - } - catch (Exception e) - { - _logger.LogError(e, "An exception has occurred."); - - ErrorHandle(e); - } - } - - public void RemoveSelected() - { - if (SelectedItem.Value != null) - { - Items.Remove(SelectedItem.Value); - } - } - - public OutputExtension[] GetExtensions(string file) - { - return _outputService.GetExtensions(file); - } - - public void Save() - { - _outputService.SaveItems(); - } - - public void Restore() - { - _outputService.RestoreItems(); - } - - public override void Dispose() - { - } -} diff --git a/src/Beutl/ViewModels/Tools/OutputTabViewModel.cs b/src/Beutl/ViewModels/Tools/OutputTabViewModel.cs new file mode 100644 index 000000000..9201ce998 --- /dev/null +++ b/src/Beutl/ViewModels/Tools/OutputTabViewModel.cs @@ -0,0 +1,103 @@ +using System.Text.Json.Nodes; +using Beutl.Logging; +using Beutl.Services; +using Beutl.Services.PrimitiveImpls; +using Microsoft.Extensions.Logging; +using Reactive.Bindings; + +namespace Beutl.ViewModels.Tools; + +public class OutputTabViewModel : IToolContext +{ + private readonly OutputService _outputService; + private readonly ILogger _logger = Log.CreateLogger(); + + public OutputTabViewModel(EditViewModel editViewModel) + { + EditViewModel = editViewModel; + _outputService = new OutputService(editViewModel); + CanRemove = SelectedItem + .SelectMany(x => x?.Context?.IsEncoding?.Not() ?? Observable.Return(false)) + .ToReadOnlyReactivePropertySlim(); + CreateDefaultProfile(); + SelectedItem.Value = Items.FirstOrDefault(); + } + + public EditViewModel EditViewModel { get; } + + public ToolTabExtension Extension => OutputTabExtension.Instance; + + public IReactiveProperty IsSelected { get; } = new ReactiveProperty(); + + public IReactiveProperty Placement { get; } + = new ReactiveProperty(ToolTabExtension.TabPlacement.RightUpperBottom); + + public IReactiveProperty DisplayMode { get; } + = new ReactiveProperty(); + + public string Header => Strings.Output; + + public ICoreList Items => _outputService.Items; + + public IReactiveProperty SelectedItem => _outputService.SelectedItem; + + public ReadOnlyReactivePropertySlim CanRemove { get; } + + public void AddItem(OutputExtension extension) + { + try + { + _outputService.AddItem(EditViewModel.Scene.FileName, extension); + } + catch (Exception e) + { + _logger.LogError(e, "An exception has occurred."); + e.Handle(); + } + } + + public void RemoveSelected() + { + if (SelectedItem.Value != null) + { + Items.Remove(SelectedItem.Value); + } + } + + public void Save() + { + _outputService.SaveItems(); + } + + public void Dispose() + { + } + + public void WriteToJson(JsonObject json) + { + _outputService.SaveItems(); + } + + public void ReadFromJson(JsonObject json) + { + _outputService.RestoreItems(); + CreateDefaultProfile(); + SelectedItem.Value = Items.FirstOrDefault(); + } + + private void CreateDefaultProfile() + { + if (Items.Count != 0) return; + + var ext = OutputService.GetExtensions(EditViewModel.Scene.FileName); + if (ext.Length == 1) + { + AddItem(ext[0]); + } + } + + public object? GetService(Type serviceType) + { + return null; + } +} diff --git a/src/Beutl/ViewModels/OutputViewModel.cs b/src/Beutl/ViewModels/Tools/OutputViewModel.cs similarity index 91% rename from src/Beutl/ViewModels/OutputViewModel.cs rename to src/Beutl/ViewModels/Tools/OutputViewModel.cs index 1c27f399e..60732128b 100644 --- a/src/Beutl/ViewModels/OutputViewModel.cs +++ b/src/Beutl/ViewModels/Tools/OutputViewModel.cs @@ -18,7 +18,7 @@ using Reactive.Bindings; using Reactive.Bindings.Extensions; -namespace Beutl.ViewModels; +namespace Beutl.ViewModels.Tools; public sealed class OutputViewModel : IOutputContext { @@ -81,14 +81,14 @@ public OutputViewModel(SceneFile model) public string TargetFile => Model.FileName; + public IReactiveProperty Name { get; } = new ReactiveProperty(""); + public ReactivePropertySlim DestinationFile { get; } = new(); public ReactivePropertySlim SelectedEncoder { get; } = new(); public ReadOnlyObservableCollection Encoders => _encoders; - public ReactivePropertySlim IsEncodersExpanded { get; } = new(); - public ReadOnlyReactivePropertySlim CanEncode { get; } public ReadOnlyReactivePropertySlim Controller { get; } @@ -97,8 +97,6 @@ public OutputViewModel(SceneFile model) public ReadOnlyReactivePropertySlim AudioSettings { get; } - public ReactivePropertySlim ScrollOffset { get; } = new(); - public ReactiveProperty ProgressMax { get; } = new(); public ReactiveProperty ProgressValue { get; } = new(); @@ -147,7 +145,7 @@ static string[] ToPatterns(ControllableEncodingExtension encoder) .ToArray(); } - public async void StartEncode() + public async Task StartEncode() { try { @@ -250,14 +248,13 @@ public void WriteToJson(JsonObject json) } } + json[nameof(Name)] = Name.Value; json[nameof(DestinationFile)] = DestinationFile.Value; if (SelectedEncoder.Value != null) { json[nameof(SelectedEncoder)] = TypeFormat.ToString(SelectedEncoder.Value.GetType()); } - json[nameof(IsEncodersExpanded)] = IsEncodersExpanded.Value; - json[nameof(ScrollOffset)] = ScrollOffset.Value.ToString(); json[nameof(VideoSettings)] = Serialize(VideoSettings.Value?.Settings); json[nameof(AudioSettings)] = Serialize(AudioSettings.Value?.Settings); } @@ -284,6 +281,13 @@ void Deserialize(MediaEncoderSettings? settings, JsonObject json) DestinationFile.Value = dstFile; } + if (json.TryGetPropertyValue(nameof(Name), out JsonNode? nameNode) + && nameNode is JsonValue nameValue + && nameValue.TryGetValue(out string? name)) + { + Name.Value = name; + } + if (json.TryGetPropertyValue(nameof(SelectedEncoder), out JsonNode? encoderNode) && encoderNode is JsonValue encoderValue && encoderValue.TryGetValue(out string? encoderStr) @@ -294,21 +298,6 @@ void Deserialize(MediaEncoderSettings? settings, JsonObject json) SelectedEncoder.Value = encoder; } - if (json.TryGetPropertyValue(nameof(IsEncodersExpanded), out JsonNode? isExpandedNode) - && isExpandedNode is JsonValue isExpandedValue - && isExpandedValue.TryGetValue(out bool isExpanded)) - { - IsEncodersExpanded.Value = isExpanded; - } - - if (json.TryGetPropertyValue(nameof(ScrollOffset), out JsonNode? scrollOfstNode) - && scrollOfstNode is JsonValue scrollOfstValue - && scrollOfstValue.TryGetValue(out string? scrollOfstStr) - && Graphics.Vector.TryParse(scrollOfstStr, out Graphics.Vector vec)) - { - ScrollOffset.Value = new Avalonia.Vector(vec.X, vec.Y); - } - // 上のSelectedEncoder.Value = encoder;でnull以外が指定された場合、VideoSettings, AudioSettingsもnullじゃなくなる。 if (json.TryGetPropertyValue(nameof(VideoSettings), out JsonNode? videoNode) && videoNode is JsonObject videoObj) diff --git a/src/Beutl/Views/Dialogs/AddOutputProfileDialog.axaml b/src/Beutl/Views/Dialogs/AddOutputProfileDialog.axaml new file mode 100644 index 000000000..cf0d7b056 --- /dev/null +++ b/src/Beutl/Views/Dialogs/AddOutputProfileDialog.axaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Beutl/Views/Dialogs/AddOutputProfileDialog.axaml.cs b/src/Beutl/Views/Dialogs/AddOutputProfileDialog.axaml.cs new file mode 100644 index 000000000..3a2b668b8 --- /dev/null +++ b/src/Beutl/Views/Dialogs/AddOutputProfileDialog.axaml.cs @@ -0,0 +1,21 @@ +using Beutl.ViewModels.Dialogs; +using FluentAvalonia.UI.Controls; + +namespace Beutl.Views.Dialogs; + +public partial class AddOutputProfileDialog : ContentDialog +{ + public AddOutputProfileDialog() + { + InitializeComponent(); + } + + protected override Type StyleKeyOverride => typeof(ContentDialog); + + protected override void OnPrimaryButtonClick(ContentDialogButtonClickEventArgs args) + { + base.OnPrimaryButtonClick(args); + if (DataContext is not AddOutputProfileViewModel vm) return; + vm.Add(); + } +} diff --git a/src/Beutl/Views/Dialogs/OutputProgressDialog.axaml b/src/Beutl/Views/Dialogs/OutputProgressDialog.axaml new file mode 100644 index 000000000..b3fa94f76 --- /dev/null +++ b/src/Beutl/Views/Dialogs/OutputProgressDialog.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/src/Beutl/Views/Dialogs/OutputProgressDialog.axaml.cs b/src/Beutl/Views/Dialogs/OutputProgressDialog.axaml.cs new file mode 100644 index 000000000..bd61b4a45 --- /dev/null +++ b/src/Beutl/Views/Dialogs/OutputProgressDialog.axaml.cs @@ -0,0 +1,22 @@ +using Beutl.ViewModels; +using Beutl.ViewModels.Tools; +using FluentAvalonia.UI.Controls; + +namespace Beutl.Views.Dialogs; + +public partial class OutputProgressDialog : ContentDialog +{ + public OutputProgressDialog() + { + InitializeComponent(); + } + + protected override Type StyleKeyOverride => typeof(ContentDialog); + + protected override void OnCloseButtonClick(ContentDialogButtonClickEventArgs args) + { + base.OnCloseButtonClick(args); + if (DataContext is not OutputViewModel vm) return; + vm.CancelEncode(); + } +} diff --git a/src/Beutl/Views/OutputView.axaml b/src/Beutl/Views/OutputView.axaml deleted file mode 100644 index 3aef0c23f..000000000 --- a/src/Beutl/Views/OutputView.axaml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - + + + + + + + diff --git a/src/Beutl/Views/Tools/OutputTab.axaml.cs b/src/Beutl/Views/Tools/OutputTab.axaml.cs new file mode 100644 index 000000000..b45097857 --- /dev/null +++ b/src/Beutl/Views/Tools/OutputTab.axaml.cs @@ -0,0 +1,102 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Interactivity; +using Beutl.Services; +using Beutl.ViewModels.Dialogs; +using Beutl.ViewModels.Tools; +using Beutl.Views.NodeTree; +using AddOutputProfileDialog = Beutl.Views.Dialogs.AddOutputProfileDialog; + +namespace Beutl.Views.Tools; + +public partial class OutputTab : UserControl +{ + private readonly IDataTemplate _sharedDataTemplate = new _DataTemplate(); + + public OutputTab() + { + InitializeComponent(); + contentControl.ContentTemplate = _sharedDataTemplate; + } + + private async void OnAddClick(object? sender, RoutedEventArgs e) + { + if (DataContext is not OutputTabViewModel viewModel) return; + + var ext = OutputService.GetExtensions(viewModel.EditViewModel.Scene.FileName); + if (ext.Length == 1) + { + viewModel.AddItem(ext[0]); + } + else + { + var dialogViewModel = new AddOutputProfileViewModel(viewModel); + var dialog = new AddOutputProfileDialog { DataContext = dialogViewModel }; + + await dialog.ShowAsync(); + } + + viewModel.Save(); + } + + private void OnRemoveClick(object? sender, RoutedEventArgs e) + { + if (DataContext is not OutputTabViewModel viewModel) return; + + viewModel.RemoveSelected(); + viewModel.Save(); + } + + private void OnRenameClick(object? sender, RoutedEventArgs e) + { + if (DataContext is not OutputTabViewModel viewModel) return; + + var flyout = new RenameFlyout + { + Text = viewModel.SelectedItem.Value?.Context.Name.Value ?? string.Empty + }; + + flyout.Confirmed += OnNameConfirmed; + + flyout.ShowAt(MoreButton); + } + + private void OnNameConfirmed(object? sender, string? e) + { + if (DataContext is not OutputTabViewModel viewModel) return; + if (viewModel.SelectedItem.Value == null) return; + + viewModel.SelectedItem.Value.Context.Name.Value = e ?? ""; + viewModel.Save(); + } + + private sealed class _DataTemplate : IDataTemplate + { + private readonly Dictionary _contextToViewType = []; + + public Control? Build(object? param) + { + if (param is OutputProfileItem item) + { + if (_contextToViewType.TryGetValue(item.Context.Extension, out Control? control)) + { + control.DataContext = item.Context; + return control; + } + else if (item.Context.Extension.TryCreateControl(item.Context.TargetFile, out control)) + { + _contextToViewType[item.Context.Extension] = control; + control.DataContext = item.Context; + return control; + } + } + + return null; + } + + public bool Match(object? data) + { + return data is OutputProfileItem; + } + } +} diff --git a/src/Beutl/Views/Tools/OutputView.axaml b/src/Beutl/Views/Tools/OutputView.axaml new file mode 100644 index 000000000..321f5a47a --- /dev/null +++ b/src/Beutl/Views/Tools/OutputView.axaml @@ -0,0 +1,105 @@ + + + + + + + +