Compare commits

...

7 Commits
1.8 ... 2.0

Author SHA1 Message Date
Frank Skare
3ccb80f359 - 2019-03-27 21:41:44 +01:00
Frank Skare
636e36583d - 2019-03-27 02:40:31 +01:00
Frank Skare
d7617bda6d 1.9 2019-03-27 02:38:56 +01:00
Frank Skare
12f1d70969 - 2019-03-27 02:35:46 +01:00
Frank Skare
f7b908a5c4 - 2019-03-27 01:57:33 +01:00
Frank Skare
1080d0afd7 - 2019-03-27 01:52:51 +01:00
Frank Skare
77ba7f105c - 2019-03-25 23:25:51 +01:00
32 changed files with 582 additions and 479 deletions

View File

@@ -23,7 +23,7 @@ Public Class CSScriptAddon
Try Try
CSScriptLibrary.CSScript.Evaluator.LoadCode(File.ReadAllText(i)) CSScriptLibrary.CSScript.Evaluator.LoadCode(File.ReadAllText(i))
Catch ex As Exception Catch ex As Exception
MsgError(ex.ToString) MainForm.Instance.ShowMsgBox(ex.ToString(), MessageBoxIcon.Error)
End Try End Try
Next Next
End Sub End Sub

View File

@@ -10,11 +10,13 @@ Table of contents
------- -------
- [Features](#features) - [Features](#features)
- [Screenshots](#Screenshots)
- [Context Menu](#context-menu) - [Context Menu](#context-menu)
- [Settings](#settings) - [Settings](#settings)
- [C# Scripting](#cs-scripting) - [C# Scripting](#cs-scripting)
- [Python Scripting](#python-scripting) - [Python Scripting](#python-scripting)
- [PowerShell Scripting](#powershell-scripting) - [PowerShell Scripting](#powershell-scripting)
- [Support](#Support)
- [Changelog](#changelog) - [Changelog](#changelog)
### Features ### Features
@@ -24,7 +26,11 @@ Table of contents
- 5 different scripting languages are supported, Python scripting implemented with IronPython, C# implemented with CS-Script, Lua and JavaScript implemented in libmpv and PowerShell - 5 different scripting languages are supported, Python scripting implemented with IronPython, C# implemented with CS-Script, Lua and JavaScript implemented in libmpv and PowerShell
- mpv's OSC, IPC, conf files and more - mpv's OSC, IPC, conf files and more
![](https://raw.githubusercontent.com/stax76/mpv.net/master/screenshot.png) ### Screenshots
![](https://raw.githubusercontent.com/stax76/mpv.net/master/screenshots/screenshot.png)
![](https://raw.githubusercontent.com/stax76/mpv.net/master/screenshots/mpvSettingsEditor.png)
### Context Menu ### Context Menu
@@ -130,8 +136,33 @@ $position = [mp]::get_property_number("time-pos");
``` ```
Please note that PowerShell don't allow assigning to events and mpv.net uses as workaround a matching script filename, a list of available events can be found in the mpv manual or in the file mp.cs in the mpv.net source code. Please note that PowerShell don't allow assigning to events and mpv.net uses as workaround a matching script filename, a list of available events can be found in the mpv manual or in the file mp.cs in the mpv.net source code.
### Support
<https://forum.doom9.org/showthread.php?t=174841>
<https://forum.videohelp.com/threads/392514-mpv-net-a-extendable-media-player-for-windows>
<https://github.com/stax76/mpv.net/issues>
### Changelog ### Changelog
### soon
- setting track-auto-selection added to settings editor (<https://mpv.io/manual/master/#options-track-auto-selection>)
- setting loop-playlist added to settings editor (<https://mpv.io/manual/master/#options-loop-playlist>)
- setting audio-file-auto added to settings editor (<https://mpv.io/manual/master/#options-audio-file-auto>)
- setting video-sync added to settings editor (<https://mpv.io/manual/master/#options-video-sync>)
- command execute-mpv-command added to menu: Tools > Enter a mpv command for execution
- added youtube-dl.exe, please note this will only work when a certain Visual C++ runtime is installed
- added drag & drop support to drag & drop a youtube URL on mpv.net
- added support to open a youtube URL from command line
- added support for opening a URL from the menu: Open > Open URL
### 1.9
- improved settings editor
- all info and error messages are shown now on the main window thread having the main window as parent
### 1.8 ### 1.8
- new config editor added - new config editor added

View File

@@ -5,8 +5,6 @@ using System.ComponentModel.Composition.Hosting;
using System.IO; using System.IO;
using System.Windows.Forms; using System.Windows.Forms;
using static mpvnet.StaticUsing;
namespace mpvnet namespace mpvnet
{ {
public class Addon public class Addon
@@ -42,7 +40,7 @@ namespace mpvnet
} }
catch (Exception e) catch (Exception e)
{ {
MsgError(e.ToString()); MainForm.Instance.ShowMsgBox(e.ToString(), MessageBoxIcon.Error);
} }
} }
} }

View File

@@ -5,8 +5,6 @@ using System.IO;
using System.Reflection; using System.Reflection;
using System.Windows.Forms; using System.Windows.Forms;
using static mpvnet.StaticUsing;
namespace mpvnet namespace mpvnet
{ {
public class Command public class Command
@@ -72,12 +70,7 @@ namespace mpvnet
public static void show_conf_editor(string[] args) public static void show_conf_editor(string[] args)
{ {
using (var p = new Process()) Process.Start(Application.StartupPath + "\\mpvSettingsEditor.exe");
{
p.StartInfo.FileName = Application.StartupPath + "\\mpvSettingsEditor.exe";
p.StartInfo.WorkingDirectory = Path.GetDirectoryName(Application.ExecutablePath);
p.Start();
}
} }
public static void history(string[] args) public static void history(string[] args)
@@ -87,7 +80,7 @@ namespace mpvnet
if (File.Exists(fp)) if (File.Exists(fp))
Process.Start(fp); Process.Start(fp);
else else
if (MsgQuestion("Create history.txt file in config folder?\n\nmpv.net will write the date, time and filename of opened files to it.") == DialogResult.OK) if (MainForm.Instance.ShowMsgBox("Create history.txt file in config folder?\n\nmpv.net will write the date, time and filename of opened files to it.", MessageBoxIcon.Question) == DialogResult.OK)
File.WriteAllText(fp, ""); File.WriteAllText(fp, "");
} }
@@ -121,59 +114,83 @@ namespace mpvnet
public static void show_info(string[] args) public static void show_info(string[] args)
{ {
var fileInfo = new FileInfo(mp.get_property_string("path")); try
using (var mediaInfo = new MediaInfo(fileInfo.FullName))
{ {
string width = mediaInfo.GetInfo(MediaInfoStreamKind.Video, "Width"); var fileInfo = new FileInfo(mp.get_property_string("path"));
if (width == "") using (var mediaInfo = new MediaInfo(fileInfo.FullName))
{ {
string performer = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Performer"); string width = mediaInfo.GetInfo(MediaInfoStreamKind.Video, "Width");
string title = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Title");
string album = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Album");
string genre = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Genre");
string date = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Recorded_Date");
string duration = mediaInfo.GetInfo(MediaInfoStreamKind.Audio, "Duration/String");
string text = ""; if (width == "")
{
string performer = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Performer");
string title = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Title");
string album = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Album");
string genre = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Genre");
string date = mediaInfo.GetInfo(MediaInfoStreamKind.General, "Recorded_Date");
string duration = mediaInfo.GetInfo(MediaInfoStreamKind.Audio, "Duration/String");
if (performer != "") text += "Artist: " + performer + "\n"; string text = "";
if (title != "") text += "Title: " + title + "\n";
if (album != "") text += "Album: " + album + "\n";
if (genre != "") text += "Genre: " + genre + "\n";
if (date != "") text += "Year: " + date + "\n";
if (duration != "") text += "Length: " + duration + "\n";
mp.commandv("show-text", text, "5000"); if (performer != "") text += "Artist: " + performer + "\n";
if (title != "") text += "Title: " + title + "\n";
if (album != "") text += "Album: " + album + "\n";
if (genre != "") text += "Genre: " + genre + "\n";
if (date != "") text += "Year: " + date + "\n";
if (duration != "") text += "Length: " + duration + "\n";
mp.commandv("show-text", text, "5000");
}
else
{
string height = mediaInfo.GetInfo(MediaInfoStreamKind.Video, "Height");
TimeSpan position = TimeSpan.FromSeconds(mp.get_property_number("time-pos"));
TimeSpan duration = TimeSpan.FromSeconds(mp.get_property_number("duration"));
string bitrate = mediaInfo.GetInfo(MediaInfoStreamKind.Video, "BitRate");
if (bitrate == "")
bitrate = "0";
var bitrate2 = Convert.ToDouble(bitrate) / 1000.0 / 1000.0;
var videoCodec = mp.get_property_string("video-format").ToUpper();
var filename = fileInfo.Name;
var text =
FormatTime(position.TotalMinutes) + ":" +
FormatTime(position.Seconds) + " / " +
FormatTime(duration.TotalMinutes) + ":" +
FormatTime(duration.Seconds) + "\n" +
Convert.ToInt32(fileInfo.Length / 1024 / 1024).ToString() +
$" MB - {width} x {height}\n{videoCodec} - {bitrate2.ToString("f1")} Mb/s" + "\n" + filename;
mp.commandv("show-text", text, "5000");
}
string FormatTime(double value) => ((int)value).ToString("00");
} }
else
{
string height = mediaInfo.GetInfo(MediaInfoStreamKind.Video, "Height");
TimeSpan position = TimeSpan.FromSeconds(mp.get_property_number("time-pos"));
TimeSpan duration = TimeSpan.FromSeconds(mp.get_property_number("duration"));
string bitrate = mediaInfo.GetInfo(MediaInfoStreamKind.Video, "BitRate");
if (bitrate == "")
bitrate = "0";
var bitrate2 = Convert.ToDouble(bitrate) / 1000.0 / 1000.0;
var videoCodec = mp.get_property_string("video-format").ToUpper();
var filename = fileInfo.Name;
var text =
FormatTime(position.TotalMinutes) + ":" +
FormatTime(position.Seconds) + " / " +
FormatTime(duration.TotalMinutes) + ":" +
FormatTime(duration.Seconds) + "\n" +
Convert.ToInt32(fileInfo.Length / 1024 / 1024).ToString() +
$" MB - {width} x {height}\n{videoCodec} - {bitrate2.ToString("f1")} Mb/s" + "\n" + filename;
mp.commandv("show-text", text, "5000");
}
string FormatTime(double value) => ((int)value).ToString("00");
} }
catch (Exception)
{
}
}
public static void execute_mpv_command(string[] args)
{
MainForm.Instance.Invoke(new Action(() => {
string command = Microsoft.VisualBasic.Interaction.InputBox("Enter a mpv command to be executed.");
if (string.IsNullOrEmpty(command)) return;
mp.command_string(command, false);
}));
}
public static void open_url(string[] args)
{
MainForm.Instance.Invoke(new Action(() => {
string command = Microsoft.VisualBasic.Interaction.InputBox("Enter URL to be opened.");
if (string.IsNullOrEmpty(command)) return;
mp.LoadURL(command);
}));
} }
} }
} }

View File

@@ -1,30 +0,0 @@
using System.Collections.Generic;
using System.Linq;
namespace mpvnet
{
public static class Extensions
{
public static string Join(this IEnumerable<string> instance, string delimiter, bool removeEmpty = false)
{
if (instance == null)
return null;
bool containsEmpty = false;
foreach (var i in instance)
{
if (string.IsNullOrEmpty(i))
{
containsEmpty = true;
break;
}
}
if (containsEmpty && removeEmpty)
instance = instance.Where(arg => !string.IsNullOrEmpty(arg));
return string.Join(delimiter, instance);
}
}
}

View File

@@ -5,10 +5,7 @@ using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
using System.Diagnostics; using System.Diagnostics;
using static mpvnet.StaticUsing;
using System.Linq; using System.Linq;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace mpvnet namespace mpvnet
{ {
@@ -46,7 +43,7 @@ namespace mpvnet
} }
catch (Exception e) catch (Exception e)
{ {
MsgError(e.ToString()); MainForm.Instance.ShowMsgBox(e.ToString(), MessageBoxIcon.Error);
} }
} }
@@ -139,20 +136,15 @@ namespace mpvnet
public void BuildMenu() public void BuildMenu()
{ {
foreach (var i in File.ReadAllText(mp.InputConfPath).SplitLinesNoEmpty()) foreach (var i in File.ReadAllText(mp.InputConfPath).Split("\r\n".ToCharArray(), StringSplitOptions.RemoveEmptyEntries))
{ {
if (!i.Contains("#menu:")) if (!i.Contains("#menu:")) continue;
continue; var left = i.Substring(0, i.IndexOf("#menu:")).Trim();
if (left.StartsWith("#")) continue;
var left = i.Left("#menu:").Trim(); var cmd = left.Substring(left.IndexOf(" ") + 1).Trim();
var menu = i.Substring(i.IndexOf("#menu:") + "#menu:".Length).Trim();
if (left.StartsWith("#")) var key = menu.Substring(0, menu.IndexOf(";")).Trim();
continue; var path = menu.Substring(menu.IndexOf(";") + 1).Trim();
var cmd = left.Right(" ").Trim();
var menu = i.Right("#menu:").Trim();
var key = menu.Left(";").Trim();
var path = menu.Right(";").Trim();
if (path == "" || cmd == "") if (path == "" || cmd == "")
continue; continue;
@@ -164,7 +156,7 @@ namespace mpvnet
} }
catch (Exception e) catch (Exception e)
{ {
MsgError(e.ToString()); MainForm.Instance.ShowMsgBox(e.ToString(), MessageBoxIcon.Error);
} }
}); });
@@ -206,7 +198,7 @@ namespace mpvnet
private void Application_ThreadException(object sender, ThreadExceptionEventArgs e) private void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
{ {
MsgError(e.Exception.ToString()); ShowMsgBox(e.Exception.ToString(), MessageBoxIcon.Error);
} }
private void mp_VideoSizeChanged() private void mp_VideoSizeChanged()
@@ -219,10 +211,7 @@ namespace mpvnet
BeginInvoke(new Action(() => Close())); BeginInvoke(new Action(() => Close()));
} }
public bool IsFullscreen public bool IsFullscreen => WindowState == FormWindowState.Maximized;
{
get => WindowState == FormWindowState.Maximized;
}
void mp_ChangeFullscreen(bool value) void mp_ChangeFullscreen(bool value)
{ {
@@ -307,16 +296,17 @@ namespace mpvnet
{ {
base.OnDragEnter(e); base.OnDragEnter(e);
if (e.Data.GetDataPresent(DataFormats.FileDrop)) if (e.Data.GetDataPresent(DataFormats.FileDrop) || e.Data.GetDataPresent(DataFormats.Text))
e.Effect = DragDropEffects.Copy; e.Effect = DragDropEffects.Copy;
} }
protected override void OnDragDrop(DragEventArgs e) protected override void OnDragDrop(DragEventArgs e)
{ {
base.OnDragDrop(e); base.OnDragDrop(e);
if (e.Data.GetDataPresent(DataFormats.FileDrop)) if (e.Data.GetDataPresent(DataFormats.FileDrop))
mp.LoadFiles(e.Data.GetData(DataFormats.FileDrop) as String[]); mp.LoadFiles(e.Data.GetData(DataFormats.FileDrop) as String[]);
if (e.Data.GetDataPresent(DataFormats.Text))
mp.LoadURL(e.Data.GetData(DataFormats.Text).ToString());
} }
protected override void OnMouseDown(MouseEventArgs e) protected override void OnMouseDown(MouseEventArgs e)

View File

@@ -10,10 +10,8 @@ namespace mpvnet
{ {
public static readonly string[] FileTypes = "264 265 3gp aac ac3 avc avi avs bmp divx dts dtshd dtshr dtsma eac3 evo flac flv h264 h265 hevc hvc jpg jpeg m2t m2ts m2v m4a m4v mka mkv mlp mov mp2 mp3 mp4 mpa mpeg mpg mpv mts ogg ogm opus pcm png pva raw rmvb thd thd+ac3 true-hd truehd ts vdr vob vpy w64 wav webm wmv y4m".Split(' '); public static readonly string[] FileTypes = "264 265 3gp aac ac3 avc avi avs bmp divx dts dtshd dtshr dtsma eac3 evo flac flv h264 h265 hevc hvc jpg jpeg m2t m2ts m2v m4a m4v mka mkv mlp mov mp2 mp3 mp4 mpa mpeg mpg mpv mts ogg ogm opus pcm png pva raw rmvb thd thd+ac3 true-hd truehd ts vdr vob vpy w64 wav webm wmv y4m".Split(' ');
public static string GetFilter(IEnumerable<string> values) public static string GetFilter(IEnumerable<string> values) => "*." +
{ String.Join(";*.", values) + "|*." + String.Join(";*.", values) + "|All Files|*.*";
return "*." + values.Join(";*.") + "|*." + values.Join(";*.") + "|All Files|*.*";
}
} }
public class StringLogicalComparer : IComparer, IComparer<string> public class StringLogicalComparer : IComparer, IComparer<string>
@@ -27,34 +25,13 @@ namespace mpvnet
int IComparer<string>.Compare(string x, string y) => IComparerOfString_Compare(x, y); int IComparer<string>.Compare(string x, string y) => IComparerOfString_Compare(x, y);
} }
public class StaticUsing public class OSVersion
{ {
public static void MsgInfo(string message) public static float Windows7 { get; } = 6.1f;
{ public static float Windows8 { get; } = 6.2f;
MessageBox.Show(message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information); public static float Windows81 { get; } = 6.3f;
} public static float Windows10 { get; } = 10f;
public static void MsgError(string message) public static float Current => Environment.OSVersion.Version.Major + Environment.OSVersion.Version.Minor / 10f;
{
MessageBox.Show(message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
public static DialogResult MsgQuestion(string message)
{
return MessageBox.Show(message, Application.ProductName, MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
}
} }
//public class OSVersion
//{
// public static float Windows7 { get; set; } = 6.1f;
// public static float Windows8 { get; set; } = 6.2f;
// public static float Windows81 { get; set; } = 6.3f;
// public static float Windows10 { get; set; } = 10f;
// public static float Current
// {
// get => Environment.OSVersion.Version.Major + Environment.OSVersion.Version.Minor / 10f;
// }
//}
} }

View File

@@ -59,25 +59,10 @@ namespace mpvnet
Bottom = bottom; Bottom = bottom;
} }
public Rectangle ToRectangle() public Rectangle ToRectangle() { return Rectangle.FromLTRB(Left, Top, Right, Bottom); }
{ public Size Size => new Size(Right - Left, Bottom - Top);
return Rectangle.FromLTRB(Left, Top, Right, Bottom); public int Width => Right - Left;
} public int Height => Bottom - Top;
public Size Size
{
get => new Size(Right - Left, Bottom - Top);
}
public int Width
{
get => Right - Left;
}
public int Height
{
get => Bottom - Top;
}
} }
} }
} }

