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 _parameterDefinitions = new List(); private Dictionary _propertyValues = new Dictionary(); 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(_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>(json) ?? new List(); 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(json) ?? new AppSettings(); txtRootPath.Text = _appSettings.RootPath; txtBinName.Text = _appSettings.BinaryName; } if (_appSettings.Profiles == null || _appSettings.Profiles.Count == 0) { _appSettings.Profiles = new Dictionary>(); _appSettings.Profiles["Default"] = new Dictionary(_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(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().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(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(_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> Profiles { get; set; } = new Dictionary>(); } public class UnshackleParameter { public string Flag { get; set; } = ""; public string Name { get; set; } = ""; public string Type { get; set; } = "Text"; public List? Options { get; set; } public object? Default { get; set; } public string Category { get; set; } = "Misc"; } }