Implemented Smart Save logic for profiles

This commit is contained in:
Bl4DiEDiEBL4
2026-02-10 10:41:51 +01:00
parent 3bc63e0c88
commit 3d3815692a
4 changed files with 714 additions and 338 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

View File

@@ -1,3 +1 @@
# Shackle # Shackle
![picture](https://blvckout.foo/Bl4ckOut/Schackle/raw/branch/master/2026-02-08_12-18-46.png)

View File

@@ -14,7 +14,7 @@
private System.Windows.Forms.Button btnRun; private System.Windows.Forms.Button btnRun;
private System.Windows.Forms.Button btnStop; private System.Windows.Forms.Button btnStop;
private System.Windows.Forms.Button btnOpenCookies; 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 btnEditYaml;
private System.Windows.Forms.Button btnBrowse; private System.Windows.Forms.Button btnBrowse;
private System.Windows.Forms.Button btnClearLog; private System.Windows.Forms.Button btnClearLog;
@@ -22,219 +22,244 @@
private System.Windows.Forms.PropertyGrid pgProfile; private System.Windows.Forms.PropertyGrid pgProfile;
private System.Windows.Forms.Label lblPath; 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() private void InitializeComponent()
{ {
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
txtRootPath = new TextBox(); this.txtRootPath = new System.Windows.Forms.TextBox();
txtBinName = new TextBox(); this.txtBinName = new System.Windows.Forms.TextBox();
comboProfiles = new ComboBox(); this.comboProfiles = new System.Windows.Forms.ComboBox();
btnAddProfile = new Button(); this.btnAddProfile = new System.Windows.Forms.Button();
btnRemoveProfile = new Button(); this.btnRemoveProfile = new System.Windows.Forms.Button();
comboService = new ComboBox(); this.comboService = new System.Windows.Forms.ComboBox();
txtURL = new TextBox(); this.txtURL = new System.Windows.Forms.TextBox();
txtCommandPreview = new TextBox(); this.txtCommandPreview = new System.Windows.Forms.TextBox();
btnRun = new Button(); this.btnRun = new System.Windows.Forms.Button();
btnStop = new Button(); this.btnStop = new System.Windows.Forms.Button();
btnOpenCookies = new Button(); this.btnOpenCookies = new System.Windows.Forms.Button();
btnEditServiceConfig = new Button(); this.btnEditServiceConfig = new System.Windows.Forms.Button();
btnEditYaml = new Button(); this.btnEditYaml = new System.Windows.Forms.Button();
btnBrowse = new Button(); this.btnBrowse = new System.Windows.Forms.Button();
btnClearLog = new Button(); this.btnClearLog = new System.Windows.Forms.Button();
txtLog = new TextBox(); this.txtLog = new System.Windows.Forms.TextBox();
pgProfile = new PropertyGrid(); this.pgProfile = new System.Windows.Forms.PropertyGrid();
lblPath = new Label(); this.lblPath = new System.Windows.Forms.Label();
SuspendLayout(); this.btnSaveProfile = new System.Windows.Forms.Button();
this.SuspendLayout();
// //
// txtRootPath // txtRootPath
// //
txtRootPath.Location = new Point(12, 35); this.txtRootPath.Location = new System.Drawing.Point(12, 35);
txtRootPath.Name = "txtRootPath"; this.txtRootPath.Name = "txtRootPath";
txtRootPath.Size = new Size(280, 27); this.txtRootPath.Size = new System.Drawing.Size(280, 27);
txtRootPath.TabIndex = 0; this.txtRootPath.TabIndex = 0;
// //
// txtBinName // txtBinName
// //
txtBinName.Location = new Point(340, 35); this.txtBinName.Location = new System.Drawing.Point(340, 35);
txtBinName.Name = "txtBinName"; this.txtBinName.Name = "txtBinName";
txtBinName.Size = new Size(60, 27); this.txtBinName.Size = new System.Drawing.Size(60, 27);
txtBinName.TabIndex = 2; this.txtBinName.TabIndex = 2;
// //
// comboProfiles // comboProfiles
// //
comboProfiles.Anchor = AnchorStyles.Top | AnchorStyles.Right; this.comboProfiles.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
comboProfiles.Location = new Point(1177, 35); this.comboProfiles.Location = new System.Drawing.Point(1177, 35);
comboProfiles.Name = "comboProfiles"; this.comboProfiles.Name = "comboProfiles";
comboProfiles.Size = new Size(240, 28); this.comboProfiles.Size = new System.Drawing.Size(240, 28);
comboProfiles.TabIndex = 4; this.comboProfiles.TabIndex = 4;
// //
// btnAddProfile // btnAddProfile
// //
btnAddProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right; this.btnAddProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
btnAddProfile.Location = new Point(1422, 34); this.btnAddProfile.Location = new System.Drawing.Point(1422, 34);
btnAddProfile.Name = "btnAddProfile"; this.btnAddProfile.Name = "btnAddProfile";
btnAddProfile.Size = new Size(35, 29); this.btnAddProfile.Size = new System.Drawing.Size(35, 29);
btnAddProfile.TabIndex = 5; this.btnAddProfile.TabIndex = 5;
btnAddProfile.Text = "+"; this.btnAddProfile.Text = "+";
// //
// btnRemoveProfile // btnRemoveProfile
// //
btnRemoveProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right; this.btnRemoveProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
btnRemoveProfile.Location = new Point(1462, 34); this.btnRemoveProfile.Location = new System.Drawing.Point(1462, 34);
btnRemoveProfile.Name = "btnRemoveProfile"; this.btnRemoveProfile.Name = "btnRemoveProfile";
btnRemoveProfile.Size = new Size(35, 29); this.btnRemoveProfile.Size = new System.Drawing.Size(35, 29);
btnRemoveProfile.TabIndex = 6; this.btnRemoveProfile.TabIndex = 6;
btnRemoveProfile.Text = "-"; this.btnRemoveProfile.Text = "-";
// //
// comboService // comboService
// //
comboService.Location = new Point(12, 80); this.comboService.Location = new System.Drawing.Point(12, 80);
comboService.Name = "comboService"; this.comboService.Name = "comboService";
comboService.Size = new Size(100, 28); this.comboService.Size = new System.Drawing.Size(100, 28);
comboService.TabIndex = 7; this.comboService.TabIndex = 7;
// //
// txtURL // txtURL
// //
txtURL.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; this.txtURL.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
txtURL.Location = new Point(115, 80); this.txtURL.Location = new System.Drawing.Point(115, 80);
txtURL.Name = "txtURL"; this.txtURL.Name = "txtURL";
txtURL.PlaceholderText = "Paste ID / URL here..."; this.txtURL.PlaceholderText = "Paste ID / URL here...";
txtURL.Size = new Size(801, 27); this.txtURL.Size = new System.Drawing.Size(801, 27);
txtURL.TabIndex = 8; this.txtURL.TabIndex = 8;
// //
// txtCommandPreview // txtCommandPreview
// //
txtCommandPreview.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; this.txtCommandPreview.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
txtCommandPreview.BackColor = Color.FromArgb(20, 20, 20); this.txtCommandPreview.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(20)))), ((int)(((byte)(20)))));
txtCommandPreview.Font = new Font("Consolas", 10F, FontStyle.Bold); this.txtCommandPreview.Font = new System.Drawing.Font("Consolas", 10F, System.Drawing.FontStyle.Bold);
txtCommandPreview.ForeColor = Color.Cyan; this.txtCommandPreview.ForeColor = System.Drawing.Color.Cyan;
txtCommandPreview.Location = new Point(12, 115); this.txtCommandPreview.Location = new System.Drawing.Point(12, 115);
txtCommandPreview.Name = "txtCommandPreview"; this.txtCommandPreview.Name = "txtCommandPreview";
txtCommandPreview.Size = new Size(1130, 27); this.txtCommandPreview.Size = new System.Drawing.Size(1130, 27);
txtCommandPreview.TabIndex = 9; this.txtCommandPreview.TabIndex = 9;
// //
// btnRun // btnRun
// //
btnRun.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; this.btnRun.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
btnRun.Location = new Point(12, 150); this.btnRun.Location = new System.Drawing.Point(12, 150);
btnRun.Name = "btnRun"; this.btnRun.Name = "btnRun";
btnRun.Size = new Size(1130, 45); this.btnRun.Size = new System.Drawing.Size(1130, 45);
btnRun.TabIndex = 10; this.btnRun.TabIndex = 10;
btnRun.Text = "RUN"; this.btnRun.Text = "RUN";
// //
// btnStop // btnStop
// //
btnStop.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; this.btnStop.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
btnStop.BackColor = Color.Maroon; this.btnStop.BackColor = System.Drawing.Color.Maroon;
btnStop.Location = new Point(12, 150); this.btnStop.Location = new System.Drawing.Point(12, 150);
btnStop.Name = "btnStop"; this.btnStop.Name = "btnStop";
btnStop.Size = new Size(1130, 45); this.btnStop.Size = new System.Drawing.Size(1130, 45);
btnStop.TabIndex = 11; this.btnStop.TabIndex = 11;
btnStop.Text = "STOP"; this.btnStop.Text = "STOP";
btnStop.UseVisualStyleBackColor = false; this.btnStop.UseVisualStyleBackColor = false;
btnStop.Visible = false; this.btnStop.Visible = false;
// //
// btnOpenCookies // btnOpenCookies
// //
btnOpenCookies.Anchor = AnchorStyles.Top | AnchorStyles.Right; this.btnOpenCookies.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
btnOpenCookies.Location = new Point(922, 79); this.btnOpenCookies.Location = new System.Drawing.Point(922, 79);
btnOpenCookies.Name = "btnOpenCookies"; this.btnOpenCookies.Name = "btnOpenCookies";
btnOpenCookies.Size = new Size(115, 30); this.btnOpenCookies.Size = new System.Drawing.Size(115, 30);
btnOpenCookies.TabIndex = 12; this.btnOpenCookies.TabIndex = 12;
btnOpenCookies.Text = "🍪 Cookies"; this.btnOpenCookies.Text = "🍪 Cookies";
// //
// btnEditServiceConfig // btnEditServiceConfig
// //
btnEditServiceConfig.Anchor = AnchorStyles.Top | AnchorStyles.Right; this.btnEditServiceConfig.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
btnEditServiceConfig.Location = new Point(1043, 79); this.btnEditServiceConfig.Location = new System.Drawing.Point(1043, 79);
btnEditServiceConfig.Name = "btnEditServiceConfig"; this.btnEditServiceConfig.Name = "btnEditServiceConfig";
btnEditServiceConfig.Size = new Size(99, 30); this.btnEditServiceConfig.Size = new System.Drawing.Size(99, 30);
btnEditServiceConfig.TabIndex = 13; this.btnEditServiceConfig.TabIndex = 13;
btnEditServiceConfig.Text = "⚙️ Service"; this.btnEditServiceConfig.Text = "⚙️ Service";
// //
// btnEditYaml // btnEditYaml
// //
btnEditYaml.Location = new Point(410, 34); this.btnEditYaml.Location = new System.Drawing.Point(410, 34);
btnEditYaml.Name = "btnEditYaml"; this.btnEditYaml.Name = "btnEditYaml";
btnEditYaml.Size = new Size(144, 29); this.btnEditYaml.Size = new System.Drawing.Size(144, 29);
btnEditYaml.TabIndex = 3; this.btnEditYaml.TabIndex = 3;
btnEditYaml.Text = "📝 Main Config"; this.btnEditYaml.Text = "📝 Main Config";
// //
// btnBrowse // btnBrowse
// //
btnBrowse.Location = new Point(295, 34); this.btnBrowse.Location = new System.Drawing.Point(295, 34);
btnBrowse.Name = "btnBrowse"; this.btnBrowse.Name = "btnBrowse";
btnBrowse.Size = new Size(35, 29); this.btnBrowse.Size = new System.Drawing.Size(35, 29);
btnBrowse.TabIndex = 1; this.btnBrowse.TabIndex = 1;
btnBrowse.Text = "📂"; this.btnBrowse.Text = "📂";
// //
// btnClearLog // btnClearLog
// //
btnClearLog.Anchor = AnchorStyles.Top | AnchorStyles.Right; this.btnClearLog.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
btnClearLog.Location = new Point(1022, 201); this.btnClearLog.Location = new System.Drawing.Point(1022, 201);
btnClearLog.Name = "btnClearLog"; this.btnClearLog.Name = "btnClearLog";
btnClearLog.Size = new Size(120, 36); this.btnClearLog.Size = new System.Drawing.Size(120, 36);
btnClearLog.TabIndex = 14; this.btnClearLog.TabIndex = 14;
btnClearLog.Text = "Clear Log"; this.btnClearLog.Text = "Clear Log";
// //
// txtLog // txtLog
// //
txtLog.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; 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)));
txtLog.BackColor = Color.Black; this.txtLog.BackColor = System.Drawing.Color.Black;
txtLog.Font = new Font("Consolas", 9F); this.txtLog.Font = new System.Drawing.Font("Consolas", 9F);
txtLog.ForeColor = Color.Lime; this.txtLog.ForeColor = System.Drawing.Color.Lime;
txtLog.Location = new Point(12, 243); this.txtLog.Location = new System.Drawing.Point(12, 243);
txtLog.Multiline = true; this.txtLog.Multiline = true;
txtLog.Name = "txtLog"; this.txtLog.Name = "txtLog";
txtLog.ScrollBars = ScrollBars.Vertical; this.txtLog.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
txtLog.Size = new Size(1130, 550); this.txtLog.Size = new System.Drawing.Size(1130, 550);
txtLog.TabIndex = 15; this.txtLog.TabIndex = 15;
// //
// pgProfile // pgProfile
// //
pgProfile.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right; this.pgProfile.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Right)));
pgProfile.CategoryForeColor = SystemColors.ActiveCaption; this.pgProfile.CategoryForeColor = System.Drawing.SystemColors.ActiveCaption;
pgProfile.Location = new Point(1177, 80); this.pgProfile.Location = new System.Drawing.Point(1177, 80);
pgProfile.Name = "pgProfile"; this.pgProfile.Name = "pgProfile";
pgProfile.Size = new Size(320, 713); this.pgProfile.Size = new System.Drawing.Size(320, 713);
pgProfile.TabIndex = 16; this.pgProfile.TabIndex = 16;
// //
// lblPath // lblPath
// //
lblPath.Location = new Point(12, 12); this.lblPath.Location = new System.Drawing.Point(12, 12);
lblPath.Name = "lblPath"; this.lblPath.Name = "lblPath";
lblPath.Size = new Size(150, 23); this.lblPath.Size = new System.Drawing.Size(150, 23);
lblPath.TabIndex = 17; this.lblPath.TabIndex = 17;
lblPath.Text = "Root Path & Binary:"; 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 // MainForm
// //
ClientSize = new Size(1519, 808); this.ClientSize = new System.Drawing.Size(1550, 808); // Slightly widened to fit new button
Controls.Add(txtRootPath); this.Controls.Add(this.txtRootPath);
Controls.Add(btnBrowse); this.Controls.Add(this.btnBrowse);
Controls.Add(txtBinName); this.Controls.Add(this.txtBinName);
Controls.Add(btnEditYaml); this.Controls.Add(this.btnEditYaml);
Controls.Add(comboProfiles); this.Controls.Add(this.comboProfiles);
Controls.Add(btnAddProfile); this.Controls.Add(this.btnAddProfile);
Controls.Add(btnRemoveProfile); this.Controls.Add(this.btnRemoveProfile);
Controls.Add(comboService); // FIXED: Added btnSaveProfile to controls list
Controls.Add(txtURL); this.Controls.Add(this.btnSaveProfile);
Controls.Add(txtCommandPreview); this.Controls.Add(this.comboService);
Controls.Add(btnRun); this.Controls.Add(this.txtURL);
Controls.Add(btnStop); this.Controls.Add(this.txtCommandPreview);
Controls.Add(btnOpenCookies); this.Controls.Add(this.btnRun);
Controls.Add(btnEditServiceConfig); this.Controls.Add(this.btnStop);
Controls.Add(btnClearLog); this.Controls.Add(this.btnOpenCookies);
Controls.Add(txtLog); this.Controls.Add(this.btnEditServiceConfig);
Controls.Add(pgProfile); this.Controls.Add(this.btnClearLog);
Controls.Add(lblPath); this.Controls.Add(this.txtLog);
Icon = (Icon)resources.GetObject("$this.Icon"); this.Controls.Add(this.pgProfile);
MinimumSize = new Size(960, 680); this.Controls.Add(this.lblPath);
Name = "MainForm"; this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
Text = "Unshackle Master GUI"; this.MinimumSize = new System.Drawing.Size(960, 680);
ResumeLayout(false); this.Name = "MainForm";
PerformLayout(); this.Text = "Unshackle Master GUI";
this.ResumeLayout(false);
this.PerformLayout();
} }
} }
} }