View File

@@ -2,10 +2,9 @@
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Management.Automation.Runspaces; using System.Management.Automation.Runspaces;
using static mpvnet.StaticUsing;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms;
namespace mpvnet namespace mpvnet
{ {
@@ -50,10 +49,10 @@ Using namespace System;
} }
catch catch
{ {
MsgError("PowerShell Setup Problem\r\n\r\nEnsure you have at least PowerShell 5.1 installed."); MainForm.Instance.ShowMsgBox("PowerShell Setup Problem\n\nEnsure you have at least PowerShell 5.1 installed.", MessageBoxIcon.Error);
return null; return null;
} }
MsgError(ex.ToString()); MainForm.Instance.ShowMsgBox(ex.ToString(), MessageBoxIcon.Error);
} }
} }
} }

View File

@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.8.0.0")] [assembly: AssemblyVersion("1.9.0.0")]
[assembly: AssemblyFileVersion("1.8.0.0")] [assembly: AssemblyFileVersion("1.9.0.0")]

View File

@@ -69,7 +69,8 @@ namespace mpvnet.Properties {
/// Enter cycle pause /// Enter cycle pause
/// s stop #menu: S ; Stop /// s stop #menu: S ; Stop
/// _ ignore #menu: _ ; - /// _ ignore #menu: _ ; -
/// f cycle fullscreen #menu: F ; Toggle Fullscreen /// [rest of string was truncated]&quot;;. /// f cycle fullscreen #menu: F ; Toggle Fullscreen
/// [rest of string was truncated]&quot;;.
/// </summary> /// </summary>
internal static string input_conf { internal static string input_conf {
get { get {

View File

@@ -1,9 +1,9 @@
using System; using System;
using System.Reflection; using System.Reflection;
using System.Windows.Forms;
using IronPython.Hosting; using IronPython.Hosting;
using Microsoft.Scripting.Hosting; using Microsoft.Scripting.Hosting;
using static mpvnet.StaticUsing;
using PyRT = IronPython.Runtime; using PyRT = IronPython.Runtime;
namespace mpvnet namespace mpvnet
@@ -27,7 +27,7 @@ namespace mpvnet
} }
catch (Exception ex) catch (Exception ex)
{ {
MsgError(ex.ToString()); MainForm.Instance.ShowMsgBox(ex.ToString(), MessageBoxIcon.Error);
} }
} }
} }

