1 Commits
v1.1 ... v1.2

Author SHA1 Message Date
Bl4DiEDiEBL4
3cbe73259d Added scheduler 2026-02-12 00:03:14 +01:00
6 changed files with 18223 additions and 320 deletions

View File

@@ -21,245 +21,347 @@
private System.Windows.Forms.TextBox txtLog; private System.Windows.Forms.TextBox txtLog;
private System.Windows.Forms.PropertyGrid pgProfile; private System.Windows.Forms.PropertyGrid pgProfile;
private System.Windows.Forms.Label lblPath; private System.Windows.Forms.Label lblPath;
// FIXED: Correct variable name (was bntnSaveProfile)
private System.Windows.Forms.Button btnSaveProfile; private System.Windows.Forms.Button btnSaveProfile;
private System.Windows.Forms.Button btnScheduler;
private System.Windows.Forms.Button btnToggleScheduler; // <--- NEW BUTTON
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
if (disposing && (components != null)) components.Dispose(); if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing); 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));
this.txtRootPath = new System.Windows.Forms.TextBox(); txtRootPath = new TextBox();
this.txtBinName = new System.Windows.Forms.TextBox(); txtBinName = new TextBox();
this.comboProfiles = new System.Windows.Forms.ComboBox(); comboProfiles = new ComboBox();
this.btnAddProfile = new System.Windows.Forms.Button(); btnAddProfile = new Button();
this.btnRemoveProfile = new System.Windows.Forms.Button(); btnRemoveProfile = new Button();
this.comboService = new System.Windows.Forms.ComboBox(); comboService = new ComboBox();
this.txtURL = new System.Windows.Forms.TextBox(); txtURL = new TextBox();
this.txtCommandPreview = new System.Windows.Forms.TextBox(); txtCommandPreview = new TextBox();
this.btnRun = new System.Windows.Forms.Button(); btnRun = new Button();
this.btnStop = new System.Windows.Forms.Button(); btnStop = new Button();
this.btnOpenCookies = new System.Windows.Forms.Button(); btnOpenCookies = new Button();
this.btnEditServiceConfig = new System.Windows.Forms.Button(); btnEditServiceConfig = new Button();
this.btnEditYaml = new System.Windows.Forms.Button(); btnEditYaml = new Button();
this.btnBrowse = new System.Windows.Forms.Button(); btnBrowse = new Button();
this.btnClearLog = new System.Windows.Forms.Button(); btnClearLog = new Button();
this.txtLog = new System.Windows.Forms.TextBox(); txtLog = new TextBox();
this.pgProfile = new System.Windows.Forms.PropertyGrid(); pgProfile = new PropertyGrid();
this.lblPath = new System.Windows.Forms.Label(); lblPath = new Label();
this.btnSaveProfile = new System.Windows.Forms.Button(); btnSaveProfile = new Button();
this.SuspendLayout(); btnScheduler = new Button();
btnToggleScheduler = new Button();
SuspendLayout();
// //
// txtRootPath // txtRootPath
// //
this.txtRootPath.Location = new System.Drawing.Point(12, 35); txtRootPath.BackColor = Color.FromArgb(45, 45, 48);
this.txtRootPath.Name = "txtRootPath"; txtRootPath.BorderStyle = BorderStyle.FixedSingle;
this.txtRootPath.Size = new System.Drawing.Size(280, 27); txtRootPath.ForeColor = Color.White;
this.txtRootPath.TabIndex = 0; txtRootPath.Location = new Point(12, 35);
txtRootPath.Name = "txtRootPath";
txtRootPath.Size = new Size(280, 27);
txtRootPath.TabIndex = 0;
// //
// txtBinName // txtBinName
// //
this.txtBinName.Location = new System.Drawing.Point(340, 35); txtBinName.BackColor = Color.FromArgb(45, 45, 48);
this.txtBinName.Name = "txtBinName"; txtBinName.BorderStyle = BorderStyle.FixedSingle;
this.txtBinName.Size = new System.Drawing.Size(60, 27); txtBinName.ForeColor = Color.White;
this.txtBinName.TabIndex = 2; txtBinName.Location = new Point(340, 35);
txtBinName.Name = "txtBinName";
txtBinName.Size = new Size(60, 27);
txtBinName.TabIndex = 2;
// //
// comboProfiles // comboProfiles
// //
this.comboProfiles.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); comboProfiles.Anchor = AnchorStyles.Top | AnchorStyles.Right;
this.comboProfiles.Location = new System.Drawing.Point(1177, 35); comboProfiles.BackColor = Color.FromArgb(45, 45, 48);
this.comboProfiles.Name = "comboProfiles"; comboProfiles.FlatStyle = FlatStyle.Flat;
this.comboProfiles.Size = new System.Drawing.Size(240, 28); comboProfiles.ForeColor = Color.White;
this.comboProfiles.TabIndex = 4; comboProfiles.Location = new Point(1177, 35);
comboProfiles.Name = "comboProfiles";
comboProfiles.Size = new Size(240, 28);
comboProfiles.TabIndex = 4;
// //
// btnAddProfile // btnAddProfile
// //
this.btnAddProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); btnAddProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right;
this.btnAddProfile.Location = new System.Drawing.Point(1422, 34); btnAddProfile.BackColor = Color.FromArgb(45, 45, 48);
this.btnAddProfile.Name = "btnAddProfile"; btnAddProfile.FlatAppearance.BorderColor = Color.Gray;
this.btnAddProfile.Size = new System.Drawing.Size(35, 29); btnAddProfile.FlatStyle = FlatStyle.Flat;
this.btnAddProfile.TabIndex = 5; btnAddProfile.ForeColor = Color.White;
this.btnAddProfile.Text = "+"; btnAddProfile.Location = new Point(1422, 34);
btnAddProfile.Name = "btnAddProfile";
btnAddProfile.Size = new Size(35, 29);
btnAddProfile.TabIndex = 5;
btnAddProfile.Text = "+";
btnAddProfile.UseVisualStyleBackColor = false;
// //
// btnRemoveProfile // btnRemoveProfile
// //
this.btnRemoveProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); btnRemoveProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right;
this.btnRemoveProfile.Location = new System.Drawing.Point(1462, 34); btnRemoveProfile.BackColor = Color.FromArgb(45, 45, 48);
this.btnRemoveProfile.Name = "btnRemoveProfile"; btnRemoveProfile.FlatAppearance.BorderColor = Color.Gray;
this.btnRemoveProfile.Size = new System.Drawing.Size(35, 29); btnRemoveProfile.FlatStyle = FlatStyle.Flat;
this.btnRemoveProfile.TabIndex = 6; btnRemoveProfile.ForeColor = Color.White;
this.btnRemoveProfile.Text = "-"; btnRemoveProfile.Location = new Point(1462, 34);
btnRemoveProfile.Name = "btnRemoveProfile";
btnRemoveProfile.Size = new Size(35, 29);
btnRemoveProfile.TabIndex = 6;
btnRemoveProfile.Text = "-";
btnRemoveProfile.UseVisualStyleBackColor = false;
// //
// comboService // comboService
// //
this.comboService.Location = new System.Drawing.Point(12, 80); comboService.BackColor = Color.FromArgb(45, 45, 48);
this.comboService.Name = "comboService"; comboService.FlatStyle = FlatStyle.Flat;
this.comboService.Size = new System.Drawing.Size(100, 28); comboService.ForeColor = Color.White;
this.comboService.TabIndex = 7; comboService.Location = new Point(12, 80);
comboService.Name = "comboService";
comboService.Size = new Size(100, 28);
comboService.TabIndex = 7;
// //
// txtURL // txtURL
// //
this.txtURL.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); txtURL.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
this.txtURL.Location = new System.Drawing.Point(115, 80); txtURL.BackColor = Color.FromArgb(45, 45, 48);
this.txtURL.Name = "txtURL"; txtURL.BorderStyle = BorderStyle.FixedSingle;
this.txtURL.PlaceholderText = "Paste ID / URL here..."; txtURL.ForeColor = Color.White;
this.txtURL.Size = new System.Drawing.Size(801, 27); txtURL.Location = new Point(115, 80);
this.txtURL.TabIndex = 8; txtURL.Name = "txtURL";
txtURL.PlaceholderText = "Paste ID / URL here...";
txtURL.Size = new Size(801, 27);
txtURL.TabIndex = 8;
// //
// txtCommandPreview // txtCommandPreview
// //
this.txtCommandPreview.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); txtCommandPreview.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
this.txtCommandPreview.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(20)))), ((int)(((byte)(20))))); txtCommandPreview.BackColor = Color.FromArgb(20, 20, 20);
this.txtCommandPreview.Font = new System.Drawing.Font("Consolas", 10F, System.Drawing.FontStyle.Bold); txtCommandPreview.BorderStyle = BorderStyle.FixedSingle;
this.txtCommandPreview.ForeColor = System.Drawing.Color.Cyan; txtCommandPreview.Font = new Font("Consolas", 10F, FontStyle.Bold);
this.txtCommandPreview.Location = new System.Drawing.Point(12, 115); txtCommandPreview.ForeColor = Color.Cyan;
this.txtCommandPreview.Name = "txtCommandPreview"; txtCommandPreview.Location = new Point(12, 115);
this.txtCommandPreview.Size = new System.Drawing.Size(1130, 27); txtCommandPreview.Name = "txtCommandPreview";
this.txtCommandPreview.TabIndex = 9; txtCommandPreview.Size = new Size(1130, 27);
txtCommandPreview.TabIndex = 9;
// //
// btnRun // btnRun
// //
this.btnRun.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); btnRun.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
this.btnRun.Location = new System.Drawing.Point(12, 150); btnRun.BackColor = Color.SteelBlue;
this.btnRun.Name = "btnRun"; btnRun.FlatAppearance.BorderColor = Color.Gray;
this.btnRun.Size = new System.Drawing.Size(1130, 45); btnRun.FlatStyle = FlatStyle.Flat;
this.btnRun.TabIndex = 10; btnRun.ForeColor = Color.White;
this.btnRun.Text = "RUN"; btnRun.Location = new Point(12, 150);
btnRun.Name = "btnRun";
btnRun.Size = new Size(1130, 45);
btnRun.TabIndex = 10;
btnRun.Text = "RUN";
btnRun.UseVisualStyleBackColor = false;
// //
// btnStop // btnStop
// //
this.btnStop.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); btnStop.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
this.btnStop.BackColor = System.Drawing.Color.Maroon; btnStop.BackColor = Color.Maroon;
this.btnStop.Location = new System.Drawing.Point(12, 150); btnStop.FlatAppearance.BorderColor = Color.Gray;
this.btnStop.Name = "btnStop"; btnStop.FlatStyle = FlatStyle.Flat;
this.btnStop.Size = new System.Drawing.Size(1130, 45); btnStop.ForeColor = Color.White;
this.btnStop.TabIndex = 11; btnStop.Location = new Point(12, 150);
this.btnStop.Text = "STOP"; btnStop.Name = "btnStop";
this.btnStop.UseVisualStyleBackColor = false; btnStop.Size = new Size(1130, 45);
this.btnStop.Visible = false; btnStop.TabIndex = 11;
btnStop.Text = "STOP";
btnStop.UseVisualStyleBackColor = false;
btnStop.Visible = false;
// //
// btnOpenCookies // btnOpenCookies
// //
this.btnOpenCookies.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); btnOpenCookies.Anchor = AnchorStyles.Top | AnchorStyles.Right;
this.btnOpenCookies.Location = new System.Drawing.Point(922, 79); btnOpenCookies.BackColor = Color.OliveDrab;
this.btnOpenCookies.Name = "btnOpenCookies"; btnOpenCookies.FlatAppearance.BorderColor = Color.Gray;
this.btnOpenCookies.Size = new System.Drawing.Size(115, 30); btnOpenCookies.FlatStyle = FlatStyle.Flat;
this.btnOpenCookies.TabIndex = 12; btnOpenCookies.ForeColor = Color.White;
this.btnOpenCookies.Text = "🍪 Cookies"; btnOpenCookies.Location = new Point(922, 79);
btnOpenCookies.Name = "btnOpenCookies";
btnOpenCookies.Size = new Size(115, 30);
btnOpenCookies.TabIndex = 12;
btnOpenCookies.Text = "🍪 Cookies";
btnOpenCookies.UseVisualStyleBackColor = false;
// //
// btnEditServiceConfig // btnEditServiceConfig
// //
this.btnEditServiceConfig.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); btnEditServiceConfig.Anchor = AnchorStyles.Top | AnchorStyles.Right;
this.btnEditServiceConfig.Location = new System.Drawing.Point(1043, 79); btnEditServiceConfig.BackColor = Color.BlueViolet;
this.btnEditServiceConfig.Name = "btnEditServiceConfig"; btnEditServiceConfig.FlatAppearance.BorderColor = Color.Gray;
this.btnEditServiceConfig.Size = new System.Drawing.Size(99, 30); btnEditServiceConfig.FlatStyle = FlatStyle.Flat;
this.btnEditServiceConfig.TabIndex = 13; btnEditServiceConfig.ForeColor = Color.White;
this.btnEditServiceConfig.Text = "⚙️ Service"; btnEditServiceConfig.Location = new Point(1043, 79);
btnEditServiceConfig.Name = "btnEditServiceConfig";
btnEditServiceConfig.Size = new Size(99, 30);
btnEditServiceConfig.TabIndex = 13;
btnEditServiceConfig.Text = "⚙️ Service";
btnEditServiceConfig.UseVisualStyleBackColor = false;
// //
// btnEditYaml // btnEditYaml
// //
this.btnEditYaml.Location = new System.Drawing.Point(410, 34); btnEditYaml.BackColor = Color.FromArgb(255, 128, 0);
this.btnEditYaml.Name = "btnEditYaml"; btnEditYaml.FlatAppearance.BorderColor = Color.Gray;
this.btnEditYaml.Size = new System.Drawing.Size(144, 29); btnEditYaml.FlatStyle = FlatStyle.Flat;
this.btnEditYaml.TabIndex = 3; btnEditYaml.ForeColor = Color.White;
this.btnEditYaml.Text = "📝 Main Config"; btnEditYaml.Location = new Point(410, 34);
btnEditYaml.Name = "btnEditYaml";
btnEditYaml.Size = new Size(144, 29);
btnEditYaml.TabIndex = 3;
btnEditYaml.Text = "📝 Main Config";
btnEditYaml.UseVisualStyleBackColor = false;
// //
// btnBrowse // btnBrowse
// //
this.btnBrowse.Location = new System.Drawing.Point(295, 34); btnBrowse.BackColor = Color.FromArgb(45, 45, 48);
this.btnBrowse.Name = "btnBrowse"; btnBrowse.FlatAppearance.BorderColor = Color.Gray;
this.btnBrowse.Size = new System.Drawing.Size(35, 29); btnBrowse.FlatStyle = FlatStyle.Flat;
this.btnBrowse.TabIndex = 1; btnBrowse.ForeColor = Color.White;
this.btnBrowse.Text = "📂"; btnBrowse.Location = new Point(295, 34);
btnBrowse.Name = "btnBrowse";
btnBrowse.Size = new Size(35, 29);
btnBrowse.TabIndex = 1;
btnBrowse.Text = "📂";
btnBrowse.UseVisualStyleBackColor = false;
// //
// btnClearLog // btnClearLog
// //
this.btnClearLog.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); btnClearLog.Anchor = AnchorStyles.Top | AnchorStyles.Right;
this.btnClearLog.Location = new System.Drawing.Point(1022, 201); btnClearLog.BackColor = Color.FromArgb(45, 45, 48);
this.btnClearLog.Name = "btnClearLog"; btnClearLog.FlatAppearance.BorderColor = Color.Gray;
this.btnClearLog.Size = new System.Drawing.Size(120, 36); btnClearLog.FlatStyle = FlatStyle.Flat;
this.btnClearLog.TabIndex = 14; btnClearLog.ForeColor = Color.White;
this.btnClearLog.Text = "Clear Log"; btnClearLog.Location = new Point(1022, 201);
btnClearLog.Name = "btnClearLog";
btnClearLog.Size = new Size(120, 36);
btnClearLog.TabIndex = 14;
btnClearLog.Text = "Clear Log";
btnClearLog.UseVisualStyleBackColor = false;
// //
// txtLog // txtLog
// //
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.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
this.txtLog.BackColor = System.Drawing.Color.Black; txtLog.BackColor = Color.Black;
this.txtLog.Font = new System.Drawing.Font("Consolas", 9F); txtLog.BorderStyle = BorderStyle.FixedSingle;
this.txtLog.ForeColor = System.Drawing.Color.Lime; txtLog.Font = new Font("Consolas", 9F);
this.txtLog.Location = new System.Drawing.Point(12, 243); txtLog.ForeColor = Color.Lime;
this.txtLog.Multiline = true; txtLog.Location = new Point(12, 243);
this.txtLog.Name = "txtLog"; txtLog.Multiline = true;
this.txtLog.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; txtLog.Name = "txtLog";
this.txtLog.Size = new System.Drawing.Size(1130, 550); txtLog.ReadOnly = true;
this.txtLog.TabIndex = 15; txtLog.ScrollBars = ScrollBars.Vertical;
txtLog.Size = new Size(1130, 550);
txtLog.TabIndex = 15;
// //
// pgProfile // pgProfile
// //
this.pgProfile.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Right))); pgProfile.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right;
this.pgProfile.CategoryForeColor = System.Drawing.SystemColors.ActiveCaption; pgProfile.BackColor = Color.FromArgb(37, 37, 38);
this.pgProfile.Location = new System.Drawing.Point(1177, 80); pgProfile.CategoryForeColor = SystemColors.ActiveCaption;
this.pgProfile.Name = "pgProfile"; pgProfile.DisabledItemForeColor = Color.FromArgb(127, 255, 255, 255);
this.pgProfile.Size = new System.Drawing.Size(320, 713); pgProfile.LineColor = Color.FromArgb(45, 45, 48);
this.pgProfile.TabIndex = 16; pgProfile.Location = new Point(1177, 80);
pgProfile.Name = "pgProfile";
pgProfile.Size = new Size(320, 713);
pgProfile.TabIndex = 16;
pgProfile.ViewBackColor = Color.FromArgb(37, 37, 38);
pgProfile.ViewForeColor = Color.White;
// //
// lblPath // lblPath
// //
this.lblPath.Location = new System.Drawing.Point(12, 12); lblPath.ForeColor = Color.White;
this.lblPath.Name = "lblPath"; lblPath.Location = new Point(12, 12);
this.lblPath.Size = new System.Drawing.Size(150, 23); lblPath.Name = "lblPath";
this.lblPath.TabIndex = 17; lblPath.Size = new Size(150, 23);
this.lblPath.Text = "Root Path & Binary:"; lblPath.TabIndex = 17;
lblPath.Text = "Root Path & Binary:";
// //
// btnSaveProfile // btnSaveProfile
// //
this.btnSaveProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); btnSaveProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right;
this.btnSaveProfile.FlatStyle = System.Windows.Forms.FlatStyle.Flat; btnSaveProfile.BackColor = Color.FromArgb(45, 45, 48);
this.btnSaveProfile.FlatAppearance.BorderColor = System.Drawing.Color.Gray; btnSaveProfile.FlatAppearance.BorderColor = Color.Gray;
this.btnSaveProfile.ForeColor = System.Drawing.Color.White; btnSaveProfile.FlatStyle = FlatStyle.Flat;
// FIXED: Calculated position to be right next to the Remove (-) button btnSaveProfile.ForeColor = Color.White;
this.btnSaveProfile.Location = new System.Drawing.Point(1502, 34); btnSaveProfile.Location = new Point(1502, 34);
this.btnSaveProfile.Name = "btnSaveProfile"; btnSaveProfile.Name = "btnSaveProfile";
this.btnSaveProfile.Size = new System.Drawing.Size(35, 29); btnSaveProfile.Size = new Size(35, 29);
this.btnSaveProfile.TabIndex = 18; btnSaveProfile.TabIndex = 18;
this.btnSaveProfile.Text = "💾"; btnSaveProfile.Text = "💾";
this.btnSaveProfile.UseVisualStyleBackColor = true; btnSaveProfile.UseVisualStyleBackColor = false;
this.btnSaveProfile.Click += new System.EventHandler(this.btnSaveProfile_Click); btnSaveProfile.Click += btnSaveProfile_Click;
//
// btnScheduler
//
btnScheduler.BackColor = Color.Teal;
btnScheduler.FlatAppearance.BorderColor = Color.Gray;
btnScheduler.FlatStyle = FlatStyle.Flat;
btnScheduler.ForeColor = Color.White;
btnScheduler.Location = new Point(560, 34);
btnScheduler.Name = "btnScheduler";
btnScheduler.Size = new Size(151, 29);
btnScheduler.TabIndex = 19;
btnScheduler.Text = "🕒 Open Scheduler";
btnScheduler.UseVisualStyleBackColor = false;
btnScheduler.Click += BtnScheduler_Click;
//
// btnToggleScheduler
//
btnToggleScheduler.BackColor = Color.DimGray;
btnToggleScheduler.FlatAppearance.BorderColor = Color.Gray;
btnToggleScheduler.FlatStyle = FlatStyle.Flat;
btnToggleScheduler.ForeColor = Color.LightGreen;
btnToggleScheduler.Location = new Point(717, 34);
btnToggleScheduler.Name = "btnToggleScheduler";
btnToggleScheduler.Size = new Size(149, 29);
btnToggleScheduler.TabIndex = 20;
btnToggleScheduler.Text = "▶ Start Scheduler";
btnToggleScheduler.UseVisualStyleBackColor = false;
// //
// MainForm // MainForm
// //
this.ClientSize = new System.Drawing.Size(1550, 808); // Slightly widened to fit new button BackColor = Color.FromArgb(30, 30, 30);
this.Controls.Add(this.txtRootPath); ClientSize = new Size(1550, 808);
this.Controls.Add(this.btnBrowse); Controls.Add(btnToggleScheduler);
this.Controls.Add(this.txtBinName); Controls.Add(btnScheduler);
this.Controls.Add(this.btnEditYaml); Controls.Add(txtRootPath);
this.Controls.Add(this.comboProfiles); Controls.Add(btnBrowse);
this.Controls.Add(this.btnAddProfile); Controls.Add(txtBinName);
this.Controls.Add(this.btnRemoveProfile); Controls.Add(btnEditYaml);
// FIXED: Added btnSaveProfile to controls list Controls.Add(comboProfiles);
this.Controls.Add(this.btnSaveProfile); Controls.Add(btnAddProfile);
this.Controls.Add(this.comboService); Controls.Add(btnRemoveProfile);
this.Controls.Add(this.txtURL); Controls.Add(btnSaveProfile);
this.Controls.Add(this.txtCommandPreview); Controls.Add(comboService);
this.Controls.Add(this.btnRun); Controls.Add(txtURL);
this.Controls.Add(this.btnStop); Controls.Add(txtCommandPreview);
this.Controls.Add(this.btnOpenCookies); Controls.Add(btnRun);
this.Controls.Add(this.btnEditServiceConfig); Controls.Add(btnStop);
this.Controls.Add(this.btnClearLog); Controls.Add(btnOpenCookies);
this.Controls.Add(this.txtLog); Controls.Add(btnEditServiceConfig);
this.Controls.Add(this.pgProfile); Controls.Add(btnClearLog);
this.Controls.Add(this.lblPath); Controls.Add(txtLog);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); Controls.Add(pgProfile);
this.MinimumSize = new System.Drawing.Size(960, 680); Controls.Add(lblPath);
this.Name = "MainForm"; ForeColor = Color.White;
this.Text = "Unshackle Master GUI"; Icon = (Icon)resources.GetObject("$this.Icon");
this.ResumeLayout(false); MinimumSize = new Size(960, 680);
this.PerformLayout(); Name = "MainForm";
Text = "Shackle v1.2";
ResumeLayout(false);
PerformLayout();
} }
} }
} }

