From 6f161d260f02b0a630a63015918747c833b4d66a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 04:35:40 +0000 Subject: [PATCH 1/9] Modernize GUI design for both Python and C# versions Python improvements: - Switch to ttkbootstrap for modern dark theme (cyborg) - Add grouped sections with LabelFrames (Configuration, Log, Results) - Add progress bar during calculation - Color-coded log output (cyan headers, gold session times, green totals) - Threaded calculation to keep UI responsive - Better typography (Segoe UI, Cascadia Code) - Status bar with colored feedback C# improvements: - Dark theme with cyan/green accents matching Star Citizen aesthetic - Add environment auto-detection (LIVE, PTU, EPTU, TECH-PREVIEW) - Add status bar with colored messages - Add more format options (Minutes, Seconds, Days) - Add progress bar during calculation - Async calculation for responsive UI - Grouped panels for better visual hierarchy - Larger window (700x530) with better spacing - Color-coded log output --- csharp/Form1.Designer.cs | 452 +++++++++++++++++++++++++++++++-------- csharp/Form1.cs | 296 +++++++++++++++++++------ python/requirements.txt | 1 + python/sc_main.py | 438 +++++++++++++++++++++++++++---------- 4 files changed, 920 insertions(+), 267 deletions(-) diff --git a/csharp/Form1.Designer.cs b/csharp/Form1.Designer.cs index 038d37c..9823106 100644 --- a/csharp/Form1.Designer.cs +++ b/csharp/Form1.Designer.cs @@ -1,4 +1,4 @@ -using System.Drawing; +using System.Drawing; namespace StarCitizenPlaytimeCalculator { @@ -17,121 +17,397 @@ protected override void Dispose(bool disposing) private void InitializeComponent() { - this.btnBrowse = new System.Windows.Forms.Button(); + this.components = new System.ComponentModel.Container(); + + // Initialize all controls + this.panelHeader = new System.Windows.Forms.Panel(); + this.lblTitle = new System.Windows.Forms.Label(); + this.lblVersion = new System.Windows.Forms.Label(); + + this.panelConfig = new System.Windows.Forms.Panel(); + this.lblConfigTitle = new System.Windows.Forms.Label(); + this.lblEnvironment = new System.Windows.Forms.Label(); + this.comboEnvironment = new System.Windows.Forms.ComboBox(); + this.btnRefresh = new System.Windows.Forms.Button(); + this.lblLogFolder = new System.Windows.Forms.Label(); this.txtFolderPath = new System.Windows.Forms.TextBox(); + this.btnBrowse = new System.Windows.Forms.Button(); this.btnProcessLogs = new System.Windows.Forms.Button(); - this.txtTotalPlayTime = new System.Windows.Forms.TextBox(); - this.btnCopyToClipboard = new System.Windows.Forms.Button(); + + this.panelLog = new System.Windows.Forms.Panel(); + this.lblLogTitle = new System.Windows.Forms.Label(); + this.progressBar = new System.Windows.Forms.ProgressBar(); this.txtOutput = new System.Windows.Forms.RichTextBox(); - this.toolTip = new System.Windows.Forms.ToolTip(); + + this.panelResults = new System.Windows.Forms.Panel(); + this.lblResultsTitle = new System.Windows.Forms.Label(); + this.lblDisplayAs = new System.Windows.Forms.Label(); this.comboBoxFormat = new System.Windows.Forms.ComboBox(); + this.lblTotalPlaytime = new System.Windows.Forms.Label(); + this.txtTotalPlayTime = new System.Windows.Forms.TextBox(); + this.btnCopyToClipboard = new System.Windows.Forms.Button(); + + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.statusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.toolTip = new System.Windows.Forms.ToolTip(this.components); + this.SuspendLayout(); - // + + // + // Form colors + // + Color bgDark = Color.FromArgb(26, 26, 46); + Color bgPanel = Color.FromArgb(22, 33, 62); + Color accentCyan = Color.FromArgb(0, 212, 255); + Color accentGreen = Color.FromArgb(0, 255, 136); + Color textLight = Color.FromArgb(224, 224, 224); + Color textSecondary = Color.FromArgb(160, 160, 160); + + // + // panelHeader + // + this.panelHeader.BackColor = bgPanel; + this.panelHeader.Dock = System.Windows.Forms.DockStyle.Top; + this.panelHeader.Height = 60; + this.panelHeader.Padding = new System.Windows.Forms.Padding(20, 15, 20, 15); + this.panelHeader.Controls.Add(this.lblVersion); + this.panelHeader.Controls.Add(this.lblTitle); + + // + // lblTitle + // + this.lblTitle.AutoSize = true; + this.lblTitle.Font = new System.Drawing.Font("Segoe UI", 16F, System.Drawing.FontStyle.Bold); + this.lblTitle.ForeColor = accentCyan; + this.lblTitle.Location = new System.Drawing.Point(20, 15); + this.lblTitle.Text = "Star Citizen Playtime Calculator"; + + // + // lblVersion + // + this.lblVersion.Anchor = System.Windows.Forms.AnchorStyles.Right; + this.lblVersion.AutoSize = true; + this.lblVersion.Font = new System.Drawing.Font("Segoe UI", 9F); + this.lblVersion.ForeColor = textSecondary; + this.lblVersion.Location = new System.Drawing.Point(650, 22); + this.lblVersion.Text = "v2.0"; + + // + // panelConfig + // + this.panelConfig.BackColor = bgPanel; + this.panelConfig.Location = new System.Drawing.Point(12, 72); + this.panelConfig.Size = new System.Drawing.Size(676, 110); + this.panelConfig.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + this.panelConfig.Padding = new System.Windows.Forms.Padding(15); + this.panelConfig.Controls.Add(this.lblConfigTitle); + this.panelConfig.Controls.Add(this.lblEnvironment); + this.panelConfig.Controls.Add(this.comboEnvironment); + this.panelConfig.Controls.Add(this.btnRefresh); + this.panelConfig.Controls.Add(this.lblLogFolder); + this.panelConfig.Controls.Add(this.txtFolderPath); + this.panelConfig.Controls.Add(this.btnBrowse); + this.panelConfig.Controls.Add(this.btnProcessLogs); + + // + // lblConfigTitle + // + this.lblConfigTitle.AutoSize = true; + this.lblConfigTitle.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold); + this.lblConfigTitle.ForeColor = accentCyan; + this.lblConfigTitle.Location = new System.Drawing.Point(15, 8); + this.lblConfigTitle.Text = "Configuration"; + + // + // lblEnvironment + // + this.lblEnvironment.AutoSize = true; + this.lblEnvironment.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + this.lblEnvironment.ForeColor = textLight; + this.lblEnvironment.Location = new System.Drawing.Point(15, 35); + this.lblEnvironment.Text = "Environment:"; + + // + // comboEnvironment + // + this.comboEnvironment.BackColor = bgDark; + this.comboEnvironment.ForeColor = textLight; + this.comboEnvironment.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.comboEnvironment.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.comboEnvironment.Font = new System.Drawing.Font("Segoe UI", 9F); + this.comboEnvironment.Location = new System.Drawing.Point(110, 32); + this.comboEnvironment.Size = new System.Drawing.Size(200, 23); + this.comboEnvironment.SelectedIndexChanged += new System.EventHandler(this.comboEnvironment_SelectedIndexChanged); + + // + // btnRefresh + // + this.btnRefresh.BackColor = Color.FromArgb(15, 52, 96); + this.btnRefresh.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnRefresh.FlatAppearance.BorderColor = accentCyan; + this.btnRefresh.ForeColor = accentCyan; + this.btnRefresh.Font = new System.Drawing.Font("Segoe UI", 9F); + this.btnRefresh.Location = new System.Drawing.Point(320, 30); + this.btnRefresh.Size = new System.Drawing.Size(80, 27); + this.btnRefresh.Text = "Refresh"; + this.btnRefresh.UseVisualStyleBackColor = false; + this.btnRefresh.Click += new System.EventHandler(this.btnRefresh_Click); + + // + // lblLogFolder + // + this.lblLogFolder.AutoSize = true; + this.lblLogFolder.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + this.lblLogFolder.ForeColor = textLight; + this.lblLogFolder.Location = new System.Drawing.Point(15, 70); + this.lblLogFolder.Text = "Log Folder:"; + + // + // txtFolderPath + // + this.txtFolderPath.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + this.txtFolderPath.BackColor = bgDark; + this.txtFolderPath.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.txtFolderPath.ForeColor = textLight; + this.txtFolderPath.Font = new System.Drawing.Font("Segoe UI", 9F); + this.txtFolderPath.Location = new System.Drawing.Point(110, 67); + this.txtFolderPath.Size = new System.Drawing.Size(360, 23); + + // // btnBrowse - // - this.btnBrowse.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); - this.btnBrowse.Location = new System.Drawing.Point(12, 12); - this.btnBrowse.Name = "btnBrowse"; - this.btnBrowse.Size = new System.Drawing.Size(75, 23); - this.btnBrowse.TabIndex = 0; + // + this.btnBrowse.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; + this.btnBrowse.BackColor = Color.FromArgb(60, 60, 80); + this.btnBrowse.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnBrowse.FlatAppearance.BorderColor = textSecondary; + this.btnBrowse.ForeColor = textLight; + this.btnBrowse.Font = new System.Drawing.Font("Segoe UI", 9F); + this.btnBrowse.Location = new System.Drawing.Point(480, 65); + this.btnBrowse.Size = new System.Drawing.Size(80, 27); this.btnBrowse.Text = "Browse"; - this.btnBrowse.UseVisualStyleBackColor = true; + this.btnBrowse.UseVisualStyleBackColor = false; this.btnBrowse.Click += new System.EventHandler(this.btnBrowse_Click); - // - // txtFolderPath - // - this.txtFolderPath.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.txtFolderPath.Location = new System.Drawing.Point(93, 14); - this.txtFolderPath.Name = "txtFolderPath"; - this.txtFolderPath.Size = new System.Drawing.Size(395, 20); - this.txtFolderPath.TabIndex = 1; - // + + // // btnProcessLogs - // - this.btnProcessLogs.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left))); - this.btnProcessLogs.Location = new System.Drawing.Point(12, 41); - this.btnProcessLogs.Name = "btnProcessLogs"; - this.btnProcessLogs.Size = new System.Drawing.Size(75, 23); - this.btnProcessLogs.TabIndex = 2; - this.btnProcessLogs.Text = "Process Logs"; - this.btnProcessLogs.UseVisualStyleBackColor = true; + // + this.btnProcessLogs.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; + this.btnProcessLogs.BackColor = accentGreen; + this.btnProcessLogs.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnProcessLogs.FlatAppearance.BorderSize = 0; + this.btnProcessLogs.ForeColor = bgDark; + this.btnProcessLogs.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Bold); + this.btnProcessLogs.Location = new System.Drawing.Point(570, 65); + this.btnProcessLogs.Size = new System.Drawing.Size(90, 27); + this.btnProcessLogs.Text = "Calculate"; + this.btnProcessLogs.UseVisualStyleBackColor = false; this.btnProcessLogs.Click += new System.EventHandler(this.btnProcessLogs_Click); - // - // txtTotalPlayTime - // - this.txtTotalPlayTime.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right))); - this.txtTotalPlayTime.Location = new System.Drawing.Point(93, 43); - this.txtTotalPlayTime.Name = "txtTotalPlayTime"; - this.txtTotalPlayTime.ReadOnly = true; - this.txtTotalPlayTime.Size = new System.Drawing.Size(280, 20); - this.txtTotalPlayTime.TabIndex = 3; - // + + // + // panelLog + // + this.panelLog.BackColor = bgPanel; + this.panelLog.Location = new System.Drawing.Point(12, 194); + this.panelLog.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + this.panelLog.Size = new System.Drawing.Size(676, 220); + this.panelLog.Padding = new System.Windows.Forms.Padding(15); + this.panelLog.Controls.Add(this.lblLogTitle); + this.panelLog.Controls.Add(this.progressBar); + this.panelLog.Controls.Add(this.txtOutput); + + // + // lblLogTitle + // + this.lblLogTitle.AutoSize = true; + this.lblLogTitle.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold); + this.lblLogTitle.ForeColor = textSecondary; + this.lblLogTitle.Location = new System.Drawing.Point(15, 8); + this.lblLogTitle.Text = "Processing Log"; + + // + // progressBar + // + this.progressBar.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + this.progressBar.Location = new System.Drawing.Point(15, 30); + this.progressBar.Size = new System.Drawing.Size(646, 5); + this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.progressBar.MarqueeAnimationSpeed = 30; + this.progressBar.Visible = false; + + // + // txtOutput + // + this.txtOutput.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + this.txtOutput.BackColor = bgDark; + this.txtOutput.BorderStyle = System.Windows.Forms.BorderStyle.None; + this.txtOutput.ForeColor = textLight; + this.txtOutput.Font = new System.Drawing.Font("Cascadia Code", 9F); + this.txtOutput.Location = new System.Drawing.Point(15, 40); + this.txtOutput.ReadOnly = true; + this.txtOutput.Size = new System.Drawing.Size(646, 165); + + // + // panelResults + // + this.panelResults.BackColor = bgPanel; + this.panelResults.Anchor = System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + this.panelResults.Location = new System.Drawing.Point(12, 426); + this.panelResults.Size = new System.Drawing.Size(676, 70); + this.panelResults.Padding = new System.Windows.Forms.Padding(15); + this.panelResults.Controls.Add(this.lblResultsTitle); + this.panelResults.Controls.Add(this.lblDisplayAs); + this.panelResults.Controls.Add(this.comboBoxFormat); + this.panelResults.Controls.Add(this.lblTotalPlaytime); + this.panelResults.Controls.Add(this.txtTotalPlayTime); + this.panelResults.Controls.Add(this.btnCopyToClipboard); + + // + // lblResultsTitle + // + this.lblResultsTitle.AutoSize = true; + this.lblResultsTitle.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold); + this.lblResultsTitle.ForeColor = accentGreen; + this.lblResultsTitle.Location = new System.Drawing.Point(15, 8); + this.lblResultsTitle.Text = "Results"; + + // + // lblDisplayAs + // + this.lblDisplayAs.AutoSize = true; + this.lblDisplayAs.Font = new System.Drawing.Font("Segoe UI", 9F); + this.lblDisplayAs.ForeColor = textLight; + this.lblDisplayAs.Location = new System.Drawing.Point(15, 38); + this.lblDisplayAs.Text = "Display as:"; + + // // comboBoxFormat - // - this.comboBoxFormat.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + // + this.comboBoxFormat.BackColor = bgDark; + this.comboBoxFormat.ForeColor = textLight; + this.comboBoxFormat.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.comboBoxFormat.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.comboBoxFormat.Font = new System.Drawing.Font("Segoe UI", 9F); this.comboBoxFormat.FormattingEnabled = true; this.comboBoxFormat.Items.AddRange(new object[] { - "Default", - "Hours"}); - this.comboBoxFormat.Location = new System.Drawing.Point(379, 43); - this.comboBoxFormat.Name = "comboBoxFormat"; - this.comboBoxFormat.Size = new System.Drawing.Size(70, 21); - this.comboBoxFormat.TabIndex = 4; + "Default", + "Hours", + "Minutes", + "Seconds", + "Days" + }); + this.comboBoxFormat.Location = new System.Drawing.Point(85, 35); + this.comboBoxFormat.Size = new System.Drawing.Size(90, 23); this.comboBoxFormat.SelectedIndexChanged += new System.EventHandler(this.comboBoxFormat_SelectedIndexChanged); - // + + // + // lblTotalPlaytime + // + this.lblTotalPlaytime.AutoSize = true; + this.lblTotalPlaytime.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Bold); + this.lblTotalPlaytime.ForeColor = textLight; + this.lblTotalPlaytime.Location = new System.Drawing.Point(195, 37); + this.lblTotalPlaytime.Text = "Total Playtime:"; + + // + // txtTotalPlayTime + // + this.txtTotalPlayTime.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right; + this.txtTotalPlayTime.BackColor = bgDark; + this.txtTotalPlayTime.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.txtTotalPlayTime.ForeColor = accentGreen; + this.txtTotalPlayTime.Font = new System.Drawing.Font("Segoe UI", 11F, System.Drawing.FontStyle.Bold); + this.txtTotalPlayTime.Location = new System.Drawing.Point(310, 33); + this.txtTotalPlayTime.ReadOnly = true; + this.txtTotalPlayTime.Size = new System.Drawing.Size(310, 27); + + // // btnCopyToClipboard - // - this.btnCopyToClipboard.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); - this.btnCopyToClipboard.Location = new System.Drawing.Point(454, 41); - this.btnCopyToClipboard.Name = "btnCopyToClipboard"; - this.btnCopyToClipboard.Size = new System.Drawing.Size(34, 23); - this.btnCopyToClipboard.TabIndex = 5; + // + this.btnCopyToClipboard.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; + this.btnCopyToClipboard.BackColor = Color.FromArgb(15, 52, 96); + this.btnCopyToClipboard.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.btnCopyToClipboard.FlatAppearance.BorderColor = accentGreen; + this.btnCopyToClipboard.ForeColor = accentGreen; + this.btnCopyToClipboard.Font = new System.Drawing.Font("Segoe UI", 9F); + this.btnCopyToClipboard.Location = new System.Drawing.Point(630, 32); + this.btnCopyToClipboard.Size = new System.Drawing.Size(30, 28); this.toolTip.SetToolTip(this.btnCopyToClipboard, "Copy to clipboard"); - this.btnCopyToClipboard.UseVisualStyleBackColor = true; + this.btnCopyToClipboard.UseVisualStyleBackColor = false; this.btnCopyToClipboard.Click += new System.EventHandler(this.btnCopyToClipboard_Click); - // - // txtOutput - // - this.txtOutput.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) - | System.Windows.Forms.AnchorStyles.Right - | System.Windows.Forms.AnchorStyles.Bottom)); - this.txtOutput.Location = new System.Drawing.Point(12, 70); - this.txtOutput.Multiline = true; - this.txtOutput.Name = "txtOutput"; - this.txtOutput.ScrollBars = System.Windows.Forms.RichTextBoxScrollBars.Vertical; - this.txtOutput.Size = new System.Drawing.Size(476, 150); - this.txtOutput.TabIndex = 6; - // + + // + // statusStrip + // + this.statusStrip.BackColor = bgPanel; + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.statusLabel + }); + this.statusStrip.Location = new System.Drawing.Point(0, 508); + this.statusStrip.Size = new System.Drawing.Size(700, 22); + + // + // statusLabel + // + this.statusLabel.ForeColor = textSecondary; + this.statusLabel.Font = new System.Drawing.Font("Segoe UI", 9F); + this.statusLabel.Text = "Ready - Select an environment or browse to a log folder"; + + // // Form1 - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(500, 232); - this.Controls.Add(this.txtOutput); - this.Controls.Add(this.btnCopyToClipboard); - this.Controls.Add(this.comboBoxFormat); - this.Controls.Add(this.txtTotalPlayTime); - this.Controls.Add(this.btnProcessLogs); - this.Controls.Add(this.txtFolderPath); - this.Controls.Add(this.btnBrowse); - this.MinimumSize = new System.Drawing.Size(520, 270); + this.BackColor = bgDark; + this.ClientSize = new System.Drawing.Size(700, 530); + this.Font = new System.Drawing.Font("Segoe UI", 9F); + this.Controls.Add(this.panelHeader); + this.Controls.Add(this.panelConfig); + this.Controls.Add(this.panelLog); + this.Controls.Add(this.panelResults); + this.Controls.Add(this.statusStrip); + this.MinimumSize = new System.Drawing.Size(716, 569); this.Name = "Form1"; - this.Text = "Star Citizen Logs Analyzer - Calculate Total Time Played"; - this.Load += new System.EventHandler(this.Form1_Load); // Subscribe to Load event + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Star Citizen Playtime Calculator"; + this.Load += new System.EventHandler(this.Form1_Load); this.ResumeLayout(false); this.PerformLayout(); } - private System.Windows.Forms.Button btnBrowse; + // Header + private System.Windows.Forms.Panel panelHeader; + private System.Windows.Forms.Label lblTitle; + private System.Windows.Forms.Label lblVersion; + + // Configuration panel + private System.Windows.Forms.Panel panelConfig; + private System.Windows.Forms.Label lblConfigTitle; + private System.Windows.Forms.Label lblEnvironment; + private System.Windows.Forms.ComboBox comboEnvironment; + private System.Windows.Forms.Button btnRefresh; + private System.Windows.Forms.Label lblLogFolder; private System.Windows.Forms.TextBox txtFolderPath; + private System.Windows.Forms.Button btnBrowse; private System.Windows.Forms.Button btnProcessLogs; - private System.Windows.Forms.TextBox txtTotalPlayTime; - private System.Windows.Forms.Button btnCopyToClipboard; + + // Log panel + private System.Windows.Forms.Panel panelLog; + private System.Windows.Forms.Label lblLogTitle; + private System.Windows.Forms.ProgressBar progressBar; private System.Windows.Forms.RichTextBox txtOutput; + + // Results panel + private System.Windows.Forms.Panel panelResults; + private System.Windows.Forms.Label lblResultsTitle; + private System.Windows.Forms.Label lblDisplayAs; private System.Windows.Forms.ComboBox comboBoxFormat; + private System.Windows.Forms.Label lblTotalPlaytime; + private System.Windows.Forms.TextBox txtTotalPlayTime; + private System.Windows.Forms.Button btnCopyToClipboard; + + // Status bar + private System.Windows.Forms.StatusStrip statusStrip; + private System.Windows.Forms.ToolStripStatusLabel statusLabel; private System.Windows.Forms.ToolTip toolTip; } -} +} diff --git a/csharp/Form1.cs b/csharp/Form1.cs index 12606b8..2cf42ed 100644 --- a/csharp/Form1.cs +++ b/csharp/Form1.cs @@ -1,8 +1,9 @@ -using System; +using System; +using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; -using System.Runtime.InteropServices; +using System.Threading.Tasks; using System.Windows.Forms; namespace StarCitizenPlaytimeCalculator @@ -11,16 +12,101 @@ public partial class Form1 : Form { private const string DefaultPath = @"C:\Program Files\Roberts Space Industries\StarCitizen\LIVE\logbackups"; private TimeSpan totalPlayTime = TimeSpan.Zero; + private Dictionary detectedPaths = new Dictionary(); + + // Colors for formatting + private readonly Color accentCyan = Color.FromArgb(0, 212, 255); + private readonly Color accentGreen = Color.FromArgb(0, 255, 136); + private readonly Color accentGold = Color.FromArgb(255, 215, 0); + private readonly Color textLight = Color.FromArgb(224, 224, 224); public Form1() { InitializeComponent(); - // Pre-fill the folder path with the default value if it exists - if (Directory.Exists(DefaultPath)) + comboBoxFormat.SelectedIndex = 0; + } + + private void Form1_Load(object sender, EventArgs e) + { + // Load and resize the clipboard icon + try + { + var originalImage = StarCitizenPlaytimeCalculator.Properties.Resources.clipboard_icon; + var resizedImage = new Bitmap(originalImage, new Size(this.btnCopyToClipboard.Height - 4, this.btnCopyToClipboard.Height - 4)); + this.btnCopyToClipboard.Image = resizedImage; + } + catch + { + // Icon loading is optional + } + + // Detect installations on load + DetectInstallations(); + } + + private void DetectInstallations() + { + detectedPaths.Clear(); + comboEnvironment.Items.Clear(); + + // Common installation paths to check + string[] drivesToCheck = { "C", "D", "E", "F", "G" }; + string[] environments = { "LIVE", "PTU", "EPTU", "TECH-PREVIEW" }; + + foreach (var drive in drivesToCheck) + { + string basePath = $@"{drive}:\Program Files\Roberts Space Industries\StarCitizen"; + + if (Directory.Exists(basePath)) + { + foreach (var env in environments) + { + string logPath = Path.Combine(basePath, env, "logbackups"); + if (Directory.Exists(logPath)) + { + string key = drive == "C" ? env : $"{env} ({drive}:)"; + detectedPaths[key] = logPath; + } + } + } + } + + if (detectedPaths.Count > 0) { - txtFolderPath.Text = DefaultPath; + foreach (var env in detectedPaths.Keys) + { + comboEnvironment.Items.Add(env); + } + comboEnvironment.SelectedIndex = 0; + UpdateStatus($"Found {detectedPaths.Count} environment(s)", StatusType.Info); + } + else + { + comboEnvironment.Items.Add("No installations found"); + comboEnvironment.SelectedIndex = 0; + + // Set default path for manual browsing + if (Directory.Exists(DefaultPath)) + { + txtFolderPath.Text = DefaultPath; + } + UpdateStatus("No Star Citizen installation detected - please browse manually", StatusType.Warning); } - comboBoxFormat.SelectedIndex = 0; // Default to "Default" format + } + + private void comboEnvironment_SelectedIndexChanged(object sender, EventArgs e) + { + string selected = comboEnvironment.SelectedItem?.ToString(); + if (selected != null && detectedPaths.ContainsKey(selected)) + { + txtFolderPath.Text = detectedPaths[selected]; + UpdateStatus($"Selected {selected} environment", StatusType.Info); + } + } + + private void btnRefresh_Click(object sender, EventArgs e) + { + DetectInstallations(); } private void btnBrowse_Click(object sender, EventArgs e) @@ -28,33 +114,69 @@ private void btnBrowse_Click(object sender, EventArgs e) using (var folderDialog = new FolderBrowserDialog()) { folderDialog.Description = "Browse for logbackups folder"; - folderDialog.SelectedPath = DefaultPath; - if (folderDialog.ShowDialog() == DialogResult.OK) + if (!string.IsNullOrEmpty(txtFolderPath.Text) && Directory.Exists(txtFolderPath.Text)) { - txtFolderPath.Text = folderDialog.SelectedPath; + folderDialog.SelectedPath = txtFolderPath.Text; } else { - if (Directory.Exists(folderDialog.SelectedPath)) - { - FolderBrowserDialogHelper.ScrollToPath(folderDialog); - } + folderDialog.SelectedPath = DefaultPath; + } + + if (folderDialog.ShowDialog() == DialogResult.OK) + { + txtFolderPath.Text = folderDialog.SelectedPath; + UpdateStatus($"Selected: {folderDialog.SelectedPath}", StatusType.Info); } } } - private void btnProcessLogs_Click(object sender, EventArgs e) + private async void btnProcessLogs_Click(object sender, EventArgs e) { - if (Directory.Exists(txtFolderPath.Text)) + if (!Directory.Exists(txtFolderPath.Text)) + { + MessageBox.Show("The selected folder does not exist.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + UpdateStatus("Error: Path does not exist", StatusType.Error); + return; + } + + // Disable button and show progress + btnProcessLogs.Enabled = false; + progressBar.Visible = true; + txtOutput.Clear(); + UpdateStatus("Calculating...", StatusType.Info); + + try { - txtOutput.Clear(); - totalPlayTime = CalculateTotalPlayTime(txtFolderPath.Text); + // Run calculation asynchronously + await Task.Run(() => + { + totalPlayTime = CalculateTotalPlayTime(txtFolderPath.Text); + }); + + // Update display DisplayTotalPlayTime(); + + var logFiles = Directory.GetFiles(txtFolderPath.Text, "*.log", SearchOption.AllDirectories); + if (logFiles.Length > 0) + { + UpdateStatus($"Processed {logFiles.Length} log file(s) successfully", StatusType.Success); + } + else + { + UpdateStatus("No valid log files found in the selected path", StatusType.Warning); + } } - else + catch (Exception ex) { - MessageBox.Show("The selected folder does not exist.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + UpdateStatus($"Error: {ex.Message}", StatusType.Error); + } + finally + { + // Re-enable button and hide progress + btnProcessLogs.Enabled = true; + progressBar.Visible = false; } } @@ -63,16 +185,22 @@ private void btnCopyToClipboard_Click(object sender, EventArgs e) if (!string.IsNullOrEmpty(txtTotalPlayTime.Text)) { Clipboard.SetText(txtTotalPlayTime.Text); + UpdateStatus("Copied to clipboard!", StatusType.Success); toolTip.SetToolTip(btnCopyToClipboard, "Copied!"); - } - } - private void Form1_Load(object sender, EventArgs e) - { - // Load and resize the clipboard icon - var originalImage = StarCitizenPlaytimeCalculator.Properties.Resources.clipboard_icon; - var resizedImage = new Bitmap(originalImage, new Size(this.btnCopyToClipboard.Height - 4, this.btnCopyToClipboard.Height - 4)); - this.btnCopyToClipboard.Image = resizedImage; + // Reset tooltip after 2 seconds + Task.Delay(2000).ContinueWith(_ => + { + if (this.InvokeRequired) + { + this.Invoke(new Action(() => + { + toolTip.SetToolTip(btnCopyToClipboard, "Copy to clipboard"); + UpdateStatus("Ready", StatusType.Info); + })); + } + }); + } } private TimeSpan CalculateTotalPlayTime(string folderPath) @@ -82,8 +210,13 @@ private TimeSpan CalculateTotalPlayTime(string folderPath) foreach (var logFile in logFiles) { - AppendBoldText("File: "); - AppendText($"{logFile}, "); + // Use Invoke for thread-safe UI updates + this.Invoke(new Action(() => + { + AppendColoredText("File: ", accentCyan, true); + AppendColoredText($"{logFile}\n", textLight, false); + })); + var lines = File.ReadAllLines(logFile); DateTime? firstTimestamp = null; DateTime? lastTimestamp = null; @@ -108,39 +241,60 @@ private TimeSpan CalculateTotalPlayTime(string folderPath) { var sessionTime = lastTimestamp.Value - firstTimestamp.Value; totalPlayTime += sessionTime; - AppendBoldText("Session Time: "); - AppendText($"{sessionTime}{Environment.NewLine}"); + + this.Invoke(new Action(() => + { + AppendColoredText("Session Time: ", accentCyan, true); + AppendColoredText($"{sessionTime}\n", accentGold, false); + })); } } - AppendBoldText("Total logs processed: "); - AppendText($"{logFiles.Length}{Environment.NewLine}"); - AppendBoldText("Total Play Time: "); - AppendText($"{FormatPlayTime(totalPlayTime)}{Environment.NewLine}"); - return totalPlayTime; - } + this.Invoke(new Action(() => + { + AppendColoredText($"\nTotal logs processed: {logFiles.Length}\n", accentGreen, true); + AppendColoredText($"Total Play Time: {FormatPlayTime(totalPlayTime)}\n", accentGreen, true); + })); - private void AppendBoldText(string text) - { - txtOutput.SelectionFont = new Font(txtOutput.Font, FontStyle.Bold); - txtOutput.AppendText(text); - txtOutput.SelectionFont = new Font(txtOutput.Font, FontStyle.Regular); - txtOutput.ScrollToCaret(); + return totalPlayTime; } - private void AppendText(string text) + private void AppendColoredText(string text, Color color, bool bold) { + txtOutput.SelectionStart = txtOutput.TextLength; + txtOutput.SelectionLength = 0; + txtOutput.SelectionColor = color; + txtOutput.SelectionFont = new Font(txtOutput.Font, bold ? FontStyle.Bold : FontStyle.Regular); txtOutput.AppendText(text); + txtOutput.SelectionColor = txtOutput.ForeColor; txtOutput.ScrollToCaret(); } private void DisplayTotalPlayTime() { - string formattedPlayTime = comboBoxFormat.SelectedItem.ToString() == "Hours" - ? FormatPlayTimeInHours(totalPlayTime) - : FormatPlayTime(totalPlayTime); + string format = comboBoxFormat.SelectedItem?.ToString() ?? "Default"; + string formattedPlayTime; + + switch (format) + { + case "Hours": + formattedPlayTime = FormatPlayTimeInHours(totalPlayTime); + break; + case "Minutes": + formattedPlayTime = $"{totalPlayTime.TotalMinutes:F2} minutes"; + break; + case "Seconds": + formattedPlayTime = $"{totalPlayTime.TotalSeconds:F0} seconds"; + break; + case "Days": + formattedPlayTime = $"{totalPlayTime.TotalDays:F2} days"; + break; + default: + formattedPlayTime = FormatPlayTime(totalPlayTime); + break; + } - txtTotalPlayTime.Text = $"Total Playtime: {formattedPlayTime}"; + txtTotalPlayTime.Text = formattedPlayTime; } private string FormatPlayTime(TimeSpan totalPlayTime) @@ -165,30 +319,38 @@ private string FormatPlayTimeInHours(TimeSpan totalPlayTime) private void comboBoxFormat_SelectedIndexChanged(object sender, EventArgs e) { - DisplayTotalPlayTime(); + if (totalPlayTime != TimeSpan.Zero) + { + DisplayTotalPlayTime(); + } } - } - public static class FolderBrowserDialogHelper - { - [DllImport("user32.dll", CharSet = CharSet.Auto)] - private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); - - private const int BFFM_INITIALIZED = 1; - private const int BFFM_SETSELECTIONW = 1126; + private enum StatusType + { + Info, + Success, + Warning, + Error + } - public static void ScrollToPath(FolderBrowserDialog fbd) + private void UpdateStatus(string message, StatusType type) { - IntPtr hwnd = IntPtr.Zero; - IntPtr pathPtr = Marshal.StringToHGlobalUni(fbd.SelectedPath); - try - { - SendMessage(hwnd, BFFM_INITIALIZED, IntPtr.Zero, IntPtr.Zero); - SendMessage(hwnd, BFFM_SETSELECTIONW, IntPtr.Zero, pathPtr); - } - finally + statusLabel.Text = message; + + switch (type) { - Marshal.FreeHGlobal(pathPtr); + case StatusType.Success: + statusLabel.ForeColor = accentGreen; + break; + case StatusType.Warning: + statusLabel.ForeColor = Color.FromArgb(255, 193, 7); + break; + case StatusType.Error: + statusLabel.ForeColor = Color.FromArgb(220, 53, 69); + break; + default: + statusLabel.ForeColor = Color.FromArgb(160, 160, 160); + break; } } } diff --git a/python/requirements.txt b/python/requirements.txt index 9a05ed5..5d6c83b 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -1,2 +1,3 @@ python-dateutil>=2.8.0 pillow>=9.0.0 +ttkbootstrap>=1.10.0 diff --git a/python/sc_main.py b/python/sc_main.py index 838d89d..fb8cd3d 100644 --- a/python/sc_main.py +++ b/python/sc_main.py @@ -1,8 +1,11 @@ +import ttkbootstrap as ttk +from ttkbootstrap.constants import * +from ttkbootstrap.dialogs import Messagebox +from tkinter import filedialog import tkinter as tk -from tkinter import ttk, filedialog, messagebox -import tkinter.font as tkfont import os import sys +import threading import sc_playtime from PIL import Image, ImageTk @@ -16,137 +19,279 @@ class SCPlaytimeCalculator: def __init__(self): - self.root = tk.Tk() - self.root.title("Star Citizen Playtime Calculator") + # Use cyborg theme for sci-fi look matching Star Citizen + self.root = ttk.Window( + title="Star Citizen Playtime Calculator", + themename="cyborg", + size=(900, 700), + minsize=(750, 550) + ) + self.time_delta = None self.copy_icon = None self.detected_paths = {} + self.is_calculating = False - self._setup_window() + self._center_window() self._create_widgets() self._detect_installations() - def _setup_window(self): - """Configure the main window.""" - # Calculate center position + def _center_window(self): + """Center the window on screen.""" + self.root.update_idletasks() screen_width = self.root.winfo_screenwidth() screen_height = self.root.winfo_screenheight() - window_width = 850 - window_height = 650 - x = (screen_width // 2) - (window_width // 2) - y = (screen_height // 2) - (window_height // 2) + x = (screen_width // 2) - (900 // 2) + y = (screen_height // 2) - (700 // 2) + self.root.geometry(f"+{x}+{y}") - self.root.geometry(f"{window_width}x{window_height}+{x}+{y}") - self.root.minsize(700, 500) + def _create_widgets(self): + """Create all UI widgets with modern styling.""" + # Main container with padding + self.mainframe = ttk.Frame(self.root, padding=20) + self.mainframe.pack(fill=BOTH, expand=YES) - # Configure style - style = ttk.Style() - style.configure('Header.TLabel', font=('Helvetica', 12, 'bold')) - style.configure('Status.TLabel', font=('Helvetica', 9)) - style.configure('Result.TEntry', font=('Helvetica', 10, 'bold')) + # Header section + self._create_header() - def _create_widgets(self): - """Create all UI widgets.""" - # Main container - self.mainframe = ttk.Frame(self.root, padding="15") - self.mainframe.grid(column=0, row=0, sticky=(tk.N, tk.W, tk.E, tk.S)) - self.root.columnconfigure(0, weight=1) - self.root.rowconfigure(0, weight=1) - - # Configure grid weights - self.mainframe.columnconfigure(1, weight=1) - self.mainframe.rowconfigure(4, weight=1) - - # Row 0: Environment selector - env_label = ttk.Label(self.mainframe, text="Environment:", style='Header.TLabel') - env_label.grid(column=0, row=0, sticky=tk.W, pady=(0, 5)) - - self.env_combobox = ttk.Combobox(self.mainframe, state='readonly', width=20) - self.env_combobox.grid(column=1, row=0, sticky=tk.W, pady=(0, 5)) - self.env_combobox.bind("<>", self._on_environment_change) + # Configuration section + self._create_config_section() + + # Log output section + self._create_log_section() - detect_btn = ttk.Button(self.mainframe, text="Refresh", command=self._detect_installations) - detect_btn.grid(column=2, row=0, sticky=tk.W, padx=(5, 0), pady=(0, 5)) + # Results section + self._create_results_section() - # Row 1: Path selection - path_label = ttk.Label(self.mainframe, text="Log folder path:") - path_label.grid(column=0, row=1, sticky=tk.W, pady=(5, 5)) + # Status bar + self._create_status_bar() - self.path_entry = ttk.Entry(self.mainframe) - self.path_entry.grid(column=1, row=1, sticky=(tk.W, tk.E), pady=(5, 5)) + def _create_header(self): + """Create the header with title and branding.""" + header_frame = ttk.Frame(self.mainframe) + header_frame.pack(fill=X, pady=(0, 15)) - path_btn_frame = ttk.Frame(self.mainframe) - path_btn_frame.grid(column=2, row=1, columnspan=2, sticky=tk.W, pady=(5, 5)) + # Title + title_label = ttk.Label( + header_frame, + text="Star Citizen Playtime Calculator", + font=("Segoe UI", 18, "bold"), + bootstyle="inverse-primary" + ) + title_label.pack(side=LEFT) + + # Version/info + version_label = ttk.Label( + header_frame, + text="v2.0", + font=("Segoe UI", 10), + bootstyle="secondary" + ) + version_label.pack(side=RIGHT, padx=5) - browse_btn = ttk.Button(path_btn_frame, text="Browse", command=self._select_directory) - browse_btn.pack(side=tk.LEFT, padx=(5, 2)) + def _create_config_section(self): + """Create the configuration/input section.""" + config_frame = ttk.LabelFrame( + self.mainframe, + text="Configuration", + bootstyle="info", + padding=15 + ) + config_frame.pack(fill=X, pady=(0, 15)) - calculate_btn = ttk.Button(path_btn_frame, text="Calculate", command=self._calculate_playtime) - calculate_btn.pack(side=tk.LEFT, padx=2) + # Environment row + env_row = ttk.Frame(config_frame) + env_row.pack(fill=X, pady=(0, 10)) - # Row 2: Separator - separator = ttk.Separator(self.mainframe, orient='horizontal') - separator.grid(column=0, row=2, columnspan=4, sticky=(tk.W, tk.E), pady=10) + env_label = ttk.Label( + env_row, + text="Environment:", + font=("Segoe UI", 10, "bold"), + width=12 + ) + env_label.pack(side=LEFT) - # Row 3: Log label - log_label = ttk.Label(self.mainframe, text="Processing Log:") - log_label.grid(column=0, row=3, sticky=tk.W, columnspan=4) + self.env_combobox = ttk.Combobox( + env_row, + state='readonly', + width=25, + bootstyle="info" + ) + self.env_combobox.pack(side=LEFT, padx=(0, 10)) + self.env_combobox.bind("<>", self._on_environment_change) - # Row 4: Log text area with scrollbar - log_frame = ttk.Frame(self.mainframe) - log_frame.grid(column=0, row=4, columnspan=4, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(5, 10)) - log_frame.columnconfigure(0, weight=1) - log_frame.rowconfigure(0, weight=1) + refresh_btn = ttk.Button( + env_row, + text="Refresh", + command=self._detect_installations, + bootstyle="info-outline", + width=10 + ) + refresh_btn.pack(side=LEFT) - self.log_text = tk.Text(log_frame, height=15, wrap=tk.WORD, font=('Consolas', 9)) - self.log_text.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + # Path row + path_row = ttk.Frame(config_frame) + path_row.pack(fill=X) + + path_label = ttk.Label( + path_row, + text="Log Folder:", + font=("Segoe UI", 10, "bold"), + width=12 + ) + path_label.pack(side=LEFT) + + self.path_entry = ttk.Entry(path_row, font=("Segoe UI", 10)) + self.path_entry.pack(side=LEFT, fill=X, expand=YES, padx=(0, 10)) + + browse_btn = ttk.Button( + path_row, + text="Browse", + command=self._select_directory, + bootstyle="secondary", + width=10 + ) + browse_btn.pack(side=LEFT, padx=(0, 5)) + + self.calculate_btn = ttk.Button( + path_row, + text="Calculate", + command=self._calculate_playtime, + bootstyle="success", + width=12 + ) + self.calculate_btn.pack(side=LEFT) + + def _create_log_section(self): + """Create the log output section.""" + log_frame = ttk.LabelFrame( + self.mainframe, + text="Processing Log", + bootstyle="secondary", + padding=10 + ) + log_frame.pack(fill=BOTH, expand=YES, pady=(0, 15)) + + # Progress bar (hidden by default) + self.progress = ttk.Progressbar( + log_frame, + mode='indeterminate', + bootstyle="success-striped" + ) + self.progress.pack(fill=X, pady=(0, 10)) + self.progress.pack_forget() # Hide initially + + # Log text with scrollbar + log_container = ttk.Frame(log_frame) + log_container.pack(fill=BOTH, expand=YES) + + self.log_text = tk.Text( + log_container, + height=12, + wrap=tk.WORD, + font=("Cascadia Code", 9), + bg="#1a1a2e", + fg="#e0e0e0", + insertbackground="#00d4ff", + selectbackground="#0f3460", + relief="flat", + padx=10, + pady=10 + ) + self.log_text.pack(side=LEFT, fill=BOTH, expand=YES) - scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview) - scrollbar.grid(column=1, row=0, sticky=(tk.N, tk.S)) + scrollbar = ttk.Scrollbar( + log_container, + orient=VERTICAL, + command=self.log_text.yview, + bootstyle="round-info" + ) + scrollbar.pack(side=RIGHT, fill=Y) self.log_text.config(yscrollcommand=scrollbar.set) - # Row 5: Results section - results_frame = ttk.Frame(self.mainframe) - results_frame.grid(column=0, row=5, columnspan=4, sticky=(tk.W, tk.E), pady=(0, 10)) - results_frame.columnconfigure(1, weight=1) + # Configure text tags for colored output + self.log_text.tag_configure("header", foreground="#00d4ff", font=("Cascadia Code", 9, "bold")) + self.log_text.tag_configure("success", foreground="#00ff88") + self.log_text.tag_configure("info", foreground="#e0e0e0") + self.log_text.tag_configure("highlight", foreground="#ffd700") + + def _create_results_section(self): + """Create the results display section.""" + results_frame = ttk.LabelFrame( + self.mainframe, + text="Results", + bootstyle="success", + padding=15 + ) + results_frame.pack(fill=X, pady=(0, 15)) + + # Results row + results_row = ttk.Frame(results_frame) + results_row.pack(fill=X) # Format selector - format_label = ttk.Label(results_frame, text="Display as:") - format_label.grid(column=0, row=0, sticky=tk.W, padx=(0, 5)) + format_label = ttk.Label( + results_row, + text="Display as:", + font=("Segoe UI", 10) + ) + format_label.pack(side=LEFT, padx=(0, 5)) self.unit_combobox = ttk.Combobox( - results_frame, + results_row, values=["Default", "Hours", "Minutes", "Seconds", "Days"], state='readonly', - width=12 + width=10, + bootstyle="success" ) self.unit_combobox.set("Default") - self.unit_combobox.grid(column=1, row=0, sticky=tk.W) + self.unit_combobox.pack(side=LEFT, padx=(0, 20)) self.unit_combobox.bind("<>", lambda e: self._update_result_display()) - # Result display - result_label = ttk.Label(results_frame, text="Total Playtime:", style='Header.TLabel') - result_label.grid(column=0, row=1, sticky=tk.W, pady=(10, 0)) + # Total playtime label + playtime_label = ttk.Label( + results_row, + text="Total Playtime:", + font=("Segoe UI", 11, "bold") + ) + playtime_label.pack(side=LEFT, padx=(0, 10)) - self.result_entry = ttk.Entry(results_frame, font=('Helvetica', 11, 'bold')) - self.result_entry.grid(column=1, row=1, sticky=(tk.W, tk.E), pady=(10, 0), padx=(5, 5)) + # Result entry (larger and more prominent) + self.result_entry = ttk.Entry( + results_row, + font=("Segoe UI", 12, "bold"), + bootstyle="success" + ) + self.result_entry.pack(side=LEFT, fill=X, expand=YES, padx=(0, 10)) + + # Copy button + self.copy_btn = ttk.Button( + results_row, + text="Copy", + command=self._copy_to_clipboard, + bootstyle="success-outline", + width=8 + ) + self.copy_btn.pack(side=LEFT) - self.copy_btn = ttk.Button(results_frame, text="Copy", command=self._copy_to_clipboard) - self.copy_btn.grid(column=2, row=1, sticky=tk.W, pady=(10, 0)) + # Load clipboard icon after window is ready + self.root.after(100, self._load_icon) - # Row 6: Status bar + def _create_status_bar(self): + """Create the status bar at the bottom.""" self.status_var = tk.StringVar(value="Ready - Select an environment or browse to a log folder") - status_bar = ttk.Label( - self.mainframe, + + status_frame = ttk.Frame(self.mainframe) + status_frame.pack(fill=X) + + self.status_label = ttk.Label( + status_frame, textvariable=self.status_var, - style='Status.TLabel', - relief=tk.SUNKEN, - padding=(5, 2) + font=("Segoe UI", 9), + bootstyle="secondary", + padding=(10, 5) ) - status_bar.grid(column=0, row=6, columnspan=4, sticky=(tk.W, tk.E)) - - # Load clipboard icon - self.root.after(100, self._load_icon) + self.status_label.pack(fill=X) def _detect_installations(self): """Detect installed Star Citizen environments.""" @@ -157,7 +302,7 @@ def _detect_installations(self): self.env_combobox['values'] = environments self.env_combobox.set(environments[0]) self._on_environment_change(None) - self.status_var.set(f"Found {len(environments)} environment(s): {', '.join(environments)}") + self._update_status(f"Found {len(environments)} environment(s): {', '.join(environments)}", "info") else: self.env_combobox['values'] = ["No installations found"] self.env_combobox.set("No installations found") @@ -165,7 +310,20 @@ def _detect_installations(self): default = r'C:\Program Files\Roberts Space Industries\StarCitizen\LIVE\logbackups' self.path_entry.delete(0, tk.END) self.path_entry.insert(0, default) - self.status_var.set("No Star Citizen installation detected - please browse manually") + self._update_status("No Star Citizen installation detected - please browse manually", "warning") + + def _update_status(self, message, status_type="info"): + """Update status bar with colored message.""" + self.status_var.set(message) + + # Update status label style based on type + style_map = { + "info": "secondary", + "success": "success", + "warning": "warning", + "error": "danger" + } + self.status_label.configure(bootstyle=style_map.get(status_type, "secondary")) def _on_environment_change(self, event): """Handle environment selection change.""" @@ -173,7 +331,7 @@ def _on_environment_change(self, event): if selected in self.detected_paths: self.path_entry.delete(0, tk.END) self.path_entry.insert(0, self.detected_paths[selected]) - self.status_var.set(f"Selected {selected} environment") + self._update_status(f"Selected {selected} environment", "info") def _select_directory(self): """Open directory browser.""" @@ -185,41 +343,98 @@ def _select_directory(self): if directory: self.path_entry.delete(0, tk.END) self.path_entry.insert(0, directory) - self.status_var.set(f"Selected: {directory}") + self._update_status(f"Selected: {directory}", "info") def _calculate_playtime(self): """Calculate total playtime from log files.""" + if self.is_calculating: + return + path = self.path_entry.get() if not path: - messagebox.showwarning("Warning", "Please select a log folder path.") + Messagebox.show_warning("Please select a log folder path.", "Warning") return if not os.path.exists(path): - messagebox.showerror("Error", f"Path does not exist:\n{path}") - self.status_var.set("Error: Path does not exist") + Messagebox.show_error(f"Path does not exist:\n{path}", "Error") + self._update_status("Error: Path does not exist", "error") return - self.status_var.set("Calculating...") - self.root.update() + # Start calculation in background thread + self.is_calculating = True + self.calculate_btn.configure(state="disabled") + self._update_status("Calculating...", "info") + + # Show and start progress bar + self.progress.pack(fill=X, pady=(0, 10)) + self.progress.start(10) # Clear previous log self.log_text.delete(1.0, tk.END) - # Calculate playtime - log_output, self.time_delta, file_count = sc_playtime.just_do_it(path) + # Run calculation in thread + thread = threading.Thread(target=self._do_calculation, args=(path,)) + thread.daemon = True + thread.start() - # Display log - self.log_text.insert(tk.END, log_output) - self.log_text.see(tk.END) + def _do_calculation(self, path): + """Perform the calculation in a background thread.""" + try: + log_output, self.time_delta, file_count = sc_playtime.just_do_it(path) + + # Update UI in main thread + self.root.after(0, lambda: self._calculation_complete(log_output, file_count)) + except Exception as e: + self.root.after(0, lambda: self._calculation_error(str(e))) + + def _calculation_complete(self, log_output, file_count): + """Handle calculation completion.""" + # Stop and hide progress bar + self.progress.stop() + self.progress.pack_forget() + + # Display log with formatting + self._display_formatted_log(log_output) # Update result display self._update_result_display() + # Update status if file_count > 0: - self.status_var.set(f"Processed {file_count} log file(s) successfully") + self._update_status(f"Processed {file_count} log file(s) successfully", "success") else: - self.status_var.set("No valid log files found in the selected path") + self._update_status("No valid log files found in the selected path", "warning") + + # Re-enable button + self.calculate_btn.configure(state="normal") + self.is_calculating = False + + def _calculation_error(self, error_message): + """Handle calculation error.""" + self.progress.stop() + self.progress.pack_forget() + self._update_status(f"Error: {error_message}", "error") + self.calculate_btn.configure(state="normal") + self.is_calculating = False + + def _display_formatted_log(self, log_output): + """Display log output with color formatting.""" + self.log_text.delete(1.0, tk.END) + + for line in log_output.split('\n'): + if line.startswith('File:'): + self.log_text.insert(tk.END, "File: ", "header") + self.log_text.insert(tk.END, line[5:] + "\n", "info") + elif line.startswith('Session Time:'): + self.log_text.insert(tk.END, "Session Time: ", "header") + self.log_text.insert(tk.END, line[13:] + "\n", "highlight") + elif line.startswith('Total'): + self.log_text.insert(tk.END, line + "\n", "success") + elif line.strip(): + self.log_text.insert(tk.END, line + "\n", "info") + + self.log_text.see(tk.END) def _update_result_display(self): """Update the result entry based on selected format.""" @@ -254,13 +469,12 @@ def _copy_to_clipboard(self): if result: self.root.clipboard_clear() self.root.clipboard_append(result) - self.status_var.set("Copied to clipboard!") - self.root.after(2000, lambda: self.status_var.set("Ready")) + self._update_status("Copied to clipboard!", "success") + self.root.after(2000, lambda: self._update_status("Ready", "info")) def _load_icon(self): """Load the clipboard icon for the copy button.""" try: - # Try to find the icon in various locations script_dir = os.path.dirname(os.path.abspath(__file__)) icon_paths = [ os.path.join(script_dir, "resources", "clipboard.png"), @@ -273,7 +487,7 @@ def _load_icon(self): icon = Image.open(icon_path) icon = icon.resize((16, 16), Image.LANCZOS) self.copy_icon = ImageTk.PhotoImage(icon) - self.copy_btn.config(image=self.copy_icon, compound=tk.LEFT) + self.copy_btn.configure(image=self.copy_icon, compound=LEFT) break except Exception: pass # Icon loading is optional From 33f3200c406f7012ec3b88d42af1e99536adc419 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 04:37:55 +0000 Subject: [PATCH 2/9] Update README with v2.0 design info and add VNGD to acknowledgements - Update framework description to mention ttkbootstrap - Add v2.0 Design section describing new UI features - Add VNGD (https://vngd.net/) to acknowledgements --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 375b2bb..e2bd02a 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,18 @@ Since Star Citizen doesn't expose playtime statistics in-game, SCPlay parses you | Version | Platform | Framework | Best For | |---------|----------|-----------|----------| -| **Python** | Windows, Linux, macOS | Tkinter | Cross-platform users | +| **Python** | Windows, Linux, macOS | ttkbootstrap (Modern Tkinter) | Cross-platform users | | **C#** | Windows | WinForms | Windows-only users | +### v2.0 Design + +Both versions feature a modernized dark UI with Star Citizen-inspired aesthetics: +- Dark navy theme with cyan/green accents +- Grouped panels (Configuration, Processing Log, Results) +- Color-coded log output +- Progress indicators during calculation +- Status bar with colored feedback + --- ## Installation @@ -228,6 +237,7 @@ Distributed under the MIT License. See `LICENSE.txt` for more information. - Star Citizen community - All contributors +- [VNGD](https://vngd.net/) --- From 477ed8ea196e7ec9717dad1288348d800d42521c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Nov 2025 04:40:28 +0000 Subject: [PATCH 3/9] Add Linux Python build to CI and update README CI changes: - Add new build-python-linux job on ubuntu-latest - Install python3-tk system dependency for Linux - Use colon separator for PyInstaller --add-data on Linux - Update create-release to produce 3 zips README changes: - Update Available Downloads table with 3 release options - Add note for macOS users to run from source - Update CI/CD section to mention 3 executables --- .github/workflows/build.yml | 68 +++++++++++++++++++++++++++++++------ README.md | 17 ++++++---- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e28556b..4066cc8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,8 +11,8 @@ on: workflow_dispatch: jobs: - build-python: - name: Build Python Executable + build-python-windows: + name: Build Python (Windows) runs-on: windows-latest steps: @@ -38,14 +38,52 @@ jobs: --add-data "resources;resources" ` sc_main.py - - name: Upload Python artifact + - name: Upload Python Windows artifact uses: actions/upload-artifact@v4 with: name: SCPlaytime-Python-Windows path: python/dist/SCPlaytime.exe + build-python-linux: + name: Build Python (Linux) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y python3-tk + + - name: Install dependencies + working-directory: python + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install pyinstaller + + - name: Build executable + working-directory: python + run: | + pyinstaller --onefile --windowed --name "SCPlaytime" \ + --add-data "resources:resources" \ + sc_main.py + + - name: Upload Python Linux artifact + uses: actions/upload-artifact@v4 + with: + name: SCPlaytime-Python-Linux + path: python/dist/SCPlaytime + build-csharp: - name: Build C# Executable + name: Build C# (Windows) runs-on: windows-latest steps: @@ -74,7 +112,7 @@ jobs: create-release: name: Create Release - needs: [build-python, build-csharp] + needs: [build-python-windows, build-python-linux, build-csharp] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') @@ -82,11 +120,17 @@ jobs: contents: write steps: - - name: Download Python artifact + - name: Download Python Windows artifact uses: actions/download-artifact@v4 with: name: SCPlaytime-Python-Windows - path: artifacts/python + path: artifacts/python-windows + + - name: Download Python Linux artifact + uses: actions/download-artifact@v4 + with: + name: SCPlaytime-Python-Linux + path: artifacts/python-linux - name: Download C# artifact uses: actions/download-artifact@v4 @@ -97,15 +141,17 @@ jobs: - name: Create release archives run: | cd artifacts - zip -r ../SCPlaytime-Python-${{ github.ref_name }}.zip python/ - zip -r ../SCPlaytime-CSharp-${{ github.ref_name }}.zip csharp/ + zip -r ../SCPlaytime-Python-Windows-${{ github.ref_name }}.zip python-windows/ + zip -r ../SCPlaytime-Python-Linux-${{ github.ref_name }}.zip python-linux/ + zip -r ../SCPlaytime-CSharp-Windows-${{ github.ref_name }}.zip csharp/ - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: files: | - SCPlaytime-Python-${{ github.ref_name }}.zip - SCPlaytime-CSharp-${{ github.ref_name }}.zip + SCPlaytime-Python-Windows-${{ github.ref_name }}.zip + SCPlaytime-Python-Linux-${{ github.ref_name }}.zip + SCPlaytime-CSharp-Windows-${{ github.ref_name }}.zip generate_release_notes: true draft: false prerelease: false diff --git a/README.md b/README.md index e2bd02a..795fda2 100644 --- a/README.md +++ b/README.md @@ -45,15 +45,18 @@ Since Star Citizen doesn't expose playtime statistics in-game, SCPlay parses you ### Download Pre-built Executable 1. Go to [Releases](https://github.com/ckuma/scplay/releases) -2. Download the latest `.zip` for your preferred version +2. Download the appropriate `.zip` for your platform 3. Extract and run -### Available Versions +### Available Downloads -| Version | Platform | Framework | Best For | -|---------|----------|-----------|----------| -| **Python** | Windows, Linux, macOS | ttkbootstrap (Modern Tkinter) | Cross-platform users | -| **C#** | Windows | WinForms | Windows-only users | +| Release | Platform | Description | +|---------|----------|-------------| +| `SCPlaytime-Python-Windows` | Windows | Python executable (.exe) | +| `SCPlaytime-Python-Linux` | Linux | Python executable (binary) | +| `SCPlaytime-CSharp-Windows` | Windows | C# WinForms executable (.exe) | + +**Note:** macOS users should run the Python version from source (see Installation below). ### v2.0 Design @@ -195,7 +198,7 @@ Output: `bin/Release/StarCitizenPlaytimeCalculator.exe` This repository uses GitHub Actions to automatically build releases: -- **On tag push** (`v*`) - Creates a GitHub Release with both executables +- **On tag push** (`v*`) - Creates a GitHub Release with 3 executables (Python Windows, Python Linux, C# Windows) - **On PR** - Validates builds To create a new release: From 0bcf4d302cda9308a7fe239d2351afa3bc22dfe1 Mon Sep 17 00:00:00 2001 From: Ckuma Date: Wed, 19 Nov 2025 00:14:38 -0500 Subject: [PATCH 4/9] Fix ttkbootstrap LabelFrame error and tone down C# GUI colors --- csharp/Form1.Designer.cs | 10 +++++----- csharp/Form1.cs | 2 +- python/sc_main.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/csharp/Form1.Designer.cs b/csharp/Form1.Designer.cs index 9823106..177fb72 100644 --- a/csharp/Form1.Designer.cs +++ b/csharp/Form1.Designer.cs @@ -56,9 +56,9 @@ private void InitializeComponent() // // Form colors // - Color bgDark = Color.FromArgb(26, 26, 46); - Color bgPanel = Color.FromArgb(22, 33, 62); - Color accentCyan = Color.FromArgb(0, 212, 255); + Color bgDark = Color.FromArgb(30, 30, 35); + Color bgPanel = Color.FromArgb(42, 42, 50); + Color accentCyan = Color.FromArgb(130, 170, 210); Color accentGreen = Color.FromArgb(0, 255, 136); Color textLight = Color.FromArgb(224, 224, 224); Color textSecondary = Color.FromArgb(160, 160, 160); @@ -142,7 +142,7 @@ private void InitializeComponent() // // btnRefresh // - this.btnRefresh.BackColor = Color.FromArgb(15, 52, 96); + this.btnRefresh.BackColor = Color.FromArgb(55, 60, 70); this.btnRefresh.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.btnRefresh.FlatAppearance.BorderColor = accentCyan; this.btnRefresh.ForeColor = accentCyan; @@ -324,7 +324,7 @@ private void InitializeComponent() // btnCopyToClipboard // this.btnCopyToClipboard.Anchor = System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right; - this.btnCopyToClipboard.BackColor = Color.FromArgb(15, 52, 96); + this.btnCopyToClipboard.BackColor = Color.FromArgb(55, 60, 70); this.btnCopyToClipboard.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.btnCopyToClipboard.FlatAppearance.BorderColor = accentGreen; this.btnCopyToClipboard.ForeColor = accentGreen; diff --git a/csharp/Form1.cs b/csharp/Form1.cs index 2cf42ed..8696fb7 100644 --- a/csharp/Form1.cs +++ b/csharp/Form1.cs @@ -15,7 +15,7 @@ public partial class Form1 : Form private Dictionary detectedPaths = new Dictionary(); // Colors for formatting - private readonly Color accentCyan = Color.FromArgb(0, 212, 255); + private readonly Color accentCyan = Color.FromArgb(130, 170, 210); private readonly Color accentGreen = Color.FromArgb(0, 255, 136); private readonly Color accentGold = Color.FromArgb(255, 215, 0); private readonly Color textLight = Color.FromArgb(224, 224, 224); diff --git a/python/sc_main.py b/python/sc_main.py index fb8cd3d..9746252 100644 --- a/python/sc_main.py +++ b/python/sc_main.py @@ -91,7 +91,7 @@ def _create_header(self): def _create_config_section(self): """Create the configuration/input section.""" - config_frame = ttk.LabelFrame( + config_frame = ttk.Labelframe( self.mainframe, text="Configuration", bootstyle="info", @@ -164,7 +164,7 @@ def _create_config_section(self): def _create_log_section(self): """Create the log output section.""" - log_frame = ttk.LabelFrame( + log_frame = ttk.Labelframe( self.mainframe, text="Processing Log", bootstyle="secondary", @@ -217,7 +217,7 @@ def _create_log_section(self): def _create_results_section(self): """Create the results display section.""" - results_frame = ttk.LabelFrame( + results_frame = ttk.Labelframe( self.mainframe, text="Results", bootstyle="success", From 52191ade074e0e17634f982b931a59fab2bfb9c7 Mon Sep 17 00:00:00 2001 From: Ckuma Date: Wed, 19 Nov 2025 00:14:38 -0500 Subject: [PATCH 5/9] Add linting and type checking to CI workflow --- .github/workflows/build.yml | 32 +++++++++++++++++++++++++++++++- python/pyproject.toml | 24 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 python/pyproject.toml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4066cc8..5219480 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,8 +11,36 @@ on: workflow_dispatch: jobs: + lint: + name: Lint & Type Check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install lint dependencies + run: | + python -m pip install --upgrade pip + pip install ruff pyright + pip install -r python/requirements.txt + + - name: Run ruff linter + working-directory: python + run: ruff check . + + - name: Run pyright type checker + working-directory: python + run: pyright + build-python-windows: name: Build Python (Windows) + needs: [lint] runs-on: windows-latest steps: @@ -46,6 +74,7 @@ jobs: build-python-linux: name: Build Python (Linux) + needs: [lint] runs-on: ubuntu-latest steps: @@ -84,6 +113,7 @@ jobs: build-csharp: name: Build C# (Windows) + needs: [lint] runs-on: windows-latest steps: @@ -102,7 +132,7 @@ jobs: - name: Build solution working-directory: csharp - run: msbuild StarCitizenPlaytimeCalculator.sln /p:Configuration=Release /p:Platform="Any CPU" + run: msbuild StarCitizenPlaytimeCalculator.sln /p:Configuration=Release /p:Platform="Any CPU" /p:TreatWarningsAsErrors=true /p:RunAnalyzersDuringBuild=true - name: Upload C# artifact uses: actions/upload-artifact@v4 diff --git a/python/pyproject.toml b/python/pyproject.toml new file mode 100644 index 0000000..3bed7d4 --- /dev/null +++ b/python/pyproject.toml @@ -0,0 +1,24 @@ +[tool.ruff] +line-length = 120 +target-version = "py311" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "W", # pycodestyle warnings + "B", # flake8-bugbear + "I", # isort +] +ignore = [ + "E501", # line too long (handled by formatter) +] + +[tool.pyright] +pythonVersion = "3.11" +typeCheckingMode = "basic" +reportMissingImports = true +reportMissingTypeStubs = false +reportUnusedImport = true +reportUnusedVariable = true +reportAttributeAccessIssue = true From 44f17db7eac2e7f0b6d4c2876e6474ee1c040647 Mon Sep 17 00:00:00 2001 From: Ckuma Date: Wed, 19 Nov 2025 00:22:05 -0500 Subject: [PATCH 6/9] Fix ruff linting errors in Python code --- python/sc_main.py | 18 ++++++++++-------- python/sc_playtime.py | 6 +++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/python/sc_main.py b/python/sc_main.py index 9746252..cb9c738 100644 --- a/python/sc_main.py +++ b/python/sc_main.py @@ -1,13 +1,14 @@ -import ttkbootstrap as ttk -from ttkbootstrap.constants import * -from ttkbootstrap.dialogs import Messagebox -from tkinter import filedialog -import tkinter as tk import os -import sys import threading -import sc_playtime +import tkinter as tk +from tkinter import filedialog + +import ttkbootstrap as ttk from PIL import Image, ImageTk +from ttkbootstrap.constants import BOTH, LEFT, RIGHT, VERTICAL, YES, X, Y +from ttkbootstrap.dialogs import Messagebox + +import sc_playtime # Use high DPI awareness for better rendering on Windows try: @@ -386,7 +387,8 @@ def _do_calculation(self, path): # Update UI in main thread self.root.after(0, lambda: self._calculation_complete(log_output, file_count)) except Exception as e: - self.root.after(0, lambda: self._calculation_error(str(e))) + error_msg = str(e) + self.root.after(0, lambda msg=error_msg: self._calculation_error(msg)) def _calculation_complete(self, log_output, file_count): """Handle calculation completion.""" diff --git a/python/sc_playtime.py b/python/sc_playtime.py index cdc222f..1c95760 100644 --- a/python/sc_playtime.py +++ b/python/sc_playtime.py @@ -1,10 +1,10 @@ -import os import glob +import os import platform -import dateutil.parser import re from datetime import timedelta -from pathlib import Path + +import dateutil.parser # Compile regular expression for efficiency date_pattern = re.compile(r'^<(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}).*') From 7bcb965b729b0850ade88564d61cc65bba35a030 Mon Sep 17 00:00:00 2001 From: Ckuma Date: Wed, 19 Nov 2025 00:23:20 -0500 Subject: [PATCH 7/9] Fix pyright type checking errors --- python/sc_main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/sc_main.py b/python/sc_main.py index cb9c738..1aa2ea0 100644 --- a/python/sc_main.py +++ b/python/sc_main.py @@ -12,7 +12,7 @@ # Use high DPI awareness for better rendering on Windows try: - from ctypes import windll + from ctypes import windll # type: ignore[attr-defined] windll.shcore.SetProcessDpiAwareness(1) except Exception: pass # Fails on non-Windows systems @@ -487,7 +487,7 @@ def _load_icon(self): for icon_path in icon_paths: if os.path.exists(icon_path): icon = Image.open(icon_path) - icon = icon.resize((16, 16), Image.LANCZOS) + icon = icon.resize((16, 16), Image.Resampling.LANCZOS) self.copy_icon = ImageTk.PhotoImage(icon) self.copy_btn.configure(image=self.copy_icon, compound=LEFT) break From 72fe0d6b5df3d07e9707c99eda20e3759091a04b Mon Sep 17 00:00:00 2001 From: Ckuma Date: Wed, 19 Nov 2025 00:24:47 -0500 Subject: [PATCH 8/9] Parallelize workflow: C# build runs alongside Python lint --- .github/workflows/build.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5219480..cbefbfe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,8 +11,8 @@ on: workflow_dispatch: jobs: - lint: - name: Lint & Type Check + lint-python: + name: Lint Python runs-on: ubuntu-latest steps: @@ -40,7 +40,7 @@ jobs: build-python-windows: name: Build Python (Windows) - needs: [lint] + needs: [lint-python] runs-on: windows-latest steps: @@ -74,7 +74,7 @@ jobs: build-python-linux: name: Build Python (Linux) - needs: [lint] + needs: [lint-python] runs-on: ubuntu-latest steps: @@ -113,7 +113,6 @@ jobs: build-csharp: name: Build C# (Windows) - needs: [lint] runs-on: windows-latest steps: From 25497c4d87456dae97b1f66d344a36f5d8697cf3 Mon Sep 17 00:00:00 2001 From: Ckuma Date: Wed, 19 Nov 2025 00:28:02 -0500 Subject: [PATCH 9/9] Tone down Python GUI colors to neutral palette --- python/sc_main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/python/sc_main.py b/python/sc_main.py index 1aa2ea0..9498ea4 100644 --- a/python/sc_main.py +++ b/python/sc_main.py @@ -95,7 +95,7 @@ def _create_config_section(self): config_frame = ttk.Labelframe( self.mainframe, text="Configuration", - bootstyle="info", + bootstyle="secondary", padding=15 ) config_frame.pack(fill=X, pady=(0, 15)) @@ -116,7 +116,7 @@ def _create_config_section(self): env_row, state='readonly', width=25, - bootstyle="info" + bootstyle="secondary" ) self.env_combobox.pack(side=LEFT, padx=(0, 10)) self.env_combobox.bind("<>", self._on_environment_change) @@ -125,7 +125,7 @@ def _create_config_section(self): env_row, text="Refresh", command=self._detect_installations, - bootstyle="info-outline", + bootstyle="secondary-outline", width=10 ) refresh_btn.pack(side=LEFT) @@ -221,7 +221,7 @@ def _create_results_section(self): results_frame = ttk.Labelframe( self.mainframe, text="Results", - bootstyle="success", + bootstyle="secondary", padding=15 ) results_frame.pack(fill=X, pady=(0, 15)) @@ -243,7 +243,7 @@ def _create_results_section(self): values=["Default", "Hours", "Minutes", "Seconds", "Days"], state='readonly', width=10, - bootstyle="success" + bootstyle="secondary" ) self.unit_combobox.set("Default") self.unit_combobox.pack(side=LEFT, padx=(0, 20)) @@ -261,7 +261,7 @@ def _create_results_section(self): self.result_entry = ttk.Entry( results_row, font=("Segoe UI", 12, "bold"), - bootstyle="success" + bootstyle="secondary" ) self.result_entry.pack(side=LEFT, fill=X, expand=YES, padx=(0, 10)) @@ -270,7 +270,7 @@ def _create_results_section(self): results_row, text="Copy", command=self._copy_to_clipboard, - bootstyle="success-outline", + bootstyle="secondary-outline", width=8 ) self.copy_btn.pack(side=LEFT)