View File

@@ -1,6 +1,7 @@
# mpv.net key bindings, mouse bindings and context menu configuration # mpv.net key bindings, mouse bindings and context menu configuration
o script-message mpv.net open-files #menu: O ; Open Files... o script-message mpv.net open-files #menu: O ; Open > Open Files...
u script-message mpv.net open-url #menu: U ; Open > Open URL...
_ ignore #menu: _ ; - _ ignore #menu: _ ; -
Space cycle pause #menu: Space, Enter ; Play/Pause Space cycle pause #menu: Space, Enter ; Play/Pause
Enter cycle pause Enter cycle pause
@@ -113,6 +114,7 @@
Ctrl+H cycle-values hwdec "auto" "no" #menu: Ctrl+H ; Tools > Cycle Hardware Decoding Ctrl+H cycle-values hwdec "auto" "no" #menu: Ctrl+H ; Tools > Cycle Hardware Decoding
F8 show-text ${playlist} 5000 #menu: F8 ; Tools > Show Playlist F8 show-text ${playlist} 5000 #menu: F8 ; Tools > Show Playlist
F9 show-text ${track-list} 5000 #menu: F9 ; Tools > Show Audio/Video/Subtitle List F9 show-text ${track-list} 5000 #menu: F9 ; Tools > Show Audio/Video/Subtitle List
_ script-message mpv.net execute-mpv-command #menu: _ ; Tools > Enter a mpv command for execution...
_ script-message mpv.net shell-execute https://mpv.io/manual/stable/ #menu: _ ; Help > Show mpv manual _ script-message mpv.net shell-execute https://mpv.io/manual/stable/ #menu: _ ; Help > Show mpv manual
_ script-message mpv.net shell-execute https://github.com/mpv-player/mpv/blob/master/etc/input.conf #menu: _ ; Help > Show mpv default keys _ script-message mpv.net shell-execute https://github.com/mpv-player/mpv/blob/master/etc/input.conf #menu: _ ; Help > Show mpv default keys

View File

@@ -1,120 +0,0 @@
using System;
using System.Linq;
using System.IO;
public static class StringExtensions
{
public static string ExtFull(this string filepath)
{
return Ext(filepath, true);
}
public static string Ext(this string filepath)
{
return Ext(filepath, false);
}
public static string Ext(this string filepath, bool dot)
{
if (string.IsNullOrEmpty(filepath))
return "";
var chars = filepath.ToCharArray();
for (var x = filepath.Length - 1; x >= 0; x += -1)
{
if (chars[x] == Path.DirectorySeparatorChar)
return "";
if (chars[x] == '.')
return filepath.Substring(x + (dot ? 0 : 1)).ToLower();
}
return "";
}
public static string Left(this string value, int index)
{
if (string.IsNullOrEmpty(value) || index < 0)
return "";
if (index > value.Length)
return value;
return value.Substring(0, index);
}
public static string Left(this string value, string start)
{
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(start))
return "";
if (!value.Contains(start))
return "";
return value.Substring(0, value.IndexOf(start));
}
public static string LeftLast(this string value, string start)
{
if (!value.Contains(start))
return "";
return value.Substring(0, value.LastIndexOf(start));
}
public static string Right(this string value, string start)
{
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(start))
return "";
if (!value.Contains(start))
return "";
return value.Substring(value.IndexOf(start) + start.Length);
}
public static string RightLast(this string value, string start)
{
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(start))
return "";
if (!value.Contains(start))
return "";
return value.Substring(value.LastIndexOf(start) + start.Length);
}
public static string[] SplitNoEmpty(this string value, params string[] delimiters)
{
return value.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);
}
public static string[] SplitKeepEmpty(this string value, params string[] delimiters)
{
return value.Split(delimiters, StringSplitOptions.None);
}
public static string[] SplitNoEmptyAndWhiteSpace(this string value, params string[] delimiters)
{
if (string.IsNullOrEmpty(value))
return null;
var a = SplitNoEmpty(value, delimiters);
for (var i = 0; i <= a.Length - 1; i++)
a[i] = a[i].Trim();
var l = a.ToList();
while (l.Contains(""))
l.Remove("");
return l.ToArray();
}
public static string[] SplitLinesNoEmpty(this string value)
{
return SplitNoEmpty(value, Environment.NewLine);
}
}