View File

@@ -13,40 +13,84 @@ namespace UnshackleGUI
{ {
public partial class MainForm : Form public partial class MainForm : Form
{ {
// =========================================================
// 1. FILE PATHS & SETTINGS
// =========================================================
private string _configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json"); private string _configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
private string _paramsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "params.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 AppSettings _appSettings = new AppSettings();
private List<UnshackleParameter> _parameterDefinitions = new List<UnshackleParameter>(); private List<UnshackleParameter> _parameterDefinitions = new List<UnshackleParameter>();
private Dictionary<string, object> _propertyValues = new Dictionary<string, object>(); private Dictionary<string, object> _propertyValues = new Dictionary<string, object>();
// State Variables
private Process? _currentProcess; 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() public MainForm()
{ {
InitializeComponent(); InitializeComponent();
ApplyDarkTheme(); 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) => pgProfile.PropertyValueChanged += (s, e) =>
{ {
UpdateCommandPreview(); UpdateCommandPreview();
SaveCurrentProfile();
if (comboProfiles.SelectedItem != null)
{
string selected = comboProfiles.SelectedItem.ToString();
_appSettings.Profiles[selected] =
new Dictionary<string, object>(_propertyValues);
}
SaveConfig();
}; };
comboService.SelectedIndexChanged += (s, e) => UpdateCommandPreview(); // --- Service Dropdown (Netflix, Amazon, etc.) ---
txtURL.TextChanged += (s, e) => UpdateCommandPreview(); comboService.SelectedIndexChanged += (s, e) =>
txtBinName.TextChanged += (s, e) => UpdateCommandPreview(); {
// 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; btnRun.Click += btnRun_Click;
btnStop.Click += btnStop_Click; btnStop.Click += btnStop_Click;
btnBrowse.Click += btnBrowse_Click; btnBrowse.Click += btnBrowse_Click;
@@ -56,130 +100,317 @@ namespace UnshackleGUI
btnEditYaml.Click += btnEditYaml_Click; btnEditYaml.Click += btnEditYaml_Click;
btnAddProfile.Click += btnAddProfile_Click; btnAddProfile.Click += btnAddProfile_Click;
btnRemoveProfile.Click += btnRemoveProfile_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)) if (File.Exists(_paramsPath))
{ {
var json = File.ReadAllText(_paramsPath); var json = File.ReadAllText(_paramsPath);
_parameterDefinitions = JsonConvert.DeserializeObject<List<UnshackleParameter>>(json) ?? new List<UnshackleParameter>(); _parameterDefinitions = JsonConvert.DeserializeObject<List<UnshackleParameter>>(json) ?? new List<UnshackleParameter>();
}
foreach (var p in _parameterDefinitions) else
{ {
// Ensure every parameter has a value in the dictionary MessageBox.Show("Error: params.json not found! The grid will be empty.");
if (!_propertyValues.ContainsKey(p.Name))
_propertyValues[p.Name] = p.Default ?? "";
}
} }
// 2. Load Config (Paths/Profiles) // B. Load Global Config (Root Path, Binary Name)
if (File.Exists(_configPath)) if (File.Exists(_configPath))
{ {
var json = File.ReadAllText(_configPath); var json = File.ReadAllText(_configPath);
_appSettings = JsonConvert.DeserializeObject<AppSettings>(json) ?? new AppSettings(); _appSettings = JsonConvert.DeserializeObject<AppSettings>(json) ?? new AppSettings();
txtRootPath.Text = _appSettings.RootPath; txtRootPath.Text = _appSettings.RootPath;
txtBinName.Text = _appSettings.BinaryName; txtBinName.Text = _appSettings.BinaryName;
} }
if (_appSettings.Profiles == null || _appSettings.Profiles.Count == 0) // C. Populate the Services Dropdown (Read folders from disk)
{
_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(); 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(); UpdateCommandPreview();
} }
private void UpdateCommandPreview() private void RefreshProfileList()
{ {
StringBuilder sb = new StringBuilder(); // 1. Temporarily stop listening to the selection event to prevent glitches
sb.Append($"{txtBinName.Text} run unshackle dl "); 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<string, object>();
// Fill with default values from params.json
foreach (var p in _parameterDefinitions) 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<Dictionary<string, object>>(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; if (!_propertyValues.TryGetValue(p.Name, out object val)) continue;
// Handle Booleans // Case A: Boolean Flags (e.g., --no-mux)
if (p.Type == "Bool" && val is bool b && b) if (p.Type == "Bool" && val is bool isTrue && isTrue)
{ {
sb.Append($"{p.Flag} "); sb.Append($"{p.Flag} ");
} }
// Handle Text/Selection/Numbers // Case B: Text / Selection / Number
else if (p.Type != "Bool" && val != null) else if (p.Type != "Bool" && val != null)
{ {
string sVal = val.ToString(); 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 (!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} "); sb.Append($"{p.Flag} {sVal} ");
} }
} }
} }
// 3. Append Service and URL
// e.g., "AMZN https://..."
sb.Append($"{comboService.Text} {txtURL.Text}"); sb.Append($"{comboService.Text} {txtURL.Text}");
// 4. Update UI
txtCommandPreview.Text = sb.ToString(); txtCommandPreview.Text = sb.ToString();
} }
private void btnEditServiceConfig_Click(object? sender, EventArgs e) // =========================================================
{ // 7. CLI EXECUTION (The Run Button)
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) private async void btnRun_Click(object? sender, EventArgs e)
{ {
string finalCmd = txtCommandPreview.Text; string finalCmd = txtCommandPreview.Text;
if (string.IsNullOrWhiteSpace(finalCmd)) return; if (string.IsNullOrWhiteSpace(finalCmd)) return;
// Disable buttons while running
ToggleUI(true); ToggleUI(true);
txtLog.AppendText($"> Executing: {finalCmd}{Environment.NewLine}"); txtLog.AppendText($"> Executing: {finalCmd}{Environment.NewLine}");
try { await Task.Run(() => RunCli(finalCmd)); } try
catch (Exception ex) { AppendLog($"[ERROR]: {ex.Message}"); } {
finally { ToggleUI(false); } // 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) private void RunCli(string cmd)
{ {
// We use CMD.EXE instead of PowerShell to avoid Antivirus false positives
ProcessStartInfo psi = new ProcessStartInfo ProcessStartInfo psi = new ProcessStartInfo
{ {
FileName = "cmd.exe", FileName = "cmd.exe",
// /c = Run command and then terminate Arguments = $"/c chcp 65001 && {cmd}", // Force UTF-8
// 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, WorkingDirectory = txtRootPath.Text,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
@@ -188,20 +419,24 @@ namespace UnshackleGUI
StandardOutputEncoding = Encoding.UTF8 StandardOutputEncoding = Encoding.UTF8
}; };
// Ensure Python scripts know to use UTF-8 // Set Environment variable for Python
psi.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"; psi.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
using (_currentProcess = Process.Start(psi)) using (_currentProcess = Process.Start(psi))
{ {
if (_currentProcess == null) return; if (_currentProcess == null) return;
// Handle Output
_currentProcess.OutputDataReceived += (s, e) => _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")) if (!string.IsNullOrEmpty(e.Data) && !e.Data.Contains("Active code page"))
{
AppendLog(e.Data); AppendLog(e.Data);
}
}; };
// Handle Errors
_currentProcess.ErrorDataReceived += (s, e) => AppendLog(e.Data); _currentProcess.ErrorDataReceived += (s, e) => AppendLog(e.Data);
_currentProcess.BeginOutputReadLine(); _currentProcess.BeginOutputReadLine();
@@ -210,104 +445,39 @@ namespace UnshackleGUI
} }
} }
private void AppendLog(string? text) // =========================================================
{ // 8. PROFILE MANAGEMENT (Add/Remove/Save Buttons)
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) private void btnAddProfile_Click(object? sender, EventArgs e)
{ {
string name = Microsoft.VisualBasic.Interaction.InputBox( string name = Microsoft.VisualBasic.Interaction.InputBox("Enter profile name:", "New Profile", "");
"Enter profile name:",
"New Profile",
"NewProfile");
if (string.IsNullOrWhiteSpace(name)) if (string.IsNullOrWhiteSpace(name)) return;
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."); MessageBox.Show("Profile already exists.");
return; return;
} }
// Clone current settings into new profile // Save current settings as the new profile
_appSettings.Profiles[name] = SaveProfileFile(name);
new Dictionary<string, object>(_propertyValues);
comboProfiles.DataSource = null; // Reload list and select new profile
comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList(); RefreshProfileList();
comboProfiles.SelectedItem = name; comboProfiles.SelectedItem = name;
SaveConfig();
} }
private void btnRemoveProfile_Click(object? sender, EventArgs e) private void btnRemoveProfile_Click(object? sender, EventArgs e)
{ {
if (comboProfiles.SelectedItem == null) if (comboProfiles.SelectedItem == null) return;
return;
string selected = comboProfiles.SelectedItem.ToString(); string selected = comboProfiles.SelectedItem.ToString();
@@ -317,50 +487,233 @@ namespace UnshackleGUI
return; 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; return;
}
_appSettings.Profiles.Remove(selected); foreach (char c in Path.GetInvalidFileNameChars()) newName = newName.Replace(c, '_');
comboProfiles.DataSource = null; // 2. CHECK: Did the name change?
comboProfiles.DataSource = _appSettings.Profiles.Keys.ToList(); 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); } // 9. HELPER METHODS & THEME
private void btnClearLog_Click(object? sender, EventArgs e) => txtLog.Clear(); // =========================================================
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) 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) private void btnOpenCookies_Click(object? sender, EventArgs e)
{ {
string p = Path.Combine(txtRootPath.Text, "unshackle", "cookies", comboService.Text); string path = Path.Combine(txtRootPath.Text, "unshackle", "cookies", comboService.Text);
Directory.CreateDirectory(p); Process.Start("explorer.exe", p); Directory.CreateDirectory(path);
Process.Start("explorer.exe", path);
} }
private void btnEditYaml_Click(object? sender, EventArgs e) private void btnEditYaml_Click(object? sender, EventArgs e)
{ {
string yamlPath = Path.Combine(txtRootPath.Text, "unshackle/unshackle.yaml"); 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<object>().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 class AppSettings
{ {
public string RootPath { get; set; } = @"C:\DEVINE\unshackle"; public string RootPath { get; set; } = @"C:\DEVINE\unshackle";
public string BinaryName { get; set; } = "uv"; 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 class UnshackleParameter