Initial commit
This commit is contained in:
375
Shackle/MainForm.cs
Normal file
375
Shackle/MainForm.cs
Normal file
@@ -0,0 +1,375 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace UnshackleGUI
|
||||
{
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
private string _configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
|
||||
private string _paramsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "params.json");
|
||||
|
||||
private AppSettings _appSettings = new AppSettings();
|
||||
private List<UnshackleParameter> _parameterDefinitions = new List<UnshackleParameter>();
|
||||
private Dictionary<string, object> _propertyValues = new Dictionary<string, object>();
|
||||
|
||||
private Process? _currentProcess;
|
||||
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplyDarkTheme();
|
||||
LoadParamsAndConfig();
|
||||
|
||||
// EVENT: This updates the command line the moment a value changes
|
||||
pgProfile.PropertyValueChanged += (s, e) =>
|
||||
{
|
||||
UpdateCommandPreview();
|
||||
|
||||
if (comboProfiles.SelectedItem != null)
|
||||
{
|
||||
string selected = comboProfiles.SelectedItem.ToString();
|
||||
_appSettings.Profiles[selected] =
|
||||
new Dictionary<string, object>(_propertyValues);
|
||||
}
|
||||
|
||||
SaveConfig();
|
||||
};
|
||||
|
||||
comboService.SelectedIndexChanged += (s, e) => UpdateCommandPreview();
|
||||
txtURL.TextChanged += (s, e) => UpdateCommandPreview();
|
||||
txtBinName.TextChanged += (s, e) => UpdateCommandPreview();
|
||||
|
||||
btnRun.Click += btnRun_Click;
|
||||
btnStop.Click += btnStop_Click;
|
||||
btnBrowse.Click += btnBrowse_Click;
|
||||
btnOpenCookies.Click += btnOpenCookies_Click;
|
||||
btnEditServiceConfig.Click += btnEditServiceConfig_Click;
|
||||
btnClearLog.Click += btnClearLog_Click;
|
||||
btnEditYaml.Click += btnEditYaml_Click;
|
||||
btnAddProfile.Click += btnAddProfile_Click;
|
||||
btnRemoveProfile.Click += btnRemoveProfile_Click;
|
||||
}
|
||||
|
||||
private void LoadParamsAndConfig()
|
||||
{
|
||||
// 1. Load Parameters (Rules)
|
||||
if (File.Exists(_paramsPath))
|
||||
{
|
||||
var json = File.ReadAllText(_paramsPath);
|
||||
_parameterDefinitions = JsonConvert.DeserializeObject<List<UnshackleParameter>>(json) ?? new List<UnshackleParameter>();
|
||||
|
||||
foreach (var p in _parameterDefinitions)
|
||||
{
|
||||
// Ensure every parameter has a value in the dictionary
|
||||
if (!_propertyValues.ContainsKey(p.Name))
|
||||
_propertyValues[p.Name] = p.Default ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Load Config (Paths/Profiles)
|
||||
if (File.Exists(_configPath))
|
||||
{
|
||||
var json = File.ReadAllText(_configPath);
|
||||
_appSettings = JsonConvert.DeserializeObject<AppSettings>(json) ?? new AppSettings();
|
||||
txtRootPath.Text = _appSettings.RootPath;
|
||||
txtBinName.Text = _appSettings.BinaryName;
|
||||
}
|
||||
|
||||
if (_appSettings.Profiles == null || _appSettings.Profiles.Count == 0)
|
||||
{
|
||||
_appSettings.Profiles = new Dictionary<string, Dictionary<string, object>>();
|
||||
_appSettings.Profiles["Default"] = new Dictionary<string, object>(_propertyValues);
|
||||
}
|
||||
|
||||
comboProfiles.DataSource = null;
|
||||
comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList();
|
||||
|
||||
comboProfiles.SelectedIndexChanged += (s, e) =>
|
||||
{
|
||||
if (comboProfiles.SelectedItem == null) return;
|
||||
|
||||
string selected = comboProfiles.SelectedItem.ToString();
|
||||
|
||||
if (_appSettings.Profiles.TryGetValue(selected, out var values))
|
||||
{
|
||||
_propertyValues = new Dictionary<string, object>(values);
|
||||
|
||||
pgProfile.SelectedObject =
|
||||
new DynamicObject(_propertyValues, _parameterDefinitions);
|
||||
|
||||
UpdateCommandPreview();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 3. Bind to Grid using the FIXED Descriptor
|
||||
pgProfile.SelectedObject = new DynamicObject(_propertyValues, _parameterDefinitions);
|
||||
|
||||
|
||||
RefreshFolders();
|
||||
UpdateCommandPreview();
|
||||
}
|
||||
|
||||
private void UpdateCommandPreview()
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.Append($"{txtBinName.Text} run unshackle dl ");
|
||||
|
||||
foreach (var p in _parameterDefinitions)
|
||||
{
|
||||
if (!_propertyValues.TryGetValue(p.Name, out object val)) continue;
|
||||
|
||||
// Handle Booleans
|
||||
if (p.Type == "Bool" && val is bool b && b)
|
||||
{
|
||||
sb.Append($"{p.Flag} ");
|
||||
}
|
||||
// Handle Text/Selection/Numbers
|
||||
else if (p.Type != "Bool" && val != null)
|
||||
{
|
||||
string sVal = val.ToString();
|
||||
// Skip empty, "any", or "0" (for default bitrates)
|
||||
if (!string.IsNullOrWhiteSpace(sVal) && sVal != "any" && sVal != "0")
|
||||
{
|
||||
if (sVal.Contains(" ")) sVal = $"\"{sVal}\"";
|
||||
sb.Append($"{p.Flag} {sVal} ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append($"{comboService.Text} {txtURL.Text}");
|
||||
txtCommandPreview.Text = sb.ToString();
|
||||
}
|
||||
|
||||
private void btnEditServiceConfig_Click(object? sender, EventArgs e)
|
||||
{
|
||||
string yamlPath = Path.Combine(txtRootPath.Text, "unshackle", "services", comboService.Text, "config.yaml");
|
||||
if (File.Exists(yamlPath))
|
||||
Process.Start(new ProcessStartInfo(yamlPath) { UseShellExecute = true });
|
||||
else
|
||||
MessageBox.Show($"No config.yaml found for {comboService.Text}");
|
||||
}
|
||||
|
||||
private async void btnRun_Click(object? sender, EventArgs e)
|
||||
{
|
||||
string finalCmd = txtCommandPreview.Text;
|
||||
if (string.IsNullOrWhiteSpace(finalCmd)) return;
|
||||
|
||||
ToggleUI(true);
|
||||
txtLog.AppendText($"> Executing: {finalCmd}{Environment.NewLine}");
|
||||
|
||||
try { await Task.Run(() => RunCli(finalCmd)); }
|
||||
catch (Exception ex) { AppendLog($"[ERROR]: {ex.Message}"); }
|
||||
finally { ToggleUI(false); }
|
||||
}
|
||||
|
||||
private void RunCli(string cmd)
|
||||
{
|
||||
ProcessStartInfo psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
// /c = Run command and then terminate
|
||||
// chcp 65001 = Force console to use UTF-8 encoding
|
||||
// && = Run the actual command immediately after setting encoding
|
||||
Arguments = $"/c chcp 65001 && {cmd}",
|
||||
WorkingDirectory = txtRootPath.Text,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
// Ensure Python scripts know to use UTF-8
|
||||
psi.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
|
||||
|
||||
using (_currentProcess = Process.Start(psi))
|
||||
{
|
||||
if (_currentProcess == null) return;
|
||||
|
||||
_currentProcess.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
// Filter out the noisy "Active code page: 65001" message from CMD
|
||||
if (!string.IsNullOrEmpty(e.Data) && !e.Data.Contains("Active code page"))
|
||||
AppendLog(e.Data);
|
||||
};
|
||||
|
||||
_currentProcess.ErrorDataReceived += (s, e) => AppendLog(e.Data);
|
||||
|
||||
_currentProcess.BeginOutputReadLine();
|
||||
_currentProcess.BeginErrorReadLine();
|
||||
_currentProcess.WaitForExit();
|
||||
}
|
||||
}
|
||||
|
||||
private void AppendLog(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
this.BeginInvoke(new Action(() => { txtLog.AppendText(text + Environment.NewLine); }));
|
||||
}
|
||||
|
||||
private void ApplyDarkTheme()
|
||||
{
|
||||
this.BackColor = Color.FromArgb(30, 30, 30);
|
||||
this.ForeColor = Color.White;
|
||||
foreach (Control c in this.Controls) UpdateControlTheme(c);
|
||||
}
|
||||
|
||||
private void UpdateControlTheme(Control c)
|
||||
{
|
||||
c.BackColor = Color.FromArgb(45, 45, 48);
|
||||
c.ForeColor = Color.White;
|
||||
if (c is TextBox tb) tb.BorderStyle = BorderStyle.FixedSingle;
|
||||
if (c is Button btn) { btn.FlatStyle = FlatStyle.Flat; btn.FlatAppearance.BorderColor = Color.Gray; }
|
||||
if (c is PropertyGrid pg)
|
||||
{
|
||||
pg.BackColor = Color.FromArgb(37, 37, 38);
|
||||
pg.ViewBackColor = Color.FromArgb(37, 37, 38);
|
||||
pg.ViewForeColor = Color.White;
|
||||
pg.LineColor = Color.FromArgb(45, 45, 48);
|
||||
}
|
||||
foreach (Control child in c.Controls) UpdateControlTheme(child);
|
||||
}
|
||||
|
||||
private void SaveConfig()
|
||||
{
|
||||
_appSettings.RootPath = txtRootPath.Text;
|
||||
_appSettings.BinaryName = txtBinName.Text;
|
||||
File.WriteAllText(_configPath, JsonConvert.SerializeObject(_appSettings, Formatting.Indented));
|
||||
}
|
||||
|
||||
private void RefreshFolders()
|
||||
{
|
||||
comboService.Items.Clear();
|
||||
string path = Path.Combine(txtRootPath.Text, "unshackle", "services");
|
||||
if (Directory.Exists(path))
|
||||
{
|
||||
var dirs = Directory.GetDirectories(path).Select(Path.GetFileName).ToArray();
|
||||
comboService.Items.AddRange(dirs.Cast<object>().ToArray());
|
||||
if (comboService.Items.Count > 0) comboService.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void ComboProfiles_SelectedIndexChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (comboProfiles.SelectedItem == null) return;
|
||||
|
||||
string selected = comboProfiles.SelectedItem.ToString();
|
||||
|
||||
if (_appSettings.Profiles.TryGetValue(selected, out var values))
|
||||
{
|
||||
_propertyValues = new Dictionary<string, object>(values);
|
||||
|
||||
pgProfile.SelectedObject =
|
||||
new DynamicObject(_propertyValues, _parameterDefinitions);
|
||||
|
||||
UpdateCommandPreview();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void btnAddProfile_Click(object? sender, EventArgs e)
|
||||
{
|
||||
string name = Microsoft.VisualBasic.Interaction.InputBox(
|
||||
"Enter profile name:",
|
||||
"New Profile",
|
||||
"NewProfile");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return;
|
||||
|
||||
if (_appSettings.Profiles.ContainsKey(name))
|
||||
{
|
||||
MessageBox.Show("Profile already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Clone current settings into new profile
|
||||
_appSettings.Profiles[name] =
|
||||
new Dictionary<string, object>(_propertyValues);
|
||||
|
||||
comboProfiles.DataSource = null;
|
||||
comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList();
|
||||
comboProfiles.SelectedItem = name;
|
||||
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
private void btnRemoveProfile_Click(object? sender, EventArgs e)
|
||||
{
|
||||
if (comboProfiles.SelectedItem == null)
|
||||
return;
|
||||
|
||||
string selected = comboProfiles.SelectedItem.ToString();
|
||||
|
||||
if (selected == "Default")
|
||||
{
|
||||
MessageBox.Show("Default profile cannot be deleted.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_appSettings.Profiles.ContainsKey(selected))
|
||||
return;
|
||||
|
||||
_appSettings.Profiles.Remove(selected);
|
||||
|
||||
comboProfiles.DataSource = null;
|
||||
comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList();
|
||||
|
||||
comboProfiles.SelectedIndex = 0;
|
||||
|
||||
SaveConfig();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void ToggleUI(bool r) => this.BeginInvoke(new Action(() => { btnRun.Enabled = !r; btnStop.Visible = r; }));
|
||||
private void btnStop_Click(object? sender, EventArgs e) { _currentProcess?.Kill(true); }
|
||||
private void btnClearLog_Click(object? sender, EventArgs e) => txtLog.Clear();
|
||||
private void btnBrowse_Click(object? sender, EventArgs e)
|
||||
{
|
||||
using (var fbd = new FolderBrowserDialog()) if (fbd.ShowDialog() == DialogResult.OK) { txtRootPath.Text = fbd.SelectedPath; SaveConfig(); RefreshFolders(); }
|
||||
}
|
||||
private void btnOpenCookies_Click(object? sender, EventArgs e)
|
||||
{
|
||||
string p = Path.Combine(txtRootPath.Text, "unshackle", "cookies", comboService.Text);
|
||||
Directory.CreateDirectory(p); Process.Start("explorer.exe", p);
|
||||
}
|
||||
private void btnEditYaml_Click(object? sender, EventArgs e)
|
||||
{
|
||||
string yamlPath = Path.Combine(txtRootPath.Text, "unshackle/unshackle.yaml");
|
||||
if (File.Exists(yamlPath)) Process.Start(new ProcessStartInfo(yamlPath) { UseShellExecute = true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- DATA MODELS ---
|
||||
public class AppSettings
|
||||
{
|
||||
public string RootPath { get; set; } = @"C:\DEVINE\unshackle";
|
||||
public string BinaryName { get; set; } = "uv";
|
||||
|
||||
public Dictionary<string, Dictionary<string, object>> Profiles { get; set; }
|
||||
= new Dictionary<string, Dictionary<string, object>>();
|
||||
}
|
||||
|
||||
public class UnshackleParameter
|
||||
{
|
||||
public string Flag { get; set; } = "";
|
||||
public string Name { get; set; } = "";
|
||||
public string Type { get; set; } = "Text";
|
||||
public List<string>? Options { get; set; }
|
||||
public object? Default { get; set; }
|
||||
public string Category { get; set; } = "Misc";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user