View File

@@ -13,7 +13,6 @@ using System.Windows.Forms;
using static mpvnet.libmpv; using static mpvnet.libmpv;
using static mpvnet.Native; using static mpvnet.Native;
using static mpvnet.StaticUsing;
using PyRT = IronPython.Runtime; using PyRT = IronPython.Runtime;
@@ -61,7 +60,7 @@ namespace mpvnet
public static string mpvConfFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\mpv\\"; public static string mpvConfFolderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\mpv\\";
public static string InputConfPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\mpv\\input.conf"; public static string InputConfPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\mpv\\input.conf";
public static string mpvConfPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\mpv\\mpv.conf"; public static string mpvConfPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\mpv\\mpv.conf";
public static List<PythonScript> PythonScripts { get; } = new List<PythonScript>(); public static List<PythonScript> PythonScripts => new List<PythonScript>();
public static AutoResetEvent AutoResetEvent = new AutoResetEvent(false); public static AutoResetEvent AutoResetEvent = new AutoResetEvent(false);
private static Dictionary<string, string> _mpvConf; private static Dictionary<string, string> _mpvConf;
@@ -75,7 +74,7 @@ namespace mpvnet
if (File.Exists(mpvConfPath)) if (File.Exists(mpvConfPath))
foreach (var i in File.ReadAllLines(mpvConfPath)) foreach (var i in File.ReadAllLines(mpvConfPath))
if (i.Contains("=") && ! i.StartsWith("#")) if (i.Contains("=") && ! i.StartsWith("#"))
_mpvConf[i.Left("=").Trim()] = i.Right("=").Trim(); _mpvConf[i.Substring(0, i.IndexOf("=")).Trim()] = i.Substring(i.IndexOf("=") + 1).Trim();
} }
return _mpvConf; return _mpvConf;
} }
@@ -208,7 +207,7 @@ namespace mpvnet
} }
catch (Exception ex) catch (Exception ex)
{ {
MsgError(ex.GetType().Name + "\r\n\r\n" + ex.ToString()); MainForm.Instance.ShowMsgBox(ex.GetType().Name + "\n\n" + ex.ToString(), MessageBoxIcon.Error);
} }
ClientMessage?.Invoke(args); ClientMessage?.Invoke(args);
} }
@@ -419,6 +418,8 @@ namespace mpvnet
foreach (string i in args) foreach (string i in args)
if (!i.StartsWith("--") && File.Exists(i)) if (!i.StartsWith("--") && File.Exists(i))
mp.commandv("loadfile", i, "append"); mp.commandv("loadfile", i, "append");
else if (!i.StartsWith("--") && i.StartsWith("http"))
mp.LoadURL(i);
mp.set_property_string("playlist-pos", "0"); mp.set_property_string("playlist-pos", "0");
@@ -438,6 +439,15 @@ namespace mpvnet
} }
} }
public static void LoadURL(string url)
{
int count = mp.get_property_int("playlist-count");
mp.commandv("loadfile", url, "append");
mp.set_property_int("playlist-pos", count);
for (int i = 0; i < count; i++)
mp.commandv("playlist-remove", "0");
}
public static void LoadFiles(string[] files) public static void LoadFiles(string[] files)
{ {
int count = mp.get_property_int("playlist-count"); int count = mp.get_property_int("playlist-count");
@@ -465,7 +475,7 @@ namespace mpvnet
string[] types = "264 265 3gp aac ac3 avc avi avs bmp divx dts dtshd dtshr dtsma eac3 evo flac flv h264 h265 hevc hvc jpg jpeg m2t m2ts m2v m4a m4v mka mkv mlp mov mp2 mp3 mp4 mpa mpeg mpg mpv mts ogg ogm opus pcm png pva raw rmvb thd thd+ac3 true-hd truehd ts vdr vob vpy w64 wav webm wmv y4m".Split(' '); string[] types = "264 265 3gp aac ac3 avc avi avs bmp divx dts dtshd dtshr dtsma eac3 evo flac flv h264 h265 hevc hvc jpg jpeg m2t m2ts m2v m4a m4v mka mkv mlp mov mp2 mp3 mp4 mpa mpeg mpg mpv mts ogg ogm opus pcm png pva raw rmvb thd thd+ac3 true-hd truehd ts vdr vob vpy w64 wav webm wmv y4m".Split(' ');
string path = get_property_string("path"); string path = get_property_string("path");
List<string> files = Directory.GetFiles(Path.GetDirectoryName(path)).ToList(); List<string> files = Directory.GetFiles(Path.GetDirectoryName(path)).ToList();
files = files.Where((file) => types.Contains(file.Ext())).ToList(); files = files.Where((file) => types.Contains(Path.GetExtension(file).TrimStart(".".ToCharArray()).ToLower())).ToList();
files.Sort(new StringLogicalComparer()); files.Sort(new StringLogicalComparer());
int index = files.IndexOf(path); int index = files.IndexOf(path);
files.Remove(path); files.Remove(path);

View File

@@ -118,6 +118,7 @@
<SpecificVersion>False</SpecificVersion> <SpecificVersion>False</SpecificVersion>
<HintPath>IronPython\Microsoft.Scripting.dll</HintPath> <HintPath>IronPython\Microsoft.Scripting.dll</HintPath>
</Reference> </Reference>
<Reference Include="Microsoft.VisualBasic" />
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.ComponentModel.Composition" /> <Reference Include="System.ComponentModel.Composition" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
@@ -131,7 +132,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Addon.cs" /> <Compile Include="Addon.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="MediaInfo.cs" /> <Compile Include="MediaInfo.cs" />
<Compile Include="Menu.cs"> <Compile Include="Menu.cs">
<SubType>Component</SubType> <SubType>Component</SubType>
@@ -143,7 +143,6 @@
</Compile> </Compile>
<Compile Include="PowerShellScript.cs" /> <Compile Include="PowerShellScript.cs" />
<Compile Include="PythonScript.cs" /> <Compile Include="PythonScript.cs" />
<Compile Include="StringExtensions.cs" />
<Compile Include="libmpv.cs" /> <Compile Include="libmpv.cs" />
<Compile Include="MainForm.cs"> <Compile Include="MainForm.cs">
<SubType>Form</SubType> <SubType>Form</SubType>

View File

@@ -4,6 +4,27 @@
xmlns:local="clr-namespace:DynamicGUI" xmlns:local="clr-namespace:DynamicGUI"
StartupUri="MainWindow.xaml"> StartupUri="MainWindow.xaml">
<Application.Resources> <Application.Resources>
<Style TargetType="TextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Opacity" TargetName="border" Value="0.56"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="#FF7EB4EA"/>
</Trigger>
<Trigger Property="IsFocused" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="{x:Static SystemParameters.WindowGlassBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@@ -1,32 +1,9 @@
[[settings]] [[settings]]
name = "volume"
default = ""
help = "volume=<integer> Set the startup volume. 0 means silence, 100 means no volume reduction or amplification. Negative values can be passed for compatibility, but are treated as 0. Since mpv 0.18.1, this always controls the internal mixer (aka \"softvol\")."
[[settings]]
name = "screen"
default = ""
help = "screen=<default|0-32> In multi-monitor configurations (i.e. a single desktop that spans across multiple displays), this option tells mpv which screen to display the video on. Default: default."
[[settings]]
name = "osd-playing-msg"
default = ""
width = 300
help = "osd-playing-msg=<value> Show a message on OSD when playback starts. The string is expanded for properties, e.g. --osd-playing-msg='file: ${filename}' will show the message file: followed by a space and the currently played filename."
helpurl = "https://mpv.io/manual/master/#property-expansion"
[[settings]]
name = "fullscreen"
alias = "fs"
default = "no"
help = "fullscreen=<yes|no>, fs=<yes|no> Start the player in fullscreen mode. Default: no."
options = [{ name = "yes" }, { name = "no", text = "no (Default)" }]
[[settings]]
name = "hwdec" name = "hwdec"
default = "no" default = "no"
filter = "Video"
helpurl = "https://mpv.io/manual/master/#options-hwdec" helpurl = "https://mpv.io/manual/master/#options-hwdec"
help = "hwdec=<mode> Specify the hardware video decoding API that should be used if possible. Whether hardware decoding is actually done depends on the video codec. If hardware decoding is not possible, mpv will fall back on software decoding." help = "--hwdec=<mode> Specify the hardware video decoding API that should be used if possible. Whether hardware decoding is actually done depends on the video codec. If hardware decoding is not possible, mpv will fall back on software decoding.\n\nFor more information visit:"
options = [{ name = "no", text = "no (Default)", help = "always use software decoding (Default)" }, options = [{ name = "no", text = "no (Default)", help = "always use software decoding (Default)" },
{ name = "auto", help = "enable best hw decoder (see below)" }, { name = "auto", help = "enable best hw decoder (see below)" },
{ name = "yes", help = "exactly the same as auto" }, { name = "yes", help = "exactly the same as auto" },
@@ -45,44 +22,156 @@ options = [{ name = "no", text = "no (Default)", help = "always use software dec
[[settings]] [[settings]]
name = "vo" name = "vo"
default = "gpu" default = "gpu"
filter = "Video"
helpurl = "https://mpv.io/manual/master/#video-output-drivers-vo" helpurl = "https://mpv.io/manual/master/#video-output-drivers-vo"
help = "gpu=<mode> Video output drivers to be used. Default = gpu." help = "--gpu=<mode> Video output drivers to be used. Default = gpu.\n\nFor more information visit:"
options = [{ name = "direct3d", help = "Video output driver that uses the Direct3D interface" }, options = [{ name = "gpu", text = "gpu (Default)", help = "General purpose, customizable, GPU-accelerated video output driver. It supports extended scaling methods, dithering, color management, custom shaders, HDR, and more. (Default)" },
{ name = "gpu", text = "gpu (Default)", help = "General purpose, customizable, GPU-accelerated video output driver. It supports extended scaling methods, dithering, color management, custom shaders, HDR, and more. (Default)" }] { name = "direct3d", help = "Video output driver that uses the Direct3D interface" }]
[[settings]]
name = "volume"
default = ""
filter = "Audio"
help = "--volume=<integer> Set the startup volume. 0 means silence, 100 means no volume reduction or amplification. Negative values can be passed for compatibility, but are treated as 0. Since mpv 0.18.1, this always controls the internal mixer (aka \"softvol\")."
[[settings]]
name = "slang"
default = ""
filter = "Subtitle"
help = "--slang=<languagecode[,languagecode,...]> Specify a priority list of subtitle languages to use. Different container formats employ different language codes. DVDs use ISO 639-1 two letter language codes, Matroska uses ISO 639-2 three letter language codes while OGM uses a free-form identifier. See also --sid."
[[settings]]
name = "screen"
default = ""
filter = "Screen"
help = "--screen=<default|0-32> In multi-monitor configurations (i.e. a single desktop that spans across multiple displays), this option tells mpv which screen to display the video on. Default: default."
[[settings]]
name = "osd-playing-msg"
default = ""
width = 300
filter = "Screen"
help = "--osd-playing-msg=<value> Show a message on OSD when playback starts. The string is expanded for properties, e.g. --osd-playing-msg='file: ${filename}' will show the message file: followed by a space and the currently played filename.\n\nFor more information visit:"
helpurl = "https://mpv.io/manual/master/#property-expansion"
[[settings]]
name = "fullscreen"
alias = "fs"
default = "no"
filter = "Screen"
help = "--fullscreen=<yes|no>, fs=<yes|no> Start the player in fullscreen mode. Default: no."
options = [{ name = "yes" }, { name = "no", text = "no (Default)" }]
[[settings]] [[settings]]
name = "keep-open-pause" name = "keep-open-pause"
default = "yes" default = "yes"
help = "keep-open-pause=<yes|no> If set to no, instead of pausing when --keep-open is active, just stop at end of file and continue playing forward when you seek backwards until end where it stops again. Default: yes." filter = "Playback"
help = "--keep-open-pause=<yes|no> If set to no, instead of pausing when --keep-open is active, just stop at end of file and continue playing forward when you seek backwards until end where it stops again. Default: yes."
options = [{ name = "yes", text = "yes (Default)" }, { name = "no" }] options = [{ name = "yes", text = "yes (Default)" }, { name = "no" }]
[[settings]] [[settings]]
name = "keep-open" name = "keep-open"
default = "no" default = "no"
help = "keep-open=<yes|no|always> Do not terminate when playing or seeking beyond the end of the file, and there is not next file to be played (and --loop is not used). Instead, pause the player. When trying to seek beyond end of the file, the player will attempt to seek to the last frame.\n\nNormally, this will act like set pause yes on EOF, unless the --keep-open-pause=no option is set." filter = "Playback"
options = [{ name = "no", text = "no (Default)", help = "If the current file ends, go to the next file or terminate. (Default.)" }, help = "--keep-open=<yes|no|always> Do not terminate when playing or seeking beyond the end of the file, and there is not next file to be played (and --loop is not used). Instead, pause the player. When trying to seek beyond end of the file, the player will attempt to seek to the last frame.\n\nNormally, this will act like set pause yes on EOF, unless the --keep-open-pause=no option is set."
{ name = "yes", help = "Don't terminate if the current file is the last playlist entry. Equivalent to --keep-open without arguments."}, options = [{ name = "yes", help = "Don't terminate if the current file is the last playlist entry. Equivalent to --keep-open without arguments."},
{ name = "no", text = "no (Default)", help = "If the current file ends, go to the next file or terminate. (Default.)" },
{ name = "always", help = "Like yes, but also applies to files before the last playlist entry. This means playback will never automatically advance to the next file."}] { name = "always", help = "Like yes, but also applies to files before the last playlist entry. This means playback will never automatically advance to the next file."}]
[[settings]] [[settings]]
name = "loop-file" name = "loop-file"
alias = "loop" alias = "loop"
default = "" default = ""
help = "loop-file=<N|inf|no>, loop=<N|inf|no> Loop a single file N times. inf means forever, no means normal playback. For compatibility, --loop-file and --loop-file=yes are also accepted, and are the same as --loop-file=inf.\n\nThe difference to --loop-playlist is that this doesn't loop the playlist, just the file itself. If the playlist contains only a single file, the difference between the two option is that this option performs a seek on loop, instead of reloading the file.\n\n--loop is an alias for this option." filter = "Playback"
help = "--loop-file=<N|inf|no>, loop=<N|inf|no> Loop a single file N times. inf means forever, no means normal playback. For compatibility, --loop-file and --loop-file=yes are also accepted, and are the same as --loop-file=inf.\n\nThe difference to --loop-playlist is that this doesn't loop the playlist, just the file itself. If the playlist contains only a single file, the difference between the two option is that this option performs a seek on loop, instead of reloading the file.\n\n--loop is an alias for this option."
[[settings]]
name = "save-position-on-quit"
default = "no"
filter = "Playback"
help = "--save-position-on-quit=<yes|no> Always save the current playback position on quit. When this file is played again later, the player will seek to the old playback position on start. This does not happen if playback of a file is stopped in any other way than quitting. For example, going to the next file in the playlist will not save the position, and start playback at beginning the next time the file is played.\n\nThis behavior is disabled by default, but is always available when quitting the player with Shift+Q."
options = [{ name = "yes" }, { name = "no", text = "no (Default)" }]
[[settings]] [[settings]]
name = "screenshot-directory" name = "screenshot-directory"
default = "" default = ""
width = 500 width = 500
folder = true folder = true
help = "screenshot-directory=<value> Store screenshots in this directory. This path is joined with the filename generated by --screenshot-template. If the template filename is already absolute, the directory is ignored.\n\nIf the directory does not exist, it is created on the first screenshot. If it is not a directory, an error is generated when trying to write a screenshot.\n\nThis option is not set by default, and thus will write screenshots to the directory from which mpv was started. In pseudo-gui mode (see PSEUDO GUI MODE), this is set to the desktop." filter = "Screen"
help = "--screenshot-directory=<value> Store screenshots in this directory. This path is joined with the filename generated by --screenshot-template. If the template filename is already absolute, the directory is ignored.\n\nIf the directory does not exist, it is created on the first screenshot. If it is not a directory, an error is generated when trying to write a screenshot.\n\nThis option is not set by default, and thus will write screenshots to the directory from which mpv was started. In pseudo-gui mode (see PSEUDO GUI MODE), this is set to the desktop."
[[settings]] [[settings]]
name = "input-ar-delay" name = "input-ar-delay"
default = "" default = ""
help = "input-ar-delay=<integer> Delay in milliseconds before we start to autorepeat a key (0 to disable)." filter = "Input"
help = "--input-ar-delay=<integer> Delay in milliseconds before we start to autorepeat a key (0 to disable)."
[[settings]] [[settings]]
name = "input-ar-rate" name = "input-ar-rate"
default = "" default = ""
help = "input-ar-rate=<integer> Number of key presses to generate per second on autorepeat." filter = "Input"
help = "--input-ar-rate=<integer> Number of key presses to generate per second on autorepeat."
[[settings]]
name = "alang"
default = ""
filter = "Audio"
help = "--alang=<languagecode[,languagecode,...]> Specify a priority list of audio languages to use. Different container formats employ different language codes. DVDs use ISO 639-1 two-letter language codes, Matroska, MPEG-TS and NUT use ISO 639-2 three-letter language codes, while OGM uses a free-form identifier. See also --aid.\n\nExamples\n\nmpv dvd://1 --alang=hu,en chooses the Hungarian language track on a DVD and falls back on English if Hungarian is not available.\n\nmpv --alang=jpn example.mkv plays a Matroska file with Japanese audio."
[[settings]]
name = "hr-seek"
default = "absolute"
filter = "Playback"
help = "--hr-seek=<no|absolute|yes> Select when to use precise seeks that are not limited to keyframes. Such seeks require decoding video from the previous keyframe up to the target position and so can take some time depending on decoding performance. For some video formats, precise seeks are disabled. This option selects the default choice to use for seeks; it is possible to explicitly override that default in the definition of key bindings and in input commands."
options = [{ name = "yes", help = "Use precise seeks whenever possible." },
{ name = "no", help = "Never use precise seeks." },
{ name = "absolute", text = "absolute (Default)", help = "Use precise seeks if the seek is to an absolute position in the file, such as a chapter seek, but not for relative seeks like the default behavior of arrow keys (default)." },
{ name = "always", help = "Same as yes (for compatibility)." }]
[[settings]]
name = "track-auto-selection"
default = "yes"
filter = "Playback"
help = "--track-auto-selection=<yes|no> Enable the default track auto-selection (default: yes). Enabling this will make the player select streams according to --aid, --alang, and others. If it is disabled, no tracks are selected. In addition, the player will not exit if no tracks are selected, and wait instead (this wait mode is similar to pausing, but the pause option is not set).\n\nThis is useful with --lavfi-complex: you can start playback in this mode, and then set select tracks at runtime by setting the filter graph. Note that if --lavfi-complex is set before playback is started, the referenced tracks are always selected."
options = [{ name = "yes", text = "yes (Default)" },
{ name = "no" }]
[[settings]]
name = "loop-playlist"
default = ""
filter = "Playback"
help = "--loop-playlist=<N|inf|force|no>, --loop-playlist Loops playback N times. A value of 1 plays it one time (default), 2 two times, etc. inf means forever. no is the same as 1 and disables looping. If several files are specified on command line, the entire playlist is looped. --loop-playlist is the same as --loop-playlist=inf.\n\nThe force mode is like inf, but does not skip playlist entries which have been marked as failing. This means the player might waste CPU time trying to loop a file that doesn't exist. But it might be useful for playing webradios under very bad network conditions."
[[settings]]
name = "video-sync"
default = ""
filter = "Video"
help = "--video-sync=<audio|...> How the player synchronizes audio and video.\n\nFor more information visit:"
helpurl = "https://mpv.io/manual/master/#options-video-sync"
options = [{ name = "display-resample" },
{ name = "display-resample-vdrop" },
{ name = "display-resample-desync" },
{ name = "display-vdrop" },
{ name = "display-adrop" },
{ name = "display-desync" },
{ name = "desync" }]
[[settings]]
name = "audio-file-auto"
default = "no"
filter = "Audio"
help = "--audio-file-auto=<no|exact|fuzzy|all>, --no-audio-file-auto Load additional audio files matching the video filename. The parameter specifies how external audio files are matched."
options = [{ name = "no", text = "no (Default", help = "Don't automatically load external audio files (default)." },
{ name = "exact", help = "Load the media filename with audio file extension." },
{ name = "fuzzy", help = "Load all audio files containing media filename." },
{ name = "all", help = "Load all audio files in the current and --audio-file-paths directories." }]
[[settings]]
name = "sub-auto"
default = "exact"
filter = "Subtitle"
help = "--sub-auto=<no|exact|fuzzy|all>, --no-sub-auto Load additional subtitle files matching the video filename. The parameter specifies how external subtitle files are matched. exact is enabled by default."
options = [{ name = "no", help = "Don't automatically load external subtitle files." },
{ name = "exact", text = "exact (Default)", help = "Load the media filename with subtitle file extension (Default)." },
{ name = "fuzzy", help = "Load all subs containing media filename." },
{ name = "all", help = "Load all subs in the current and --sub-file-paths directories." }]

View File

@@ -0,0 +1,24 @@
using System;
using System.Diagnostics;
using System.Windows.Documents;
using System.Windows.Navigation;
namespace DynamicGUI
{
public class HyperlinkEx : Hyperlink
{
private void HyperLinkEx_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(e.Uri.AbsoluteUri);
}
public void SetURL(string url)
{
if (string.IsNullOrEmpty(url)) return;
NavigateUri = new Uri(url);
RequestNavigate += HyperLinkEx_RequestNavigate;
Inlines.Clear();
Inlines.Add(url);
}
}
}

View File

@@ -46,7 +46,9 @@ namespace DynamicGUI
} }
baseSetting.Name = setting["name"]; baseSetting.Name = setting["name"];
baseSetting.Filter = setting["filter"];
if (setting.HasKey("help")) baseSetting.Help = setting["help"]; if (setting.HasKey("help")) baseSetting.Help = setting["help"];
if (setting.HasKey("helpurl")) baseSetting.HelpURL = setting["helpurl"];
if (setting.HasKey("alias")) baseSetting.Alias = setting["alias"]; if (setting.HasKey("alias")) baseSetting.Alias = setting["alias"];
if (setting.HasKey("width")) baseSetting.Width = setting["width"]; if (setting.HasKey("width")) baseSetting.Width = setting["width"];
settingsList.Add(baseSetting); settingsList.Add(baseSetting);
@@ -61,6 +63,7 @@ namespace DynamicGUI
public string Alias { get; set; } public string Alias { get; set; }
public string Help { get; set; } public string Help { get; set; }
public string HelpURL { get; set; } public string HelpURL { get; set; }
public string Filter { get; set; }
public int Width { get; set; } public int Width { get; set; }
} }
@@ -93,8 +96,6 @@ namespace DynamicGUI
set => _Text = value; set => _Text = value;
} }
//private bool _IsChecked;
public bool IsChecked public bool IsChecked
{ {
get => OptionSetting.Value == Name ; get => OptionSetting.Value == Name ;

View File

@@ -1,7 +1,8 @@
namespace DynamicGUI namespace DynamicGUI
{ {
interface ISearch interface ISettingControl
{ {
bool Contains(string searchString); bool Contains(string searchString);
SettingBase SettingBase { get; }
} }
} }

View File

@@ -7,8 +7,8 @@
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"> d:DesignHeight="450" d:DesignWidth="800">
<Grid Margin="20,0"> <Grid Margin="20,0">
<WrapPanel Orientation="Vertical"> <StackPanel>
<TextBox x:Name="TitleTextBox" FontSize="20" Margin="0,0,0,10" BorderThickness="0" IsReadOnly="True"></TextBox> <TextBox x:Name="TitleTextBox" FontSize="24" Margin="0,10" BorderThickness="0" IsReadOnly="True" Foreground="{x:Static SystemParameters.WindowGlassBrush}"></TextBox>
<ItemsControl x:Name="ItemsControl"> <ItemsControl x:Name="ItemsControl">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
@@ -19,7 +19,10 @@
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
<TextBox x:Name="HelpTextBox" TextWrapping="WrapWithOverflow" Margin="0,10" BorderThickness="0" IsReadOnly="True" Padding="0"></TextBox> <TextBox x:Name="HelpTextBox" TextWrapping="WrapWithOverflow" BorderThickness="0" IsReadOnly="True" Margin="0,10,0,0"></TextBox>
</WrapPanel> <TextBlock x:Name="LinkTextBlock" Margin="0,10">
<local:HyperlinkEx x:Name="Link"></local:HyperlinkEx>
</TextBlock>
</StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -1,9 +1,9 @@
using DynamicGUI; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace DynamicGUI namespace DynamicGUI
{ {
public partial class OptionSettingControl : UserControl, ISearch public partial class OptionSettingControl : UserControl, ISettingControl
{ {
private OptionSetting OptionSetting; private OptionSetting OptionSetting;
@@ -14,6 +14,10 @@ namespace DynamicGUI
TitleTextBox.Text = optionSetting.Name; TitleTextBox.Text = optionSetting.Name;
HelpTextBox.Text = optionSetting.Help; HelpTextBox.Text = optionSetting.Help;
ItemsControl.ItemsSource = optionSetting.Options; ItemsControl.ItemsSource = optionSetting.Options;
Link.SetURL(optionSetting.HelpURL);
if (string.IsNullOrEmpty(optionSetting.HelpURL))
LinkTextBlock.Visibility = Visibility.Collapsed;
} }
private string _SearchableText; private string _SearchableText;
@@ -31,6 +35,7 @@ namespace DynamicGUI
} }
} }
public SettingBase SettingBase => OptionSetting;
public bool Contains(string searchString) => SearchableText.Contains(searchString.ToLower()); public bool Contains(string searchString) => SearchableText.Contains(searchString.ToLower());
} }
} }

