Compare commits
10 Commits
67db535abf
...
75bcdff3b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75bcdff3b1 | ||
|
|
9bf4588c54 | ||
|
|
23f9ab3399 | ||
|
|
f1cf86ce92 | ||
|
|
a628ed0a4d | ||
|
|
fe7fe3db00 | ||
|
|
7f3c2ca999 | ||
|
|
2e6c26178f | ||
|
|
6cf86d41f5 | ||
|
|
36bbc863f8 |
@ -8,9 +8,9 @@ public partial class App : Application
|
||||
{
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
_ = ToastShortcutRegistration.TryEnsureStartMenuShortcut(NotificationService.ToastAppUserModelId);
|
||||
_ = AppUserModelIdRegistration.TryRegister(NotificationService.ToastAppUserModelId);
|
||||
base.OnStartup(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
155
EmbyToolbox/Interop/ToastShortcutRegistration.cs
Normal file
155
EmbyToolbox/Interop/ToastShortcutRegistration.cs
Normal file
@ -0,0 +1,155 @@
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.InteropServices.ComTypes;
|
||||
|
||||
namespace EmbyToolbox.Interop;
|
||||
|
||||
internal static class ToastShortcutRegistration
|
||||
{
|
||||
private const int StgmReadwrite = 0x00000002;
|
||||
|
||||
private static readonly PropertyKey AppUserModelIdKey = new(
|
||||
new Guid("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3"),
|
||||
5);
|
||||
|
||||
public static string? LastDiagnostics { get; private set; }
|
||||
|
||||
public static bool TryEnsureStartMenuShortcut(string appUserModelId)
|
||||
{
|
||||
LastDiagnostics = null;
|
||||
|
||||
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 10240))
|
||||
{
|
||||
LastDiagnostics = "требуется Windows 10 (10240) или новее.";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var exePath = Environment.ProcessPath;
|
||||
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath))
|
||||
{
|
||||
LastDiagnostics = "не удалось определить путь к exe.";
|
||||
return false;
|
||||
}
|
||||
|
||||
var programs = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
|
||||
var shortcutPath = Path.Combine(programs, "Programs", "Emby Toolbox.lnk");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(shortcutPath)!);
|
||||
|
||||
var shellLinkObject = (object)new CShellLink();
|
||||
var shellLink = (IShellLinkW)shellLinkObject;
|
||||
shellLink.SetPath(exePath);
|
||||
shellLink.SetArguments(string.Empty);
|
||||
shellLink.SetWorkingDirectory(Path.GetDirectoryName(exePath) ?? AppContext.BaseDirectory);
|
||||
shellLink.SetDescription("Emby Toolbox");
|
||||
|
||||
if (File.Exists(Path.Combine(AppContext.BaseDirectory, "Resources", "AppIcon.ico")))
|
||||
{
|
||||
shellLink.SetIconLocation(Path.Combine(AppContext.BaseDirectory, "Resources", "AppIcon.ico"), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
shellLink.SetIconLocation(exePath, 0);
|
||||
}
|
||||
|
||||
using var appId = PropVariant.FromString(appUserModelId);
|
||||
var propertyStore = (IPropertyStore)shellLink;
|
||||
propertyStore.SetValue(AppUserModelIdKey, appId);
|
||||
propertyStore.Commit();
|
||||
|
||||
var persistFile = (IPersistFile)shellLink;
|
||||
persistFile.Save(shortcutPath, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastDiagnostics = $"{ex.GetType().Name}: {ex.Message}";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[Guid("00021401-0000-0000-C000-000000000046")]
|
||||
private sealed class CShellLink
|
||||
{
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[Guid("000214F9-0000-0000-C000-000000000046")]
|
||||
private interface IShellLinkW
|
||||
{
|
||||
void GetPath(IntPtr pszFile, int cchMaxPath, IntPtr pfd, uint fFlags);
|
||||
void GetIDList(out IntPtr ppidl);
|
||||
void SetIDList(IntPtr pidl);
|
||||
void GetDescription(IntPtr pszName, int cchMaxName);
|
||||
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
||||
void GetWorkingDirectory(IntPtr pszDir, int cchMaxPath);
|
||||
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
|
||||
void GetArguments(IntPtr pszArgs, int cchMaxPath);
|
||||
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
|
||||
void GetHotkey(out short pwHotkey);
|
||||
void SetHotkey(short wHotkey);
|
||||
void GetShowCmd(out int piShowCmd);
|
||||
void SetShowCmd(int iShowCmd);
|
||||
void GetIconLocation(IntPtr pszIconPath, int cchIconPath, out int piIcon);
|
||||
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
|
||||
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
|
||||
void Resolve(IntPtr hwnd, uint fFlags);
|
||||
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
|
||||
}
|
||||
|
||||
[ComImport]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
[Guid("00000138-0000-0000-C000-000000000046")]
|
||||
private interface IPropertyStore
|
||||
{
|
||||
void GetCount(out uint cProps);
|
||||
void GetAt(uint iProp, out PropertyKey pkey);
|
||||
void GetValue(ref PropertyKey key, out PropVariant pv);
|
||||
void SetValue(in PropertyKey key, in PropVariant pv);
|
||||
void Commit();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
private readonly struct PropertyKey(Guid formatId, int propertyId)
|
||||
{
|
||||
private readonly Guid _formatId = formatId;
|
||||
private readonly int _propertyId = propertyId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private sealed class PropVariant : IDisposable
|
||||
{
|
||||
private ushort _valueType;
|
||||
private ushort _reserved1;
|
||||
private ushort _reserved2;
|
||||
private ushort _reserved3;
|
||||
private IntPtr _value;
|
||||
private IntPtr _reserved4;
|
||||
|
||||
public static PropVariant FromString(string value)
|
||||
{
|
||||
return new PropVariant
|
||||
{
|
||||
_valueType = 31, // VT_LPWSTR
|
||||
_value = Marshal.StringToCoTaskMemUni(value)
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
PropVariantClear(this);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~PropVariant()
|
||||
{
|
||||
PropVariantClear(this);
|
||||
}
|
||||
|
||||
[DllImport("ole32.dll")]
|
||||
private static extern int PropVariantClear([In, Out] PropVariant pvar);
|
||||
}
|
||||
}
|
||||
@ -368,7 +368,6 @@
|
||||
<TabItem Header="Подробная информация">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
@ -412,37 +411,6 @@
|
||||
</Grid>
|
||||
|
||||
<Border Grid.Row="1"
|
||||
BorderBrush="{DynamicResource Ui.Brush.BorderSubtle}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="0,0,0,8"
|
||||
Margin="0,0,0,8">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||
<Button Style="{StaticResource UiButtonSecondary}" Margin="0,0,8,0" MinWidth="126" Command="{Binding ExpandAllCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Развернуть всё" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource UiButtonSecondary}" Margin="0,0,8,0" MinWidth="126" Command="{Binding CollapseAllCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="Свернуть всё" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource UiButtonSecondary}" Margin="0,0,8,0" MinWidth="132" Command="{Binding CopyJsonCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Копировать JSON" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource UiButtonSecondary}" MinWidth="130" Command="{Binding SaveJsonCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Сохранить JSON" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="2"
|
||||
Padding="8,4,8,8"
|
||||
Background="{DynamicResource Ui.Brush.Surface}"
|
||||
behaviors:VideoInfoDropTargetBehavior.IsEnabled="True">
|
||||
@ -463,43 +431,6 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.Resources>
|
||||
<Style x:Key="VideoInfoTreeItemStyle" TargetType="TreeViewItem">
|
||||
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
|
||||
<Setter Property="Padding" Value="2,1" />
|
||||
<Setter Property="Margin" Value="0,0,0,1" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ui.Brush.Text}" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<EventSetter Event="PreviewMouseRightButtonDown" Handler="OnJsonTreeItemPreviewMouseRightButtonDown" />
|
||||
<Setter Property="ContextMenu">
|
||||
<Setter.Value>
|
||||
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
|
||||
<MenuItem Header="Копировать значение"
|
||||
Command="{Binding DataContext.VideoInfo.CopyNodeValueCommand, Source={x:Reference RootWindow}}"
|
||||
CommandParameter="{Binding}" />
|
||||
<MenuItem Header="Копировать узел"
|
||||
Command="{Binding DataContext.VideoInfo.CopyNodeLineCommand, Source={x:Reference RootWindow}}"
|
||||
CommandParameter="{Binding}" />
|
||||
<MenuItem Header="Копировать узел с дочерними элементами"
|
||||
Command="{Binding DataContext.VideoInfo.CopyNodeWithChildrenCommand, Source={x:Reference RootWindow}}"
|
||||
CommandParameter="{Binding}" />
|
||||
<Separator />
|
||||
<MenuItem Header="Копировать путь к узлу"
|
||||
Command="{Binding DataContext.VideoInfo.CopyNodePathCommand, Source={x:Reference RootWindow}}"
|
||||
CommandParameter="{Binding}" />
|
||||
</ContextMenu>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Ui.Brush.DataRowSelected}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource Ui.Brush.Text}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource Ui.Brush.Text}" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Resources>
|
||||
|
||||
<Grid Grid.RowSpan="2">
|
||||
<Grid.Style>
|
||||
@ -541,9 +472,21 @@
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
|
||||
<TreeView Grid.Row="1" ItemsSource="{Binding JsonNodes}" Margin="0" BorderThickness="0" Background="Transparent">
|
||||
<TreeView.Style>
|
||||
<Style TargetType="TreeView">
|
||||
<ListBox Grid.Row="1"
|
||||
ItemsSource="{Binding FormattedJsonLines}"
|
||||
Margin="0"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
Foreground="{DynamicResource Ui.Brush.Text}"
|
||||
ScrollViewer.CanContentScroll="True"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
VirtualizingPanel.IsVirtualizing="True"
|
||||
VirtualizingPanel.VirtualizationMode="Recycling"
|
||||
VirtualizingPanel.ScrollUnit="Pixel"
|
||||
SelectionMode="Extended">
|
||||
<ListBox.Style>
|
||||
<Style TargetType="ListBox">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedFilePath}" Value="">
|
||||
@ -551,36 +494,36 @@
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TreeView.Style>
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type viewModels:JsonTreeNodeViewModel}" ItemsSource="{Binding Children}">
|
||||
<Border Background="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=Background}" Padding="2,1">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding Name}" />
|
||||
<TextBlock Text=": " />
|
||||
<TextBlock Text="{Binding Value}" Foreground="{DynamicResource Ui.Brush.Muted}" />
|
||||
</StackPanel>
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Ui.Brush.DataRowHover}" />
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=IsSelected}" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource Ui.Brush.DataRowSelected}" />
|
||||
<Setter Property="TextElement.Foreground" Value="{DynamicResource Ui.Brush.Text}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.Resources>
|
||||
<TreeView.ItemContainerStyle>
|
||||
<StaticResource ResourceKey="VideoInfoTreeItemStyle" />
|
||||
</TreeView.ItemContainerStyle>
|
||||
</TreeView>
|
||||
</ListBox.Style>
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Left" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBox Text="{Binding Mode=OneWay}"
|
||||
BorderThickness="0"
|
||||
Background="Transparent"
|
||||
Foreground="{DynamicResource Ui.Brush.Text}"
|
||||
FontFamily="Consolas"
|
||||
FontSize="13"
|
||||
IsReadOnly="True"
|
||||
IsUndoEnabled="False"
|
||||
IsInactiveSelectionHighlightEnabled="True"
|
||||
AcceptsReturn="False"
|
||||
AcceptsTab="False"
|
||||
TextWrapping="NoWrap"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Disabled" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
@ -596,40 +539,97 @@
|
||||
<TextBlock Text="Настройки" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</TabItem.Header>
|
||||
<TabItem.Resources>
|
||||
<Style x:Key="SettingsGroupBox" TargetType="GroupBox">
|
||||
<Setter Property="Padding" Value="6,4,6,6" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ui.Brush.BorderSubtle}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
</Style>
|
||||
<Style x:Key="SettingsLabel" TargetType="TextBlock" BasedOn="{StaticResource UiTextCaption}">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="0,0,6,0" />
|
||||
</Style>
|
||||
<Style x:Key="SettingsTextBox" TargetType="TextBox" BasedOn="{StaticResource UiTextInput}">
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="MinHeight" Value="26" />
|
||||
<Setter Property="Padding" Value="6,2" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style x:Key="SettingsComboBox" TargetType="ComboBox" BasedOn="{StaticResource UiCombo}">
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="MinHeight" Value="26" />
|
||||
<Setter Property="Padding" Value="6,1" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style x:Key="SettingsButton" TargetType="Button" BasedOn="{StaticResource UiButtonSecondary}">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="MinHeight" Value="28" />
|
||||
<Setter Property="Padding" Value="8,1" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style x:Key="SettingsPrimaryButton" TargetType="Button" BasedOn="{StaticResource UiButtonPrimary}">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="MinHeight" Value="28" />
|
||||
<Setter Property="Padding" Value="8,1" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style x:Key="SettingsDangerButton" TargetType="Button" BasedOn="{StaticResource UiButtonDanger}">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="MinHeight" Value="28" />
|
||||
<Setter Property="Padding" Value="8,1" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style x:Key="SettingsDataGridHeader" TargetType="DataGridColumnHeader" BasedOn="{StaticResource UiDataGridColumnHeader}">
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="MinHeight" Value="26" />
|
||||
<Setter Property="Padding" Value="4,2" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
</Style>
|
||||
<Style x:Key="SettingsDataGridCell" TargetType="DataGridCell" BasedOn="{StaticResource UiDataGridCell}">
|
||||
<Setter Property="Padding" Value="4,0" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style x:Key="SettingsDataGridRow" TargetType="DataGridRow" BasedOn="{StaticResource UiDataGridRow}">
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="MinHeight" Value="26" />
|
||||
</Style>
|
||||
</TabItem.Resources>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="6" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ScrollViewer Grid.Row="0"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Padding="0">
|
||||
<Border Style="{StaticResource UiSectionCard}"
|
||||
Margin="0,0,0,12"
|
||||
HorizontalAlignment="Stretch">
|
||||
<StackPanel HorizontalAlignment="Stretch">
|
||||
<TextBlock Style="{StaticResource UiTextH2}" Text="Каталоги" />
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2.2*" />
|
||||
<ColumnDefinition Width="6" />
|
||||
<ColumnDefinition Width="1.35*" />
|
||||
<ColumnDefinition Width="6" />
|
||||
<ColumnDefinition Width="1.15*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Style="{StaticResource UiTextCaption}"
|
||||
Margin="0,12,0,4"
|
||||
Text="TEMP-каталог" />
|
||||
|
||||
<Grid HorizontalAlignment="Stretch">
|
||||
<GroupBox Grid.Column="0" Header="Пути" Style="{StaticResource SettingsGroupBox}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="76" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="4" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid Grid.Column="0">
|
||||
<TextBlock Grid.Column="0" Text="TEMP" Style="{StaticResource SettingsLabel}" />
|
||||
<Grid Grid.Column="1">
|
||||
<TextBox x:Name="SettingsTempPathBox"
|
||||
Style="{StaticResource UiTextInput}"
|
||||
Style="{StaticResource SettingsTextBox}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding ProcessingTempDirectory, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock Margin="8,0,0,0"
|
||||
<TextBlock Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
IsHitTestVisible="False"
|
||||
Text="C:\Users\me\AppData\Local\Temp\EmbyToolbox">
|
||||
@ -645,107 +645,139 @@
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
<Button Grid.Column="2"
|
||||
MinWidth="100"
|
||||
Style="{StaticResource UiButtonSecondary}"
|
||||
<Button Grid.Column="3"
|
||||
MinWidth="74"
|
||||
Style="{StaticResource SettingsButton}"
|
||||
Command="{Binding ChooseTempDirectoryCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Выбрать" VerticalAlignment="Center" />
|
||||
<TextBlock Text="Обзор" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<TextBlock Style="{StaticResource UiTextCaption}"
|
||||
Margin="0,6,0,0"
|
||||
Text="Временный каталог для промежуточных файлов" />
|
||||
<GroupBox Grid.Column="2" Header="Выполнение" Style="{StaticResource SettingsGroupBox}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="74" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="26" />
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="26" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Лог" Style="{StaticResource SettingsLabel}" />
|
||||
<ComboBox Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource SettingsComboBox}"
|
||||
ItemsSource="{Binding LogLevelOptions}"
|
||||
SelectedItem="{Binding MinimumFileLogLevel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="GPU" Style="{StaticResource SettingsLabel}" />
|
||||
<ComboBox Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource SettingsComboBox}"
|
||||
ItemsSource="{Binding HardwareAccelerationOptions}"
|
||||
SelectedItem="{Binding HardwareAcceleration, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<TextBlock Style="{StaticResource UiTextCaption}"
|
||||
Margin="0,12,0,4"
|
||||
Text="Минимальный уровень сохранения логов" />
|
||||
<ComboBox Style="{StaticResource UiCombo}"
|
||||
MinWidth="200"
|
||||
Margin="0,0,24,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding LogLevelOptions}"
|
||||
SelectedItem="{Binding MinimumFileLogLevel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<GroupBox Grid.Column="4" Header="Уведомления" Style="{StaticResource SettingsGroupBox}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="24" />
|
||||
<RowDefinition Height="24" />
|
||||
<RowDefinition Height="4" />
|
||||
<RowDefinition Height="28" />
|
||||
</Grid.RowDefinitions>
|
||||
<CheckBox Grid.Row="0"
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding NotifyCompletionSoundAfterQueue, Mode=TwoWay}">
|
||||
<TextBlock Text="Звук после очереди" VerticalAlignment="Center" />
|
||||
</CheckBox>
|
||||
<CheckBox Grid.Row="1"
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding NotifyWindowsToastAfterQueue, Mode=TwoWay}">
|
||||
<TextBlock Text="Windows toast" VerticalAlignment="Center" />
|
||||
</CheckBox>
|
||||
<Button Grid.Row="3"
|
||||
MinWidth="118"
|
||||
HorizontalAlignment="Left"
|
||||
Style="{StaticResource SettingsButton}"
|
||||
Command="{Binding TestWindowsNotificationCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Тест" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Style="{StaticResource UiTextCaption}"
|
||||
Margin="0,12,0,4"
|
||||
Text="Аппаратное ускорение" />
|
||||
<ComboBox Style="{StaticResource UiCombo}"
|
||||
MinWidth="200"
|
||||
Margin="0,0,24,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding HardwareAccelerationOptions}"
|
||||
SelectedItem="{Binding HardwareAcceleration, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<TextBlock Style="{StaticResource UiTextH2}" Margin="0,16,0,8" Text="Уведомления о конвертации" />
|
||||
|
||||
<CheckBox Margin="0,8,0,0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Top"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
IsChecked="{Binding NotifyCompletionSoundAfterQueue, Mode=TwoWay}">
|
||||
<CheckBox.ToolTip>
|
||||
<ToolTip MaxWidth="360">
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Text="После обработки всей очереди конвертации воспроизводится системный звук успеха или ошибки (Windows)." />
|
||||
</ToolTip>
|
||||
</CheckBox.ToolTip>
|
||||
<TextBlock Text="Звуковое уведомление после завершения"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox Margin="0,6,0,0"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Top"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
IsChecked="{Binding NotifyWindowsToastAfterQueue, Mode=TwoWay}">
|
||||
<CheckBox.ToolTip>
|
||||
<ToolTip MaxWidth="360">
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Text="В центре уведомлений показывается итог обработки всей очереди (или сообщение при остановке пользователем)." />
|
||||
</ToolTip>
|
||||
</CheckBox.ToolTip>
|
||||
<TextBlock Text="Показывать уведомления Windows"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center" />
|
||||
</CheckBox>
|
||||
|
||||
<Button Margin="0,12,0,0"
|
||||
MinHeight="34"
|
||||
MinWidth="240"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
Style="{StaticResource UiButtonSecondary}"
|
||||
Command="{Binding TestWindowsNotificationCommand}">
|
||||
<Grid Grid.Row="2" Margin="0,0,0,4">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="Профили конвертации"
|
||||
Style="{StaticResource UiTextH2}"
|
||||
VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
||||
<Button Style="{StaticResource SettingsButton}" Margin="0,0,6,0" MinWidth="104" Command="{Binding AddConversionProfileCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
Text="Проверить уведомление" />
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Добавить" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource SettingsDangerButton}" MinWidth="96" Command="{Binding RemoveConversionProfileCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Удалить" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<TextBlock Style="{StaticResource UiTextH2}" Margin="0,16,0,8" Text="Профили конвертации" />
|
||||
<DataGrid ItemsSource="{Binding ConversionProfiles}"
|
||||
<DataGrid Grid.Row="3"
|
||||
ItemsSource="{Binding ConversionProfiles}"
|
||||
SelectedItem="{Binding SelectedConversionProfile, Mode=TwoWay}"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
CanUserDeleteRows="False"
|
||||
IsReadOnly="False"
|
||||
Style="{StaticResource UiDataGrid}"
|
||||
RowStyle="{StaticResource UiDataGridRow}"
|
||||
ColumnHeaderStyle="{StaticResource UiDataGridColumnHeader}"
|
||||
CellStyle="{StaticResource UiDataGridCell}"
|
||||
RowStyle="{StaticResource SettingsDataGridRow}"
|
||||
ColumnHeaderStyle="{StaticResource SettingsDataGridHeader}"
|
||||
CellStyle="{StaticResource SettingsDataGridCell}"
|
||||
HeadersVisibility="Column"
|
||||
GridLinesVisibility="Horizontal"
|
||||
GridLinesVisibility="All"
|
||||
RowHeight="26"
|
||||
ColumnHeaderHeight="26"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
MinHeight="160">
|
||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||
MinHeight="0">
|
||||
<DataGrid.Resources>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
|
||||
</Style>
|
||||
<Style TargetType="TextBox" BasedOn="{StaticResource SettingsTextBox}">
|
||||
<Setter Property="Height" Value="24" />
|
||||
<Setter Property="MinHeight" Value="24" />
|
||||
<Setter Property="Padding" Value="4,0" />
|
||||
</Style>
|
||||
<Style TargetType="ComboBox" BasedOn="{StaticResource SettingsComboBox}">
|
||||
<Setter Property="Height" Value="24" />
|
||||
<Setter Property="MinHeight" Value="24" />
|
||||
<Setter Property="Padding" Value="4,0" />
|
||||
</Style>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Header="Профиль" Width="150">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
@ -884,19 +916,6 @@
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellEditingTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Header="Субтитры" Width="95">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Subtitles}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
<DataGridTemplateColumn.CellEditingTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox ItemsSource="{Binding DataContext.ConversionYesNoOptions, ElementName=RootWindow}"
|
||||
SelectedItem="{Binding Subtitles, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellEditingTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn Header="Внешние дорожки" Width="125">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
@ -937,50 +956,44 @@
|
||||
</DataGridTemplateColumn.CellEditingTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,8,0,0" HorizontalAlignment="Left">
|
||||
<Button Style="{StaticResource UiButtonSecondary}" Margin="0,0,8,0" MinWidth="150" Command="{Binding AddConversionProfileCommand}">
|
||||
<TextBlock Text="Добавить профиль" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource UiButtonDanger}" MinWidth="150" Command="{Binding RemoveConversionProfileCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Удалить профиль" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</ScrollViewer>
|
||||
</DataGrid>
|
||||
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Style="{StaticResource UiButtonSecondary}"
|
||||
MinWidth="168"
|
||||
Margin="0,0,8,0"
|
||||
<Grid Grid.Row="4" Margin="0,6,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0"
|
||||
Style="{StaticResource SettingsButton}"
|
||||
MinWidth="154"
|
||||
HorizontalAlignment="Left"
|
||||
Command="{Binding CheckToolsCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Проверить инструменты" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource UiButtonSecondary}"
|
||||
MinWidth="112"
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Style="{StaticResource SettingsButton}"
|
||||
MinWidth="96"
|
||||
Margin="0,0,8,0"
|
||||
Command="{Binding CancelSettingsCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Отменить" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource UiButtonPrimary}"
|
||||
MinWidth="112"
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphButton}" Text="" />
|
||||
<TextBlock Text="Отменить" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Style="{StaticResource SettingsPrimaryButton}"
|
||||
MinWidth="98"
|
||||
Command="{Binding SaveSettingsCommand}">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphOnPrimary}" Text="" />
|
||||
<TextBlock Text="Сохранить" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource UiMdlGlyphOnPrimary}" Text="" />
|
||||
<TextBlock Text="Сохранить" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using EmbyToolbox.ViewModels;
|
||||
|
||||
namespace EmbyToolbox;
|
||||
@ -13,19 +10,4 @@ public partial class MainWindow
|
||||
InitializeComponent();
|
||||
DataContext = new MainWindowViewModel();
|
||||
}
|
||||
|
||||
private void OnJsonTreeItemPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var dependencyObject = e.OriginalSource as DependencyObject;
|
||||
while (dependencyObject is not null && dependencyObject is not TreeViewItem)
|
||||
{
|
||||
dependencyObject = VisualTreeHelper.GetParent(dependencyObject);
|
||||
}
|
||||
|
||||
if (dependencyObject is TreeViewItem item)
|
||||
{
|
||||
item.IsSelected = true;
|
||||
item.Focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,4 +5,5 @@ public sealed class AddFilesOptions
|
||||
public string Profile { get; init; } = "Emby";
|
||||
public bool DisableSubtitleDefault { get; init; }
|
||||
public bool RemoveForeignAudioAndSubtitles { get; init; }
|
||||
public EffectiveProfileSettings EffectiveSettings { get; init; } = new();
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ public sealed class ConversionQueueItem : INotifyPropertyChanged
|
||||
private MediaAnalysisResult? _mediaAnalysis;
|
||||
private IReadOnlyList<SidecarFile> _sidecars = System.Array.Empty<SidecarFile>();
|
||||
private IReadOnlyList<ExternalAudioFile> _externalAudioFiles = System.Array.Empty<ExternalAudioFile>();
|
||||
private EffectiveProfileSettings? _effectiveProfileSettings;
|
||||
private ConversionPlan? _lastPlan;
|
||||
private bool _isProcessed;
|
||||
private bool _processedInCurrentRun;
|
||||
@ -133,6 +134,21 @@ public sealed class ConversionQueueItem : INotifyPropertyChanged
|
||||
|
||||
public ConversionTaskOverride TaskOverride { get; } = new();
|
||||
|
||||
public EffectiveProfileSettings? EffectiveProfileSettings
|
||||
{
|
||||
get => _effectiveProfileSettings;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_effectiveProfileSettings, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_effectiveProfileSettings = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ConversionPlan? LastPlan
|
||||
{
|
||||
get => _lastPlan;
|
||||
@ -484,6 +500,7 @@ public sealed class ConversionQueueItem : INotifyPropertyChanged
|
||||
}
|
||||
|
||||
_profile = value;
|
||||
EffectiveProfileSettings = null;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
12
EmbyToolbox/Models/EffectiveProfileSettings.cs
Normal file
12
EmbyToolbox/Models/EffectiveProfileSettings.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using EmbyToolbox.Services;
|
||||
|
||||
namespace EmbyToolbox.Models;
|
||||
|
||||
public sealed class EffectiveProfileSettings
|
||||
{
|
||||
public string SourceProfileName { get; init; } = "Emby";
|
||||
public ConversionProfileSettingsEntry Profile { get; init; } = new();
|
||||
public IReadOnlyList<string> ChangedFields { get; init; } = [];
|
||||
|
||||
public bool HasOverrides => ChangedFields.Count > 0;
|
||||
}
|
||||
@ -103,7 +103,9 @@ public sealed class ConversionExecutionService
|
||||
item.ErrorDetails = null;
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
var profile = resolveProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
|
||||
var profile = item.EffectiveProfileSettings?.Profile
|
||||
?? resolveProfile(item.Profile)
|
||||
?? ConversionProfileMapping.EmbyFallback;
|
||||
targetContainer = ResolveTargetContainer(item, profile);
|
||||
finalPath = BuildFinalPath(item.FullPath, targetContainer);
|
||||
tempOut = Path.Combine(tempRoot, $"{Path.GetFileNameWithoutExtension(item.FileName)}.__processing__{GetOutputExtension(targetContainer)}");
|
||||
|
||||
@ -35,6 +35,13 @@ public sealed class NotificationService
|
||||
|
||||
private void LogAppUserModelRegistrationState()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(ToastShortcutRegistration.LastDiagnostics))
|
||||
{
|
||||
_logging.Warning(
|
||||
$"Windows toast: ярлык Start Menu не подготовлен ({ToastShortcutRegistration.LastDiagnostics})",
|
||||
"notify");
|
||||
}
|
||||
|
||||
if (string.Equals(AppUserModelIdRegistration.LastRegisteredId, ToastAppUserModelId, StringComparison.Ordinal))
|
||||
{
|
||||
_logging.Info($"App User Model ID: {ToastAppUserModelId}", "notify");
|
||||
|
||||
65
EmbyToolbox/Services/ProfileOverrideBuilder.cs
Normal file
65
EmbyToolbox/Services/ProfileOverrideBuilder.cs
Normal file
@ -0,0 +1,65 @@
|
||||
using EmbyToolbox.Models;
|
||||
|
||||
namespace EmbyToolbox.Services;
|
||||
|
||||
public sealed class ProfileOverrideBuilder
|
||||
{
|
||||
public EffectiveProfileSettings Build(
|
||||
ConversionProfileSettingsEntry source,
|
||||
ConversionProfileSettingsEntry formValues,
|
||||
string sourceProfileName)
|
||||
{
|
||||
var changed = new List<string>();
|
||||
AddIfChanged(changed, "Container", source.Container, formValues.Container);
|
||||
AddIfChanged(changed, "Video", source.Video, formValues.Video);
|
||||
AddIfChanged(changed, "PixelFormat", source.PixelFormat, formValues.PixelFormat);
|
||||
AddIfChanged(changed, "Resolution", source.Resolution, formValues.Resolution);
|
||||
AddIfChanged(changed, "Fps", source.Fps, formValues.Fps);
|
||||
AddIfChanged(changed, "Audio", source.Audio, formValues.Audio);
|
||||
AddIfChanged(changed, "Bitrate", source.Bitrate, formValues.Bitrate);
|
||||
AddIfChanged(changed, "VideoBitrateMode", source.VideoBitrateMode, formValues.VideoBitrateMode);
|
||||
if (source.VideoBitrateMbps != formValues.VideoBitrateMbps)
|
||||
{
|
||||
changed.Add("VideoBitrateMbps");
|
||||
}
|
||||
|
||||
AddIfChanged(changed, "Subtitles", source.Subtitles, formValues.Subtitles);
|
||||
AddIfChanged(changed, "ExternalTracks", source.ExternalTracks, formValues.ExternalTracks);
|
||||
AddIfChanged(changed, "ExternalSubtitles", source.ExternalSubtitles, formValues.ExternalSubtitles);
|
||||
AddIfChanged(changed, "Fonts", source.Fonts, formValues.Fonts);
|
||||
|
||||
return new EffectiveProfileSettings
|
||||
{
|
||||
SourceProfileName = sourceProfileName,
|
||||
Profile = Clone(formValues, sourceProfileName),
|
||||
ChangedFields = changed
|
||||
};
|
||||
}
|
||||
|
||||
public static ConversionProfileSettingsEntry Clone(ConversionProfileSettingsEntry source, string? profileName = null) =>
|
||||
new()
|
||||
{
|
||||
Profile = string.IsNullOrWhiteSpace(profileName) ? source.Profile : profileName!,
|
||||
Container = source.Container,
|
||||
Video = source.Video,
|
||||
PixelFormat = source.PixelFormat,
|
||||
Resolution = source.Resolution,
|
||||
Fps = source.Fps,
|
||||
Audio = source.Audio,
|
||||
Bitrate = source.Bitrate,
|
||||
VideoBitrateMode = source.VideoBitrateMode,
|
||||
VideoBitrateMbps = source.VideoBitrateMbps,
|
||||
Subtitles = source.Subtitles,
|
||||
ExternalTracks = source.ExternalTracks,
|
||||
ExternalSubtitles = source.ExternalSubtitles,
|
||||
Fonts = source.Fonts
|
||||
};
|
||||
|
||||
private static void AddIfChanged(List<string> changed, string name, string? source, string? value)
|
||||
{
|
||||
if (!string.Equals(source?.Trim(), value?.Trim(), StringComparison.Ordinal))
|
||||
{
|
||||
changed.Add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -102,7 +102,7 @@ public sealed class QueueAnalysisService
|
||||
await uiInvoke(
|
||||
() =>
|
||||
{
|
||||
var profile = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
|
||||
var profile = ResolveEffectiveProfile(item);
|
||||
item.TaskOverride.TrackOverrides.Clear();
|
||||
TrackOverrideSeeder.EnsureDefaults(
|
||||
item.TaskOverride,
|
||||
@ -297,7 +297,7 @@ public sealed class QueueAnalysisService
|
||||
|
||||
var audio = FfprobeAudioInfoParser.TryParse(result.Json) ?? new FfprobeAudioInfo(0, null, true);
|
||||
var side = discovery.Sidecars;
|
||||
var profile = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
|
||||
var profile = ResolveEffectiveProfile(item);
|
||||
// При повторном анализе sidecar-набор может измениться (добавили/удалили внешние файлы).
|
||||
// Пересобираем список дорожек, чтобы не держать устаревшие external entries.
|
||||
item.TaskOverride.TrackOverrides.Clear();
|
||||
@ -419,6 +419,11 @@ public sealed class QueueAnalysisService
|
||||
public int ErrorCount;
|
||||
}
|
||||
|
||||
private ConversionProfileSettingsEntry ResolveEffectiveProfile(ConversionQueueItem item) =>
|
||||
item.EffectiveProfileSettings?.Profile
|
||||
?? _profile.GetProfile(item.Profile)
|
||||
?? ConversionProfileMapping.EmbyFallback;
|
||||
|
||||
private static bool IsForeignLanguageForAutoRemove(string? language)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(language))
|
||||
|
||||
@ -15,6 +15,7 @@ public static class VideoBitratePolicy
|
||||
Auto,
|
||||
Source,
|
||||
"2 Mbps",
|
||||
"3 Mbps",
|
||||
"4 Mbps",
|
||||
"6 Mbps",
|
||||
"8 Mbps",
|
||||
|
||||
@ -1,102 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using EmbyToolbox.Models;
|
||||
|
||||
namespace EmbyToolbox.ViewModels;
|
||||
|
||||
public sealed class AddFilesOptionsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly Action<AddFilesOptions> _onAdd;
|
||||
private readonly Action _onCancel;
|
||||
|
||||
private bool _disableSubtitleDefault;
|
||||
private bool _removeForeignAudioAndSubtitles;
|
||||
private ConversionProfilePresetRow? _selectedProfile;
|
||||
|
||||
public AddFilesOptionsViewModel(
|
||||
IReadOnlyList<ConversionProfilePresetRow> profiles,
|
||||
string selectedProfileName,
|
||||
bool disableSubtitleDefault,
|
||||
bool removeForeignAudioAndSubtitles,
|
||||
Action<AddFilesOptions> onAdd,
|
||||
Action onCancel)
|
||||
{
|
||||
_onAdd = onAdd;
|
||||
_onCancel = onCancel;
|
||||
_disableSubtitleDefault = disableSubtitleDefault;
|
||||
_removeForeignAudioAndSubtitles = removeForeignAudioAndSubtitles;
|
||||
Profiles = new ObservableCollection<ConversionProfilePresetRow>(profiles);
|
||||
_selectedProfile = Profiles.FirstOrDefault(p => p.Profile.Equals(selectedProfileName, StringComparison.OrdinalIgnoreCase))
|
||||
?? Profiles.FirstOrDefault(p => p.Profile.Equals("Emby", StringComparison.OrdinalIgnoreCase))
|
||||
?? Profiles.FirstOrDefault();
|
||||
AddCommand = new RelayCommand(ExecuteAdd);
|
||||
CancelCommand = new RelayCommand(() => _onCancel());
|
||||
}
|
||||
|
||||
public bool DisableSubtitleDefault
|
||||
{
|
||||
get => _disableSubtitleDefault;
|
||||
set
|
||||
{
|
||||
if (_disableSubtitleDefault == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disableSubtitleDefault = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveForeignAudioAndSubtitles
|
||||
{
|
||||
get => _removeForeignAudioAndSubtitles;
|
||||
set
|
||||
{
|
||||
if (_removeForeignAudioAndSubtitles == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_removeForeignAudioAndSubtitles = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<ConversionProfilePresetRow> Profiles { get; }
|
||||
|
||||
public ConversionProfilePresetRow? SelectedProfile
|
||||
{
|
||||
get => _selectedProfile;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_selectedProfile, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedProfile = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public RelayCommand AddCommand { get; }
|
||||
public RelayCommand CancelCommand { get; }
|
||||
|
||||
private void ExecuteAdd()
|
||||
{
|
||||
_onAdd(
|
||||
new AddFilesOptions
|
||||
{
|
||||
Profile = string.IsNullOrWhiteSpace(_selectedProfile?.Profile) ? "Emby" : _selectedProfile.Profile,
|
||||
DisableSubtitleDefault = _disableSubtitleDefault,
|
||||
RemoveForeignAudioAndSubtitles = _removeForeignAudioAndSubtitles
|
||||
});
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? name = null) =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
351
EmbyToolbox/ViewModels/AddFilesSettingsViewModel.cs
Normal file
351
EmbyToolbox/ViewModels/AddFilesSettingsViewModel.cs
Normal file
@ -0,0 +1,351 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using EmbyToolbox.Models;
|
||||
using EmbyToolbox.Services;
|
||||
|
||||
namespace EmbyToolbox.ViewModels;
|
||||
|
||||
public sealed class AddFilesSettingsViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private readonly IReadOnlyList<ConversionProfilePresetRow> _profiles;
|
||||
private readonly Action<AddFilesOptions> _onAdd;
|
||||
private readonly Action _onCancel;
|
||||
private readonly ProfileOverrideBuilder _builder = new();
|
||||
private ConversionProfileSettingsEntry _sourceProfile = new();
|
||||
private ConversionProfilePresetRow? _selectedProfile;
|
||||
private string _container = string.Empty;
|
||||
private string _video = string.Empty;
|
||||
private string _pixelFormat = string.Empty;
|
||||
private string _resolution = string.Empty;
|
||||
private string _fps = string.Empty;
|
||||
private string _videoBitrateMode = VideoBitratePolicy.Auto;
|
||||
private string _videoBitrateCustomMbps = string.Empty;
|
||||
private string _audio = string.Empty;
|
||||
private string _audioBitrate = string.Empty;
|
||||
private bool _externalTracks;
|
||||
private bool _externalSubtitles;
|
||||
private bool _fonts;
|
||||
private bool _removeForeignTracks;
|
||||
private bool _disableSubtitleDefault;
|
||||
private string _validationMessage = string.Empty;
|
||||
private bool _isValid;
|
||||
|
||||
public AddFilesSettingsViewModel(
|
||||
IReadOnlyList<ConversionProfilePresetRow> profiles,
|
||||
ConversionFormOptions formOptions,
|
||||
AddFilesSettingsState? previousState,
|
||||
string fallbackProfileName,
|
||||
bool fallbackDisableSubtitleDefault,
|
||||
bool fallbackRemoveForeignTracks,
|
||||
Action<AddFilesOptions> onAdd,
|
||||
Action onCancel)
|
||||
{
|
||||
_profiles = profiles;
|
||||
_onAdd = onAdd;
|
||||
_onCancel = onCancel;
|
||||
Profiles = new ObservableCollection<ConversionProfilePresetRow>(profiles);
|
||||
ContainerOptions = formOptions.ContainerOptions;
|
||||
VideoCodecOptions = formOptions.VideoCodecOptions;
|
||||
PixelFormatOptions = formOptions.PixelFormatOptions;
|
||||
ResolutionOptions = formOptions.ResolutionOptions;
|
||||
FpsOptions = formOptions.FpsOptions;
|
||||
AudioCodecOptions = formOptions.AudioCodecOptions;
|
||||
AudioBitrateOptions = formOptions.AudioBitrateKbps;
|
||||
VideoBitrateOptions = VideoBitratePolicy.UiOptions;
|
||||
YesNoOptions = ["Да", "Нет"];
|
||||
AddCommand = new RelayCommand(ExecuteAdd, CanAdd);
|
||||
CancelCommand = new RelayCommand(() => _onCancel());
|
||||
|
||||
var profileName = string.IsNullOrWhiteSpace(previousState?.ProfileName)
|
||||
? fallbackProfileName
|
||||
: previousState!.ProfileName;
|
||||
_selectedProfile = Profiles.FirstOrDefault(p => p.Profile.Equals(profileName, StringComparison.OrdinalIgnoreCase))
|
||||
?? Profiles.FirstOrDefault(p => p.Profile.Equals("Emby", StringComparison.OrdinalIgnoreCase))
|
||||
?? Profiles.FirstOrDefault();
|
||||
|
||||
if (_selectedProfile is not null)
|
||||
{
|
||||
LoadProfile(_selectedProfile.ToSettingsEntry(), resetUserValues: true);
|
||||
}
|
||||
|
||||
if (previousState is not null)
|
||||
{
|
||||
ApplyState(previousState);
|
||||
}
|
||||
else
|
||||
{
|
||||
_disableSubtitleDefault = fallbackDisableSubtitleDefault;
|
||||
_removeForeignTracks = fallbackRemoveForeignTracks;
|
||||
}
|
||||
|
||||
Validate();
|
||||
}
|
||||
|
||||
public ObservableCollection<ConversionProfilePresetRow> Profiles { get; }
|
||||
public IReadOnlyList<string> ContainerOptions { get; }
|
||||
public IReadOnlyList<string> VideoCodecOptions { get; }
|
||||
public IReadOnlyList<string> PixelFormatOptions { get; }
|
||||
public IReadOnlyList<string> ResolutionOptions { get; }
|
||||
public IReadOnlyList<string> FpsOptions { get; }
|
||||
public IReadOnlyList<string> AudioCodecOptions { get; }
|
||||
public IReadOnlyList<string> AudioBitrateOptions { get; }
|
||||
public IReadOnlyList<string> VideoBitrateOptions { get; }
|
||||
public IReadOnlyList<string> YesNoOptions { get; }
|
||||
|
||||
public RelayCommand AddCommand { get; }
|
||||
public RelayCommand CancelCommand { get; }
|
||||
|
||||
public ConversionProfilePresetRow? SelectedProfile
|
||||
{
|
||||
get => _selectedProfile;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_selectedProfile, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_selectedProfile = value;
|
||||
OnPropertyChanged();
|
||||
if (value is not null)
|
||||
{
|
||||
LoadProfile(value.ToSettingsEntry(), resetUserValues: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string Container { get => _container; set => SetField(ref _container, value); }
|
||||
public string Video { get => _video; set => SetField(ref _video, value); }
|
||||
public string PixelFormat { get => _pixelFormat; set => SetField(ref _pixelFormat, value); }
|
||||
public string Resolution { get => _resolution; set => SetField(ref _resolution, value); }
|
||||
public string Fps { get => _fps; set => SetField(ref _fps, value); }
|
||||
public string VideoBitrateMode
|
||||
{
|
||||
get => _videoBitrateMode;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _videoBitrateMode, string.IsNullOrWhiteSpace(value) ? VideoBitratePolicy.Auto : value))
|
||||
{
|
||||
OnPropertyChanged(nameof(IsVideoBitrateCustomVisible));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string VideoBitrateCustomMbps { get => _videoBitrateCustomMbps; set => SetField(ref _videoBitrateCustomMbps, value); }
|
||||
public string Audio { get => _audio; set => SetField(ref _audio, value); }
|
||||
public string AudioBitrate { get => _audioBitrate; set => SetField(ref _audioBitrate, value); }
|
||||
public bool ExternalTracks { get => _externalTracks; set => SetField(ref _externalTracks, value); }
|
||||
public bool ExternalSubtitles { get => _externalSubtitles; set => SetField(ref _externalSubtitles, value); }
|
||||
public bool Fonts { get => _fonts; set => SetField(ref _fonts, value); }
|
||||
public bool RemoveForeignTracks { get => _removeForeignTracks; set => SetField(ref _removeForeignTracks, value); }
|
||||
public bool DisableSubtitleDefault { get => _disableSubtitleDefault; set => SetField(ref _disableSubtitleDefault, value); }
|
||||
public bool IsVideoBitrateCustomVisible => string.Equals(VideoBitrateMode, VideoBitratePolicy.Custom, StringComparison.Ordinal);
|
||||
public bool HasValidationMessage => !string.IsNullOrWhiteSpace(ValidationMessage);
|
||||
|
||||
public string ValidationMessage
|
||||
{
|
||||
get => _validationMessage;
|
||||
private set
|
||||
{
|
||||
if (_validationMessage == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_validationMessage = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(HasValidationMessage));
|
||||
}
|
||||
}
|
||||
|
||||
public AddFilesSettingsState CaptureState() =>
|
||||
new AddFilesSettingsState
|
||||
{
|
||||
ProfileName = SelectedProfile?.Profile ?? "Emby",
|
||||
ChangedFields = BuildEffectiveSettings().ChangedFields.ToArray(),
|
||||
Container = Container,
|
||||
Video = Video,
|
||||
PixelFormat = PixelFormat,
|
||||
Resolution = Resolution,
|
||||
Fps = Fps,
|
||||
VideoBitrateMode = VideoBitrateMode,
|
||||
VideoBitrateCustomMbps = VideoBitrateCustomMbps,
|
||||
Audio = Audio,
|
||||
AudioBitrate = AudioBitrate,
|
||||
ExternalTracks = ExternalTracks,
|
||||
ExternalSubtitles = ExternalSubtitles,
|
||||
Fonts = Fonts,
|
||||
RemoveForeignTracks = RemoveForeignTracks,
|
||||
DisableSubtitleDefault = DisableSubtitleDefault
|
||||
};
|
||||
|
||||
private void LoadProfile(ConversionProfileSettingsEntry profile, bool resetUserValues)
|
||||
{
|
||||
_sourceProfile = ProfileOverrideBuilder.Clone(profile);
|
||||
if (!resetUserValues)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Container = profile.Container;
|
||||
Video = profile.Video;
|
||||
PixelFormat = profile.PixelFormat;
|
||||
Resolution = profile.Resolution;
|
||||
Fps = profile.Fps;
|
||||
VideoBitrateMode = VideoBitratePolicy.NormalizeMode(profile.VideoBitrateMode);
|
||||
VideoBitrateCustomMbps = profile.VideoBitrateMbps?.ToString("0.###", CultureInfo.InvariantCulture) ?? string.Empty;
|
||||
Audio = profile.Audio;
|
||||
AudioBitrate = profile.Bitrate;
|
||||
ExternalTracks = IsYes(profile.ExternalTracks);
|
||||
ExternalSubtitles = IsYes(profile.ExternalSubtitles);
|
||||
Fonts = IsYes(profile.Fonts);
|
||||
}
|
||||
|
||||
private void ApplyState(AddFilesSettingsState state)
|
||||
{
|
||||
var changed = state.ChangedFields.ToHashSet(StringComparer.Ordinal);
|
||||
if (changed.Contains("Container")) Container = state.Container;
|
||||
if (changed.Contains("Video")) Video = state.Video;
|
||||
if (changed.Contains("PixelFormat")) PixelFormat = state.PixelFormat;
|
||||
if (changed.Contains("Resolution")) Resolution = state.Resolution;
|
||||
if (changed.Contains("Fps")) Fps = state.Fps;
|
||||
if (changed.Contains("VideoBitrateMode")) VideoBitrateMode = state.VideoBitrateMode;
|
||||
if (changed.Contains("VideoBitrateMbps")) VideoBitrateCustomMbps = state.VideoBitrateCustomMbps;
|
||||
if (changed.Contains("Audio")) Audio = state.Audio;
|
||||
if (changed.Contains("Bitrate")) AudioBitrate = state.AudioBitrate;
|
||||
if (changed.Contains("ExternalTracks")) ExternalTracks = state.ExternalTracks;
|
||||
if (changed.Contains("ExternalSubtitles")) ExternalSubtitles = state.ExternalSubtitles;
|
||||
if (changed.Contains("Fonts")) Fonts = state.Fonts;
|
||||
RemoveForeignTracks = state.RemoveForeignTracks;
|
||||
DisableSubtitleDefault = state.DisableSubtitleDefault;
|
||||
}
|
||||
|
||||
private bool CanAdd() => _isValid;
|
||||
|
||||
private void ExecuteAdd()
|
||||
{
|
||||
if (!Validate())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var effective = BuildEffectiveSettings();
|
||||
_onAdd(
|
||||
new AddFilesOptions
|
||||
{
|
||||
Profile = effective.SourceProfileName,
|
||||
DisableSubtitleDefault = DisableSubtitleDefault,
|
||||
RemoveForeignAudioAndSubtitles = RemoveForeignTracks,
|
||||
EffectiveSettings = effective
|
||||
});
|
||||
}
|
||||
|
||||
private EffectiveProfileSettings BuildEffectiveSettings()
|
||||
{
|
||||
var form = new ConversionProfileSettingsEntry
|
||||
{
|
||||
Profile = SelectedProfile?.Profile ?? "Emby",
|
||||
Container = Container,
|
||||
Video = Video,
|
||||
PixelFormat = PixelFormat,
|
||||
Resolution = Resolution,
|
||||
Fps = Fps,
|
||||
Audio = Audio,
|
||||
Bitrate = AudioBitrate,
|
||||
VideoBitrateMode = VideoBitrateMode,
|
||||
VideoBitrateMbps = IsVideoBitrateCustomVisible && TryParseCustomVideoBitrate(VideoBitrateCustomMbps, out var mbps) ? mbps : null,
|
||||
Subtitles = _sourceProfile.Subtitles,
|
||||
ExternalTracks = ToYesNo(ExternalTracks),
|
||||
ExternalSubtitles = ToYesNo(ExternalSubtitles),
|
||||
Fonts = ToYesNo(Fonts)
|
||||
};
|
||||
|
||||
return _builder.Build(_sourceProfile, form, SelectedProfile?.Profile ?? "Emby");
|
||||
}
|
||||
|
||||
private bool Validate()
|
||||
{
|
||||
if (SelectedProfile is null)
|
||||
{
|
||||
ValidationMessage = "Выберите профиль.";
|
||||
_isValid = false;
|
||||
AddCommand.RaiseCanExecuteChanged();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsVideoBitrateCustomVisible && !TryParseCustomVideoBitrate(VideoBitrateCustomMbps, out _))
|
||||
{
|
||||
ValidationMessage = "Видеобитрейт должен быть числом больше 0.";
|
||||
_isValid = false;
|
||||
AddCommand.RaiseCanExecuteChanged();
|
||||
return false;
|
||||
}
|
||||
|
||||
ValidationMessage = string.Empty;
|
||||
_isValid = true;
|
||||
AddCommand.RaiseCanExecuteChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
Validate();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseCustomVideoBitrate(string? raw, out double mbps)
|
||||
{
|
||||
mbps = 0;
|
||||
if (string.IsNullOrWhiteSpace(raw))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return double.TryParse(raw.Trim().Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out mbps)
|
||||
&& mbps > 0;
|
||||
}
|
||||
|
||||
private static bool IsYes(string? value) =>
|
||||
value is not null
|
||||
&& (value.Equals("Да", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("Yes", StringComparison.OrdinalIgnoreCase)
|
||||
|| value.Equals("true", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static string ToYesNo(bool value) => value ? "Да" : "Нет";
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AddFilesSettingsState
|
||||
{
|
||||
public string ProfileName { get; init; } = "Emby";
|
||||
public IReadOnlyList<string> ChangedFields { get; init; } = [];
|
||||
public string Container { get; init; } = string.Empty;
|
||||
public string Video { get; init; } = string.Empty;
|
||||
public string PixelFormat { get; init; } = string.Empty;
|
||||
public string Resolution { get; init; } = string.Empty;
|
||||
public string Fps { get; init; } = string.Empty;
|
||||
public string VideoBitrateMode { get; init; } = VideoBitratePolicy.Auto;
|
||||
public string VideoBitrateCustomMbps { get; init; } = string.Empty;
|
||||
public string Audio { get; init; } = string.Empty;
|
||||
public string AudioBitrate { get; init; } = string.Empty;
|
||||
public bool ExternalTracks { get; init; }
|
||||
public bool ExternalSubtitles { get; init; }
|
||||
public bool Fonts { get; init; }
|
||||
public bool RemoveForeignTracks { get; init; }
|
||||
public bool DisableSubtitleDefault { get; init; }
|
||||
}
|
||||
@ -11,7 +11,8 @@ public sealed class ConversionFormOptions
|
||||
public List<string> PixelFormatOptions { get; } = ["yuv420p", "yuv420p10le", "yuv422p", "yuv444p"];
|
||||
public List<string> ResolutionOptions { get; } = ["Без изменений", "Максимум 2160p", "Максимум 1440p", "Максимум 1080p", "Максимум 720p"];
|
||||
public List<string> FpsOptions { get; } = ["Без изменений", "Максимум 60", "Максимум 30", "Максимум 25", "Максимум 24"];
|
||||
public List<string> AudioBitrateKbps { get; } = ["128 kbps", "192 kbps", "256 kbps", "320 kbps"];
|
||||
public List<string> AudioCodecOptions { get; } = ["AAC", "AC3", "EAC3", "Opus", "MP3", "FLAC", "Copy"];
|
||||
public List<string> AudioBitrateKbps { get; } = ["96 kbps", "128 kbps", "160 kbps", "192 kbps", "256 kbps", "320 kbps"];
|
||||
public List<string> VideoBitrateModeOptions { get; } = VideoBitratePolicy.UiOptions.ToList();
|
||||
|
||||
internal void RestoreListsFromSerialized(IReadOnlyList<string>? containers,
|
||||
|
||||
@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
@ -44,6 +45,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
private bool _copyPreviousTrackSettings;
|
||||
private bool _disableSubtitleDefault;
|
||||
private bool _removeForeignTracksByDefault;
|
||||
private AddFilesSettingsState? _lastAddFilesSettingsState;
|
||||
private ConversionProfilePresetRow? _selectedDefaultProfile;
|
||||
private bool _isQueueDropHighlight;
|
||||
private bool _isExecutionRunning;
|
||||
@ -611,7 +613,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
return;
|
||||
}
|
||||
|
||||
var prof = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
|
||||
var prof = ResolveEffectiveProfile(item);
|
||||
TrackOverrideSeeder.SyncTargetFieldsFromProfile(item.TaskOverride, prof);
|
||||
var plan = _planService.Build(item.MediaAnalysis, item.Sidecars, prof, item.TaskOverride, item.ExternalAudioFiles);
|
||||
item.SetPlan(plan);
|
||||
@ -630,7 +632,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
&& (string.Equals(item.Status, ConversionQueueStatus.Pending, StringComparison.Ordinal)
|
||||
|| string.Equals(item.Status, ConversionQueueStatus.Ready, StringComparison.Ordinal)))
|
||||
{
|
||||
var prof = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
|
||||
var prof = ResolveEffectiveProfile(item);
|
||||
// Keep per-file manual overrides intact; only untouched tasks follow updated profile targets.
|
||||
if (!item.IsManuallyEdited)
|
||||
{
|
||||
@ -890,7 +892,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
var affected = 0;
|
||||
foreach (var item in analysis.MajorityItems)
|
||||
{
|
||||
var prof = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
|
||||
var prof = ResolveEffectiveProfile(item);
|
||||
var plan = _planService.Build(item.MediaAnalysis!, item.Sidecars, prof, item.TaskOverride, item.ExternalAudioFiles);
|
||||
item.IsManuallyEdited = true;
|
||||
item.SetPlan(plan);
|
||||
@ -1378,7 +1380,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
t.FfprobeAudioSizeMb,
|
||||
t.FfprobeAudioSizePartial);
|
||||
|
||||
var prof = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
|
||||
var prof = ResolveEffectiveProfile(item);
|
||||
var plan = _planService.Build(item.MediaAnalysis!, sidecars, prof, item.TaskOverride, ext);
|
||||
item.SetPlan(plan);
|
||||
item.Status = NormalizeLoadedExecutionStatus(t.Status);
|
||||
@ -1556,6 +1558,15 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
}
|
||||
|
||||
var profile = string.IsNullOrWhiteSpace(addOptions.Profile) ? "Emby" : addOptions.Profile.Trim();
|
||||
var effectiveSettings = addOptions.EffectiveSettings.Profile.Profile.Length > 0
|
||||
? addOptions.EffectiveSettings
|
||||
: new EffectiveProfileSettings
|
||||
{
|
||||
SourceProfileName = profile,
|
||||
Profile = _profile.GetProfile(profile) ?? ConversionProfileMapping.EmbyFallback,
|
||||
ChangedFields = []
|
||||
};
|
||||
LogAddFilesEffectiveSettings(effectiveSettings, addOptions);
|
||||
var added = 0;
|
||||
var dups = 0;
|
||||
var newBatch = new List<ConversionQueueItem>();
|
||||
@ -1600,8 +1611,10 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
Status = ConversionQueueStatus.Analyzing,
|
||||
Progress = 0,
|
||||
Profile = profile,
|
||||
EffectiveProfileSettings = effectiveSettings,
|
||||
PlanSummary = "Анализ…"
|
||||
};
|
||||
TrackOverrideSeeder.SyncTargetFieldsFromProfile(item.TaskOverride, effectiveSettings.Profile);
|
||||
QueueTasks.Add(item);
|
||||
newBatch.Add(item);
|
||||
added++;
|
||||
@ -1624,6 +1637,27 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
private string CurrentProfileNameForNewTasks() =>
|
||||
string.IsNullOrWhiteSpace(_defaultQueueProfile) ? "Emby" : _defaultQueueProfile;
|
||||
|
||||
private ConversionProfileSettingsEntry ResolveEffectiveProfile(ConversionQueueItem item) =>
|
||||
item.EffectiveProfileSettings?.Profile
|
||||
?? _profile.GetProfile(item.Profile)
|
||||
?? ConversionProfileMapping.EmbyFallback;
|
||||
|
||||
private void LogAddFilesEffectiveSettings(EffectiveProfileSettings effectiveSettings, AddFilesOptions addOptions)
|
||||
{
|
||||
var p = effectiveSettings.Profile;
|
||||
var changed = effectiveSettings.ChangedFields.Count == 0
|
||||
? "none"
|
||||
: string.Join(", ", effectiveSettings.ChangedFields);
|
||||
_logging.Info(
|
||||
"AddFiles effective settings:" + Environment.NewLine
|
||||
+ $"Profile={effectiveSettings.SourceProfileName}" + Environment.NewLine
|
||||
+ $"Overrides={changed}" + Environment.NewLine
|
||||
+ $"Container={p.Container}; Video={p.Video}; PixelFormat={p.PixelFormat}; Resolution={p.Resolution}; FPS={p.Fps}; VideoBitrate={p.VideoBitrateMode}; VideoBitrateMbps={p.VideoBitrateMbps?.ToString("0.###", CultureInfo.InvariantCulture) ?? "-"}" + Environment.NewLine
|
||||
+ $"Audio={p.Audio}; AudioBitrate={p.Bitrate}; Subtitles={p.Subtitles}; ExternalAudio={p.ExternalTracks}; ExternalSubtitles={p.ExternalSubtitles}; ExternalFonts={p.Fonts}" + Environment.NewLine
|
||||
+ $"ForeignTracks={(addOptions.RemoveForeignAudioAndSubtitles ? "Remove" : "Keep")}; DisableSubtitleDefault={addOptions.DisableSubtitleDefault}",
|
||||
"conversion.queue");
|
||||
}
|
||||
|
||||
private void RenumberQueue()
|
||||
{
|
||||
for (var i = 0; i < QueueTasks.Count; i++)
|
||||
@ -1769,8 +1803,10 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
};
|
||||
|
||||
AddFilesOptions? selected = null;
|
||||
var vm = new AddFilesOptionsViewModel(
|
||||
var vm = new AddFilesSettingsViewModel(
|
||||
_presetRowsForSetup(),
|
||||
FormOptions,
|
||||
_lastAddFilesSettingsState,
|
||||
CurrentProfileNameForNewTasks(),
|
||||
DisableSubtitleDefault,
|
||||
RemoveForeignTracksByDefault,
|
||||
@ -1780,6 +1816,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
_defaultQueueProfile = options.Profile;
|
||||
DisableSubtitleDefault = options.DisableSubtitleDefault;
|
||||
RemoveForeignTracksByDefault = options.RemoveForeignAudioAndSubtitles;
|
||||
_lastAddFilesSettingsState = ((AddFilesSettingsViewModel)dialog.DataContext).CaptureState();
|
||||
SyncDefaultProfileFromList(_presetRowsForSetup());
|
||||
dialog.DialogResult = true;
|
||||
dialog.Close();
|
||||
@ -2151,7 +2188,7 @@ public sealed class ConversionViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
var profile = _profile.GetProfile(item.Profile) ?? ConversionProfileMapping.EmbyFallback;
|
||||
var profile = ResolveEffectiveProfile(item);
|
||||
var plan = _planService.Build(item.MediaAnalysis, item.Sidecars, profile, item.TaskOverride, item.ExternalAudioFiles);
|
||||
item.SetPlan(plan);
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json;
|
||||
@ -23,7 +22,8 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
||||
private string _selectedFilePath = string.Empty;
|
||||
private string _analysisStateText = string.Empty;
|
||||
private string _errorMessage = string.Empty;
|
||||
private string _rawJson = string.Empty;
|
||||
private string _formattedJson = string.Empty;
|
||||
private IReadOnlyList<string> _formattedJsonLines = Array.Empty<string>();
|
||||
private string _summaryText = string.Empty;
|
||||
private bool _isBusy;
|
||||
private bool _isVideoInfoDropHighlight;
|
||||
@ -44,41 +44,16 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
||||
|
||||
SelectFileCommand = new RelayCommand(ExecuteSelectFile);
|
||||
SelectSummaryFilesCommand = new RelayCommand(ExecuteSelectSummaryFiles);
|
||||
ExpandAllCommand = new RelayCommand(ExecuteExpandAll);
|
||||
CollapseAllCommand = new RelayCommand(ExecuteCollapseAll);
|
||||
CopyJsonCommand = new RelayCommand(ExecuteCopyJson, () => !string.IsNullOrWhiteSpace(_rawJson));
|
||||
SaveJsonCommand = new RelayCommand(ExecuteSaveJson, () => !string.IsNullOrWhiteSpace(_rawJson));
|
||||
CopySummaryCommand = new RelayCommand(ExecuteCopySummary, () => !string.IsNullOrWhiteSpace(SummaryText) && SummaryText != "Файл не проанализирован");
|
||||
SaveSummaryCommand = new RelayCommand(ExecuteSaveSummary, () => !string.IsNullOrWhiteSpace(SummaryText) && SummaryText != "Файл не проанализирован");
|
||||
CopyNodeValueCommand = new RelayCommand(ExecuteCopyNodeValue, CanCopyNodeValue);
|
||||
CopyNodeLineCommand = new RelayCommand(ExecuteCopyNodeLine, CanCopyNodeLine);
|
||||
CopyNodeWithChildrenCommand = new RelayCommand(ExecuteCopyNodeWithChildren, CanCopyNodeWithChildren);
|
||||
CopyNodePathCommand = new RelayCommand(ExecuteCopyNodePath, CanCopyNodePath);
|
||||
}
|
||||
|
||||
public ObservableCollection<JsonTreeNodeViewModel> JsonNodes { get; } = new();
|
||||
|
||||
public ICommand SelectFileCommand { get; }
|
||||
public ICommand SelectSummaryFilesCommand { get; }
|
||||
|
||||
public ICommand ExpandAllCommand { get; }
|
||||
|
||||
public ICommand CollapseAllCommand { get; }
|
||||
|
||||
public ICommand CopyJsonCommand { get; }
|
||||
|
||||
public ICommand SaveJsonCommand { get; }
|
||||
public ICommand CopySummaryCommand { get; }
|
||||
public ICommand SaveSummaryCommand { get; }
|
||||
|
||||
public ICommand CopyNodeValueCommand { get; }
|
||||
|
||||
public ICommand CopyNodeLineCommand { get; }
|
||||
|
||||
public ICommand CopyNodeWithChildrenCommand { get; }
|
||||
|
||||
public ICommand CopyNodePathCommand { get; }
|
||||
|
||||
public string SelectedFilePath
|
||||
{
|
||||
get => _selectedFilePath;
|
||||
@ -124,6 +99,37 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public string FormattedJson
|
||||
{
|
||||
get => _formattedJson;
|
||||
private set
|
||||
{
|
||||
if (_formattedJson == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_formattedJson = value;
|
||||
FormattedJsonLines = SplitTextLines(value);
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<string> FormattedJsonLines
|
||||
{
|
||||
get => _formattedJsonLines;
|
||||
private set
|
||||
{
|
||||
if (ReferenceEquals(_formattedJsonLines, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_formattedJsonLines = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsBusy
|
||||
{
|
||||
get => _isBusy;
|
||||
@ -307,9 +313,8 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
||||
|
||||
private async Task AnalyzeAsync()
|
||||
{
|
||||
JsonNodes.Clear();
|
||||
ErrorMessage = string.Empty;
|
||||
_rawJson = string.Empty;
|
||||
FormattedJson = string.Empty;
|
||||
RaiseCommandStates();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SelectedFilePath))
|
||||
@ -335,8 +340,7 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
||||
|
||||
try
|
||||
{
|
||||
_rawJson = result.Json;
|
||||
BuildTree(_rawJson);
|
||||
FormattedJson = FormatJsonOrFallback(result.Json);
|
||||
AnalysisStateText = "Готово";
|
||||
RaiseCommandStates();
|
||||
_logging.Info($"ffprobe завершен: {Path.GetFileName(SelectedFilePath)}", "video-info.ffprobe", command: result.Command, stdout: result.StdOut, stderr: result.StdErr);
|
||||
@ -427,125 +431,6 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
||||
RaiseCommandStates();
|
||||
}
|
||||
|
||||
private void BuildTree(string json)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
JsonNodes.Clear();
|
||||
|
||||
if (doc.RootElement.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
foreach (var property in doc.RootElement.EnumerateObject())
|
||||
{
|
||||
JsonNodes.Add(CreateNode(property.Name, property.Value, null));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonNodes.Add(CreateNode("root", doc.RootElement, null));
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonTreeNodeViewModel CreateNode(string name, JsonElement element, JsonTreeNodeViewModel? parent)
|
||||
{
|
||||
var node = new JsonTreeNodeViewModel(name, GetPreviewValue(element), element.GetRawText(), parent);
|
||||
|
||||
switch (element.ValueKind)
|
||||
{
|
||||
case JsonValueKind.Object:
|
||||
foreach (var prop in element.EnumerateObject())
|
||||
{
|
||||
node.Children.Add(CreateNode(prop.Name, prop.Value, node));
|
||||
}
|
||||
break;
|
||||
case JsonValueKind.Array:
|
||||
var index = 0;
|
||||
foreach (var item in element.EnumerateArray())
|
||||
{
|
||||
node.Children.Add(CreateNode($"[{index}]", item, node));
|
||||
index++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
private static string GetPreviewValue(JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => "{...}",
|
||||
JsonValueKind.Array => "[...]",
|
||||
JsonValueKind.String => element.GetString() ?? string.Empty,
|
||||
JsonValueKind.Number => element.GetRawText(),
|
||||
JsonValueKind.True => "true",
|
||||
JsonValueKind.False => "false",
|
||||
JsonValueKind.Null => "null",
|
||||
_ => element.GetRawText()
|
||||
};
|
||||
}
|
||||
|
||||
private void ExecuteExpandAll()
|
||||
{
|
||||
SetExpandedState(JsonNodes, true);
|
||||
}
|
||||
|
||||
private void ExecuteCollapseAll()
|
||||
{
|
||||
SetExpandedState(JsonNodes, false);
|
||||
}
|
||||
|
||||
private static void SetExpandedState(IEnumerable<JsonTreeNodeViewModel> nodes, bool isExpanded)
|
||||
{
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
node.IsExpanded = isExpanded;
|
||||
SetExpandedState(node.Children, isExpanded);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteCopyJson()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_rawJson))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Clipboard.SetText(_rawJson);
|
||||
_logging.Info("JSON скопирован в буфер обмена", "video-info.copy");
|
||||
}
|
||||
|
||||
private void ExecuteSaveJson()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_rawJson))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultName = string.IsNullOrWhiteSpace(SelectedFilePath)
|
||||
? "ffprobe.json"
|
||||
: $"{Path.GetFileNameWithoutExtension(SelectedFilePath)}.ffprobe.json";
|
||||
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Title = "Сохранить JSON ffprobe",
|
||||
Filter = "JSON (*.json)|*.json|Все файлы|*.*",
|
||||
FileName = defaultName,
|
||||
InitialDirectory = _recentPaths.GetInitialDirectory(RecentPathScenario.SettingsOutputFolder),
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
File.WriteAllText(dialog.FileName, _rawJson);
|
||||
_recentPaths.RememberChosenFolder(
|
||||
RecentPathScenario.SettingsOutputFolder,
|
||||
Path.GetDirectoryName(dialog.FileName) ?? dialog.FileName);
|
||||
_logging.Info($"JSON сохранен: {dialog.FileName}", "video-info.save");
|
||||
}
|
||||
|
||||
private void ExecuteCopySummary()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SummaryText) || SummaryText == "Файл не проанализирован")
|
||||
@ -594,105 +479,8 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
||||
|
||||
private void RaiseCommandStates()
|
||||
{
|
||||
(CopyJsonCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(SaveJsonCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(CopySummaryCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(SaveSummaryCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(CopyNodeValueCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(CopyNodeLineCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(CopyNodeWithChildrenCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
(CopyNodePathCommand as RelayCommand)?.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
private static JsonTreeNodeViewModel? AsNode(object? parameter)
|
||||
{
|
||||
return parameter as JsonTreeNodeViewModel;
|
||||
}
|
||||
|
||||
private bool CanCopyNodeValue(object? parameter)
|
||||
{
|
||||
var node = AsNode(parameter);
|
||||
return node is not null && node.Children.Count == 0;
|
||||
}
|
||||
|
||||
private bool CanCopyNodeLine(object? parameter)
|
||||
{
|
||||
return AsNode(parameter) is not null;
|
||||
}
|
||||
|
||||
private bool CanCopyNodeWithChildren(object? parameter)
|
||||
{
|
||||
return AsNode(parameter) is not null;
|
||||
}
|
||||
|
||||
private bool CanCopyNodePath(object? parameter)
|
||||
{
|
||||
return AsNode(parameter) is not null;
|
||||
}
|
||||
|
||||
private void ExecuteCopyNodeValue(object? parameter)
|
||||
{
|
||||
var node = AsNode(parameter);
|
||||
if (node is null || node.Children.Count > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Clipboard.SetText(node.Value);
|
||||
_logging.Info("узел JSON скопирован", "video-info.copy");
|
||||
}
|
||||
|
||||
private void ExecuteCopyNodeLine(object? parameter)
|
||||
{
|
||||
var node = AsNode(parameter);
|
||||
if (node is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Clipboard.SetText($"{node.Name}: {node.Value}");
|
||||
_logging.Info("узел JSON скопирован", "video-info.copy");
|
||||
}
|
||||
|
||||
private void ExecuteCopyNodeWithChildren(object? parameter)
|
||||
{
|
||||
var node = AsNode(parameter);
|
||||
if (node is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var formatted = FormatJsonOrFallback(node.SubtreeJson);
|
||||
Clipboard.SetText(formatted);
|
||||
_logging.Info("узел JSON скопирован", "video-info.copy");
|
||||
}
|
||||
|
||||
private void ExecuteCopyNodePath(object? parameter)
|
||||
{
|
||||
var node = AsNode(parameter);
|
||||
if (node is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Clipboard.SetText(BuildNodePath(node));
|
||||
_logging.Info("узел JSON скопирован", "video-info.copy");
|
||||
}
|
||||
|
||||
private static string BuildNodePath(JsonTreeNodeViewModel node)
|
||||
{
|
||||
if (node.Parent is null)
|
||||
{
|
||||
return node.Name;
|
||||
}
|
||||
|
||||
var parentPath = BuildNodePath(node.Parent);
|
||||
if (node.Name.StartsWith("[", StringComparison.Ordinal))
|
||||
{
|
||||
return $"{parentPath}{node.Name}";
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(parentPath) ? node.Name : $"{parentPath}.{node.Name}";
|
||||
}
|
||||
|
||||
private static string FormatJsonOrFallback(string json)
|
||||
@ -707,5 +495,15 @@ public sealed class VideoInfoViewModel : INotifyPropertyChanged
|
||||
return json;
|
||||
}
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> SplitTextLines(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return text.ReplaceLineEndings("\n").Split('\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,96 +1,178 @@
|
||||
<Window x:Class="EmbyToolbox.Views.AddFilesOptionsDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="clr-namespace:EmbyToolbox.Converters"
|
||||
Title="Параметры добавления"
|
||||
Width="520"
|
||||
Height="230"
|
||||
ResizeMode="NoResize"
|
||||
Width="620"
|
||||
Height="537"
|
||||
MinWidth="600"
|
||||
MinHeight="460"
|
||||
ResizeMode="CanResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="{DynamicResource Ui.Brush.Surface}">
|
||||
<Grid Margin="14">
|
||||
<Window.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
<Style x:Key="AddSettingsLabel" TargetType="TextBlock" BasedOn="{StaticResource UiTextCaption}">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="Margin" Value="0,0,6,3" />
|
||||
</Style>
|
||||
<Style x:Key="AddSettingsCombo" TargetType="ComboBox" BasedOn="{StaticResource UiCombo}">
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="MinHeight" Value="26" />
|
||||
<Setter Property="Margin" Value="0,0,8,6" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style x:Key="AddSettingsTextBox" TargetType="TextBox" BasedOn="{StaticResource UiTextInput}">
|
||||
<Setter Property="Height" Value="26" />
|
||||
<Setter Property="MinHeight" Value="26" />
|
||||
<Setter Property="Margin" Value="0,0,8,6" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style x:Key="AddSettingsGroup" TargetType="GroupBox">
|
||||
<Setter Property="Padding" Value="6,4,6,4" />
|
||||
<Setter Property="Margin" Value="0,0,0,6" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource Ui.Brush.BorderSubtle}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="16" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0">
|
||||
<Grid Grid.Row="0" Margin="0,0,0,6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="220" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="Профиль"
|
||||
Style="{StaticResource UiTextCaption}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,12,0" />
|
||||
<TextBlock Grid.Column="0" Text="Профиль:" Style="{StaticResource AddSettingsLabel}" Margin="0,0,8,0" />
|
||||
<ComboBox Grid.Column="1"
|
||||
Height="32"
|
||||
Style="{StaticResource UiCombo}"
|
||||
Style="{StaticResource AddSettingsCombo}"
|
||||
Margin="0"
|
||||
ItemsSource="{Binding Profiles}"
|
||||
SelectedItem="{Binding SelectedProfile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Profile, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding Profile}" TextTrimming="CharacterEllipsis" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="2"
|
||||
Height="32"
|
||||
VerticalAlignment="Top">
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel>
|
||||
<GroupBox Header="Видео" Style="{StaticResource AddSettingsGroup}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Контейнер" Style="{StaticResource AddSettingsLabel}" />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="Видео codec" Style="{StaticResource AddSettingsLabel}" />
|
||||
<ComboBox Grid.Row="1" Grid.Column="0" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding ContainerOptions}" SelectedItem="{Binding Container, Mode=TwoWay}" />
|
||||
<ComboBox Grid.Row="1" Grid.Column="1" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding VideoCodecOptions}" SelectedItem="{Binding Video, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Pixel format" Style="{StaticResource AddSettingsLabel}" />
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="Resolution" Style="{StaticResource AddSettingsLabel}" />
|
||||
<ComboBox Grid.Row="3" Grid.Column="0" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding PixelFormatOptions}" SelectedItem="{Binding PixelFormat, Mode=TwoWay}" />
|
||||
<ComboBox Grid.Row="3" Grid.Column="1" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding ResolutionOptions}" SelectedItem="{Binding Resolution, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="FPS" Style="{StaticResource AddSettingsLabel}" />
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Text="Видеобитрейт" Style="{StaticResource AddSettingsLabel}" />
|
||||
<ComboBox Grid.Row="5" Grid.Column="0" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding FpsOptions}" SelectedItem="{Binding Fps, Mode=TwoWay}" />
|
||||
<Grid Grid.Row="5" Grid.Column="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="86" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ComboBox Grid.Column="0" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding VideoBitrateOptions}" SelectedItem="{Binding VideoBitrateMode, Mode=TwoWay}" />
|
||||
<Grid Grid.Column="1" Visibility="{Binding IsVideoBitrateCustomVisible, Converter={StaticResource BoolToVis}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Grid.Column="0"
|
||||
Style="{StaticResource AddSettingsTextBox}"
|
||||
Text="{Binding VideoBitrateCustomMbps, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Header="Аудио" Style="{StaticResource AddSettingsGroup}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Audio codec" Style="{StaticResource AddSettingsLabel}" />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="Audio bitrate" Style="{StaticResource AddSettingsLabel}" />
|
||||
<ComboBox Grid.Row="1" Grid.Column="0" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding AudioCodecOptions}" SelectedItem="{Binding Audio, Mode=TwoWay}" />
|
||||
<ComboBox Grid.Row="1" Grid.Column="1" Style="{StaticResource AddSettingsCombo}" ItemsSource="{Binding AudioBitrateOptions}" SelectedItem="{Binding AudioBitrate, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<GroupBox Grid.Column="0" Header="Внешние дорожки" Style="{StaticResource AddSettingsGroup}">
|
||||
<WrapPanel>
|
||||
<CheckBox Content="Внешние аудио" IsChecked="{Binding ExternalTracks, Mode=TwoWay}" Margin="0,0,14,4" />
|
||||
<CheckBox Content="Внешние субтитры" IsChecked="{Binding ExternalSubtitles, Mode=TwoWay}" Margin="0,0,14,4" />
|
||||
<CheckBox Content="Внешние шрифты" IsChecked="{Binding Fonts, Mode=TwoWay}" Margin="0,0,14,4" />
|
||||
</WrapPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Grid.Column="2" Header="Прочие настройки" Style="{StaticResource AddSettingsGroup}">
|
||||
<WrapPanel>
|
||||
<CheckBox Content="Удалять иностранные дорожки" IsChecked="{Binding RemoveForeignTracks, Mode=TwoWay}" Margin="0,0,14,4" />
|
||||
<CheckBox Content="Отключать субтитры" IsChecked="{Binding DisableSubtitleDefault, Mode=TwoWay}" Margin="0,0,14,4" />
|
||||
</WrapPanel>
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="2" Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="18" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="24" />
|
||||
<ColumnDefinition Width="18" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<CheckBox Grid.Column="0"
|
||||
Width="18"
|
||||
Height="18"
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding DisableSubtitleDefault, Mode=TwoWay}" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="Отключать субтитры"
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding ValidationMessage}"
|
||||
Foreground="{DynamicResource Ui.Brush.ErrorText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,0,0,0"
|
||||
FontSize="12" />
|
||||
|
||||
<CheckBox Grid.Column="3"
|
||||
Width="18"
|
||||
Height="18"
|
||||
Margin="0"
|
||||
VerticalAlignment="Center"
|
||||
IsChecked="{Binding RemoveForeignAudioAndSubtitles, Mode=TwoWay}" />
|
||||
<TextBlock Grid.Column="4"
|
||||
Text="Удалять иностранные дорожки"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,0,0,0"
|
||||
FontSize="12" />
|
||||
Visibility="{Binding HasValidationMessage, Converter={StaticResource BoolToVis}}" />
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||
<Button MinWidth="96"
|
||||
Margin="0,0,8,0"
|
||||
Style="{StaticResource UiButtonPrimary}"
|
||||
Foreground="White"
|
||||
Content="Добавить"
|
||||
IsDefault="True"
|
||||
Command="{Binding AddCommand}" />
|
||||
<Button MinWidth="96"
|
||||
Style="{StaticResource UiButtonSecondary}"
|
||||
Content="Отмена"
|
||||
IsCancel="True"
|
||||
Command="{Binding CancelCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||
<Button MinWidth="100"
|
||||
Margin="0,0,8,0"
|
||||
Style="{StaticResource UiButtonPrimary}"
|
||||
Foreground="White"
|
||||
Content="Добавить"
|
||||
IsDefault="True"
|
||||
Command="{Binding AddCommand}" />
|
||||
<Button MinWidth="100"
|
||||
Style="{StaticResource UiButtonSecondary}"
|
||||
Content="Отмена"
|
||||
IsCancel="True"
|
||||
Command="{Binding CancelCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user