View File

@@ -1,4 +1,4 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Drawing; using System.Drawing;
@@ -14,72 +14,63 @@ namespace UnshackleGUI
public partial class MainForm : Form public partial class MainForm : Form
{ {
// ========================================================= // =========================================================
// 1. FILE PATHS & SETTINGS // 1. VARIABLES
// ========================================================= // =========================================================
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"); 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 bool _isLoaded = false;
private string _lastLoadedProfile = ""; // Tracks the original name private string _lastLoadedProfile = "";
// Scheduler Reference
private SchedulerForm? _scheduler = null;
// ========================================================= // =========================================================
// 2. CONSTRUCTOR & INITIALIZATION // 2. CONSTRUCTOR
// ========================================================= // =========================================================
public MainForm() public MainForm()
{ {
InitializeComponent(); InitializeComponent();
ApplyDarkTheme(); // ApplyDarkTheme();
// Ensure the Profiles folder exists
if (!Directory.Exists(_profilesDir)) if (!Directory.Exists(_profilesDir))
{ {
Directory.CreateDirectory(_profilesDir); Directory.CreateDirectory(_profilesDir);
} }
// Step 1: Set up all button clicks and text changes
SetupEventHandlers(); SetupEventHandlers();
// Step 2: Load data from disk (Config, Params, Profiles)
LoadData(); LoadData();
} }
private void SetupEventHandlers() private void SetupEventHandlers()
{ {
// --- Property Grid (The Settings List) --- // Property Grid
// 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(); SaveCurrentProfile();
}; };
// --- Service Dropdown (Netflix, Amazon, etc.) --- // Service Dropdown
comboService.SelectedIndexChanged += (s, e) => comboService.SelectedIndexChanged += (s, e) =>
{ {
// Don't run this logic if the app is still starting up
if (!_isLoaded) return; if (!_isLoaded) return;
if (comboService.SelectedItem != null) if (comboService.SelectedItem != null)
{ {
// Update the "Service" value in our data dictionary
_propertyValues["Service"] = comboService.SelectedItem.ToString(); _propertyValues["Service"] = comboService.SelectedItem.ToString();
// Update the command preview
UpdateCommandPreview(); UpdateCommandPreview();
// Save this change to the JSON file immediately
SaveCurrentProfile(); SaveCurrentProfile();
} }
}; };
// --- Text Box Changes --- // Text Boxes
txtURL.TextChanged += (s, e) => UpdateCommandPreview(); txtURL.TextChanged += (s, e) => UpdateCommandPreview();
txtBinName.TextChanged += (s, e) => txtBinName.TextChanged += (s, e) =>
@@ -90,7 +81,7 @@ namespace UnshackleGUI
txtRootPath.TextChanged += (s, e) => SaveGlobalConfig(); txtRootPath.TextChanged += (s, e) => SaveGlobalConfig();
// --- Button Clicks --- // Main Buttons
btnRun.Click += btnRun_Click; btnRun.Click += btnRun_Click;
btnStop.Click += btnStop_Click; btnStop.Click += btnStop_Click;
btnBrowse.Click += btnBrowse_Click; btnBrowse.Click += btnBrowse_Click;
@@ -100,19 +91,86 @@ 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; btnSaveProfile.Click += btnSaveProfile_Click;
// Scheduler Buttons
btnToggleScheduler.Click += BtnToggleScheduler_Click;
// btnScheduler click is already wired in Designer or can be added here:
// btnScheduler.Click += BtnScheduler_Click;
} }
// ========================================================= // =========================================================
// 3. DATA LOADING LOGIC // 3. SCHEDULER LOGIC (OPEN & TOGGLE)
// =========================================================
// A. THE CLOCK BUTTON: Opens the window
private void BtnScheduler_Click(object sender, EventArgs e)
{
// If it doesn't exist, create it (auto-starts timer)
if (_scheduler == null || _scheduler.IsDisposed)
{
CreateScheduler();
}
// Show it
if (!_scheduler.Visible)
{
_scheduler.Show();
}
_scheduler.BringToFront();
}
// B. THE START/STOP BUTTON: Controls the process
private void BtnToggleScheduler_Click(object sender, EventArgs e)
{
if (_scheduler != null && !_scheduler.IsDisposed)
{
// STOPPING
_scheduler.Shutdown();
_scheduler = null;
UpdateSchedulerButtonState(false);
MessageBox.Show("Scheduler stopped.", "Unshackle", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
// STARTING
CreateScheduler();
// We create it but don't show the window yet (runs in background)
MessageBox.Show("Scheduler started in background.", "Unshackle", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
private void CreateScheduler()
{
var profiles = comboProfiles.Items.Cast<string>().ToList();
_scheduler = new SchedulerForm(profiles, txtRootPath.Text, txtBinName.Text);
UpdateSchedulerButtonState(true);
}
private void UpdateSchedulerButtonState(bool isRunning)
{
if (isRunning)
{
// Running State -> Show STOP button (Red)
btnToggleScheduler.Text = "🛑 Stop Scheduler";
btnToggleScheduler.ForeColor = Color.Red;
}
else
{
// Stopped State -> Show START button (Green)
btnToggleScheduler.Text = "▶ Start Scheduler";
btnToggleScheduler.ForeColor = Color.LightGreen;
}
}
// =========================================================
// 4. DATA LOADING
// ========================================================= // =========================================================
private void LoadData() private void LoadData()
{ {
_isLoaded = false; // Stop events from firing while we load _isLoaded = false;
// 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);
@@ -120,10 +178,9 @@ namespace UnshackleGUI
} }
else else
{ {
MessageBox.Show("Error: params.json not found! The grid will be empty."); MessageBox.Show("Error: params.json not found!");
} }
// 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);
@@ -133,46 +190,34 @@ namespace UnshackleGUI
txtBinName.Text = _appSettings.BinaryName; txtBinName.Text = _appSettings.BinaryName;
} }
// C. Populate the Services Dropdown (Read folders from disk)
RefreshFolders(); RefreshFolders();
// D. Load Profiles and select the default one
RefreshProfileList(); RefreshProfileList();
_isLoaded = true; // Loading done, enable events _isLoaded = true;
// Force an initial update of the command text
UpdateCommandPreview(); UpdateCommandPreview();
} }
private void RefreshProfileList() private void RefreshProfileList()
{ {
// 1. Temporarily stop listening to the selection event to prevent glitches
comboProfiles.SelectedIndexChanged -= ComboProfiles_SelectedIndexChanged; comboProfiles.SelectedIndexChanged -= ComboProfiles_SelectedIndexChanged;
// 2. Find all .json files in the Profiles folder
var files = Directory.GetFiles(_profilesDir, "*.json"); var files = Directory.GetFiles(_profilesDir, "*.json");
var profileNames = files.Select(Path.GetFileNameWithoutExtension).ToList(); var profileNames = files.Select(Path.GetFileNameWithoutExtension).ToList();
// 3. If no profiles exist, create a "Default" one
if (profileNames.Count == 0) if (profileNames.Count == 0)
{ {
CreateDefaultProfile(); CreateDefaultProfile();
profileNames.Add("Default"); profileNames.Add("Default");
} }
// 4. Update the ComboBox
comboProfiles.DataSource = null; comboProfiles.DataSource = null;
comboProfiles.DataSource = profileNames; comboProfiles.DataSource = profileNames;
// 5. Select "Default" or the first available profile
string targetProfile = profileNames.Contains("Default") ? "Default" : profileNames[0]; string targetProfile = profileNames.Contains("Default") ? "Default" : profileNames[0];
comboProfiles.SelectedItem = targetProfile; comboProfiles.SelectedItem = targetProfile;
// 6. Manually trigger the load for this profile
LoadProfile(targetProfile); LoadProfile(targetProfile);
// 7. Start listening to events again
comboProfiles.SelectedIndexChanged += ComboProfiles_SelectedIndexChanged; comboProfiles.SelectedIndexChanged += ComboProfiles_SelectedIndexChanged;
} }
@@ -180,13 +225,11 @@ namespace UnshackleGUI
{ {
_propertyValues = new Dictionary<string, object>(); _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 ?? ""; _propertyValues[p.Name] = p.Default ?? "";
} }
// Set default service
if (comboService.Items.Count > 0) if (comboService.Items.Count > 0)
{ {
_propertyValues["Service"] = comboService.Items[0].ToString(); _propertyValues["Service"] = comboService.Items[0].ToString();
@@ -195,22 +238,15 @@ namespace UnshackleGUI
SaveProfileFile("Default"); SaveProfileFile("Default");
} }
// =========================================================
// 4. PROFILE SWITCHING & LOADING
// =========================================================
private void ComboProfiles_SelectedIndexChanged(object? sender, EventArgs e) private void ComboProfiles_SelectedIndexChanged(object? sender, EventArgs e)
{ {
if (comboProfiles.SelectedItem == null) return; if (comboProfiles.SelectedItem == null) return;
LoadProfile(comboProfiles.SelectedItem.ToString());
string selectedProfile = comboProfiles.SelectedItem.ToString();
LoadProfile(selectedProfile);
} }
private void LoadProfile(string profileName) private void LoadProfile(string profileName)
{ {
_lastLoadedProfile = profileName; _lastLoadedProfile = profileName;
string path = Path.Combine(_profilesDir, $"{profileName}.json"); string path = Path.Combine(_profilesDir, $"{profileName}.json");
if (!File.Exists(path)) return; if (!File.Exists(path)) return;
@@ -224,9 +260,6 @@ namespace UnshackleGUI
{ {
_propertyValues = loadedValues; _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) foreach (var p in _parameterDefinitions)
{ {
if (!_propertyValues.ContainsKey(p.Name)) if (!_propertyValues.ContainsKey(p.Name))
@@ -235,33 +268,24 @@ namespace UnshackleGUI
} }
} }
// --- FIX 2: Handle the Service Dropdown ---
// Check if this profile has a saved Service (e.g., "AMZN")
if (_propertyValues.TryGetValue("Service", out var serviceObj)) if (_propertyValues.TryGetValue("Service", out var serviceObj))
{ {
string serviceName = serviceObj.ToString(); string serviceName = serviceObj.ToString();
// Only try to switch if the service actually exists in the list
if (comboService.Items.Contains(serviceName)) if (comboService.Items.Contains(serviceName))
{ {
// Temporarily disable _isLoaded to change dropdown without triggering a save loop
bool wasLoaded = _isLoaded; bool wasLoaded = _isLoaded;
_isLoaded = false; _isLoaded = false;
comboService.SelectedItem = serviceName; comboService.SelectedItem = serviceName;
_isLoaded = wasLoaded; _isLoaded = wasLoaded;
} }
} }
else if (comboService.SelectedItem != null) else if (comboService.SelectedItem != null)
{ {
// If profile has NO service saved, save the current one to it
_propertyValues["Service"] = comboService.SelectedItem.ToString(); _propertyValues["Service"] = comboService.SelectedItem.ToString();
} }
// --- FIX 3: Bind to PropertyGrid and Refresh ---
pgProfile.SelectedObject = new DynamicObject(_propertyValues, _parameterDefinitions); pgProfile.SelectedObject = new DynamicObject(_propertyValues, _parameterDefinitions);
pgProfile.Refresh(); // Crucial to prevent grey box pgProfile.Refresh();
UpdateCommandPreview(); UpdateCommandPreview();
} }
@@ -272,14 +296,9 @@ namespace UnshackleGUI
} }
} }
// =========================================================
// 5. SAVING LOGIC
// =========================================================
private void SaveCurrentProfile() private void SaveCurrentProfile()
{ {
// Don't save if we are loading or if nothing is selected
if (!_isLoaded || comboProfiles.SelectedItem == null) return; if (!_isLoaded || comboProfiles.SelectedItem == null) return;
string name = comboProfiles.SelectedItem.ToString(); string name = comboProfiles.SelectedItem.ToString();
SaveProfileFile(name); SaveProfileFile(name);
} }
@@ -288,7 +307,6 @@ namespace UnshackleGUI
{ {
try try
{ {
// Ensure the currently selected service is saved into the dictionary
if (comboService.SelectedItem != null) if (comboService.SelectedItem != null)
{ {
_propertyValues["Service"] = comboService.SelectedItem.ToString(); _propertyValues["Service"] = comboService.SelectedItem.ToString();
@@ -316,66 +334,46 @@ namespace UnshackleGUI
} }
catch catch
{ {
// Ignore errors while typing (file might be busy) // Ignore errors
} }
} }
// =========================================================
// 6. COMMAND GENERATION
// =========================================================
private void UpdateCommandPreview() private void UpdateCommandPreview()
{ {
if (!_isLoaded) return; if (!_isLoaded) return;
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
// 1. Start with binary and command
// e.g., "uv run unshackle dl "
sb.Append($"{txtBinName.Text} run unshackle dl "); sb.Append($"{txtBinName.Text} run unshackle dl ");
// 2. Loop through all parameters in the grid
foreach (var p in _parameterDefinitions) 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;
// Case A: Boolean Flags (e.g., --no-mux)
if (p.Type == "Bool" && val is bool isTrue && isTrue) if (p.Type == "Bool" && val is bool isTrue && isTrue)
{ {
sb.Append($"{p.Flag} "); sb.Append($"{p.Flag} ");
} }
// 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();
// 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")
{ {
// Quote the value if it has spaces
if (sVal.Contains(" ")) if (sVal.Contains(" "))
{ {
sVal = $"\"{sVal}\""; 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();
} }
// ========================================================= // =========================================================
// 7. CLI EXECUTION (The Run Button) // 8. CLI EXECUTION
// ========================================================= // =========================================================
private async void btnRun_Click(object? sender, EventArgs e) private async void btnRun_Click(object? sender, EventArgs e)
{ {
@@ -383,14 +381,11 @@ namespace UnshackleGUI
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 try
{ {
// Run in background thread to keep UI responsive
await Task.Run(() => RunCli(finalCmd)); await Task.Run(() => RunCli(finalCmd));
} }
catch (Exception ex) catch (Exception ex)
@@ -399,44 +394,36 @@ namespace UnshackleGUI
} }
finally finally
{ {
// Re-enable buttons
ToggleUI(false); 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 psi.FileName = "cmd.exe";
{ psi.Arguments = $"/c chcp 65001 && {cmd}";
FileName = "cmd.exe", psi.WorkingDirectory = txtRootPath.Text;
Arguments = $"/c chcp 65001 && {cmd}", // Force UTF-8 psi.RedirectStandardOutput = true;
WorkingDirectory = txtRootPath.Text, psi.RedirectStandardError = true;
RedirectStandardOutput = true, psi.UseShellExecute = false;
RedirectStandardError = true, psi.CreateNoWindow = true;
UseShellExecute = false, psi.StandardOutputEncoding = Encoding.UTF8;
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8
};
// 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 "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();
@@ -446,7 +433,7 @@ namespace UnshackleGUI
} }
// ========================================================= // =========================================================
// 8. PROFILE MANAGEMENT (Add/Remove/Save Buttons) // 9. HELPER METHODS
// ========================================================= // =========================================================
private void btnAddProfile_Click(object? sender, EventArgs e) private void btnAddProfile_Click(object? sender, EventArgs e)
{ {
@@ -454,23 +441,18 @@ namespace UnshackleGUI
if (string.IsNullOrWhiteSpace(name)) return; if (string.IsNullOrWhiteSpace(name)) return;
// Sanitize filename (remove illegal characters like / \ : *)
foreach (char c in Path.GetInvalidFileNameChars()) foreach (char c in Path.GetInvalidFileNameChars())
{ {
name = name.Replace(c, '_'); name = name.Replace(c, '_');
} }
// Check if exists
if (File.Exists(Path.Combine(_profilesDir, $"{name}.json"))) if (File.Exists(Path.Combine(_profilesDir, $"{name}.json")))
{ {
MessageBox.Show("Profile already exists."); MessageBox.Show("Profile already exists.");
return; return;
} }
// Save current settings as the new profile
SaveProfileFile(name); SaveProfileFile(name);
// Reload list and select new profile
RefreshProfileList(); RefreshProfileList();
comboProfiles.SelectedItem = name; comboProfiles.SelectedItem = name;
} }
@@ -501,22 +483,21 @@ namespace UnshackleGUI
{ {
string newName = comboProfiles.Text.Trim(); string newName = comboProfiles.Text.Trim();
// 1. Validation
if (string.IsNullOrWhiteSpace(newName)) if (string.IsNullOrWhiteSpace(newName))
{ {
MessageBox.Show("Please enter a profile name."); MessageBox.Show("Please enter a profile name.");
return; return;
} }
foreach (char c in Path.GetInvalidFileNameChars()) newName = newName.Replace(c, '_'); foreach (char c in Path.GetInvalidFileNameChars())
{
newName = newName.Replace(c, '_');
}
// 2. CHECK: Did the name change?
if (newName != _lastLoadedProfile && !string.IsNullOrEmpty(_lastLoadedProfile)) if (newName != _lastLoadedProfile && !string.IsNullOrEmpty(_lastLoadedProfile))
{ {
// Special Case: Cannot rename Default
if (_lastLoadedProfile == "Default") if (_lastLoadedProfile == "Default")
{ {
// Just create new, don't ask to rename Default
SaveProfileFile(newName); SaveProfileFile(newName);
RefreshProfileList(); RefreshProfileList();
comboProfiles.SelectedItem = newName; comboProfiles.SelectedItem = newName;
@@ -524,7 +505,6 @@ namespace UnshackleGUI
return; return;
} }
// Ask the user what to do
DialogResult choice = MessageBox.Show( DialogResult choice = MessageBox.Show(
$"You changed the name from '{_lastLoadedProfile}' to '{newName}'.\n\n" + $"You changed the name from '{_lastLoadedProfile}' to '{newName}'.\n\n" +
"Click YES to RENAME (Delete old).\n" + "Click YES to RENAME (Delete old).\n" +
@@ -535,20 +515,17 @@ namespace UnshackleGUI
if (choice == DialogResult.Cancel) return; if (choice == DialogResult.Cancel) return;
if (choice == DialogResult.Yes) // RENAME if (choice == DialogResult.Yes)
{ {
// Save New
SaveProfileFile(newName); SaveProfileFile(newName);
// Delete Old
string oldPath = Path.Combine(_profilesDir, $"{_lastLoadedProfile}.json"); string oldPath = Path.Combine(_profilesDir, $"{_lastLoadedProfile}.json");
if (File.Exists(oldPath)) File.Delete(oldPath); if (File.Exists(oldPath)) File.Delete(oldPath);
RefreshProfileList(); RefreshProfileList();
comboProfiles.SelectedItem = newName; // This updates _lastLoadedProfile automatically comboProfiles.SelectedItem = newName;
MessageBox.Show($"Renamed to '{newName}'."); MessageBox.Show($"Renamed to '{newName}'.");
} }
else // CREATE COPY else
{ {
SaveProfileFile(newName); SaveProfileFile(newName);
RefreshProfileList(); RefreshProfileList();
@@ -558,23 +535,15 @@ namespace UnshackleGUI
} }
else else
{ {
// 3. Name didn't change (Normal Save)
SaveProfileFile(newName); SaveProfileFile(newName);
MessageBox.Show($"Profile '{newName}' saved!", "Saved", MessageBoxButtons.OK, MessageBoxIcon.Information); MessageBox.Show($"Profile '{newName}' saved!", "Saved", MessageBoxButtons.OK, MessageBoxIcon.Information);
} }
} }
// =========================================================
// 9. HELPER METHODS & THEME
// =========================================================
private void AppendLog(string? text) private void AppendLog(string? text)
{ {
if (string.IsNullOrEmpty(text)) return; if (string.IsNullOrEmpty(text)) return;
// Ensure we update the UI on the main thread
this.BeginInvoke(new Action(() => this.BeginInvoke(new Action(() =>
{ {
txtLog.AppendText(text + Environment.NewLine); txtLog.AppendText(text + Environment.NewLine);
@@ -662,19 +631,17 @@ namespace UnshackleGUI
} }
} }
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) private void UpdateControlTheme(Control c)
{ {
// --- FIX: DO NOT TOUCH THESE BUTTONS ---
if (c == btnScheduler || c == btnToggleScheduler || c == btnRun || c == btnStop)
{
return;
}
// Apply standard dark theme to everything else
c.BackColor = Color.FromArgb(45, 45, 48); c.BackColor = Color.FromArgb(45, 45, 48);
c.ForeColor = Color.White; c.ForeColor = Color.White;
@@ -697,7 +664,6 @@ namespace UnshackleGUI
pg.LineColor = Color.FromArgb(45, 45, 48); pg.LineColor = Color.FromArgb(45, 45, 48);
} }
// Recursive call for panels and group boxes
foreach (Control child in c.Controls) foreach (Control child in c.Controls)
{ {
UpdateControlTheme(child); UpdateControlTheme(child);
@@ -705,7 +671,6 @@ namespace UnshackleGUI
} }
} }
// ========================================================= // =========================================================
// 10. DATA MODELS // 10. DATA MODELS
// ========================================================= // =========================================================

592
Shackle/SchedulerForm.Designer.cs generated Normal file
View File

@@ -0,0 +1,592 @@
namespace UnshackleGUI
{
partial class SchedulerForm
{
private System.ComponentModel.IContainer components = null;
// UI Controls
private System.Windows.Forms.Panel pnlToolbar;
private System.Windows.Forms.Button btnToolNew;
private System.Windows.Forms.Button btnToolSave;
private System.Windows.Forms.Button btnToolRun;
private System.Windows.Forms.Button btnToolDelete;
private System.Windows.Forms.DataGridView gridJobs;
private System.Windows.Forms.ContextMenuStrip contextMenu;
private System.Windows.Forms.ToolStripMenuItem menuRun;
private System.Windows.Forms.ToolStripMenuItem menuDelete;
private System.Windows.Forms.GroupBox grpDetails;
private System.Windows.Forms.Button btnAddUpdate;
// Inputs
private System.Windows.Forms.TextBox txtName;
private System.Windows.Forms.Label lblName;
private System.Windows.Forms.TextBox txtUrl;
private System.Windows.Forms.Label lblUrl;
private System.Windows.Forms.ComboBox comboProfile;
private System.Windows.Forms.Label lblProfile;
private System.Windows.Forms.TextBox txtPostCmd;
private System.Windows.Forms.Label lblPostCmd;
// Retry Controls
private System.Windows.Forms.NumericUpDown numRetries;
private System.Windows.Forms.Label lblRetries;
private System.Windows.Forms.NumericUpDown numRetryDelay;
private System.Windows.Forms.Label lblRetryDelay;
private System.Windows.Forms.Label lblRetryStatus;
// --- TRIGGER CONTROLS ---
private System.Windows.Forms.GroupBox grpTrigger;
private System.Windows.Forms.RadioButton rbOneTime;
private System.Windows.Forms.RadioButton rbDaily;
private System.Windows.Forms.RadioButton rbWeekly;
private System.Windows.Forms.DateTimePicker dtPicker;
private System.Windows.Forms.Label lblStartTime;
private System.Windows.Forms.Panel pnlWeekly;
private System.Windows.Forms.CheckBox chkMon;
private System.Windows.Forms.CheckBox chkTue;
private System.Windows.Forms.CheckBox chkWed;
private System.Windows.Forms.CheckBox chkThu;
private System.Windows.Forms.CheckBox chkFri;
private System.Windows.Forms.CheckBox chkSat;
private System.Windows.Forms.CheckBox chkSun;
// ------------------------
private System.Windows.Forms.TextBox txtLog;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null)) components.Dispose();
base.Dispose(disposing);
}
private void InitializeComponent()
{
components = new System.ComponentModel.Container();
DataGridViewCellStyle dataGridViewCellStyle1 = new DataGridViewCellStyle();
DataGridViewCellStyle dataGridViewCellStyle2 = new DataGridViewCellStyle();
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SchedulerForm));
pnlToolbar = new Panel();
btnToolNew = new Button();
btnToolSave = new Button();
btnToolDelete = new Button();
btnToolRun = new Button();
gridJobs = new DataGridView();
contextMenu = new ContextMenuStrip(components);
menuRun = new ToolStripMenuItem();
menuDelete = new ToolStripMenuItem();
grpDetails = new GroupBox();
lblName = new Label();
txtName = new TextBox();
lblUrl = new Label();
txtUrl = new TextBox();
lblProfile = new Label();
comboProfile = new ComboBox();
grpTrigger = new GroupBox();
rbOneTime = new RadioButton();
rbDaily = new RadioButton();
rbWeekly = new RadioButton();
lblStartTime = new Label();
dtPicker = new DateTimePicker();
pnlWeekly = new Panel();
chkMon = new CheckBox();
chkTue = new CheckBox();
chkWed = new CheckBox();
chkThu = new CheckBox();
chkFri = new CheckBox();
chkSat = new CheckBox();
chkSun = new CheckBox();
lblRetries = new Label();
numRetries = new NumericUpDown();
lblRetryStatus = new Label();
numRetryDelay = new NumericUpDown();
lblRetryDelay = new Label();
lblPostCmd = new Label();
txtPostCmd = new TextBox();
btnAddUpdate = new Button();
txtLog = new TextBox();
pnlToolbar.SuspendLayout();
((System.ComponentModel.ISupportInitialize)gridJobs).BeginInit();
contextMenu.SuspendLayout();
grpDetails.SuspendLayout();
grpTrigger.SuspendLayout();
pnlWeekly.SuspendLayout();
((System.ComponentModel.ISupportInitialize)numRetries).BeginInit();
((System.ComponentModel.ISupportInitialize)numRetryDelay).BeginInit();
SuspendLayout();
//
// pnlToolbar
//
pnlToolbar.BackColor = Color.FromArgb(45, 45, 48);
pnlToolbar.Controls.Add(btnToolNew);
pnlToolbar.Controls.Add(btnToolSave);
pnlToolbar.Controls.Add(btnToolDelete);
pnlToolbar.Controls.Add(btnToolRun);
pnlToolbar.Dock = DockStyle.Top;
pnlToolbar.Location = new Point(0, 0);
pnlToolbar.Name = "pnlToolbar";
pnlToolbar.Size = new Size(1181, 40);
pnlToolbar.TabIndex = 1;
//
// btnToolNew
//
btnToolNew.BackColor = Color.Transparent;
btnToolNew.Cursor = Cursors.Hand;
btnToolNew.FlatAppearance.BorderSize = 0;
btnToolNew.FlatStyle = FlatStyle.Flat;
btnToolNew.ForeColor = Color.White;
btnToolNew.Location = new Point(10, 5);
btnToolNew.Name = "btnToolNew";
btnToolNew.Size = new Size(110, 30);
btnToolNew.TabIndex = 0;
btnToolNew.Text = " New Task";
btnToolNew.TextAlign = ContentAlignment.MiddleLeft;
btnToolNew.UseVisualStyleBackColor = false;
//
// btnToolSave
//
btnToolSave.BackColor = Color.Transparent;
btnToolSave.Cursor = Cursors.Hand;
btnToolSave.FlatAppearance.BorderSize = 0;
btnToolSave.FlatStyle = FlatStyle.Flat;
btnToolSave.ForeColor = Color.White;
btnToolSave.Location = new Point(125, 5);
btnToolSave.Name = "btnToolSave";
btnToolSave.Size = new Size(110, 30);
btnToolSave.TabIndex = 1;
btnToolSave.Text = "💾 Save All";
btnToolSave.TextAlign = ContentAlignment.MiddleLeft;
btnToolSave.UseVisualStyleBackColor = false;
//
// btnToolDelete
//
btnToolDelete.BackColor = Color.Transparent;
btnToolDelete.Cursor = Cursors.Hand;
btnToolDelete.FlatAppearance.BorderSize = 0;
btnToolDelete.FlatStyle = FlatStyle.Flat;
btnToolDelete.ForeColor = Color.Salmon;
btnToolDelete.Location = new Point(325, 5);
btnToolDelete.Name = "btnToolDelete";
btnToolDelete.Size = new Size(110, 30);
btnToolDelete.TabIndex = 2;
btnToolDelete.Text = "❌ Delete";
btnToolDelete.TextAlign = ContentAlignment.MiddleLeft;
btnToolDelete.UseVisualStyleBackColor = false;
//
// btnToolRun
//
btnToolRun.BackColor = Color.Transparent;
btnToolRun.Cursor = Cursors.Hand;
btnToolRun.FlatAppearance.BorderSize = 0;
btnToolRun.FlatStyle = FlatStyle.Flat;
btnToolRun.ForeColor = Color.LightGreen;
btnToolRun.Location = new Point(240, 5);
btnToolRun.Name = "btnToolRun";
btnToolRun.Size = new Size(110, 30);
btnToolRun.TabIndex = 3;
btnToolRun.Text = "▶ Run Selected";
btnToolRun.TextAlign = ContentAlignment.MiddleLeft;
btnToolRun.UseVisualStyleBackColor = false;
//
// gridJobs
//
gridJobs.AllowUserToAddRows = false;
gridJobs.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
gridJobs.BackgroundColor = Color.FromArgb(30, 30, 30);
gridJobs.BorderStyle = BorderStyle.None;
gridJobs.ColumnHeadersBorderStyle = DataGridViewHeaderBorderStyle.Single;
dataGridViewCellStyle1.BackColor = Color.FromArgb(45, 45, 48);
dataGridViewCellStyle1.Font = new Font("Segoe UI", 9F, FontStyle.Bold);
dataGridViewCellStyle1.ForeColor = Color.White;
gridJobs.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1;
gridJobs.ColumnHeadersHeight = 29;
gridJobs.ContextMenuStrip = contextMenu;
gridJobs.EnableHeadersVisualStyles = false;
gridJobs.Location = new Point(0, 41);
gridJobs.MultiSelect = false;
gridJobs.Name = "gridJobs";
gridJobs.RowHeadersVisible = false;
gridJobs.RowHeadersWidth = 51;
dataGridViewCellStyle2.BackColor = Color.FromArgb(30, 30, 30);
dataGridViewCellStyle2.ForeColor = Color.White;
dataGridViewCellStyle2.SelectionBackColor = Color.DarkOrange;
dataGridViewCellStyle2.SelectionForeColor = Color.Black;
gridJobs.RowsDefaultCellStyle = dataGridViewCellStyle2;
gridJobs.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
gridJobs.Size = new Size(1181, 451);
gridJobs.TabIndex = 2;
//
// contextMenu
//
contextMenu.ImageScalingSize = new Size(20, 20);
contextMenu.Items.AddRange(new ToolStripItem[] { menuRun, menuDelete });
contextMenu.Name = "contextMenu";
contextMenu.Size = new Size(208, 52);
//
// menuRun
//
menuRun.Name = "menuRun";
menuRun.Size = new Size(207, 24);
menuRun.Text = "▶ Run Immediately";
//
// menuDelete
//
menuDelete.Name = "menuDelete";
menuDelete.Size = new Size(207, 24);
menuDelete.Text = "❌ Delete Task";
//
// grpDetails
//
grpDetails.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
grpDetails.BackColor = Color.FromArgb(30, 30, 30);
grpDetails.Controls.Add(lblName);
grpDetails.Controls.Add(txtName);
grpDetails.Controls.Add(lblUrl);
grpDetails.Controls.Add(txtUrl);
grpDetails.Controls.Add(lblProfile);
grpDetails.Controls.Add(comboProfile);
grpDetails.Controls.Add(grpTrigger);
grpDetails.Controls.Add(lblPostCmd);
grpDetails.Controls.Add(txtPostCmd);
grpDetails.Controls.Add(btnAddUpdate);
grpDetails.ForeColor = Color.White;
grpDetails.Location = new Point(12, 498);
grpDetails.Name = "grpDetails";
grpDetails.Size = new Size(1157, 235);
grpDetails.TabIndex = 3;
grpDetails.TabStop = false;
grpDetails.Text = "Task Details";
//
// lblName
//
lblName.AutoSize = true;
lblName.Location = new Point(20, 30);
lblName.Name = "lblName";
lblName.Size = new Size(52, 20);
lblName.TabIndex = 0;
lblName.Text = "Name:";
//
// txtName
//
txtName.BackColor = Color.FromArgb(45, 45, 48);
txtName.BorderStyle = BorderStyle.FixedSingle;
txtName.ForeColor = Color.White;
txtName.Location = new Point(20, 50);
txtName.Name = "txtName";
txtName.Size = new Size(200, 27);
txtName.TabIndex = 1;
//
// lblUrl
//
lblUrl.AutoSize = true;
lblUrl.Location = new Point(240, 30);
lblUrl.Name = "lblUrl";
lblUrl.Size = new Size(67, 20);
lblUrl.TabIndex = 2;
lblUrl.Text = "ID / URL:";
//
// txtUrl
//
txtUrl.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
txtUrl.BackColor = Color.FromArgb(45, 45, 48);
txtUrl.BorderStyle = BorderStyle.FixedSingle;
txtUrl.ForeColor = Color.White;
txtUrl.Location = new Point(240, 50);
txtUrl.Name = "txtUrl";
txtUrl.Size = new Size(650, 27);
txtUrl.TabIndex = 3;
//
// lblProfile
//
lblProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right;
lblProfile.AutoSize = true;
lblProfile.Location = new Point(910, 30);
lblProfile.Name = "lblProfile";
lblProfile.Size = new Size(55, 20);
lblProfile.TabIndex = 4;
lblProfile.Text = "Profile:";
//
// comboProfile
//
comboProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right;
comboProfile.BackColor = Color.FromArgb(45, 45, 48);
comboProfile.FlatStyle = FlatStyle.Flat;
comboProfile.ForeColor = Color.White;
comboProfile.Location = new Point(910, 50);
comboProfile.Name = "comboProfile";
comboProfile.Size = new Size(230, 28);
comboProfile.TabIndex = 5;
//
// grpTrigger
//
grpTrigger.Controls.Add(rbOneTime);
grpTrigger.Controls.Add(rbDaily);
grpTrigger.Controls.Add(rbWeekly);
grpTrigger.Controls.Add(lblStartTime);
grpTrigger.Controls.Add(dtPicker);
grpTrigger.Controls.Add(pnlWeekly);
grpTrigger.Controls.Add(lblRetries);
grpTrigger.Controls.Add(numRetries);
grpTrigger.Controls.Add(lblRetryStatus);
grpTrigger.Controls.Add(numRetryDelay);
grpTrigger.Controls.Add(lblRetryDelay);
grpTrigger.ForeColor = Color.LightGray;
grpTrigger.Location = new Point(20, 90);
grpTrigger.Name = "grpTrigger";
grpTrigger.Size = new Size(550, 127);
grpTrigger.TabIndex = 6;
grpTrigger.TabStop = false;
grpTrigger.Text = "Triggers";
//
// rbOneTime
//
rbOneTime.AutoSize = true;
rbOneTime.Checked = true;
rbOneTime.Location = new Point(15, 25);
rbOneTime.Name = "rbOneTime";
rbOneTime.Size = new Size(94, 24);
rbOneTime.TabIndex = 0;
rbOneTime.TabStop = true;
rbOneTime.Text = "One Time";
//
// rbDaily
//
rbDaily.AutoSize = true;
rbDaily.Location = new Point(115, 25);
rbDaily.Name = "rbDaily";
rbDaily.Size = new Size(64, 24);
rbDaily.TabIndex = 1;
rbDaily.Text = "Daily";
//
// rbWeekly
//
rbWeekly.AutoSize = true;
rbWeekly.Location = new Point(184, 25);
rbWeekly.Name = "rbWeekly";
rbWeekly.Size = new Size(77, 24);
rbWeekly.TabIndex = 2;
rbWeekly.Text = "Weekly";
//
// lblStartTime
//
lblStartTime.AutoSize = true;
lblStartTime.Location = new Point(15, 60);
lblStartTime.Name = "lblStartTime";
lblStartTime.Size = new Size(62, 20);
lblStartTime.TabIndex = 3;
lblStartTime.Text = "Start At:";
//
// dtPicker
//
dtPicker.CustomFormat = "yyyy-MM-dd HH:mm";
dtPicker.Format = DateTimePickerFormat.Custom;
dtPicker.Location = new Point(83, 55);
dtPicker.Name = "dtPicker";
dtPicker.Size = new Size(164, 27);
dtPicker.TabIndex = 4;
//
// pnlWeekly
//
pnlWeekly.Controls.Add(chkMon);
pnlWeekly.Controls.Add(chkTue);
pnlWeekly.Controls.Add(chkWed);
pnlWeekly.Controls.Add(chkThu);
pnlWeekly.Controls.Add(chkFri);
pnlWeekly.Controls.Add(chkSat);
pnlWeekly.Controls.Add(chkSun);
pnlWeekly.Location = new Point(288, 20);
pnlWeekly.Name = "pnlWeekly";
pnlWeekly.Size = new Size(256, 60);
pnlWeekly.TabIndex = 5;
pnlWeekly.Visible = false;
//
// chkMon
//
chkMon.AutoSize = true;
chkMon.Location = new Point(0, 0);
chkMon.Name = "chkMon";
chkMon.Size = new Size(61, 24);
chkMon.TabIndex = 0;
chkMon.Text = "Mon";
//
// chkTue
//
chkTue.AutoSize = true;
chkTue.Location = new Point(60, 0);
chkTue.Name = "chkTue";
chkTue.Size = new Size(55, 24);
chkTue.TabIndex = 1;
chkTue.Text = "Tue";
//
// chkWed
//
chkWed.AutoSize = true;
chkWed.Location = new Point(115, 0);
chkWed.Name = "chkWed";
chkWed.Size = new Size(61, 24);
chkWed.TabIndex = 2;
chkWed.Text = "Wed";
//
// chkThu
//
chkThu.AutoSize = true;
chkThu.Location = new Point(177, 0);
chkThu.Name = "chkThu";
chkThu.Size = new Size(55, 24);
chkThu.TabIndex = 3;
chkThu.Text = "Thu";
//
// chkFri
//
chkFri.AutoSize = true;
chkFri.Location = new Point(0, 30);
chkFri.Name = "chkFri";
chkFri.Size = new Size(47, 24);
chkFri.TabIndex = 4;
chkFri.Text = "Fri";
//
// chkSat
//
chkSat.AutoSize = true;
chkSat.Location = new Point(60, 30);
chkSat.Name = "chkSat";
chkSat.Size = new Size(52, 24);
chkSat.TabIndex = 5;
chkSat.Text = "Sat";
//
// chkSun
//
chkSun.AutoSize = true;
chkSun.Location = new Point(114, 30);
chkSun.Name = "chkSun";
chkSun.Size = new Size(55, 24);
chkSun.TabIndex = 6;
chkSun.Text = "Sun";
//
// lblRetries
//
lblRetries.AutoSize = true;
lblRetries.Location = new Point(15, 92);
lblRetries.Name = "lblRetries";
lblRetries.Size = new Size(89, 20);
lblRetries.TabIndex = 9;
lblRetries.Text = "Max Retries:";
//
// numRetries
//
numRetries.BackColor = Color.FromArgb(45, 45, 48);
numRetries.BorderStyle = BorderStyle.FixedSingle;
numRetries.ForeColor = Color.White;
numRetries.Location = new Point(110, 88);
numRetries.Name = "numRetries";
numRetries.Size = new Size(50, 27);
numRetries.TabIndex = 10;
//
// lblRetryStatus
//
lblRetryStatus.AutoSize = true;
lblRetryStatus.ForeColor = Color.Orange;
lblRetryStatus.Location = new Point(166, 92);
lblRetryStatus.Name = "lblRetryStatus";
lblRetryStatus.Size = new Size(79, 20);
lblRetryStatus.TabIndex = 13;
lblRetryStatus.Text = "Attempt: 0";
//
// numRetryDelay
//
numRetryDelay.BackColor = Color.FromArgb(45, 45, 48);
numRetryDelay.BorderStyle = BorderStyle.FixedSingle;
numRetryDelay.ForeColor = Color.White;
numRetryDelay.Location = new Point(373, 88);
numRetryDelay.Name = "numRetryDelay";
numRetryDelay.Size = new Size(60, 27);
numRetryDelay.TabIndex = 12;
numRetryDelay.Value = new decimal(new int[] { 15, 0, 0, 0 });
//
// lblRetryDelay
//
lblRetryDelay.AutoSize = true;
lblRetryDelay.Location = new Point(290, 92);
lblRetryDelay.Name = "lblRetryDelay";
lblRetryDelay.Size = new Size(77, 20);
lblRetryDelay.TabIndex = 11;
lblRetryDelay.Text = "Delay (m):";
//
// lblPostCmd
//
lblPostCmd.AutoSize = true;
lblPostCmd.Location = new Point(590, 90);
lblPostCmd.Name = "lblPostCmd";
lblPostCmd.Size = new Size(143, 20);
lblPostCmd.TabIndex = 7;
lblPostCmd.Text = "Post-Run Command:";
//
// txtPostCmd
//
txtPostCmd.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
txtPostCmd.BackColor = Color.FromArgb(45, 45, 48);
txtPostCmd.BorderStyle = BorderStyle.FixedSingle;
txtPostCmd.ForeColor = Color.White;
txtPostCmd.Location = new Point(590, 110);
txtPostCmd.Name = "txtPostCmd";
txtPostCmd.Size = new Size(550, 27);
txtPostCmd.TabIndex = 8;
//
// btnAddUpdate
//
btnAddUpdate.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
btnAddUpdate.BackColor = Color.FromArgb(60, 60, 60);
btnAddUpdate.FlatAppearance.BorderColor = Color.Gray;
btnAddUpdate.FlatStyle = FlatStyle.Flat;
btnAddUpdate.ForeColor = Color.White;
btnAddUpdate.Location = new Point(1020, 185);
btnAddUpdate.Name = "btnAddUpdate";
btnAddUpdate.Size = new Size(120, 30);
btnAddUpdate.TabIndex = 14;
btnAddUpdate.Text = "Add Task";
btnAddUpdate.UseVisualStyleBackColor = false;
//
// txtLog
//
txtLog.Anchor = AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
txtLog.BackColor = Color.Black;
txtLog.BorderStyle = BorderStyle.FixedSingle;
txtLog.Font = new Font("Consolas", 9F);
txtLog.ForeColor = Color.Lime;
txtLog.Location = new Point(12, 739);
txtLog.Multiline = true;
txtLog.Name = "txtLog";
txtLog.ReadOnly = true;
txtLog.ScrollBars = ScrollBars.Vertical;
txtLog.Size = new Size(1157, 160);
txtLog.TabIndex = 4;
//
// SchedulerForm
//
BackColor = Color.FromArgb(30, 30, 30);
ClientSize = new Size(1181, 910);
Controls.Add(pnlToolbar);
Controls.Add(gridJobs);
Controls.Add(grpDetails);
Controls.Add(txtLog);
Icon = (Icon)resources.GetObject("$this.Icon");
Name = "SchedulerForm";
StartPosition = FormStartPosition.CenterScreen;
Text = "Task Scheduler";
pnlToolbar.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)gridJobs).EndInit();
contextMenu.ResumeLayout(false);
grpDetails.ResumeLayout(false);
grpDetails.PerformLayout();
grpTrigger.ResumeLayout(false);
grpTrigger.PerformLayout();
pnlWeekly.ResumeLayout(false);
pnlWeekly.PerformLayout();
((System.ComponentModel.ISupportInitialize)numRetries).EndInit();
((System.ComponentModel.ISupportInitialize)numRetryDelay).EndInit();
ResumeLayout(false);
PerformLayout();
}
}
}

704
Shackle/SchedulerForm.cs Normal file
View File

@@ -0,0 +1,704 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
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 SchedulerForm : Form
{
// ==========================================
// VARIABLES & PATHS
// ==========================================
private string _jobsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "jobs.json");
private string _successStringsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "success_strings.txt");
private BindingList<SchedulerJob> _jobs = new BindingList<SchedulerJob>();
private List<string> _successKeywords = new List<string>();
private System.Windows.Forms.Timer _runnerTimer;
private List<string> _profiles;
private string _rootPath;
private string _binaryName;
private SchedulerJob? _editingJob = null;
// Flag to allow the Main Form to kill this window completely
private bool _forceClose = false;
// ==========================================
// CONSTRUCTOR
// ==========================================
public SchedulerForm(List<string> profiles, string rootPath, string binaryName)
{
InitializeComponent();
_profiles = profiles;
_rootPath = rootPath;
_binaryName = binaryName;
LoadSuccessStrings();
InitializeCustomUI();
LoadJobs();
// Setup Background Runner Timer
_runnerTimer = new System.Windows.Forms.Timer();
_runnerTimer.Interval = 10000; // Runs every 10 seconds
_runnerTimer.Tick += CheckAndRunJobs;
_runnerTimer.Start();
}
// ==========================================
// PUBLIC CONTROL METHODS
// ==========================================
// Called by MainForm when "Stop" is clicked
public void Shutdown()
{
_forceClose = true;
_runnerTimer.Stop();
this.Close();
}
// ==========================================
// INITIALIZATION & UI WIRING
// ==========================================
private void InitializeCustomUI()
{
// Disable "New Row" to prevent ghost jobs
gridJobs.AllowUserToAddRows = false;
// 1. Setup Dropdowns & Data
comboProfile.DataSource = _profiles;
gridJobs.AutoGenerateColumns = false;
gridJobs.DataSource = _jobs;
// 2. Clear and Define Grid Columns
gridJobs.Columns.Clear();
gridJobs.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "Name",
HeaderText = "Name",
Width = 150
});
gridJobs.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "Recurrence",
HeaderText = "Trigger",
Width = 80
});
gridJobs.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "ScheduledTime",
HeaderText = "Next Run",
Width = 130
});
gridJobs.Columns.Add(new DataGridViewTextBoxColumn
{
DataPropertyName = "Status",
HeaderText = "Status",
Width = 120
});
// URL (Auto-Fill)
var urlCol = new DataGridViewTextBoxColumn
{
DataPropertyName = "Url",
HeaderText = "URL"
};
urlCol.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
gridJobs.Columns.Add(urlCol);
// Active
gridJobs.Columns.Add(new DataGridViewCheckBoxColumn
{
DataPropertyName = "Enabled",
HeaderText = "Active",
Width = 60
});
// 3. Force Row Height
gridJobs.RowTemplate.Height = 30;
// 4. Wire Up Events
btnAddUpdate.Click += BtnAdd_Click;
btnToolSave.Click += (s, e) => SaveJobs();
btnToolNew.Click += (s, e) => ClearInputs();
btnToolDelete.Click += BtnRemove_Click;
btnToolRun.Click += BtnRunNow_Click;
menuRun.Click += BtnRunNow_Click;
menuDelete.Click += BtnRemove_Click;
gridJobs.SelectionChanged += GridJobs_SelectionChanged;
rbWeekly.CheckedChanged += (s, e) => pnlWeekly.Visible = rbWeekly.Checked;
// 5. Auto-Save on Checkbox Click
gridJobs.CellValueChanged += (s, e) =>
{
SaveJobs();
};
gridJobs.CurrentCellDirtyStateChanged += (s, e) =>
{
if (gridJobs.IsCurrentCellDirty)
{
gridJobs.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
};
// 6. Start Clean
ClearInputs();
}
private void LoadSuccessStrings()
{
if (File.Exists(_successStringsPath))
{
try
{
_successKeywords = File.ReadAllLines(_successStringsPath)
.Where(line => !string.IsNullOrWhiteSpace(line))
.Select(line => line.Trim().ToLower())
.ToList();
}
catch
{
// Ignore errors
}
}
if (_successKeywords.Count == 0)
{
_successKeywords = new List<string> { "downloaded in", "processed all titles", "100%" };
try
{
File.WriteAllLines(_successStringsPath, _successKeywords);
}
catch { }
}
}
// ==========================================
// UI INTERACTION LOGIC
// ==========================================
private void GridJobs_SelectionChanged(object? sender, EventArgs e)
{
if (gridJobs.CurrentRow?.DataBoundItem is SchedulerJob job)
{
_editingJob = job;
txtName.Text = job.Name;
txtUrl.Text = job.Url;
if (comboProfile.Items.Contains(job.ProfileName))
comboProfile.SelectedItem = job.ProfileName;
dtPicker.Value = job.ScheduledTime;
numRetries.Value = job.MaxRetries;
txtPostCmd.Text = job.PostRunCommand;
numRetryDelay.Value = job.RetryDelayInMinutes;
rbOneTime.Checked = (job.Recurrence == "OneTime");
rbDaily.Checked = (job.Recurrence == "Daily");
rbWeekly.Checked = (job.Recurrence == "Weekly");
chkMon.Checked = job.WeeklyDays.Contains("Monday");
chkTue.Checked = job.WeeklyDays.Contains("Tuesday");
chkWed.Checked = job.WeeklyDays.Contains("Wednesday");
chkThu.Checked = job.WeeklyDays.Contains("Thursday");
chkFri.Checked = job.WeeklyDays.Contains("Friday");
chkSat.Checked = job.WeeklyDays.Contains("Saturday");
chkSun.Checked = job.WeeklyDays.Contains("Sunday");
if (lblRetryStatus != null)
lblRetryStatus.Text = $"Attempt: {job.CurrentRetries}/{job.MaxRetries}";
btnAddUpdate.Text = "Update Task";
btnAddUpdate.BackColor = System.Drawing.Color.DarkOrange;
btnAddUpdate.ForeColor = System.Drawing.Color.Black;
}
}
private void ClearInputs()
{
_editingJob = null;
txtName.Clear();
txtUrl.Clear();
txtPostCmd.Clear();
numRetries.Value = 3;
numRetryDelay.Value = 15;
dtPicker.Value = DateTime.Now;
if (lblRetryStatus != null)
lblRetryStatus.Text = "Attempt: 0";
rbOneTime.Checked = true;
foreach (Control c in pnlWeekly.Controls)
{
if (c is CheckBox cb) cb.Checked = false;
}
btnAddUpdate.Text = "Add Task";
btnAddUpdate.BackColor = System.Drawing.Color.FromArgb(60, 60, 60);
btnAddUpdate.ForeColor = System.Drawing.Color.White;
gridJobs.ClearSelection();
}
private void BtnAdd_Click(object? sender, EventArgs e)
{
if (string.IsNullOrWhiteSpace(txtUrl.Text))
{
MessageBox.Show("Please enter a URL or ID.", "Missing Input", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
SchedulerJob job;
if (_editingJob != null)
{
job = _editingJob;
}
else
{
job = new SchedulerJob();
}
job.Name = string.IsNullOrWhiteSpace(txtName.Text) ? "New Task" : txtName.Text;
job.Url = txtUrl.Text;
job.ProfileName = comboProfile.Text;
job.ScheduledTime = dtPicker.Value;
job.MaxRetries = (int)numRetries.Value;
job.RetryDelayInMinutes = (int)numRetryDelay.Value;
job.PostRunCommand = txtPostCmd.Text;
if (rbDaily.Checked) job.Recurrence = "Daily";
else if (rbWeekly.Checked) job.Recurrence = "Weekly";
else job.Recurrence = "OneTime";
job.WeeklyDays.Clear();
if (chkMon.Checked) job.WeeklyDays.Add("Monday");
if (chkTue.Checked) job.WeeklyDays.Add("Tuesday");
if (chkWed.Checked) job.WeeklyDays.Add("Wednesday");
if (chkThu.Checked) job.WeeklyDays.Add("Thursday");
if (chkFri.Checked) job.WeeklyDays.Add("Friday");
if (chkSat.Checked) job.WeeklyDays.Add("Saturday");
if (chkSun.Checked) job.WeeklyDays.Add("Sunday");
// Enable and Reset
job.Enabled = true;
job.Status = "Pending";
job.CurrentRetries = 0;
// Recalculate Date Immediately
if (job.Recurrence != "OneTime")
{
CalculateNextRun(job);
}
if (_editingJob == null)
{
_jobs.Add(job);
}
else
{
MessageBox.Show("Task Updated!");
}
SaveJobs();
gridJobs.Refresh();
ClearInputs();
}
private void BtnRemove_Click(object? sender, EventArgs e)
{
if (gridJobs.CurrentRow?.DataBoundItem is SchedulerJob job)
{
if (MessageBox.Show($"Delete task '{job.Name}'?", "Confirm Delete", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.Yes)
{
_jobs.Remove(job);
SaveJobs();
ClearInputs();
}
}
}
private async void BtnRunNow_Click(object? sender, EventArgs e)
{
if (gridJobs.CurrentRow?.DataBoundItem is SchedulerJob job)
{
if (MessageBox.Show($"Run '{job.Name}' immediately?", "Run Now", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
AppendLog($"[USER] Manually triggered run for '{job.Name}'...");
await RunJob(job);
MessageBox.Show("Run completed. Check log for details.");
}
}
else
{
MessageBox.Show("Please select a task to run.");
}
}
// ==========================================
// BACKGROUND RUNNER ENGINE
// ==========================================
private async void CheckAndRunJobs(object? sender, EventArgs e)
{
var now = DateTime.Now;
var dueJobs = _jobs.Where(j =>
j.Enabled &&
(j.Status == "Pending" || j.Status.StartsWith("Retrying")) &&
j.ScheduledTime <= now).ToList();
foreach (var job in dueJobs)
{
await RunJob(job);
}
}
private async Task RunJob(SchedulerJob job)
{
job.Status = "Running...";
gridJobs.Refresh();
string cmd = BuildCommand(job);
AppendLog($"Executing Job '{job.Name}'...");
AppendLog($"[CMD]: {cmd}");
var result = await RunProcessAsync(cmd, _rootPath);
int exitCode = result.ExitCode;
string output = result.Output.ToLower();
bool success = _successKeywords.Any(k => output.Contains(k)) || exitCode == 0;
if (output.Contains("already exists") || output.Contains("skipping"))
{
AppendLog($"[INFO] Job '{job.Name}' skipped (File exists).");
HandleSuccess(job);
}
else if (success)
{
AppendLog($"[SUCCESS] Job '{job.Name}' finished.");
if (!string.IsNullOrWhiteSpace(job.PostRunCommand))
{
AppendLog($"Running Post-Command...");
RunPostCommand(job);
}
HandleSuccess(job);
}
else if (output.Contains("unauthorized") || output.Contains("login failed") || output.Contains("403"))
{
job.Status = "Auth Failed";
job.Enabled = false;
AppendLog($"[FATAL] Job '{job.Name}' auth failed.");
}
else
{
HandleFailure(job);
}
SaveJobs();
gridJobs.Refresh();
if (_editingJob == job && lblRetryStatus != null)
{
lblRetryStatus.Text = $"Attempt: {job.CurrentRetries}/{job.MaxRetries}";
}
}
private void HandleSuccess(SchedulerJob job)
{
job.CurrentRetries = 0;
if (job.Recurrence == "OneTime")
{
job.Status = "Success";
job.Enabled = false;
}
else
{
CalculateNextRun(job);
job.Status = "Pending";
job.Enabled = true;
AppendLog($"-> Rescheduled '{job.Name}' to {job.ScheduledTime}");
}
}
private void HandleFailure(SchedulerJob job)
{
if (job.CurrentRetries < job.MaxRetries)
{
job.CurrentRetries++;
job.Status = $"Retrying ({job.CurrentRetries}/{job.MaxRetries})";
int delay = job.RetryDelayInMinutes > 0 ? job.RetryDelayInMinutes : 15;
job.ScheduledTime = DateTime.Now.AddMinutes(delay);
AppendLog($"[WARNING] Job '{job.Name}' failed. Retrying in {delay}m.");
}
else
{
job.Status = "Failed";
job.Enabled = false;
AppendLog($"[FAILURE] Job '{job.Name}' failed permanently.");
}
}
private void CalculateNextRun(SchedulerJob job)
{
DateTime checkDate = job.ScheduledTime;
if (job.Recurrence == "Daily")
{
while (checkDate <= DateTime.Now)
{
checkDate = checkDate.AddDays(1);
}
}
else if (job.Recurrence == "Weekly" && job.WeeklyDays.Count > 0)
{
while (true)
{
bool isFuture = checkDate > DateTime.Now;
bool isCorrectDay = job.WeeklyDays.Contains(checkDate.DayOfWeek.ToString());
if (isFuture && isCorrectDay)
{
break;
}
checkDate = checkDate.AddDays(1);
}
}
job.ScheduledTime = checkDate;
}
// ==========================================
// HELPER METHODS
// ==========================================
private void LoadJobs()
{
if (File.Exists(_jobsPath))
{
try
{
string json = File.ReadAllText(_jobsPath);
var list = JsonConvert.DeserializeObject<List<SchedulerJob>>(json);
if (list != null)
{
_jobs = new BindingList<SchedulerJob>(list);
}
}
catch
{
// Handle corrupt JSON
}
}
gridJobs.DataSource = _jobs;
}
private void SaveJobs()
{
try
{
string json = JsonConvert.SerializeObject(_jobs, Formatting.Indented);
File.WriteAllText(_jobsPath, json);
}
catch (Exception ex)
{
MessageBox.Show($"Error saving jobs: {ex.Message}");
}
}
private string BuildCommand(SchedulerJob job)
{
string profilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Profiles", $"{job.ProfileName}.json");
var props = new Dictionary<string, object>();
if (File.Exists(profilePath))
{
props = JsonConvert.DeserializeObject<Dictionary<string, object>>(File.ReadAllText(profilePath))
?? new Dictionary<string, object>();
}
string paramsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "params.json");
var defs = JsonConvert.DeserializeObject<List<UnshackleParameter>>(File.ReadAllText(paramsPath))
?? new List<UnshackleParameter>();
StringBuilder sb = new StringBuilder();
sb.Append($"{_binaryName} run unshackle dl ");
foreach (var p in defs)
{
if (props.TryGetValue(p.Name, out object val))
{
if (p.Type == "Bool" && val is bool b && b)
{
sb.Append($"{p.Flag} ");
}
else if (p.Type != "Bool" && val != null)
{
string sVal = val.ToString();
if (!string.IsNullOrWhiteSpace(sVal) && sVal != "any" && sVal != "0")
{
if (sVal.Contains(" "))
{
sVal = $"\"{sVal}\"";
}
sb.Append($"{p.Flag} {sVal} ");
}
}
}
}
if (props.TryGetValue("Service", out object serviceObj))
{
sb.Append($"{serviceObj} ");
}
string cleanUrl = job.Url.Trim();
cleanUrl = cleanUrl.Trim('"');
cleanUrl = cleanUrl.Trim('\'');
sb.Append($"{cleanUrl}");
return sb.ToString();
}
private void RunPostCommand(SchedulerJob job)
{
try
{
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = "cmd.exe";
psi.Arguments = $"/c {job.PostRunCommand}";
psi.UseShellExecute = true;
psi.CreateNoWindow = true;
Process.Start(psi);
}
catch (Exception ex)
{
AppendLog($"[ERR] Post-Command failed: {ex.Message}");
}
}
private void AppendLog(string msg)
{
if (this.IsDisposed) return;
string timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
string line = $"[{timestamp}] {msg}";
this.BeginInvoke(new Action(() =>
{
if (txtLog != null && !txtLog.IsDisposed)
{
txtLog.AppendText(line + Environment.NewLine);
}
}));
try
{
File.AppendAllText("scheduler.log", line + Environment.NewLine);
}
catch
{
// Ignore file lock issues
}
}
private Task<(int ExitCode, string Output)> RunProcessAsync(string cmd, string workDir)
{
var tcs = new TaskCompletionSource<(int, string)>();
StringBuilder outputBuilder = new StringBuilder();
ProcessStartInfo psi = new ProcessStartInfo();
psi.FileName = "cmd.exe";
psi.Arguments = $"/c chcp 65001 && {cmd}";
psi.WorkingDirectory = workDir;
psi.UseShellExecute = false;
psi.CreateNoWindow = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
psi.StandardOutputEncoding = Encoding.UTF8;
psi.StandardErrorEncoding = Encoding.UTF8;
psi.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
psi.EnvironmentVariables["PYTHONUTF8"] = "1";
var p = new Process { StartInfo = psi, EnableRaisingEvents = true };
p.OutputDataReceived += (s, e) =>
{
if (!string.IsNullOrWhiteSpace(e.Data))
{
AppendLog($"[CMD]: {e.Data}");
outputBuilder.AppendLine(e.Data);
}
};
p.ErrorDataReceived += (s, e) =>
{
if (!string.IsNullOrWhiteSpace(e.Data))
{
AppendLog($"[ERR]: {e.Data}");
outputBuilder.AppendLine(e.Data);
}
};
p.Exited += (s, e) =>
{
tcs.SetResult((p.ExitCode, outputBuilder.ToString()));
p.Dispose();
};
try
{
p.Start();
p.BeginOutputReadLine();
p.BeginErrorReadLine();
}
catch (Exception ex)
{
AppendLog($"[FATAL] Start Error: {ex.Message}");
tcs.SetResult((-1, ""));
}
return tcs.Task;
}
// ==========================================
// PREVENT CLOSING (HIDE WINDOW)
// ==========================================
protected override void OnFormClosing(FormClosingEventArgs e)
{
// If the user clicks X, we hide it.
// If Shutdown() is called, _forceClose is true, so we close it.
if (e.CloseReason == CloseReason.UserClosing && !_forceClose)
{
e.Cancel = true;
this.Hide();
}
else
{
base.OnFormClosing(e);
}
}
}
}

16510
Shackle/SchedulerForm.resx Normal file

File diff suppressed because it is too large Load Diff

30
Shackle/SchedulerJob.cs Normal file
View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
namespace UnshackleGUI
{
public class SchedulerJob
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public string Name { get; set; } = "New Task";
public string Url { get; set; } = "";
public string ProfileName { get; set; } = "Default";
// Scheduling
public DateTime ScheduledTime { get; set; } = DateTime.Now;
public bool Enabled { get; set; } = false; // Default disabled until added
public string Status { get; set; } = "Draft";
// Recurrence
public string Recurrence { get; set; } = "OneTime";
public int RepeatInterval { get; set; } = 1;
public List<string> WeeklyDays { get; set; } = new List<string>();
// Retry Logic
public int MaxRetries { get; set; } = 3;
public int CurrentRetries { get; set; } = 0;
public int RetryDelayInMinutes { get; set; } = 15; // <--- NEW PROPERTY
public string PostRunCommand { get; set; } = "";
}
}