View File

@@ -8,8 +8,8 @@
d:DesignHeight="450" d:DesignHeight="450"
d:DesignWidth="800" > d:DesignWidth="800" >
<Grid Margin="20,0"> <Grid Margin="20,0">
<WrapPanel Orientation="Vertical"> <StackPanel>
<TextBox x:Name="TitleTextBox" FontSize="20" Margin="0,0,0,10" BorderThickness="0" IsReadOnly="True"></TextBox> <TextBox x:Name="TitleTextBox" FontSize="24" Margin="0,10" BorderThickness="0" IsReadOnly="True" Foreground="{x:Static SystemParameters.WindowGlassBrush}"></TextBox>
<Grid Margin="0,0,0,10"> <Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -19,6 +19,9 @@
<Button x:Name="Button" Height="20" Grid.Column="1" Visibility="{Binding Path=Text, ElementName=StringSettingControl1}" Margin="5,0,0,0" Width="20" Click="Button_Click">...</Button> <Button x:Name="Button" Height="20" Grid.Column="1" Visibility="{Binding Path=Text, ElementName=StringSettingControl1}" Margin="5,0,0,0" Width="20" Click="Button_Click">...</Button>
</Grid> </Grid>
<TextBox x:Name="HelpTextBox" TextWrapping="WrapWithOverflow" Margin="0,0,0,10" BorderThickness="0" IsReadOnly="True"></TextBox> <TextBox x:Name="HelpTextBox" TextWrapping="WrapWithOverflow" Margin="0,0,0,10" BorderThickness="0" IsReadOnly="True"></TextBox>
</WrapPanel> <TextBlock x:Name="LinkTextBlock" Margin="0,10">
<local:HyperlinkEx x:Name="Link"></local:HyperlinkEx>
</TextBlock>
</StackPanel>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -3,7 +3,7 @@ using System.Windows.Controls;
namespace DynamicGUI namespace DynamicGUI
{ {
public partial class StringSettingControl : UserControl, ISearch public partial class StringSettingControl : UserControl, ISettingControl
{ {
private StringSetting StringSetting; private StringSetting StringSetting;
@@ -11,7 +11,6 @@ namespace DynamicGUI
{ {
StringSetting = stringSetting; StringSetting = stringSetting;
InitializeComponent(); InitializeComponent();
TitleTextBox.Text = stringSetting.Name; TitleTextBox.Text = stringSetting.Name;
HelpTextBox.Text = stringSetting.Help; HelpTextBox.Text = stringSetting.Help;
ValueTextBox.Text = stringSetting.Value; ValueTextBox.Text = stringSetting.Value;
@@ -19,6 +18,10 @@ namespace DynamicGUI
ValueTextBox.Width = stringSetting.Width; ValueTextBox.Width = stringSetting.Width;
if (!StringSetting.IsFolder) if (!StringSetting.IsFolder)
Button.Visibility = Visibility.Hidden; Button.Visibility = Visibility.Hidden;
Link.SetURL(StringSetting.HelpURL);
if (string.IsNullOrEmpty(stringSetting.HelpURL))
LinkTextBlock.Visibility = Visibility.Collapsed;
} }
private string _SearchableText; private string _SearchableText;
@@ -33,6 +36,7 @@ namespace DynamicGUI
} }
public bool Contains(string searchString) => SearchableText.Contains(searchString.ToLower()); public bool Contains(string searchString) => SearchableText.Contains(searchString.ToLower());
public SettingBase SettingBase => StringSetting;
public string Text public string Text
{ {

View File

@@ -1,22 +1,40 @@
<Window xmlns:DynamicGUI="clr-namespace:DynamicGUI" x:Name="MainWindow1" x:Class="DynamicGUI.MainWindow" <Window x:Name="MainWindow1" x:Class="mpvSettingsEditor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DynamicGUI"
mc:Ignorable="d" mc:Ignorable="d"
Height="500" Width="800"> Height="500" Width="700" Loaded="MainWindow1_Loaded">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="4*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Background="White" Width="300" Margin="0,0,0,10"> <Grid.ColumnDefinitions>
<TextBlock Margin="5,2" MinWidth="50" Text="Search..." Foreground="LightSteelBlue" IsHitTestVisible="False" /> <ColumnDefinition Width="10*" />
<TextBox MinWidth="50" Name="SearchTextBox" Background="Transparent" TextChanged="SearchTextBox_TextChanged" /> <ColumnDefinition Width="60*" />
</Grid.ColumnDefinitions>
<Grid x:Name="SearchGrid" Background="White" Width="300" Margin="0,0,0,10" Grid.ColumnSpan="2">
<TextBlock x:Name="SearchTextBlock" Margin="5,2" Text="Find a setting" Foreground="LightSteelBlue" VerticalAlignment="Center" />
<TextBox Name="SearchTextBox" Padding="1,2,0,0" BorderThickness="2" Background="Transparent" TextChanged="SearchTextBox_TextChanged" Height="25" />
<Button x:Name="SearchClearButton" Background="Transparent" HorizontalAlignment="Right" Margin="2,0,4,0" FontSize="5" Width="17" Height="17" Visibility="Hidden" Click="SearchClearButton_Click"></Button>
</Grid> </Grid>
<ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="1"> <ScrollViewer x:Name="MainScrollViewer" VerticalScrollBarVisibility="Auto" Grid.Row="1" Grid.Column="1">
<WrapPanel x:Name="MainWrapPanel"></WrapPanel> <StackPanel x:Name="MainStackPanel"></StackPanel>
</ScrollViewer> </ScrollViewer>
<StackPanel Margin="20,0,0,0" Grid.Row="1">
<ListBox x:Name="FilterListBox" ItemsSource="{Binding FilterStrings}" BorderThickness="0" SelectionChanged="ListBox_SelectionChanged" Foreground="{x:Static SystemParameters.WindowGlassBrush}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}" FontSize="16" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<TextBlock x:Name="OpenSettingsTextBlock" Margin="0,30,0,0" Cursor="Hand" TextWrapping="WrapWithOverflow" Foreground="{x:Static SystemParameters.WindowGlassBrush}" MouseUp="OpenSettingsTextBlock_MouseUp">Open settings folder</TextBlock>
<TextBlock x:Name="ShowManualTextBlock" Margin="0,15,0,0" Cursor="Hand" TextWrapping="WrapWithOverflow" Foreground="{x:Static SystemParameters.WindowGlassBrush}" MouseUp="ShowManualTextBlock_MouseUp">Show mpv manual</TextBlock>
<TextBlock x:Name="SupportTextBlock" Margin="0,15,0,0" Cursor="Hand" TextWrapping="WrapWithOverflow" Foreground="{x:Static SystemParameters.WindowGlassBrush}" MouseUp="SupportTextBlock_MouseUp">Show support forum</TextBlock>
</StackPanel>
</Grid> </Grid>
</Window> </Window>

View File

@@ -1,27 +1,32 @@
using System; using DynamicGUI;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using DynamicGUI; using System.Windows.Input;
namespace DynamicGUI namespace mpvSettingsEditor
{ {
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
public string mpvConfPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\mpv\\mpv.conf"; public string mpvConfPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\mpv\\mpv.conf";
private List<SettingBase> mpvSettings = Settings.LoadSettings("Definitions.toml"); private List<SettingBase> DynamicSettings = Settings.LoadSettings(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + "\\Definitions.toml");
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
DataContext = this;
Title = (Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyProductAttribute), true)[0] as AssemblyProductAttribute).Product + " " + Assembly.GetExecutingAssembly().GetName().Version.ToString(); Title = (Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyProductAttribute), true)[0] as AssemblyProductAttribute).Product + " " + Assembly.GetExecutingAssembly().GetName().Version.ToString();
foreach (var setting in mpvSettings) foreach (var setting in DynamicSettings)
{ {
if (!FilterStrings.Contains(setting.Filter))
FilterStrings.Add(setting.Filter);
foreach (var pair in mpvConf) foreach (var pair in mpvConf)
{ {
if (setting.Name == pair.Key || setting.Alias == pair.Key) if (setting.Name == pair.Key || setting.Alias == pair.Key)
@@ -38,10 +43,10 @@ namespace DynamicGUI
switch (setting) switch (setting)
{ {
case StringSetting s: case StringSetting s:
MainWrapPanel.Children.Add(new StringSettingControl(s)); MainStackPanel.Children.Add(new StringSettingControl(s));
break; break;
case OptionSetting s: case OptionSetting s:
MainWrapPanel.Children.Add(new OptionSettingControl(s)); MainStackPanel.Children.Add(new OptionSettingControl(s));
break; break;
} }
} }
@@ -67,11 +72,13 @@ namespace DynamicGUI
} }
} }
public ObservableCollection<string> FilterStrings { get; } = new ObservableCollection<string>();
protected override void OnClosed(EventArgs e) protected override void OnClosed(EventArgs e)
{ {
base.OnClosed(e); base.OnClosed(e);
foreach (var mpvSetting in mpvSettings) foreach (var mpvSetting in DynamicSettings)
{ {
switch (mpvSetting) switch (mpvSetting)
{ {
@@ -95,7 +102,7 @@ namespace DynamicGUI
List<string> lines = File.ReadAllLines(mpvConfPath).ToList(); List<string> lines = File.ReadAllLines(mpvConfPath).ToList();
foreach (var mpvSetting in mpvSettings) foreach (var mpvSetting in DynamicSettings)
{ {
foreach (var line in lines.ToArray()) foreach (var line in lines.ToArray())
{ {
@@ -128,7 +135,7 @@ namespace DynamicGUI
lines.Add(pair.Key + " = " + pair.Value); lines.Add(pair.Key + " = " + pair.Value);
} }
foreach (var mpvSetting in mpvSettings) foreach (var mpvSetting in DynamicSettings)
{ {
foreach (var line in lines.ToArray()) foreach (var line in lines.ToArray())
{ {
@@ -140,18 +147,79 @@ namespace DynamicGUI
} }
File.WriteAllText(mpvConfPath, String.Join(Environment.NewLine, lines)); File.WriteAllText(mpvConfPath, String.Join(Environment.NewLine, lines));
MessageBox.Show("If running, restart mpv/mpv.net", Title, MessageBoxButton.OK, MessageBoxImage.Information);
foreach (Process process in Process.GetProcesses())
if (process.ProcessName == "mpvnet")
MessageBox.Show("Restart mpv.net in order to apply changed settings.", Title, MessageBoxButton.OK, MessageBoxImage.Information);
else if (process.ProcessName == "mpv")
MessageBox.Show("Restart mpv in order to apply changed settings.", Title, MessageBoxButton.OK, MessageBoxImage.Information);
} }
private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e) private void SearchTextBox_TextChanged(object sender, TextChangedEventArgs e)
{ {
for (int i = MainWrapPanel.Children.Count - 1; i >= 0; i--) SearchTextBlock.Text = SearchTextBox.Text == "" ? "Find a setting" : "";
if (SearchTextBox.Text == "")
SearchClearButton.Visibility = Visibility.Hidden;
else
SearchClearButton.Visibility = Visibility.Visible;
string activeFilter = "";
foreach (var i in FilterStrings)
if (SearchTextBox.Text == i + ":")
activeFilter = i;
if (activeFilter == "")
{ {
if ((MainWrapPanel.Children[i] as ISearch).Contains(SearchTextBox.Text)) foreach (UIElement i in MainStackPanel.Children)
MainWrapPanel.Children[i].Visibility = Visibility.Visible; if ((i as ISettingControl).Contains(SearchTextBox.Text))
else i.Visibility = Visibility.Visible;
MainWrapPanel.Children[i].Visibility = Visibility.Collapsed; else
i.Visibility = Visibility.Collapsed;
FilterListBox.SelectedItem = null;
} }
else
foreach (UIElement i in MainStackPanel.Children)
if ((i as ISettingControl).SettingBase.Filter == activeFilter)
i.Visibility = Visibility.Visible;
else
i.Visibility = Visibility.Collapsed;
MainScrollViewer.ScrollToTop();
}
private void MainWindow1_Loaded(object sender, RoutedEventArgs e)
{
Keyboard.Focus(SearchTextBox);
}
private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
SearchTextBox.Text = e.AddedItems[0].ToString() + ":";
}
private void SearchClearButton_Click(object sender, RoutedEventArgs e)
{
SearchTextBox.Text = "";
Keyboard.Focus(SearchTextBox);
}
private void OpenSettingsTextBlock_MouseUp(object sender, MouseButtonEventArgs e)
{
Process.Start(Path.GetDirectoryName(mpvConfPath));
}
private void ShowManualTextBlock_MouseUp(object sender, MouseButtonEventArgs e)
{
Process.Start("https://mpv.io/manual/master/");
}
private void SupportTextBlock_MouseUp(object sender, MouseButtonEventArgs e)
{
Process.Start("https://github.com/stax76/mpv.net#Support");
} }
} }
} }

View File

@@ -7,11 +7,11 @@ using System.Windows;
// General Information about an assembly is controlled through the following // General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information // set of attributes. Change these attribute values to modify the information
// associated with an assembly. // associated with an assembly.
[assembly: AssemblyTitle("mpv settings editor")] [assembly: AssemblyTitle("mpv(.net) settings editor")]
[assembly: AssemblyDescription("")] [assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")] [assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")] [assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("mpv settings editor")] [assembly: AssemblyProduct("mpv(.net) settings editor")]
[assembly: AssemblyCopyright("Copyright © stax76")] [assembly: AssemblyCopyright("Copyright © stax76")]
[assembly: AssemblyTrademark("")] [assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")] [assembly: AssemblyCulture("")]
@@ -51,5 +51,5 @@ using System.Windows;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.1.0.0")] [assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")]

BIN
mpvSettingsEditor/mpv.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -41,6 +41,9 @@
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
<LangVersion>8.0</LangVersion> <LangVersion>8.0</LangVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup>
<ApplicationIcon>mpv.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Data" /> <Reference Include="System.Data" />
@@ -63,6 +66,7 @@
<Generator>MSBuild:Compile</Generator> <Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType> <SubType>Designer</SubType>
</ApplicationDefinition> </ApplicationDefinition>
<Compile Include="DynamicGUI\Controls.cs" />
<Compile Include="DynamicGUI\DynamicGUI.cs" /> <Compile Include="DynamicGUI\DynamicGUI.cs" />
<Compile Include="DynamicGUI\OptionSettingControl.xaml.cs"> <Compile Include="DynamicGUI\OptionSettingControl.xaml.cs">
<DependentUpon>OptionSettingControl.xaml</DependentUpon> <DependentUpon>OptionSettingControl.xaml</DependentUpon>
@@ -124,5 +128,8 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Resource Include="mpv.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project> </Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB