diff --git a/2026-02-08_12-18-46.png b/2026-02-08_12-18-46.png deleted file mode 100644 index 34105c9..0000000 Binary files a/2026-02-08_12-18-46.png and /dev/null differ diff --git a/README.md b/README.md index 13b6625..ffe3736 100644 --- a/README.md +++ b/README.md @@ -1,3 +1 @@ -# Shackle - -![picture](https://blvckout.foo/Bl4ckOut/Schackle/raw/branch/master/2026-02-08_12-18-46.png) \ No newline at end of file +# Shackle \ No newline at end of file diff --git a/Shackle/MainForm.Designer.cs b/Shackle/MainForm.Designer.cs index e1628d4..84e1004 100644 --- a/Shackle/MainForm.Designer.cs +++ b/Shackle/MainForm.Designer.cs @@ -14,7 +14,7 @@ private System.Windows.Forms.Button btnRun; private System.Windows.Forms.Button btnStop; private System.Windows.Forms.Button btnOpenCookies; - private System.Windows.Forms.Button btnEditServiceConfig; // New + private System.Windows.Forms.Button btnEditServiceConfig; private System.Windows.Forms.Button btnEditYaml; private System.Windows.Forms.Button btnBrowse; private System.Windows.Forms.Button btnClearLog; @@ -22,219 +22,244 @@ private System.Windows.Forms.PropertyGrid pgProfile; private System.Windows.Forms.Label lblPath; - protected override void Dispose(bool disposing) { if (disposing && (components != null)) components.Dispose(); base.Dispose(disposing); } + // FIXED: Correct variable name (was bntnSaveProfile) + private System.Windows.Forms.Button btnSaveProfile; + + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) components.Dispose(); + base.Dispose(disposing); + } private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - txtRootPath = new TextBox(); - txtBinName = new TextBox(); - comboProfiles = new ComboBox(); - btnAddProfile = new Button(); - btnRemoveProfile = new Button(); - comboService = new ComboBox(); - txtURL = new TextBox(); - txtCommandPreview = new TextBox(); - btnRun = new Button(); - btnStop = new Button(); - btnOpenCookies = new Button(); - btnEditServiceConfig = new Button(); - btnEditYaml = new Button(); - btnBrowse = new Button(); - btnClearLog = new Button(); - txtLog = new TextBox(); - pgProfile = new PropertyGrid(); - lblPath = new Label(); - SuspendLayout(); + this.txtRootPath = new System.Windows.Forms.TextBox(); + this.txtBinName = new System.Windows.Forms.TextBox(); + this.comboProfiles = new System.Windows.Forms.ComboBox(); + this.btnAddProfile = new System.Windows.Forms.Button(); + this.btnRemoveProfile = new System.Windows.Forms.Button(); + this.comboService = new System.Windows.Forms.ComboBox(); + this.txtURL = new System.Windows.Forms.TextBox(); + this.txtCommandPreview = new System.Windows.Forms.TextBox(); + this.btnRun = new System.Windows.Forms.Button(); + this.btnStop = new System.Windows.Forms.Button(); + this.btnOpenCookies = new System.Windows.Forms.Button(); + this.btnEditServiceConfig = new System.Windows.Forms.Button(); + this.btnEditYaml = new System.Windows.Forms.Button(); + this.btnBrowse = new System.Windows.Forms.Button(); + this.btnClearLog = new System.Windows.Forms.Button(); + this.txtLog = new System.Windows.Forms.TextBox(); + this.pgProfile = new System.Windows.Forms.PropertyGrid(); + this.lblPath = new System.Windows.Forms.Label(); + this.btnSaveProfile = new System.Windows.Forms.Button(); + this.SuspendLayout(); // // txtRootPath // - txtRootPath.Location = new Point(12, 35); - txtRootPath.Name = "txtRootPath"; - txtRootPath.Size = new Size(280, 27); - txtRootPath.TabIndex = 0; + this.txtRootPath.Location = new System.Drawing.Point(12, 35); + this.txtRootPath.Name = "txtRootPath"; + this.txtRootPath.Size = new System.Drawing.Size(280, 27); + this.txtRootPath.TabIndex = 0; // // txtBinName // - txtBinName.Location = new Point(340, 35); - txtBinName.Name = "txtBinName"; - txtBinName.Size = new Size(60, 27); - txtBinName.TabIndex = 2; + this.txtBinName.Location = new System.Drawing.Point(340, 35); + this.txtBinName.Name = "txtBinName"; + this.txtBinName.Size = new System.Drawing.Size(60, 27); + this.txtBinName.TabIndex = 2; // // comboProfiles // - comboProfiles.Anchor = AnchorStyles.Top | AnchorStyles.Right; - comboProfiles.Location = new Point(1177, 35); - comboProfiles.Name = "comboProfiles"; - comboProfiles.Size = new Size(240, 28); - comboProfiles.TabIndex = 4; + this.comboProfiles.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.comboProfiles.Location = new System.Drawing.Point(1177, 35); + this.comboProfiles.Name = "comboProfiles"; + this.comboProfiles.Size = new System.Drawing.Size(240, 28); + this.comboProfiles.TabIndex = 4; // // btnAddProfile // - btnAddProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnAddProfile.Location = new Point(1422, 34); - btnAddProfile.Name = "btnAddProfile"; - btnAddProfile.Size = new Size(35, 29); - btnAddProfile.TabIndex = 5; - btnAddProfile.Text = "+"; + this.btnAddProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnAddProfile.Location = new System.Drawing.Point(1422, 34); + this.btnAddProfile.Name = "btnAddProfile"; + this.btnAddProfile.Size = new System.Drawing.Size(35, 29); + this.btnAddProfile.TabIndex = 5; + this.btnAddProfile.Text = "+"; // // btnRemoveProfile // - btnRemoveProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnRemoveProfile.Location = new Point(1462, 34); - btnRemoveProfile.Name = "btnRemoveProfile"; - btnRemoveProfile.Size = new Size(35, 29); - btnRemoveProfile.TabIndex = 6; - btnRemoveProfile.Text = "-"; + this.btnRemoveProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnRemoveProfile.Location = new System.Drawing.Point(1462, 34); + this.btnRemoveProfile.Name = "btnRemoveProfile"; + this.btnRemoveProfile.Size = new System.Drawing.Size(35, 29); + this.btnRemoveProfile.TabIndex = 6; + this.btnRemoveProfile.Text = "-"; // // comboService // - comboService.Location = new Point(12, 80); - comboService.Name = "comboService"; - comboService.Size = new Size(100, 28); - comboService.TabIndex = 7; + this.comboService.Location = new System.Drawing.Point(12, 80); + this.comboService.Name = "comboService"; + this.comboService.Size = new System.Drawing.Size(100, 28); + this.comboService.TabIndex = 7; // // txtURL // - txtURL.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - txtURL.Location = new Point(115, 80); - txtURL.Name = "txtURL"; - txtURL.PlaceholderText = "Paste ID / URL here..."; - txtURL.Size = new Size(801, 27); - txtURL.TabIndex = 8; + this.txtURL.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.txtURL.Location = new System.Drawing.Point(115, 80); + this.txtURL.Name = "txtURL"; + this.txtURL.PlaceholderText = "Paste ID / URL here..."; + this.txtURL.Size = new System.Drawing.Size(801, 27); + this.txtURL.TabIndex = 8; // // txtCommandPreview // - txtCommandPreview.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - txtCommandPreview.BackColor = Color.FromArgb(20, 20, 20); - txtCommandPreview.Font = new Font("Consolas", 10F, FontStyle.Bold); - txtCommandPreview.ForeColor = Color.Cyan; - txtCommandPreview.Location = new Point(12, 115); - txtCommandPreview.Name = "txtCommandPreview"; - txtCommandPreview.Size = new Size(1130, 27); - txtCommandPreview.TabIndex = 9; + this.txtCommandPreview.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.txtCommandPreview.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(20)))), ((int)(((byte)(20))))); + this.txtCommandPreview.Font = new System.Drawing.Font("Consolas", 10F, System.Drawing.FontStyle.Bold); + this.txtCommandPreview.ForeColor = System.Drawing.Color.Cyan; + this.txtCommandPreview.Location = new System.Drawing.Point(12, 115); + this.txtCommandPreview.Name = "txtCommandPreview"; + this.txtCommandPreview.Size = new System.Drawing.Size(1130, 27); + this.txtCommandPreview.TabIndex = 9; // // btnRun // - btnRun.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - btnRun.Location = new Point(12, 150); - btnRun.Name = "btnRun"; - btnRun.Size = new Size(1130, 45); - btnRun.TabIndex = 10; - btnRun.Text = "RUN"; + this.btnRun.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.btnRun.Location = new System.Drawing.Point(12, 150); + this.btnRun.Name = "btnRun"; + this.btnRun.Size = new System.Drawing.Size(1130, 45); + this.btnRun.TabIndex = 10; + this.btnRun.Text = "RUN"; // // btnStop // - btnStop.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - btnStop.BackColor = Color.Maroon; - btnStop.Location = new Point(12, 150); - btnStop.Name = "btnStop"; - btnStop.Size = new Size(1130, 45); - btnStop.TabIndex = 11; - btnStop.Text = "STOP"; - btnStop.UseVisualStyleBackColor = false; - btnStop.Visible = false; + this.btnStop.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.btnStop.BackColor = System.Drawing.Color.Maroon; + this.btnStop.Location = new System.Drawing.Point(12, 150); + this.btnStop.Name = "btnStop"; + this.btnStop.Size = new System.Drawing.Size(1130, 45); + this.btnStop.TabIndex = 11; + this.btnStop.Text = "STOP"; + this.btnStop.UseVisualStyleBackColor = false; + this.btnStop.Visible = false; // // btnOpenCookies // - btnOpenCookies.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnOpenCookies.Location = new Point(922, 79); - btnOpenCookies.Name = "btnOpenCookies"; - btnOpenCookies.Size = new Size(115, 30); - btnOpenCookies.TabIndex = 12; - btnOpenCookies.Text = "🍪 Cookies"; + this.btnOpenCookies.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnOpenCookies.Location = new System.Drawing.Point(922, 79); + this.btnOpenCookies.Name = "btnOpenCookies"; + this.btnOpenCookies.Size = new System.Drawing.Size(115, 30); + this.btnOpenCookies.TabIndex = 12; + this.btnOpenCookies.Text = "🍪 Cookies"; // // btnEditServiceConfig // - btnEditServiceConfig.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnEditServiceConfig.Location = new Point(1043, 79); - btnEditServiceConfig.Name = "btnEditServiceConfig"; - btnEditServiceConfig.Size = new Size(99, 30); - btnEditServiceConfig.TabIndex = 13; - btnEditServiceConfig.Text = "⚙️ Service"; + this.btnEditServiceConfig.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnEditServiceConfig.Location = new System.Drawing.Point(1043, 79); + this.btnEditServiceConfig.Name = "btnEditServiceConfig"; + this.btnEditServiceConfig.Size = new System.Drawing.Size(99, 30); + this.btnEditServiceConfig.TabIndex = 13; + this.btnEditServiceConfig.Text = "⚙️ Service"; // // btnEditYaml // - btnEditYaml.Location = new Point(410, 34); - btnEditYaml.Name = "btnEditYaml"; - btnEditYaml.Size = new Size(144, 29); - btnEditYaml.TabIndex = 3; - btnEditYaml.Text = "📝 Main Config"; + this.btnEditYaml.Location = new System.Drawing.Point(410, 34); + this.btnEditYaml.Name = "btnEditYaml"; + this.btnEditYaml.Size = new System.Drawing.Size(144, 29); + this.btnEditYaml.TabIndex = 3; + this.btnEditYaml.Text = "📝 Main Config"; // // btnBrowse // - btnBrowse.Location = new Point(295, 34); - btnBrowse.Name = "btnBrowse"; - btnBrowse.Size = new Size(35, 29); - btnBrowse.TabIndex = 1; - btnBrowse.Text = "📂"; + this.btnBrowse.Location = new System.Drawing.Point(295, 34); + this.btnBrowse.Name = "btnBrowse"; + this.btnBrowse.Size = new System.Drawing.Size(35, 29); + this.btnBrowse.TabIndex = 1; + this.btnBrowse.Text = "📂"; // // btnClearLog // - btnClearLog.Anchor = AnchorStyles.Top | AnchorStyles.Right; - btnClearLog.Location = new Point(1022, 201); - btnClearLog.Name = "btnClearLog"; - btnClearLog.Size = new Size(120, 36); - btnClearLog.TabIndex = 14; - btnClearLog.Text = "Clear Log"; + this.btnClearLog.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnClearLog.Location = new System.Drawing.Point(1022, 201); + this.btnClearLog.Name = "btnClearLog"; + this.btnClearLog.Size = new System.Drawing.Size(120, 36); + this.btnClearLog.TabIndex = 14; + this.btnClearLog.Text = "Clear Log"; // // txtLog // - txtLog.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; - txtLog.BackColor = Color.Black; - txtLog.Font = new Font("Consolas", 9F); - txtLog.ForeColor = Color.Lime; - txtLog.Location = new Point(12, 243); - txtLog.Multiline = true; - txtLog.Name = "txtLog"; - txtLog.ScrollBars = ScrollBars.Vertical; - txtLog.Size = new Size(1130, 550); - txtLog.TabIndex = 15; + this.txtLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); + this.txtLog.BackColor = System.Drawing.Color.Black; + this.txtLog.Font = new System.Drawing.Font("Consolas", 9F); + this.txtLog.ForeColor = System.Drawing.Color.Lime; + this.txtLog.Location = new System.Drawing.Point(12, 243); + this.txtLog.Multiline = true; + this.txtLog.Name = "txtLog"; + this.txtLog.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; + this.txtLog.Size = new System.Drawing.Size(1130, 550); + this.txtLog.TabIndex = 15; // // pgProfile // - pgProfile.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; - pgProfile.CategoryForeColor = SystemColors.ActiveCaption; - pgProfile.Location = new Point(1177, 80); - pgProfile.Name = "pgProfile"; - pgProfile.Size = new Size(320, 713); - pgProfile.TabIndex = 16; + this.pgProfile.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Right))); + this.pgProfile.CategoryForeColor = System.Drawing.SystemColors.ActiveCaption; + this.pgProfile.Location = new System.Drawing.Point(1177, 80); + this.pgProfile.Name = "pgProfile"; + this.pgProfile.Size = new System.Drawing.Size(320, 713); + this.pgProfile.TabIndex = 16; // // lblPath // - lblPath.Location = new Point(12, 12); - lblPath.Name = "lblPath"; - lblPath.Size = new Size(150, 23); - lblPath.TabIndex = 17; - lblPath.Text = "Root Path & Binary:"; + this.lblPath.Location = new System.Drawing.Point(12, 12); + this.lblPath.Name = "lblPath"; + this.lblPath.Size = new System.Drawing.Size(150, 23); + this.lblPath.TabIndex = 17; + this.lblPath.Text = "Root Path & Binary:"; + // + // btnSaveProfile + // + this.btnSaveProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnSaveProfile.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnSaveProfile.FlatAppearance.BorderColor = System.Drawing.Color.Gray; + this.btnSaveProfile.ForeColor = System.Drawing.Color.White; + // FIXED: Calculated position to be right next to the Remove (-) button + this.btnSaveProfile.Location = new System.Drawing.Point(1502, 34); + this.btnSaveProfile.Name = "btnSaveProfile"; + this.btnSaveProfile.Size = new System.Drawing.Size(35, 29); + this.btnSaveProfile.TabIndex = 18; + this.btnSaveProfile.Text = "💾"; + this.btnSaveProfile.UseVisualStyleBackColor = true; + this.btnSaveProfile.Click += new System.EventHandler(this.btnSaveProfile_Click); // // MainForm // - ClientSize = new Size(1519, 808); - Controls.Add(txtRootPath); - Controls.Add(btnBrowse); - Controls.Add(txtBinName); - Controls.Add(btnEditYaml); - Controls.Add(comboProfiles); - Controls.Add(btnAddProfile); - Controls.Add(btnRemoveProfile); - Controls.Add(comboService); - Controls.Add(txtURL); - Controls.Add(txtCommandPreview); - Controls.Add(btnRun); - Controls.Add(btnStop); - Controls.Add(btnOpenCookies); - Controls.Add(btnEditServiceConfig); - Controls.Add(btnClearLog); - Controls.Add(txtLog); - Controls.Add(pgProfile); - Controls.Add(lblPath); - Icon = (Icon)resources.GetObject("$this.Icon"); - MinimumSize = new Size(960, 680); - Name = "MainForm"; - Text = "Unshackle Master GUI"; - ResumeLayout(false); - PerformLayout(); + this.ClientSize = new System.Drawing.Size(1550, 808); // Slightly widened to fit new button + this.Controls.Add(this.txtRootPath); + this.Controls.Add(this.btnBrowse); + this.Controls.Add(this.txtBinName); + this.Controls.Add(this.btnEditYaml); + this.Controls.Add(this.comboProfiles); + this.Controls.Add(this.btnAddProfile); + this.Controls.Add(this.btnRemoveProfile); + // FIXED: Added btnSaveProfile to controls list + this.Controls.Add(this.btnSaveProfile); + this.Controls.Add(this.comboService); + this.Controls.Add(this.txtURL); + this.Controls.Add(this.txtCommandPreview); + this.Controls.Add(this.btnRun); + this.Controls.Add(this.btnStop); + this.Controls.Add(this.btnOpenCookies); + this.Controls.Add(this.btnEditServiceConfig); + this.Controls.Add(this.btnClearLog); + this.Controls.Add(this.txtLog); + this.Controls.Add(this.pgProfile); + this.Controls.Add(this.lblPath); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.MinimumSize = new System.Drawing.Size(960, 680); + this.Name = "MainForm"; + this.Text = "Unshackle Master GUI"; + this.ResumeLayout(false); + this.PerformLayout(); } } } \ No newline at end of file diff --git a/Shackle/MainForm.cs b/Shackle/MainForm.cs index dfcddd4..6810a02 100644 --- a/Shackle/MainForm.cs +++ b/Shackle/MainForm.cs @@ -13,40 +13,84 @@ namespace UnshackleGUI { public partial class MainForm : Form { + // ========================================================= + // 1. FILE PATHS & SETTINGS + // ========================================================= private string _configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"); private string _paramsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "params.json"); + private string _profilesDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Profiles"); + // Data Storage private AppSettings _appSettings = new AppSettings(); private List _parameterDefinitions = new List(); private Dictionary _propertyValues = new Dictionary(); + // State Variables private Process? _currentProcess; - + private bool _isLoaded = false; // Flag to prevent saving while the app is starting up + private string _lastLoadedProfile = ""; // Tracks the original name + // ========================================================= + // 2. CONSTRUCTOR & INITIALIZATION + // ========================================================= public MainForm() { InitializeComponent(); ApplyDarkTheme(); - LoadParamsAndConfig(); - // EVENT: This updates the command line the moment a value changes + // Ensure the Profiles folder exists + if (!Directory.Exists(_profilesDir)) + { + Directory.CreateDirectory(_profilesDir); + } + + // Step 1: Set up all button clicks and text changes + SetupEventHandlers(); + + // Step 2: Load data from disk (Config, Params, Profiles) + LoadData(); + } + + private void SetupEventHandlers() + { + // --- Property Grid (The Settings List) --- + // Whenever a value changes in the grid, update the command text and save the profile immediately. pgProfile.PropertyValueChanged += (s, e) => { UpdateCommandPreview(); - - if (comboProfiles.SelectedItem != null) - { - string selected = comboProfiles.SelectedItem.ToString(); - _appSettings.Profiles[selected] = - new Dictionary(_propertyValues); - } - - SaveConfig(); + SaveCurrentProfile(); }; - comboService.SelectedIndexChanged += (s, e) => UpdateCommandPreview(); - txtURL.TextChanged += (s, e) => UpdateCommandPreview(); - txtBinName.TextChanged += (s, e) => UpdateCommandPreview(); + // --- Service Dropdown (Netflix, Amazon, etc.) --- + comboService.SelectedIndexChanged += (s, e) => + { + // Don't run this logic if the app is still starting up + if (!_isLoaded) return; + if (comboService.SelectedItem != null) + { + // Update the "Service" value in our data dictionary + _propertyValues["Service"] = comboService.SelectedItem.ToString(); + + // Update the command preview + UpdateCommandPreview(); + + // Save this change to the JSON file immediately + SaveCurrentProfile(); + } + }; + + // --- Text Box Changes --- + txtURL.TextChanged += (s, e) => UpdateCommandPreview(); + + txtBinName.TextChanged += (s, e) => + { + UpdateCommandPreview(); + SaveGlobalConfig(); + }; + + txtRootPath.TextChanged += (s, e) => SaveGlobalConfig(); + + // --- Button Clicks --- btnRun.Click += btnRun_Click; btnStop.Click += btnStop_Click; btnBrowse.Click += btnBrowse_Click; @@ -56,130 +100,317 @@ namespace UnshackleGUI btnEditYaml.Click += btnEditYaml_Click; btnAddProfile.Click += btnAddProfile_Click; btnRemoveProfile.Click += btnRemoveProfile_Click; + + // This line connects the click to the function below + btnSaveProfile.Click += btnSaveProfile_Click; } - private void LoadParamsAndConfig() + // ========================================================= + // 3. DATA LOADING LOGIC + // ========================================================= + private void LoadData() { - // 1. Load Parameters (Rules) + _isLoaded = false; // Stop events from firing while we load + + // A. Load Parameters (The definitions for flags like -q, -v, etc.) 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 ?? ""; - } + } + else + { + MessageBox.Show("Error: params.json not found! The grid will be empty."); } - // 2. Load Config (Paths/Profiles) + // B. Load Global Config (Root Path, Binary Name) 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); - - + // C. Populate the Services Dropdown (Read folders from disk) RefreshFolders(); + + // D. Load Profiles and select the default one + RefreshProfileList(); + + _isLoaded = true; // Loading done, enable events + + // Force an initial update of the command text UpdateCommandPreview(); } - private void UpdateCommandPreview() + private void RefreshProfileList() { - StringBuilder sb = new StringBuilder(); - sb.Append($"{txtBinName.Text} run unshackle dl "); + // 1. Temporarily stop listening to the selection event to prevent glitches + comboProfiles.SelectedIndexChanged -= ComboProfiles_SelectedIndexChanged; + // 2. Find all .json files in the Profiles folder + var files = Directory.GetFiles(_profilesDir, "*.json"); + var profileNames = files.Select(Path.GetFileNameWithoutExtension).ToList(); + + // 3. If no profiles exist, create a "Default" one + if (profileNames.Count == 0) + { + CreateDefaultProfile(); + profileNames.Add("Default"); + } + + // 4. Update the ComboBox + comboProfiles.DataSource = null; + comboProfiles.DataSource = profileNames; + + // 5. Select "Default" or the first available profile + string targetProfile = profileNames.Contains("Default") ? "Default" : profileNames[0]; + comboProfiles.SelectedItem = targetProfile; + + // 6. Manually trigger the load for this profile + LoadProfile(targetProfile); + + // 7. Start listening to events again + comboProfiles.SelectedIndexChanged += ComboProfiles_SelectedIndexChanged; + } + + private void CreateDefaultProfile() + { + _propertyValues = new Dictionary(); + + // Fill with default values from params.json foreach (var p in _parameterDefinitions) { + _propertyValues[p.Name] = p.Default ?? ""; + } + + // Set default service + if (comboService.Items.Count > 0) + { + _propertyValues["Service"] = comboService.Items[0].ToString(); + } + + SaveProfileFile("Default"); + } + + // ========================================================= + // 4. PROFILE SWITCHING & LOADING + // ========================================================= + private void ComboProfiles_SelectedIndexChanged(object? sender, EventArgs e) + { + if (comboProfiles.SelectedItem == null) return; + + string selectedProfile = comboProfiles.SelectedItem.ToString(); + LoadProfile(selectedProfile); + } + + private void LoadProfile(string profileName) + { + + _lastLoadedProfile = profileName; + + string path = Path.Combine(_profilesDir, $"{profileName}.json"); + + if (!File.Exists(path)) return; + + try + { + var json = File.ReadAllText(path); + var loadedValues = JsonConvert.DeserializeObject>(json); + + if (loadedValues != null) + { + _propertyValues = loadedValues; + + // --- FIX 1: Add missing parameters --- + // If we added new options to params.json, old profiles won't have them. + // This adds them with default values. + foreach (var p in _parameterDefinitions) + { + if (!_propertyValues.ContainsKey(p.Name)) + { + _propertyValues[p.Name] = p.Default ?? ""; + } + } + + // --- FIX 2: Handle the Service Dropdown --- + // Check if this profile has a saved Service (e.g., "AMZN") + if (_propertyValues.TryGetValue("Service", out var serviceObj)) + { + string serviceName = serviceObj.ToString(); + + // Only try to switch if the service actually exists in the list + if (comboService.Items.Contains(serviceName)) + { + // Temporarily disable _isLoaded to change dropdown without triggering a save loop + bool wasLoaded = _isLoaded; + _isLoaded = false; + + comboService.SelectedItem = serviceName; + + _isLoaded = wasLoaded; + } + } + else if (comboService.SelectedItem != null) + { + // If profile has NO service saved, save the current one to it + _propertyValues["Service"] = comboService.SelectedItem.ToString(); + } + + // --- FIX 3: Bind to PropertyGrid and Refresh --- + pgProfile.SelectedObject = new DynamicObject(_propertyValues, _parameterDefinitions); + pgProfile.Refresh(); // Crucial to prevent grey box + + UpdateCommandPreview(); + } + } + catch (Exception ex) + { + MessageBox.Show($"Error loading profile: {ex.Message}"); + } + } + + // ========================================================= + // 5. SAVING LOGIC + // ========================================================= + private void SaveCurrentProfile() + { + // Don't save if we are loading or if nothing is selected + if (!_isLoaded || comboProfiles.SelectedItem == null) return; + + string name = comboProfiles.SelectedItem.ToString(); + SaveProfileFile(name); + } + + private void SaveProfileFile(string name) + { + try + { + // Ensure the currently selected service is saved into the dictionary + if (comboService.SelectedItem != null) + { + _propertyValues["Service"] = comboService.SelectedItem.ToString(); + } + + string path = Path.Combine(_profilesDir, $"{name}.json"); + File.WriteAllText(path, JsonConvert.SerializeObject(_propertyValues, Formatting.Indented)); + } + catch (Exception ex) + { + AppendLog($"[Error Saving Profile]: {ex.Message}"); + } + } + + private void SaveGlobalConfig() + { + if (!_isLoaded) return; + + _appSettings.RootPath = txtRootPath.Text; + _appSettings.BinaryName = txtBinName.Text; + + try + { + File.WriteAllText(_configPath, JsonConvert.SerializeObject(_appSettings, Formatting.Indented)); + } + catch + { + // Ignore errors while typing (file might be busy) + } + } + + // ========================================================= + // 6. COMMAND GENERATION + // ========================================================= + private void UpdateCommandPreview() + { + if (!_isLoaded) return; + + StringBuilder sb = new StringBuilder(); + + // 1. Start with binary and command + // e.g., "uv run unshackle dl " + sb.Append($"{txtBinName.Text} run unshackle dl "); + + // 2. Loop through all parameters in the grid + foreach (var p in _parameterDefinitions) + { + // If value doesn't exist in our data, skip it if (!_propertyValues.TryGetValue(p.Name, out object val)) continue; - // Handle Booleans - if (p.Type == "Bool" && val is bool b && b) + // Case A: Boolean Flags (e.g., --no-mux) + if (p.Type == "Bool" && val is bool isTrue && isTrue) { sb.Append($"{p.Flag} "); } - // Handle Text/Selection/Numbers + // Case B: Text / Selection / Number else if (p.Type != "Bool" && val != null) { string sVal = val.ToString(); - // Skip empty, "any", or "0" (for default bitrates) + + // Validation: + // 1. Not empty + // 2. Not "any" (Default) + // 3. Not "0" (Default for numbers) if (!string.IsNullOrWhiteSpace(sVal) && sVal != "any" && sVal != "0") { - if (sVal.Contains(" ")) sVal = $"\"{sVal}\""; + // Quote the value if it has spaces + if (sVal.Contains(" ")) + { + sVal = $"\"{sVal}\""; + } + sb.Append($"{p.Flag} {sVal} "); } } } + // 3. Append Service and URL + // e.g., "AMZN https://..." sb.Append($"{comboService.Text} {txtURL.Text}"); + + // 4. Update UI 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}"); - } - + // ========================================================= + // 7. CLI EXECUTION (The Run Button) + // ========================================================= private async void btnRun_Click(object? sender, EventArgs e) { string finalCmd = txtCommandPreview.Text; + if (string.IsNullOrWhiteSpace(finalCmd)) return; + // Disable buttons while running ToggleUI(true); + txtLog.AppendText($"> Executing: {finalCmd}{Environment.NewLine}"); - try { await Task.Run(() => RunCli(finalCmd)); } - catch (Exception ex) { AppendLog($"[ERROR]: {ex.Message}"); } - finally { ToggleUI(false); } + try + { + // Run in background thread to keep UI responsive + await Task.Run(() => RunCli(finalCmd)); + } + catch (Exception ex) + { + AppendLog($"[ERROR]: {ex.Message}"); + } + finally + { + // Re-enable buttons + ToggleUI(false); + } } private void RunCli(string cmd) { + // We use CMD.EXE instead of PowerShell to avoid Antivirus false positives 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}", + Arguments = $"/c chcp 65001 && {cmd}", // Force UTF-8 WorkingDirectory = txtRootPath.Text, RedirectStandardOutput = true, RedirectStandardError = true, @@ -188,20 +419,24 @@ namespace UnshackleGUI StandardOutputEncoding = Encoding.UTF8 }; - // Ensure Python scripts know to use UTF-8 + // Set Environment variable for Python psi.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; using (_currentProcess = Process.Start(psi)) { if (_currentProcess == null) return; + // Handle Output _currentProcess.OutputDataReceived += (s, e) => { - // Filter out the noisy "Active code page: 65001" message from CMD + // Filter out the "Active code page: 65001" noise if (!string.IsNullOrEmpty(e.Data) && !e.Data.Contains("Active code page")) + { AppendLog(e.Data); + } }; + // Handle Errors _currentProcess.ErrorDataReceived += (s, e) => AppendLog(e.Data); _currentProcess.BeginOutputReadLine(); @@ -210,104 +445,39 @@ namespace UnshackleGUI } } - 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(); - } - } - - + // ========================================================= + // 8. PROFILE MANAGEMENT (Add/Remove/Save Buttons) + // ========================================================= private void btnAddProfile_Click(object? sender, EventArgs e) { - string name = Microsoft.VisualBasic.Interaction.InputBox( - "Enter profile name:", - "New Profile", - "NewProfile"); + string name = Microsoft.VisualBasic.Interaction.InputBox("Enter profile name:", "New Profile", ""); - if (string.IsNullOrWhiteSpace(name)) - return; + if (string.IsNullOrWhiteSpace(name)) return; - if (_appSettings.Profiles.ContainsKey(name)) + // Sanitize filename (remove illegal characters like / \ : *) + foreach (char c in Path.GetInvalidFileNameChars()) + { + name = name.Replace(c, '_'); + } + + // Check if exists + if (File.Exists(Path.Combine(_profilesDir, $"{name}.json"))) { MessageBox.Show("Profile already exists."); return; } - // Clone current settings into new profile - _appSettings.Profiles[name] = - new Dictionary(_propertyValues); + // Save current settings as the new profile + SaveProfileFile(name); - comboProfiles.DataSource = null; - comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList(); + // Reload list and select new profile + RefreshProfileList(); comboProfiles.SelectedItem = name; - - SaveConfig(); } private void btnRemoveProfile_Click(object? sender, EventArgs e) { - if (comboProfiles.SelectedItem == null) - return; + if (comboProfiles.SelectedItem == null) return; string selected = comboProfiles.SelectedItem.ToString(); @@ -317,50 +487,233 @@ namespace UnshackleGUI return; } - if (!_appSettings.Profiles.ContainsKey(selected)) + string path = Path.Combine(_profilesDir, $"{selected}.json"); + + if (File.Exists(path)) + { + File.Delete(path); + } + + RefreshProfileList(); + } + + private void btnSaveProfile_Click(object? sender, EventArgs e) + { + string newName = comboProfiles.Text.Trim(); + + // 1. Validation + if (string.IsNullOrWhiteSpace(newName)) + { + MessageBox.Show("Please enter a profile name."); return; + } - _appSettings.Profiles.Remove(selected); + foreach (char c in Path.GetInvalidFileNameChars()) newName = newName.Replace(c, '_'); - comboProfiles.DataSource = null; - comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList(); + // 2. CHECK: Did the name change? + if (newName != _lastLoadedProfile && !string.IsNullOrEmpty(_lastLoadedProfile)) + { + // Special Case: Cannot rename Default + if (_lastLoadedProfile == "Default") + { + // Just create new, don't ask to rename Default + SaveProfileFile(newName); + RefreshProfileList(); + comboProfiles.SelectedItem = newName; + MessageBox.Show($"Created new profile '{newName}' from Default.", "New Profile Created"); + return; + } - comboProfiles.SelectedIndex = 0; + // Ask the user what to do + DialogResult choice = MessageBox.Show( + $"You changed the name from '{_lastLoadedProfile}' to '{newName}'.\n\n" + + "Click YES to RENAME (Delete old).\n" + + "Click NO to CREATE COPY (Keep old).", + "Rename or Copy?", + MessageBoxButtons.YesNoCancel, + MessageBoxIcon.Question); - SaveConfig(); + if (choice == DialogResult.Cancel) return; + + if (choice == DialogResult.Yes) // RENAME + { + // Save New + SaveProfileFile(newName); + + // Delete Old + string oldPath = Path.Combine(_profilesDir, $"{_lastLoadedProfile}.json"); + if (File.Exists(oldPath)) File.Delete(oldPath); + + RefreshProfileList(); + comboProfiles.SelectedItem = newName; // This updates _lastLoadedProfile automatically + MessageBox.Show($"Renamed to '{newName}'."); + } + else // CREATE COPY + { + SaveProfileFile(newName); + RefreshProfileList(); + comboProfiles.SelectedItem = newName; + MessageBox.Show($"Created copy '{newName}'."); + } + } + else + { + // 3. Name didn't change (Normal Save) + SaveProfileFile(newName); + MessageBox.Show($"Profile '{newName}' saved!", "Saved", MessageBoxButtons.OK, MessageBoxIcon.Information); + } } - 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(); + // ========================================================= + // 9. HELPER METHODS & THEME + // ========================================================= + + private void AppendLog(string? text) + { + if (string.IsNullOrEmpty(text)) return; + + // Ensure we update the UI on the main thread + this.BeginInvoke(new Action(() => + { + txtLog.AppendText(text + Environment.NewLine); + })); + } + + private void ToggleUI(bool isRunning) + { + this.BeginInvoke(new Action(() => + { + btnRun.Enabled = !isRunning; + btnStop.Visible = isRunning; + })); + } + + private void btnStop_Click(object? sender, EventArgs e) + { + if (_currentProcess != null && !_currentProcess.HasExited) + { + _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(); } + using (var fbd = new FolderBrowserDialog()) + { + if (fbd.ShowDialog() == DialogResult.OK) + { + txtRootPath.Text = fbd.SelectedPath; + SaveGlobalConfig(); + 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); + string path = Path.Combine(txtRootPath.Text, "unshackle", "cookies", comboService.Text); + Directory.CreateDirectory(path); + Process.Start("explorer.exe", path); } + 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 }); + if (File.Exists(yamlPath)) + { + Process.Start(new ProcessStartInfo(yamlPath) { UseShellExecute = true }); + } + } + + 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 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 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); + } + + // Recursive call for panels and group boxes + foreach (Control child in c.Controls) + { + UpdateControlTheme(child); + } } } + // ========================================================= + // 10. DATA MODELS + // ========================================================= - // --- 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