Added scheduler
This commit is contained in:
440
Shackle/MainForm.Designer.cs
generated
440
Shackle/MainForm.Designer.cs
generated
@@ -21,245 +21,347 @@
|
||||
private System.Windows.Forms.TextBox txtLog;
|
||||
private System.Windows.Forms.PropertyGrid pgProfile;
|
||||
private System.Windows.Forms.Label lblPath;
|
||||
|
||||
// FIXED: Correct variable name (was bntnSaveProfile)
|
||||
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)
|
||||
{
|
||||
if (disposing && (components != null)) components.Dispose();
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm));
|
||||
this.txtRootPath = new System.Windows.Forms.TextBox();
|
||||
this.txtBinName = new System.Windows.Forms.TextBox();
|
||||
this.comboProfiles = new System.Windows.Forms.ComboBox();
|
||||
this.btnAddProfile = new System.Windows.Forms.Button();
|
||||
this.btnRemoveProfile = new System.Windows.Forms.Button();
|
||||
this.comboService = new System.Windows.Forms.ComboBox();
|
||||
this.txtURL = new System.Windows.Forms.TextBox();
|
||||
this.txtCommandPreview = new System.Windows.Forms.TextBox();
|
||||
this.btnRun = new System.Windows.Forms.Button();
|
||||
this.btnStop = new System.Windows.Forms.Button();
|
||||
this.btnOpenCookies = new System.Windows.Forms.Button();
|
||||
this.btnEditServiceConfig = new System.Windows.Forms.Button();
|
||||
this.btnEditYaml = new System.Windows.Forms.Button();
|
||||
this.btnBrowse = new System.Windows.Forms.Button();
|
||||
this.btnClearLog = new System.Windows.Forms.Button();
|
||||
this.txtLog = new System.Windows.Forms.TextBox();
|
||||
this.pgProfile = new System.Windows.Forms.PropertyGrid();
|
||||
this.lblPath = new System.Windows.Forms.Label();
|
||||
this.btnSaveProfile = new System.Windows.Forms.Button();
|
||||
this.SuspendLayout();
|
||||
txtRootPath = new TextBox();
|
||||
txtBinName = new TextBox();
|
||||
comboProfiles = new ComboBox();
|
||||
btnAddProfile = new Button();
|
||||
btnRemoveProfile = new Button();
|
||||
comboService = new ComboBox();
|
||||
txtURL = new TextBox();
|
||||
txtCommandPreview = new TextBox();
|
||||
btnRun = new Button();
|
||||
btnStop = new Button();
|
||||
btnOpenCookies = new Button();
|
||||
btnEditServiceConfig = new Button();
|
||||
btnEditYaml = new Button();
|
||||
btnBrowse = new Button();
|
||||
btnClearLog = new Button();
|
||||
txtLog = new TextBox();
|
||||
pgProfile = new PropertyGrid();
|
||||
lblPath = new Label();
|
||||
btnSaveProfile = new Button();
|
||||
btnScheduler = new Button();
|
||||
btnToggleScheduler = new Button();
|
||||
SuspendLayout();
|
||||
//
|
||||
// txtRootPath
|
||||
//
|
||||
this.txtRootPath.Location = new System.Drawing.Point(12, 35);
|
||||
this.txtRootPath.Name = "txtRootPath";
|
||||
this.txtRootPath.Size = new System.Drawing.Size(280, 27);
|
||||
this.txtRootPath.TabIndex = 0;
|
||||
txtRootPath.BackColor = Color.FromArgb(45, 45, 48);
|
||||
txtRootPath.BorderStyle = BorderStyle.FixedSingle;
|
||||
txtRootPath.ForeColor = Color.White;
|
||||
txtRootPath.Location = new Point(12, 35);
|
||||
txtRootPath.Name = "txtRootPath";
|
||||
txtRootPath.Size = new Size(280, 27);
|
||||
txtRootPath.TabIndex = 0;
|
||||
//
|
||||
// txtBinName
|
||||
//
|
||||
this.txtBinName.Location = new System.Drawing.Point(340, 35);
|
||||
this.txtBinName.Name = "txtBinName";
|
||||
this.txtBinName.Size = new System.Drawing.Size(60, 27);
|
||||
this.txtBinName.TabIndex = 2;
|
||||
txtBinName.BackColor = Color.FromArgb(45, 45, 48);
|
||||
txtBinName.BorderStyle = BorderStyle.FixedSingle;
|
||||
txtBinName.ForeColor = Color.White;
|
||||
txtBinName.Location = new Point(340, 35);
|
||||
txtBinName.Name = "txtBinName";
|
||||
txtBinName.Size = new Size(60, 27);
|
||||
txtBinName.TabIndex = 2;
|
||||
//
|
||||
// comboProfiles
|
||||
//
|
||||
this.comboProfiles.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.comboProfiles.Location = new System.Drawing.Point(1177, 35);
|
||||
this.comboProfiles.Name = "comboProfiles";
|
||||
this.comboProfiles.Size = new System.Drawing.Size(240, 28);
|
||||
this.comboProfiles.TabIndex = 4;
|
||||
comboProfiles.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
comboProfiles.BackColor = Color.FromArgb(45, 45, 48);
|
||||
comboProfiles.FlatStyle = FlatStyle.Flat;
|
||||
comboProfiles.ForeColor = Color.White;
|
||||
comboProfiles.Location = new Point(1177, 35);
|
||||
comboProfiles.Name = "comboProfiles";
|
||||
comboProfiles.Size = new Size(240, 28);
|
||||
comboProfiles.TabIndex = 4;
|
||||
//
|
||||
// btnAddProfile
|
||||
//
|
||||
this.btnAddProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnAddProfile.Location = new System.Drawing.Point(1422, 34);
|
||||
this.btnAddProfile.Name = "btnAddProfile";
|
||||
this.btnAddProfile.Size = new System.Drawing.Size(35, 29);
|
||||
this.btnAddProfile.TabIndex = 5;
|
||||
this.btnAddProfile.Text = "+";
|
||||
btnAddProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
btnAddProfile.BackColor = Color.FromArgb(45, 45, 48);
|
||||
btnAddProfile.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnAddProfile.FlatStyle = FlatStyle.Flat;
|
||||
btnAddProfile.ForeColor = Color.White;
|
||||
btnAddProfile.Location = new Point(1422, 34);
|
||||
btnAddProfile.Name = "btnAddProfile";
|
||||
btnAddProfile.Size = new Size(35, 29);
|
||||
btnAddProfile.TabIndex = 5;
|
||||
btnAddProfile.Text = "+";
|
||||
btnAddProfile.UseVisualStyleBackColor = false;
|
||||
//
|
||||
// btnRemoveProfile
|
||||
//
|
||||
this.btnRemoveProfile.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnRemoveProfile.Location = new System.Drawing.Point(1462, 34);
|
||||
this.btnRemoveProfile.Name = "btnRemoveProfile";
|
||||
this.btnRemoveProfile.Size = new System.Drawing.Size(35, 29);
|
||||
this.btnRemoveProfile.TabIndex = 6;
|
||||
this.btnRemoveProfile.Text = "-";
|
||||
btnRemoveProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
btnRemoveProfile.BackColor = Color.FromArgb(45, 45, 48);
|
||||
btnRemoveProfile.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnRemoveProfile.FlatStyle = FlatStyle.Flat;
|
||||
btnRemoveProfile.ForeColor = Color.White;
|
||||
btnRemoveProfile.Location = new Point(1462, 34);
|
||||
btnRemoveProfile.Name = "btnRemoveProfile";
|
||||
btnRemoveProfile.Size = new Size(35, 29);
|
||||
btnRemoveProfile.TabIndex = 6;
|
||||
btnRemoveProfile.Text = "-";
|
||||
btnRemoveProfile.UseVisualStyleBackColor = false;
|
||||
//
|
||||
// comboService
|
||||
//
|
||||
this.comboService.Location = new System.Drawing.Point(12, 80);
|
||||
this.comboService.Name = "comboService";
|
||||
this.comboService.Size = new System.Drawing.Size(100, 28);
|
||||
this.comboService.TabIndex = 7;
|
||||
comboService.BackColor = Color.FromArgb(45, 45, 48);
|
||||
comboService.FlatStyle = FlatStyle.Flat;
|
||||
comboService.ForeColor = Color.White;
|
||||
comboService.Location = new Point(12, 80);
|
||||
comboService.Name = "comboService";
|
||||
comboService.Size = new Size(100, 28);
|
||||
comboService.TabIndex = 7;
|
||||
//
|
||||
// txtURL
|
||||
//
|
||||
this.txtURL.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.txtURL.Location = new System.Drawing.Point(115, 80);
|
||||
this.txtURL.Name = "txtURL";
|
||||
this.txtURL.PlaceholderText = "Paste ID / URL here...";
|
||||
this.txtURL.Size = new System.Drawing.Size(801, 27);
|
||||
this.txtURL.TabIndex = 8;
|
||||
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(115, 80);
|
||||
txtURL.Name = "txtURL";
|
||||
txtURL.PlaceholderText = "Paste ID / URL here...";
|
||||
txtURL.Size = new Size(801, 27);
|
||||
txtURL.TabIndex = 8;
|
||||
//
|
||||
// txtCommandPreview
|
||||
//
|
||||
this.txtCommandPreview.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.txtCommandPreview.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(20)))), ((int)(((byte)(20)))), ((int)(((byte)(20)))));
|
||||
this.txtCommandPreview.Font = new System.Drawing.Font("Consolas", 10F, System.Drawing.FontStyle.Bold);
|
||||
this.txtCommandPreview.ForeColor = System.Drawing.Color.Cyan;
|
||||
this.txtCommandPreview.Location = new System.Drawing.Point(12, 115);
|
||||
this.txtCommandPreview.Name = "txtCommandPreview";
|
||||
this.txtCommandPreview.Size = new System.Drawing.Size(1130, 27);
|
||||
this.txtCommandPreview.TabIndex = 9;
|
||||
txtCommandPreview.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
|
||||
txtCommandPreview.BackColor = Color.FromArgb(20, 20, 20);
|
||||
txtCommandPreview.BorderStyle = BorderStyle.FixedSingle;
|
||||
txtCommandPreview.Font = new Font("Consolas", 10F, FontStyle.Bold);
|
||||
txtCommandPreview.ForeColor = Color.Cyan;
|
||||
txtCommandPreview.Location = new Point(12, 115);
|
||||
txtCommandPreview.Name = "txtCommandPreview";
|
||||
txtCommandPreview.Size = new Size(1130, 27);
|
||||
txtCommandPreview.TabIndex = 9;
|
||||
//
|
||||
// btnRun
|
||||
//
|
||||
this.btnRun.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnRun.Location = new System.Drawing.Point(12, 150);
|
||||
this.btnRun.Name = "btnRun";
|
||||
this.btnRun.Size = new System.Drawing.Size(1130, 45);
|
||||
this.btnRun.TabIndex = 10;
|
||||
this.btnRun.Text = "RUN";
|
||||
btnRun.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
|
||||
btnRun.BackColor = Color.SteelBlue;
|
||||
btnRun.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnRun.FlatStyle = FlatStyle.Flat;
|
||||
btnRun.ForeColor = Color.White;
|
||||
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
|
||||
//
|
||||
this.btnStop.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnStop.BackColor = System.Drawing.Color.Maroon;
|
||||
this.btnStop.Location = new System.Drawing.Point(12, 150);
|
||||
this.btnStop.Name = "btnStop";
|
||||
this.btnStop.Size = new System.Drawing.Size(1130, 45);
|
||||
this.btnStop.TabIndex = 11;
|
||||
this.btnStop.Text = "STOP";
|
||||
this.btnStop.UseVisualStyleBackColor = false;
|
||||
this.btnStop.Visible = false;
|
||||
btnStop.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
|
||||
btnStop.BackColor = Color.Maroon;
|
||||
btnStop.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnStop.FlatStyle = FlatStyle.Flat;
|
||||
btnStop.ForeColor = Color.White;
|
||||
btnStop.Location = new Point(12, 150);
|
||||
btnStop.Name = "btnStop";
|
||||
btnStop.Size = new Size(1130, 45);
|
||||
btnStop.TabIndex = 11;
|
||||
btnStop.Text = "STOP";
|
||||
btnStop.UseVisualStyleBackColor = false;
|
||||
btnStop.Visible = false;
|
||||
//
|
||||
// btnOpenCookies
|
||||
//
|
||||
this.btnOpenCookies.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnOpenCookies.Location = new System.Drawing.Point(922, 79);
|
||||
this.btnOpenCookies.Name = "btnOpenCookies";
|
||||
this.btnOpenCookies.Size = new System.Drawing.Size(115, 30);
|
||||
this.btnOpenCookies.TabIndex = 12;
|
||||
this.btnOpenCookies.Text = "🍪 Cookies";
|
||||
btnOpenCookies.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
btnOpenCookies.BackColor = Color.OliveDrab;
|
||||
btnOpenCookies.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnOpenCookies.FlatStyle = FlatStyle.Flat;
|
||||
btnOpenCookies.ForeColor = Color.White;
|
||||
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
|
||||
//
|
||||
this.btnEditServiceConfig.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnEditServiceConfig.Location = new System.Drawing.Point(1043, 79);
|
||||
this.btnEditServiceConfig.Name = "btnEditServiceConfig";
|
||||
this.btnEditServiceConfig.Size = new System.Drawing.Size(99, 30);
|
||||
this.btnEditServiceConfig.TabIndex = 13;
|
||||
this.btnEditServiceConfig.Text = "⚙️ Service";
|
||||
btnEditServiceConfig.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
btnEditServiceConfig.BackColor = Color.BlueViolet;
|
||||
btnEditServiceConfig.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnEditServiceConfig.FlatStyle = FlatStyle.Flat;
|
||||
btnEditServiceConfig.ForeColor = Color.White;
|
||||
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
|
||||
//
|
||||
this.btnEditYaml.Location = new System.Drawing.Point(410, 34);
|
||||
this.btnEditYaml.Name = "btnEditYaml";
|
||||
this.btnEditYaml.Size = new System.Drawing.Size(144, 29);
|
||||
this.btnEditYaml.TabIndex = 3;
|
||||
this.btnEditYaml.Text = "📝 Main Config";
|
||||
btnEditYaml.BackColor = Color.FromArgb(255, 128, 0);
|
||||
btnEditYaml.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnEditYaml.FlatStyle = FlatStyle.Flat;
|
||||
btnEditYaml.ForeColor = Color.White;
|
||||
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
|
||||
//
|
||||
this.btnBrowse.Location = new System.Drawing.Point(295, 34);
|
||||
this.btnBrowse.Name = "btnBrowse";
|
||||
this.btnBrowse.Size = new System.Drawing.Size(35, 29);
|
||||
this.btnBrowse.TabIndex = 1;
|
||||
this.btnBrowse.Text = "📂";
|
||||
btnBrowse.BackColor = Color.FromArgb(45, 45, 48);
|
||||
btnBrowse.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnBrowse.FlatStyle = FlatStyle.Flat;
|
||||
btnBrowse.ForeColor = Color.White;
|
||||
btnBrowse.Location = new Point(295, 34);
|
||||
btnBrowse.Name = "btnBrowse";
|
||||
btnBrowse.Size = new Size(35, 29);
|
||||
btnBrowse.TabIndex = 1;
|
||||
btnBrowse.Text = "📂";
|
||||
btnBrowse.UseVisualStyleBackColor = false;
|
||||
//
|
||||
// btnClearLog
|
||||
//
|
||||
this.btnClearLog.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.btnClearLog.Location = new System.Drawing.Point(1022, 201);
|
||||
this.btnClearLog.Name = "btnClearLog";
|
||||
this.btnClearLog.Size = new System.Drawing.Size(120, 36);
|
||||
this.btnClearLog.TabIndex = 14;
|
||||
this.btnClearLog.Text = "Clear Log";
|
||||
btnClearLog.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
btnClearLog.BackColor = Color.FromArgb(45, 45, 48);
|
||||
btnClearLog.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnClearLog.FlatStyle = FlatStyle.Flat;
|
||||
btnClearLog.ForeColor = Color.White;
|
||||
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
|
||||
//
|
||||
this.txtLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.txtLog.BackColor = System.Drawing.Color.Black;
|
||||
this.txtLog.Font = new System.Drawing.Font("Consolas", 9F);
|
||||
this.txtLog.ForeColor = System.Drawing.Color.Lime;
|
||||
this.txtLog.Location = new System.Drawing.Point(12, 243);
|
||||
this.txtLog.Multiline = true;
|
||||
this.txtLog.Name = "txtLog";
|
||||
this.txtLog.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
|
||||
this.txtLog.Size = new System.Drawing.Size(1130, 550);
|
||||
this.txtLog.TabIndex = 15;
|
||||
txtLog.Anchor = AnchorStyles.Top | 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, 243);
|
||||
txtLog.Multiline = true;
|
||||
txtLog.Name = "txtLog";
|
||||
txtLog.ReadOnly = true;
|
||||
txtLog.ScrollBars = ScrollBars.Vertical;
|
||||
txtLog.Size = new Size(1130, 550);
|
||||
txtLog.TabIndex = 15;
|
||||
//
|
||||
// pgProfile
|
||||
//
|
||||
this.pgProfile.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Right)));
|
||||
this.pgProfile.CategoryForeColor = System.Drawing.SystemColors.ActiveCaption;
|
||||
this.pgProfile.Location = new System.Drawing.Point(1177, 80);
|
||||
this.pgProfile.Name = "pgProfile";
|
||||
this.pgProfile.Size = new System.Drawing.Size(320, 713);
|
||||
this.pgProfile.TabIndex = 16;
|
||||
pgProfile.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
pgProfile.BackColor = Color.FromArgb(37, 37, 38);
|
||||
pgProfile.CategoryForeColor = SystemColors.ActiveCaption;
|
||||
pgProfile.DisabledItemForeColor = Color.FromArgb(127, 255, 255, 255);
|
||||
pgProfile.LineColor = Color.FromArgb(45, 45, 48);
|
||||
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
|
||||
//
|
||||
this.lblPath.Location = new System.Drawing.Point(12, 12);
|
||||
this.lblPath.Name = "lblPath";
|
||||
this.lblPath.Size = new System.Drawing.Size(150, 23);
|
||||
this.lblPath.TabIndex = 17;
|
||||
this.lblPath.Text = "Root Path & Binary:";
|
||||
lblPath.ForeColor = Color.White;
|
||||
lblPath.Location = new Point(12, 12);
|
||||
lblPath.Name = "lblPath";
|
||||
lblPath.Size = new Size(150, 23);
|
||||
lblPath.TabIndex = 17;
|
||||
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);
|
||||
btnSaveProfile.Anchor = AnchorStyles.Top | AnchorStyles.Right;
|
||||
btnSaveProfile.BackColor = Color.FromArgb(45, 45, 48);
|
||||
btnSaveProfile.FlatAppearance.BorderColor = Color.Gray;
|
||||
btnSaveProfile.FlatStyle = FlatStyle.Flat;
|
||||
btnSaveProfile.ForeColor = Color.White;
|
||||
btnSaveProfile.Location = new Point(1502, 34);
|
||||
btnSaveProfile.Name = "btnSaveProfile";
|
||||
btnSaveProfile.Size = new Size(35, 29);
|
||||
btnSaveProfile.TabIndex = 18;
|
||||
btnSaveProfile.Text = "💾";
|
||||
btnSaveProfile.UseVisualStyleBackColor = false;
|
||||
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
|
||||
//
|
||||
this.ClientSize = new System.Drawing.Size(1550, 808); // Slightly widened to fit new button
|
||||
this.Controls.Add(this.txtRootPath);
|
||||
this.Controls.Add(this.btnBrowse);
|
||||
this.Controls.Add(this.txtBinName);
|
||||
this.Controls.Add(this.btnEditYaml);
|
||||
this.Controls.Add(this.comboProfiles);
|
||||
this.Controls.Add(this.btnAddProfile);
|
||||
this.Controls.Add(this.btnRemoveProfile);
|
||||
// FIXED: Added btnSaveProfile to controls list
|
||||
this.Controls.Add(this.btnSaveProfile);
|
||||
this.Controls.Add(this.comboService);
|
||||
this.Controls.Add(this.txtURL);
|
||||
this.Controls.Add(this.txtCommandPreview);
|
||||
this.Controls.Add(this.btnRun);
|
||||
this.Controls.Add(this.btnStop);
|
||||
this.Controls.Add(this.btnOpenCookies);
|
||||
this.Controls.Add(this.btnEditServiceConfig);
|
||||
this.Controls.Add(this.btnClearLog);
|
||||
this.Controls.Add(this.txtLog);
|
||||
this.Controls.Add(this.pgProfile);
|
||||
this.Controls.Add(this.lblPath);
|
||||
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
|
||||
this.MinimumSize = new System.Drawing.Size(960, 680);
|
||||
this.Name = "MainForm";
|
||||
this.Text = "Unshackle Master GUI";
|
||||
this.ResumeLayout(false);
|
||||
this.PerformLayout();
|
||||
BackColor = Color.FromArgb(30, 30, 30);
|
||||
ClientSize = new Size(1550, 808);
|
||||
Controls.Add(btnToggleScheduler);
|
||||
Controls.Add(btnScheduler);
|
||||
Controls.Add(txtRootPath);
|
||||
Controls.Add(btnBrowse);
|
||||
Controls.Add(txtBinName);
|
||||
Controls.Add(btnEditYaml);
|
||||
Controls.Add(comboProfiles);
|
||||
Controls.Add(btnAddProfile);
|
||||
Controls.Add(btnRemoveProfile);
|
||||
Controls.Add(btnSaveProfile);
|
||||
Controls.Add(comboService);
|
||||
Controls.Add(txtURL);
|
||||
Controls.Add(txtCommandPreview);
|
||||
Controls.Add(btnRun);
|
||||
Controls.Add(btnStop);
|
||||
Controls.Add(btnOpenCookies);
|
||||
Controls.Add(btnEditServiceConfig);
|
||||
Controls.Add(btnClearLog);
|
||||
Controls.Add(txtLog);
|
||||
Controls.Add(pgProfile);
|
||||
Controls.Add(lblPath);
|
||||
ForeColor = Color.White;
|
||||
Icon = (Icon)resources.GetObject("$this.Icon");
|
||||
MinimumSize = new Size(960, 680);
|
||||
Name = "MainForm";
|
||||
Text = "Shackle v1.2";
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
@@ -14,72 +14,63 @@ namespace UnshackleGUI
|
||||
public partial class MainForm : Form
|
||||
{
|
||||
// =========================================================
|
||||
// 1. FILE PATHS & SETTINGS
|
||||
// 1. VARIABLES
|
||||
// =========================================================
|
||||
private string _configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
|
||||
private string _paramsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "params.json");
|
||||
private string _profilesDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Profiles");
|
||||
|
||||
// Data Storage
|
||||
private AppSettings _appSettings = new AppSettings();
|
||||
private List<UnshackleParameter> _parameterDefinitions = new List<UnshackleParameter>();
|
||||
private Dictionary<string, object> _propertyValues = new Dictionary<string, object>();
|
||||
|
||||
// State Variables
|
||||
private Process? _currentProcess;
|
||||
private bool _isLoaded = false; // Flag to prevent saving while the app is starting up
|
||||
private string _lastLoadedProfile = ""; // Tracks the original name
|
||||
private bool _isLoaded = false;
|
||||
private string _lastLoadedProfile = "";
|
||||
|
||||
// Scheduler Reference
|
||||
private SchedulerForm? _scheduler = null;
|
||||
|
||||
// =========================================================
|
||||
// 2. CONSTRUCTOR & INITIALIZATION
|
||||
// 2. CONSTRUCTOR
|
||||
// =========================================================
|
||||
public MainForm()
|
||||
{
|
||||
InitializeComponent();
|
||||
ApplyDarkTheme();
|
||||
// ApplyDarkTheme();
|
||||
|
||||
// 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.
|
||||
// Property Grid
|
||||
pgProfile.PropertyValueChanged += (s, e) =>
|
||||
{
|
||||
UpdateCommandPreview();
|
||||
SaveCurrentProfile();
|
||||
};
|
||||
|
||||
// --- Service Dropdown (Netflix, Amazon, etc.) ---
|
||||
// Service Dropdown
|
||||
comboService.SelectedIndexChanged += (s, e) =>
|
||||
{
|
||||
// Don't run this logic if the app is still starting up
|
||||
if (!_isLoaded) return;
|
||||
|
||||
if (comboService.SelectedItem != null)
|
||||
{
|
||||
// Update the "Service" value in our data dictionary
|
||||
_propertyValues["Service"] = comboService.SelectedItem.ToString();
|
||||
|
||||
// Update the command preview
|
||||
UpdateCommandPreview();
|
||||
|
||||
// Save this change to the JSON file immediately
|
||||
SaveCurrentProfile();
|
||||
}
|
||||
};
|
||||
|
||||
// --- Text Box Changes ---
|
||||
// Text Boxes
|
||||
txtURL.TextChanged += (s, e) => UpdateCommandPreview();
|
||||
|
||||
txtBinName.TextChanged += (s, e) =>
|
||||
@@ -90,7 +81,7 @@ namespace UnshackleGUI
|
||||
|
||||
txtRootPath.TextChanged += (s, e) => SaveGlobalConfig();
|
||||
|
||||
// --- Button Clicks ---
|
||||
// Main Buttons
|
||||
btnRun.Click += btnRun_Click;
|
||||
btnStop.Click += btnStop_Click;
|
||||
btnBrowse.Click += btnBrowse_Click;
|
||||
@@ -100,19 +91,86 @@ namespace UnshackleGUI
|
||||
btnEditYaml.Click += btnEditYaml_Click;
|
||||
btnAddProfile.Click += btnAddProfile_Click;
|
||||
btnRemoveProfile.Click += btnRemoveProfile_Click;
|
||||
|
||||
// This line connects the click to the function below
|
||||
btnSaveProfile.Click += btnSaveProfile_Click;
|
||||
|
||||
// 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()
|
||||
{
|
||||
_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))
|
||||
{
|
||||
var json = File.ReadAllText(_paramsPath);
|
||||
@@ -120,10 +178,9 @@ namespace UnshackleGUI
|
||||
}
|
||||
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))
|
||||
{
|
||||
var json = File.ReadAllText(_configPath);
|
||||
@@ -133,46 +190,34 @@ namespace UnshackleGUI
|
||||
txtBinName.Text = _appSettings.BinaryName;
|
||||
}
|
||||
|
||||
// C. Populate the Services Dropdown (Read folders from disk)
|
||||
RefreshFolders();
|
||||
|
||||
// D. Load Profiles and select the default one
|
||||
RefreshProfileList();
|
||||
|
||||
_isLoaded = true; // Loading done, enable events
|
||||
|
||||
// Force an initial update of the command text
|
||||
_isLoaded = true;
|
||||
UpdateCommandPreview();
|
||||
}
|
||||
|
||||
private void RefreshProfileList()
|
||||
{
|
||||
// 1. Temporarily stop listening to the selection event to prevent glitches
|
||||
comboProfiles.SelectedIndexChanged -= ComboProfiles_SelectedIndexChanged;
|
||||
|
||||
// 2. Find all .json files in the Profiles folder
|
||||
var files = Directory.GetFiles(_profilesDir, "*.json");
|
||||
var profileNames = files.Select(Path.GetFileNameWithoutExtension).ToList();
|
||||
|
||||
// 3. If no profiles exist, create a "Default" one
|
||||
if (profileNames.Count == 0)
|
||||
{
|
||||
CreateDefaultProfile();
|
||||
profileNames.Add("Default");
|
||||
}
|
||||
|
||||
// 4. Update the ComboBox
|
||||
comboProfiles.DataSource = null;
|
||||
comboProfiles.DataSource = profileNames;
|
||||
|
||||
// 5. Select "Default" or the first available profile
|
||||
string targetProfile = profileNames.Contains("Default") ? "Default" : profileNames[0];
|
||||
comboProfiles.SelectedItem = targetProfile;
|
||||
|
||||
// 6. Manually trigger the load for this profile
|
||||
LoadProfile(targetProfile);
|
||||
|
||||
// 7. Start listening to events again
|
||||
comboProfiles.SelectedIndexChanged += ComboProfiles_SelectedIndexChanged;
|
||||
}
|
||||
|
||||
@@ -180,13 +225,11 @@ namespace UnshackleGUI
|
||||
{
|
||||
_propertyValues = new Dictionary<string, object>();
|
||||
|
||||
// Fill with default values from params.json
|
||||
foreach (var p in _parameterDefinitions)
|
||||
{
|
||||
_propertyValues[p.Name] = p.Default ?? "";
|
||||
}
|
||||
|
||||
// Set default service
|
||||
if (comboService.Items.Count > 0)
|
||||
{
|
||||
_propertyValues["Service"] = comboService.Items[0].ToString();
|
||||
@@ -195,22 +238,15 @@ namespace UnshackleGUI
|
||||
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);
|
||||
LoadProfile(comboProfiles.SelectedItem.ToString());
|
||||
}
|
||||
|
||||
private void LoadProfile(string profileName)
|
||||
{
|
||||
|
||||
_lastLoadedProfile = profileName;
|
||||
|
||||
string path = Path.Combine(_profilesDir, $"{profileName}.json");
|
||||
|
||||
if (!File.Exists(path)) return;
|
||||
@@ -224,9 +260,6 @@ namespace UnshackleGUI
|
||||
{
|
||||
_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))
|
||||
@@ -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))
|
||||
{
|
||||
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
|
||||
pgProfile.Refresh();
|
||||
|
||||
UpdateCommandPreview();
|
||||
}
|
||||
@@ -272,14 +296,9 @@ namespace UnshackleGUI
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// 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);
|
||||
}
|
||||
@@ -288,7 +307,6 @@ namespace UnshackleGUI
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ensure the currently selected service is saved into the dictionary
|
||||
if (comboService.SelectedItem != null)
|
||||
{
|
||||
_propertyValues["Service"] = comboService.SelectedItem.ToString();
|
||||
@@ -316,66 +334,46 @@ namespace UnshackleGUI
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors while typing (file might be busy)
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// 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;
|
||||
|
||||
// Case A: Boolean Flags (e.g., --no-mux)
|
||||
if (p.Type == "Bool" && val is bool isTrue && isTrue)
|
||||
{
|
||||
sb.Append($"{p.Flag} ");
|
||||
}
|
||||
// Case B: Text / Selection / Number
|
||||
else if (p.Type != "Bool" && val != null)
|
||||
{
|
||||
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")
|
||||
{
|
||||
// Quote the value if it has spaces
|
||||
if (sVal.Contains(" "))
|
||||
{
|
||||
sVal = $"\"{sVal}\"";
|
||||
}
|
||||
|
||||
sb.Append($"{p.Flag} {sVal} ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Append Service and URL
|
||||
// e.g., "AMZN https://..."
|
||||
sb.Append($"{comboService.Text} {txtURL.Text}");
|
||||
|
||||
// 4. Update UI
|
||||
txtCommandPreview.Text = sb.ToString();
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// 7. CLI EXECUTION (The Run Button)
|
||||
// 8. CLI EXECUTION
|
||||
// =========================================================
|
||||
private async void btnRun_Click(object? sender, EventArgs e)
|
||||
{
|
||||
@@ -383,14 +381,11 @@ namespace UnshackleGUI
|
||||
|
||||
if (string.IsNullOrWhiteSpace(finalCmd)) return;
|
||||
|
||||
// Disable buttons while running
|
||||
ToggleUI(true);
|
||||
|
||||
txtLog.AppendText($"> Executing: {finalCmd}{Environment.NewLine}");
|
||||
|
||||
try
|
||||
{
|
||||
// Run in background thread to keep UI responsive
|
||||
await Task.Run(() => RunCli(finalCmd));
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -399,44 +394,36 @@ namespace UnshackleGUI
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Re-enable buttons
|
||||
ToggleUI(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void RunCli(string cmd)
|
||||
{
|
||||
// We use CMD.EXE instead of PowerShell to avoid Antivirus false positives
|
||||
ProcessStartInfo psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/c chcp 65001 && {cmd}", // Force UTF-8
|
||||
WorkingDirectory = txtRootPath.Text,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8
|
||||
};
|
||||
ProcessStartInfo psi = new ProcessStartInfo();
|
||||
psi.FileName = "cmd.exe";
|
||||
psi.Arguments = $"/c chcp 65001 && {cmd}";
|
||||
psi.WorkingDirectory = txtRootPath.Text;
|
||||
psi.RedirectStandardOutput = true;
|
||||
psi.RedirectStandardError = true;
|
||||
psi.UseShellExecute = false;
|
||||
psi.CreateNoWindow = true;
|
||||
psi.StandardOutputEncoding = Encoding.UTF8;
|
||||
|
||||
// Set Environment variable for Python
|
||||
psi.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8";
|
||||
|
||||
using (_currentProcess = Process.Start(psi))
|
||||
{
|
||||
if (_currentProcess == null) return;
|
||||
|
||||
// Handle Output
|
||||
_currentProcess.OutputDataReceived += (s, e) =>
|
||||
{
|
||||
// Filter out the "Active code page: 65001" noise
|
||||
if (!string.IsNullOrEmpty(e.Data) && !e.Data.Contains("Active code page"))
|
||||
{
|
||||
AppendLog(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle Errors
|
||||
_currentProcess.ErrorDataReceived += (s, e) => AppendLog(e.Data);
|
||||
|
||||
_currentProcess.BeginOutputReadLine();
|
||||
@@ -446,7 +433,7 @@ namespace UnshackleGUI
|
||||
}
|
||||
|
||||
// =========================================================
|
||||
// 8. PROFILE MANAGEMENT (Add/Remove/Save Buttons)
|
||||
// 9. HELPER METHODS
|
||||
// =========================================================
|
||||
private void btnAddProfile_Click(object? sender, EventArgs e)
|
||||
{
|
||||
@@ -454,23 +441,18 @@ namespace UnshackleGUI
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name)) return;
|
||||
|
||||
// Sanitize filename (remove illegal characters like / \ : *)
|
||||
foreach (char c in Path.GetInvalidFileNameChars())
|
||||
{
|
||||
name = name.Replace(c, '_');
|
||||
}
|
||||
|
||||
// Check if exists
|
||||
if (File.Exists(Path.Combine(_profilesDir, $"{name}.json")))
|
||||
{
|
||||
MessageBox.Show("Profile already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save current settings as the new profile
|
||||
SaveProfileFile(name);
|
||||
|
||||
// Reload list and select new profile
|
||||
RefreshProfileList();
|
||||
comboProfiles.SelectedItem = name;
|
||||
}
|
||||
@@ -501,22 +483,21 @@ namespace UnshackleGUI
|
||||
{
|
||||
string newName = comboProfiles.Text.Trim();
|
||||
|
||||
// 1. Validation
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
{
|
||||
MessageBox.Show("Please enter a profile name.");
|
||||
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))
|
||||
{
|
||||
// Special Case: Cannot rename Default
|
||||
if (_lastLoadedProfile == "Default")
|
||||
{
|
||||
// Just create new, don't ask to rename Default
|
||||
SaveProfileFile(newName);
|
||||
RefreshProfileList();
|
||||
comboProfiles.SelectedItem = newName;
|
||||
@@ -524,7 +505,6 @@ namespace UnshackleGUI
|
||||
return;
|
||||
}
|
||||
|
||||
// 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" +
|
||||
@@ -535,20 +515,17 @@ namespace UnshackleGUI
|
||||
|
||||
if (choice == DialogResult.Cancel) return;
|
||||
|
||||
if (choice == DialogResult.Yes) // RENAME
|
||||
if (choice == DialogResult.Yes)
|
||||
{
|
||||
// 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
|
||||
comboProfiles.SelectedItem = newName;
|
||||
MessageBox.Show($"Renamed to '{newName}'.");
|
||||
}
|
||||
else // CREATE COPY
|
||||
else
|
||||
{
|
||||
SaveProfileFile(newName);
|
||||
RefreshProfileList();
|
||||
@@ -558,23 +535,15 @@ namespace UnshackleGUI
|
||||
}
|
||||
else
|
||||
{
|
||||
// 3. Name didn't change (Normal Save)
|
||||
SaveProfileFile(newName);
|
||||
MessageBox.Show($"Profile '{newName}' saved!", "Saved", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =========================================================
|
||||
// 9. HELPER METHODS & THEME
|
||||
// =========================================================
|
||||
|
||||
private void AppendLog(string? text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
// Ensure we update the UI on the main thread
|
||||
this.BeginInvoke(new Action(() =>
|
||||
{
|
||||
txtLog.AppendText(text + Environment.NewLine);
|
||||
@@ -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)
|
||||
{
|
||||
// --- 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.ForeColor = Color.White;
|
||||
|
||||
@@ -697,7 +664,6 @@ namespace UnshackleGUI
|
||||
pg.LineColor = Color.FromArgb(45, 45, 48);
|
||||
}
|
||||
|
||||
// Recursive call for panels and group boxes
|
||||
foreach (Control child in c.Controls)
|
||||
{
|
||||
UpdateControlTheme(child);
|
||||
@@ -705,7 +671,6 @@ namespace UnshackleGUI
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================================================
|
||||
// 10. DATA MODELS
|
||||
// =========================================================
|
||||
|
||||
592
Shackle/SchedulerForm.Designer.cs
generated
Normal file
592
Shackle/SchedulerForm.Designer.cs
generated
Normal 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
704
Shackle/SchedulerForm.cs
Normal 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
16510
Shackle/SchedulerForm.resx
Normal file
File diff suppressed because it is too large
Load Diff
30
Shackle/SchedulerJob.cs
Normal file
30
Shackle/SchedulerJob.cs
Normal 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; } = "";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user