mirror of
https://github.com/FroggMaster/CreamInstaller.git
synced 2026-06-13 03:21:22 -07:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54592230c3 | |||
| 34cb3b862c | |||
| 8040e6bcdb | |||
| 593f396c54 | |||
| 2f993bfe3b | |||
| e9f8222d8e | |||
| 68842aad9f | |||
| 1d5dc4ac8c | |||
| 31ca8a947f | |||
| 558612f098 | |||
| b7067c2621 | |||
| fe55efc072 |
@@ -75,11 +75,14 @@ internal sealed class CustomTreeView : TreeView
|
||||
|
||||
private void DrawTreeNode(object sender, DrawTreeNodeEventArgs e)
|
||||
{
|
||||
e.DrawDefault = true;
|
||||
TreeNode node = e.Node;
|
||||
if (node is not { IsVisible: true })
|
||||
{
|
||||
e.DrawDefault = true;
|
||||
return;
|
||||
}
|
||||
|
||||
bool dark = Program.DarkModeEnabled;
|
||||
bool highlighted = (e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected && Focused;
|
||||
Graphics graphics = e.Graphics;
|
||||
|
||||
@@ -103,12 +106,95 @@ internal sealed class CustomTreeView : TreeView
|
||||
}
|
||||
}
|
||||
|
||||
Form form = FindForm();
|
||||
|
||||
if (dark && CheckBoxes)
|
||||
{
|
||||
// In dark mode we take full ownership of the row so the system never
|
||||
// gets a chance to paint a light-background checkbox.
|
||||
e.DrawDefault = false;
|
||||
|
||||
// Row background
|
||||
Rectangle rowRect = new(0, node.Bounds.Top, ClientSize.Width, node.Bounds.Height);
|
||||
graphics.FillRectangle(highlighted ? selectionBrush : backBrush, rowRect);
|
||||
|
||||
// Node text
|
||||
Font nodeFont = node.NodeFont ?? Font;
|
||||
Color textColor = Enabled ? ForeColor : SystemColors.GrayText;
|
||||
TextRenderer.DrawText(graphics, node.Text, nodeFont,
|
||||
new Point(node.Bounds.Left, node.Bounds.Top + 1), textColor, TextFormatFlags.Default);
|
||||
|
||||
// Checkbox glyph – pure GDI so it matches the dark-themed CheckBox controls
|
||||
CheckBoxState cbState = node.Checked
|
||||
? (Enabled ? CheckBoxState.CheckedNormal : CheckBoxState.CheckedDisabled)
|
||||
: (Enabled ? CheckBoxState.UncheckedNormal : CheckBoxState.UncheckedDisabled);
|
||||
Size cbSize = CheckBoxRenderer.GetGlyphSize(graphics, cbState);
|
||||
int cbX = node.Bounds.Left - cbSize.Width - 2;
|
||||
int cbY = node.Bounds.Top + node.Bounds.Height / 2 - cbSize.Height / 2;
|
||||
ThemeManager.DrawDarkCheckBox(graphics, new Point(cbX, cbY), cbSize, node.Checked, Enabled);
|
||||
|
||||
// Expander glyph (expand/collapse) – the system skips this when DrawDefault=false
|
||||
if (node.Nodes.Count > 0)
|
||||
{
|
||||
int indent = Indent;
|
||||
int level = node.Level;
|
||||
int glyphSize = 13;
|
||||
int glyphX = level * indent + (indent - glyphSize) / 2 + (ShowRootLines ? 0 : -indent);
|
||||
int glyphY = node.Bounds.Top + node.Bounds.Height / 2 - glyphSize / 2;
|
||||
Rectangle glyphRect = new(glyphX, glyphY, glyphSize, glyphSize);
|
||||
Color glyphBorder = Color.FromArgb(0x6B, 0x6B, 0x6B);
|
||||
Color glyphBack = Color.FromArgb(0x2D, 0x2D, 0x2D);
|
||||
Color glyphFore = Color.FromArgb(0xD4, 0xD4, 0xD4);
|
||||
using (SolidBrush backFill = new(glyphBack))
|
||||
graphics.FillRectangle(backFill, glyphRect);
|
||||
using (Pen borderPen = new(glyphBorder))
|
||||
graphics.DrawRectangle(borderPen, glyphRect);
|
||||
int mid = glyphY + glyphSize / 2;
|
||||
int left = glyphX + 3;
|
||||
int right = glyphX + glyphSize - 3;
|
||||
using (Pen linePen = new(glyphFore))
|
||||
{
|
||||
graphics.DrawLine(linePen, left, mid, right, mid); // horizontal minus
|
||||
if (!node.IsExpanded)
|
||||
graphics.DrawLine(linePen, glyphX + glyphSize / 2, glyphY + 3, glyphX + glyphSize / 2, glyphY + glyphSize - 3); // vertical plus
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (highlighted && CheckBoxes)
|
||||
{
|
||||
// In light mode, take ownership of the row when selected so the
|
||||
// highlight fills the full width (same approach as dark mode).
|
||||
e.DrawDefault = false;
|
||||
|
||||
Rectangle rowRect = new(0, node.Bounds.Top, ClientSize.Width, node.Bounds.Height);
|
||||
graphics.FillRectangle(selectionBrush, rowRect);
|
||||
|
||||
Font nodeFont = node.NodeFont ?? Font;
|
||||
Color textColor = Enabled ? ForeColor : SystemColors.GrayText;
|
||||
TextRenderer.DrawText(graphics, node.Text, nodeFont,
|
||||
new Point(node.Bounds.Left, node.Bounds.Top + 1), textColor, TextFormatFlags.Default);
|
||||
|
||||
CheckBoxState cbState = node.Checked
|
||||
? (Enabled ? CheckBoxState.CheckedNormal : CheckBoxState.CheckedDisabled)
|
||||
: (Enabled ? CheckBoxState.UncheckedNormal : CheckBoxState.UncheckedDisabled);
|
||||
Size cbSize = CheckBoxRenderer.GetGlyphSize(graphics, cbState);
|
||||
Point cbPoint = new(node.Bounds.Left - cbSize.Width - 2,
|
||||
node.Bounds.Top + node.Bounds.Height / 2 - cbSize.Height / 2);
|
||||
CheckBoxRenderer.DrawCheckBox(graphics, cbPoint, cbState);
|
||||
}
|
||||
else
|
||||
{
|
||||
e.DrawDefault = true;
|
||||
}
|
||||
}
|
||||
|
||||
Font font = node.NodeFont ?? Font;
|
||||
Brush brush = highlighted ? (Brush)selectionBrush : backBrush;
|
||||
Rectangle bounds = node.Bounds;
|
||||
Rectangle selectionBounds = bounds;
|
||||
|
||||
Form form = FindForm();
|
||||
if (form is not SelectForm and not SelectDialogForm)
|
||||
return;
|
||||
|
||||
@@ -168,18 +254,19 @@ internal sealed class CustomTreeView : TreeView
|
||||
graphics.FillRectangle(brush, bounds);
|
||||
}
|
||||
|
||||
CheckBoxState checkBoxState = selection.UseProxy
|
||||
? Enabled ? CheckBoxState.CheckedPressed : CheckBoxState.CheckedDisabled
|
||||
: Enabled
|
||||
? CheckBoxState.UncheckedPressed
|
||||
: CheckBoxState.UncheckedDisabled;
|
||||
size = CheckBoxRenderer.GetGlyphSize(graphics, checkBoxState);
|
||||
CheckBoxState proxyState = selection.UseProxy
|
||||
? (Enabled ? CheckBoxState.CheckedNormal : CheckBoxState.CheckedDisabled)
|
||||
: (Enabled ? CheckBoxState.UncheckedNormal : CheckBoxState.UncheckedDisabled);
|
||||
size = CheckBoxRenderer.GetGlyphSize(graphics, proxyState);
|
||||
bounds = bounds with { X = bounds.X + bounds.Width, Width = size.Width };
|
||||
selectionBounds = new(selectionBounds.Location, selectionBounds.Size + bounds.Size with { Height = 0 });
|
||||
Rectangle checkBoxBounds = bounds;
|
||||
graphics.FillRectangle(backBrush, bounds);
|
||||
graphics.FillRectangle(brush, bounds);
|
||||
point = new(bounds.Left, bounds.Top + bounds.Height / 2 - size.Height / 2 - 1);
|
||||
CheckBoxRenderer.DrawCheckBox(graphics, point, checkBoxState);
|
||||
if (dark)
|
||||
ThemeManager.DrawDarkCheckBox(graphics, point, size, selection.UseProxy, Enabled);
|
||||
else
|
||||
CheckBoxRenderer.DrawCheckBox(graphics, point, proxyState);
|
||||
|
||||
text = ProxyToggleString;
|
||||
size = TextRenderer.MeasureText(graphics, text, font);
|
||||
@@ -187,7 +274,7 @@ internal sealed class CustomTreeView : TreeView
|
||||
bounds = bounds with { X = bounds.X + bounds.Width, Width = size.Width + left };
|
||||
selectionBounds = new(selectionBounds.Location, selectionBounds.Size + bounds.Size with { Height = 0 });
|
||||
checkBoxBounds = new(checkBoxBounds.Location, checkBoxBounds.Size + bounds.Size with { Height = 0 });
|
||||
graphics.FillRectangle(backBrush, bounds);
|
||||
graphics.FillRectangle(brush, bounds);
|
||||
point = new(bounds.Location.X - 1 + left, bounds.Location.Y + 1);
|
||||
TextRenderer.DrawText(graphics, text, font, point,
|
||||
Enabled ? ThemeManager.CustomTreeViewProxyColor : ThemeManager.CustomTreeViewDisabledProxyColor,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
|
||||
<UseWindowsForms>True</UseWindowsForms>
|
||||
<ApplicationIcon>Resources\program.ico</ApplicationIcon>
|
||||
<Version>5.0.2.0</Version>
|
||||
<Copyright>2025, FroggMaster (https://github.com/FroggMaster)</Copyright>
|
||||
<Version>5.0.2.3</Version>
|
||||
<Copyright>2026, FroggMaster (https://github.com/FroggMaster)</Copyright>
|
||||
<Company>CreamInstaller</Company>
|
||||
<Product>Automatic DLC Unlocker Installer & Configuration Generator</Product>
|
||||
<StartupObject>CreamInstaller.Program</StartupObject>
|
||||
|
||||
+20
-3
@@ -32,16 +32,31 @@ partial class DebugForm
|
||||
private void InitializeComponent()
|
||||
{
|
||||
debugTextBox = new RichTextBox();
|
||||
testGameButton = new Button();
|
||||
SuspendLayout();
|
||||
//
|
||||
// testGameButton
|
||||
//
|
||||
testGameButton.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
|
||||
testGameButton.AutoSize = true;
|
||||
testGameButton.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
testGameButton.Location = new System.Drawing.Point(10, 10);
|
||||
testGameButton.Name = "testGameButton";
|
||||
testGameButton.Padding = new Padding(3, 0, 3, 0);
|
||||
testGameButton.Size = new System.Drawing.Size(540, 25);
|
||||
testGameButton.TabIndex = 1;
|
||||
testGameButton.Text = "Test Game";
|
||||
testGameButton.UseVisualStyleBackColor = true;
|
||||
testGameButton.Click += OnTestGame;
|
||||
//
|
||||
// debugTextBox
|
||||
//
|
||||
debugTextBox.Dock = DockStyle.Fill;
|
||||
debugTextBox.Location = new System.Drawing.Point(10, 10);
|
||||
debugTextBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||
debugTextBox.Location = new System.Drawing.Point(10, 41);
|
||||
debugTextBox.Name = "debugTextBox";
|
||||
debugTextBox.ReadOnly = true;
|
||||
debugTextBox.ScrollBars = RichTextBoxScrollBars.ForcedBoth;
|
||||
debugTextBox.Size = new System.Drawing.Size(540, 317);
|
||||
debugTextBox.Size = new System.Drawing.Size(540, 286);
|
||||
debugTextBox.TabIndex = 0;
|
||||
debugTextBox.TabStop = false;
|
||||
debugTextBox.Text = "";
|
||||
@@ -52,6 +67,7 @@ partial class DebugForm
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new System.Drawing.Size(560, 337);
|
||||
ControlBox = false;
|
||||
Controls.Add(testGameButton);
|
||||
Controls.Add(debugTextBox);
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
MaximizeBox = false;
|
||||
@@ -68,4 +84,5 @@ partial class DebugForm
|
||||
#endregion
|
||||
|
||||
private RichTextBox debugTextBox;
|
||||
private Button testGameButton;
|
||||
}
|
||||
@@ -81,4 +81,10 @@ internal sealed partial class DebugForm : CustomForm
|
||||
debugTextBox.AppendText(text, color, true);
|
||||
});
|
||||
}
|
||||
|
||||
private void OnTestGame(object sender, EventArgs e)
|
||||
{
|
||||
using TestGameForm form = new(this);
|
||||
_ = form.ShowDialog(this);
|
||||
}
|
||||
}
|
||||
+15
-2
@@ -33,6 +33,7 @@ namespace CreamInstaller.Forms
|
||||
saveButton = new Button();
|
||||
uninstallAllButton = new Button();
|
||||
selectionTreeView = new CustomTreeView();
|
||||
filterTextBox = new System.Windows.Forms.TextBox();
|
||||
groupBox.SuspendLayout();
|
||||
allCheckBoxFlowPanel.SuspendLayout();
|
||||
SuspendLayout();
|
||||
@@ -51,15 +52,25 @@ namespace CreamInstaller.Forms
|
||||
acceptButton.Text = "OK";
|
||||
acceptButton.UseVisualStyleBackColor = true;
|
||||
//
|
||||
// filterTextBox
|
||||
//
|
||||
filterTextBox.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
|
||||
filterTextBox.Location = new System.Drawing.Point(12, 14);
|
||||
filterTextBox.Name = "filterTextBox";
|
||||
filterTextBox.PlaceholderText = "Enter the name of a game to search";
|
||||
filterTextBox.Size = new System.Drawing.Size(524, 23);
|
||||
filterTextBox.TabIndex = 0;
|
||||
filterTextBox.TextChanged += OnFilterTextChanged;
|
||||
//
|
||||
// groupBox
|
||||
//
|
||||
groupBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||
groupBox.Controls.Add(selectionTreeView);
|
||||
groupBox.Controls.Add(allCheckBoxFlowPanel);
|
||||
groupBox.Location = new System.Drawing.Point(12, 12);
|
||||
groupBox.Location = new System.Drawing.Point(12, 43);
|
||||
groupBox.MinimumSize = new System.Drawing.Size(240, 40);
|
||||
groupBox.Name = "groupBox";
|
||||
groupBox.Size = new System.Drawing.Size(524, 225);
|
||||
groupBox.Size = new System.Drawing.Size(524, 194);
|
||||
groupBox.TabIndex = 3;
|
||||
groupBox.TabStop = false;
|
||||
groupBox.Text = "Choices";
|
||||
@@ -188,6 +199,7 @@ namespace CreamInstaller.Forms
|
||||
Controls.Add(cancelButton);
|
||||
Controls.Add(acceptButton);
|
||||
Controls.Add(groupBox);
|
||||
Controls.Add(filterTextBox);
|
||||
FormBorderStyle = FormBorderStyle.FixedSingle;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
@@ -215,5 +227,6 @@ namespace CreamInstaller.Forms
|
||||
private Button saveButton;
|
||||
private CheckBox sortCheckBox;
|
||||
private Button uninstallAllButton;
|
||||
private System.Windows.Forms.TextBox filterTextBox;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ namespace CreamInstaller.Forms;
|
||||
internal sealed partial class SelectDialogForm : CustomForm
|
||||
{
|
||||
private readonly List<(Platform platform, string id, string name)> selected = new();
|
||||
private readonly List<(Platform platform, string id, string name, bool alreadySelected)> allChoices = new();
|
||||
|
||||
internal SelectDialogForm(IWin32Window owner) : base(owner)
|
||||
{
|
||||
@@ -28,12 +29,12 @@ internal sealed partial class SelectDialogForm : CustomForm
|
||||
allCheckBox.Enabled = false;
|
||||
acceptButton.Enabled = false;
|
||||
selectionTreeView.AfterCheck += OnTreeNodeChecked;
|
||||
foreach ((Platform platform, string id, string name, bool alreadySelected) in potentialChoices)
|
||||
{
|
||||
TreeNode node = new() { Tag = platform, Name = id, Text = name, Checked = alreadySelected };
|
||||
OnTreeNodeChecked(node);
|
||||
_ = selectionTreeView.Nodes.Add(node);
|
||||
}
|
||||
allChoices.Clear();
|
||||
allChoices.AddRange(potentialChoices);
|
||||
foreach ((Platform platform, string id, string name, bool alreadySelected) in allChoices)
|
||||
if (alreadySelected)
|
||||
selected.Add((platform, id, name));
|
||||
ApplyFilter();
|
||||
|
||||
if (selected.Count < 1)
|
||||
OnLoad(null, null);
|
||||
@@ -70,6 +71,32 @@ internal sealed partial class SelectDialogForm : CustomForm
|
||||
allCheckBox.CheckedChanged += OnAllCheckBoxChanged;
|
||||
}
|
||||
|
||||
private void OnFilterTextChanged(object sender, EventArgs e) => ApplyFilter();
|
||||
|
||||
private void ApplyFilter()
|
||||
{
|
||||
string filter = filterTextBox.Text.Trim();
|
||||
selectionTreeView.AfterCheck -= OnTreeNodeChecked;
|
||||
selectionTreeView.BeginUpdate();
|
||||
selectionTreeView.Nodes.Clear();
|
||||
bool hasSelections = selected.Count > 0;
|
||||
foreach ((Platform platform, string id, string name, bool alreadySelected) in allChoices)
|
||||
{
|
||||
if (filter.Length > 0 && name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
continue;
|
||||
bool checkedState = hasSelections
|
||||
? selected.Any(s => s.platform == platform && s.id == id)
|
||||
: alreadySelected;
|
||||
TreeNode node = new() { Tag = platform, Name = id, Text = name, Checked = checkedState };
|
||||
_ = selectionTreeView.Nodes.Add(node);
|
||||
}
|
||||
selectionTreeView.EndUpdate();
|
||||
selectionTreeView.AfterCheck += OnTreeNodeChecked;
|
||||
allCheckBox.CheckedChanged -= OnAllCheckBoxChanged;
|
||||
allCheckBox.Checked = selectionTreeView.Nodes.Count > 0 && selectionTreeView.Nodes.Cast<TreeNode>().All(n => n.Checked);
|
||||
allCheckBox.CheckedChanged += OnAllCheckBoxChanged;
|
||||
}
|
||||
|
||||
private void OnResize(object s, EventArgs e)
|
||||
=> Text = TextRenderer.MeasureText(Program.ApplicationName, Font).Width > Size.Width - 100
|
||||
? Program.ApplicationNameShort
|
||||
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
using System.ComponentModel;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace CreamInstaller.Forms;
|
||||
|
||||
partial class TestGameForm
|
||||
{
|
||||
private IContainer components = null;
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && components is not null)
|
||||
components.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
// All coordinates are based on ClientSize = 560 x 330
|
||||
// Left margin = 12, right edge of usable area = 548 (560 - 12)
|
||||
// Usable width = 536
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
platformGroupBox = new GroupBox();
|
||||
steamRadioButton = new RadioButton();
|
||||
epicRadioButton = new RadioButton();
|
||||
appIdLabel = new Label();
|
||||
appIdTextBox = new TextBox();
|
||||
gameNameLabel = new Label();
|
||||
gameNameTextBox = new TextBox();
|
||||
epicSearchButton = new Button();
|
||||
epicResultsListBox = new ListBox();
|
||||
dlcGroupBox = new GroupBox();
|
||||
dlcListBox = new ListBox();
|
||||
dlcIdLabel = new Label();
|
||||
dlcIdTextBox = new TextBox();
|
||||
dlcNameLabel = new Label();
|
||||
dlcNameTextBox = new TextBox();
|
||||
addDlcButton = new Button();
|
||||
removeDlcButton = new Button();
|
||||
generateButton = new Button();
|
||||
clearButton = new Button();
|
||||
closeButton = new Button();
|
||||
statusLabel = new Label();
|
||||
platformGroupBox.SuspendLayout();
|
||||
dlcGroupBox.SuspendLayout();
|
||||
SuspendLayout();
|
||||
|
||||
// ── Platform group box ── y=8, h=44
|
||||
platformGroupBox.Location = new System.Drawing.Point(12, 8);
|
||||
platformGroupBox.Size = new System.Drawing.Size(536, 44);
|
||||
platformGroupBox.TabStop = false;
|
||||
platformGroupBox.Text = "Platform";
|
||||
platformGroupBox.Controls.Add(steamRadioButton);
|
||||
platformGroupBox.Controls.Add(epicRadioButton);
|
||||
|
||||
steamRadioButton.AutoSize = true;
|
||||
steamRadioButton.Checked = true;
|
||||
steamRadioButton.Location = new System.Drawing.Point(10, 17);
|
||||
steamRadioButton.TabStop = true;
|
||||
steamRadioButton.Text = "Steam";
|
||||
steamRadioButton.CheckedChanged += OnPlatformChanged;
|
||||
|
||||
epicRadioButton.AutoSize = true;
|
||||
epicRadioButton.Location = new System.Drawing.Point(80, 17);
|
||||
epicRadioButton.Text = "Epic";
|
||||
epicRadioButton.CheckedChanged += OnPlatformChanged;
|
||||
|
||||
// ── App ID row ── y=62
|
||||
appIdLabel.AutoSize = true;
|
||||
appIdLabel.Location = new System.Drawing.Point(12, 66);
|
||||
appIdLabel.Text = "App ID:";
|
||||
|
||||
appIdTextBox.Location = new System.Drawing.Point(105, 63);
|
||||
appIdTextBox.Size = new System.Drawing.Size(443, 23);
|
||||
appIdTextBox.PlaceholderText = "e.g. 480";
|
||||
|
||||
// ── Game Name row ── y=96
|
||||
gameNameLabel.AutoSize = true;
|
||||
gameNameLabel.Location = new System.Drawing.Point(12, 100);
|
||||
gameNameLabel.Text = "Game Name:";
|
||||
|
||||
// Steam: full width; Epic: leaves room for Search button (75px + 4px gap)
|
||||
gameNameTextBox.Location = new System.Drawing.Point(105, 97);
|
||||
gameNameTextBox.Size = new System.Drawing.Size(443, 23);
|
||||
|
||||
epicSearchButton.Location = new System.Drawing.Point(468, 97);
|
||||
epicSearchButton.Size = new System.Drawing.Size(80, 23);
|
||||
epicSearchButton.Text = "Search";
|
||||
epicSearchButton.Visible = false;
|
||||
epicSearchButton.Click += OnEpicSearch;
|
||||
|
||||
// ── Epic results list ── y=130, same slot as DLC group
|
||||
epicResultsListBox.Location = new System.Drawing.Point(12, 130);
|
||||
epicResultsListBox.Size = new System.Drawing.Size(536, 80);
|
||||
epicResultsListBox.Visible = false;
|
||||
epicResultsListBox.SelectedIndexChanged += OnEpicResultSelected;
|
||||
|
||||
// ── DLC group box ── y=130, h=130
|
||||
dlcGroupBox.Location = new System.Drawing.Point(12, 130);
|
||||
dlcGroupBox.Size = new System.Drawing.Size(536, 130);
|
||||
dlcGroupBox.TabStop = false;
|
||||
dlcGroupBox.Text = "DLC Entries (Steam only)";
|
||||
dlcGroupBox.Controls.Add(dlcListBox);
|
||||
dlcGroupBox.Controls.Add(dlcIdLabel);
|
||||
dlcGroupBox.Controls.Add(dlcIdTextBox);
|
||||
dlcGroupBox.Controls.Add(dlcNameLabel);
|
||||
dlcGroupBox.Controls.Add(dlcNameTextBox);
|
||||
dlcGroupBox.Controls.Add(addDlcButton);
|
||||
dlcGroupBox.Controls.Add(removeDlcButton);
|
||||
|
||||
dlcListBox.Location = new System.Drawing.Point(6, 20);
|
||||
dlcListBox.Size = new System.Drawing.Size(524, 60);
|
||||
|
||||
// DLC row inside group box — left-to-right:
|
||||
// "DLC ID:" label + 70px box + "DLC Name:" label + 160px box + "Add"(60) + "Remove"(70)
|
||||
// Total: ~48 + 70 + ~72 + 160 + 60 + 70 = 480 (fits in 524)
|
||||
dlcIdLabel.AutoSize = true;
|
||||
dlcIdLabel.Location = new System.Drawing.Point(6, 92);
|
||||
dlcIdLabel.Text = "DLC ID:";
|
||||
|
||||
dlcIdTextBox.Location = new System.Drawing.Point(62, 89);
|
||||
dlcIdTextBox.Size = new System.Drawing.Size(70, 23);
|
||||
dlcIdTextBox.PlaceholderText = "e.g. 12345";
|
||||
|
||||
dlcNameLabel.AutoSize = true;
|
||||
dlcNameLabel.Location = new System.Drawing.Point(140, 92);
|
||||
dlcNameLabel.Text = "DLC Name:";
|
||||
|
||||
dlcNameTextBox.Location = new System.Drawing.Point(216, 89);
|
||||
dlcNameTextBox.Size = new System.Drawing.Size(184, 23);
|
||||
dlcNameTextBox.PlaceholderText = "e.g. Test DLC";
|
||||
|
||||
addDlcButton.Location = new System.Drawing.Point(406, 89);
|
||||
addDlcButton.Size = new System.Drawing.Size(52, 23);
|
||||
addDlcButton.Text = "Add";
|
||||
addDlcButton.Click += OnAddDlc;
|
||||
|
||||
removeDlcButton.Location = new System.Drawing.Point(462, 89);
|
||||
removeDlcButton.Size = new System.Drawing.Size(62, 23);
|
||||
removeDlcButton.Text = "Remove";
|
||||
removeDlcButton.Click += OnRemoveDlc;
|
||||
|
||||
// ── Action buttons ── y=270
|
||||
generateButton.Location = new System.Drawing.Point(12, 270);
|
||||
generateButton.Size = new System.Drawing.Size(150, 26);
|
||||
generateButton.Text = "Generate Test Game";
|
||||
generateButton.Click += OnGenerate;
|
||||
|
||||
clearButton.Location = new System.Drawing.Point(168, 270);
|
||||
clearButton.Size = new System.Drawing.Size(110, 26);
|
||||
clearButton.Text = "Clear All Tests";
|
||||
clearButton.Click += OnClearAll;
|
||||
|
||||
closeButton.Location = new System.Drawing.Point(284, 270);
|
||||
closeButton.Size = new System.Drawing.Size(70, 26);
|
||||
closeButton.Text = "Close";
|
||||
closeButton.Click += OnClose;
|
||||
|
||||
// ── Status label ── y=302
|
||||
statusLabel.Location = new System.Drawing.Point(12, 302);
|
||||
statusLabel.Size = new System.Drawing.Size(536, 20);
|
||||
statusLabel.Font = new System.Drawing.Font("Segoe UI", 8.25F);
|
||||
|
||||
// ── Form ──
|
||||
AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
|
||||
AutoScaleMode = AutoScaleMode.Font;
|
||||
ClientSize = new System.Drawing.Size(560, 328);
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||
MaximizeBox = false;
|
||||
MinimizeBox = false;
|
||||
StartPosition = FormStartPosition.CenterParent;
|
||||
Text = "Test Game Generator";
|
||||
Controls.Add(platformGroupBox);
|
||||
Controls.Add(appIdLabel);
|
||||
Controls.Add(appIdTextBox);
|
||||
Controls.Add(gameNameLabel);
|
||||
Controls.Add(gameNameTextBox);
|
||||
Controls.Add(epicSearchButton);
|
||||
Controls.Add(epicResultsListBox);
|
||||
Controls.Add(dlcGroupBox);
|
||||
Controls.Add(generateButton);
|
||||
Controls.Add(clearButton);
|
||||
Controls.Add(closeButton);
|
||||
Controls.Add(statusLabel);
|
||||
|
||||
platformGroupBox.ResumeLayout(false);
|
||||
platformGroupBox.PerformLayout();
|
||||
dlcGroupBox.ResumeLayout(false);
|
||||
dlcGroupBox.PerformLayout();
|
||||
ResumeLayout(false);
|
||||
PerformLayout();
|
||||
}
|
||||
|
||||
private GroupBox platformGroupBox;
|
||||
private RadioButton steamRadioButton;
|
||||
private RadioButton epicRadioButton;
|
||||
private Label appIdLabel;
|
||||
private TextBox appIdTextBox;
|
||||
private Label gameNameLabel;
|
||||
private TextBox gameNameTextBox;
|
||||
private Button epicSearchButton;
|
||||
private ListBox epicResultsListBox;
|
||||
private GroupBox dlcGroupBox;
|
||||
private ListBox dlcListBox;
|
||||
private Label dlcIdLabel;
|
||||
private TextBox dlcIdTextBox;
|
||||
private Label dlcNameLabel;
|
||||
private TextBox dlcNameTextBox;
|
||||
private Button addDlcButton;
|
||||
private Button removeDlcButton;
|
||||
private Button generateButton;
|
||||
private Button clearButton;
|
||||
private Button closeButton;
|
||||
private Label statusLabel;
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CreamInstaller.Components;
|
||||
using CreamInstaller.Platforms.Epic;
|
||||
using CreamInstaller.Platforms.Steam;
|
||||
using CreamInstaller.Utility;
|
||||
|
||||
namespace CreamInstaller.Forms;
|
||||
|
||||
internal sealed partial class TestGameForm : CustomForm
|
||||
{
|
||||
private static readonly string TestGamesRoot =
|
||||
Path.Combine(ProgramData.DirectoryPath, "TestGames");
|
||||
|
||||
private static readonly List<string> CreatedDirectories = [];
|
||||
|
||||
// Steam DLC entries per-form: (dlcId, dlcName)
|
||||
private readonly List<(string id, string name)> dlcEntries = [];
|
||||
|
||||
// Cached Epic search results from the last search: (namespace, name)
|
||||
private readonly List<(string ns, string name)> epicSearchResults = [];
|
||||
|
||||
private bool IsEpicMode => epicRadioButton.Checked;
|
||||
|
||||
internal TestGameForm(IWin32Window owner) : base(owner)
|
||||
{
|
||||
InitializeComponent();
|
||||
appIdTextBox.Leave += OnAppIdLeave;
|
||||
RefreshDlcList();
|
||||
UpdatePlatformMode();
|
||||
}
|
||||
|
||||
private void UpdatePlatformMode()
|
||||
{
|
||||
bool epic = IsEpicMode;
|
||||
|
||||
// App ID row: Steam only
|
||||
appIdLabel.Visible = !epic;
|
||||
appIdTextBox.Visible = !epic;
|
||||
|
||||
// Search button: Epic only — shrink the game name box to make room
|
||||
epicSearchButton.Visible = epic;
|
||||
gameNameTextBox.Size = new System.Drawing.Size(epic ? 354 : 443, 23);
|
||||
|
||||
// Placeholder text — call RefreshCueBanner to flush the Win32 cue so only one text shows
|
||||
gameNameTextBox.PlaceholderText = epic ? "Enter game name and click Search" : "e.g. Spacewar";
|
||||
NativeMethods.RefreshCueBanner(gameNameTextBox);
|
||||
|
||||
// DLC group and Epic results share the same vertical slot
|
||||
dlcGroupBox.Visible = !epic;
|
||||
epicResultsListBox.Visible = false; // hidden until search runs
|
||||
|
||||
if (!epic)
|
||||
epicSearchResults.Clear();
|
||||
|
||||
SetStatus(epic
|
||||
? "Enter a game name and click Search to find it on the Epic store."
|
||||
: "Enter the App ID, then tab out to auto-detect the game name.");
|
||||
}
|
||||
|
||||
private void OnPlatformChanged(object sender, EventArgs e) => UpdatePlatformMode();
|
||||
|
||||
// ── Steam: auto-detect name from AppID ──────────────────────────────────
|
||||
|
||||
private async void OnAppIdLeave(object sender, EventArgs e)
|
||||
{
|
||||
if (IsEpicMode)
|
||||
return;
|
||||
string appId = appIdTextBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(appId) || !int.TryParse(appId, out _))
|
||||
return;
|
||||
if (!string.IsNullOrWhiteSpace(gameNameTextBox.Text))
|
||||
return;
|
||||
|
||||
SetStatus("Looking up game name . . .");
|
||||
generateButton.Enabled = false;
|
||||
|
||||
string name = await Task.Run(async () =>
|
||||
{
|
||||
// Use a dedicated client with a neutral UA so Steam's store API doesn't reject the request.
|
||||
using System.Net.Http.HttpClient client = new();
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd($"{Program.Name}/{Program.Version}");
|
||||
string url = $"https://store.steampowered.com/api/appdetails?appids={appId}&filters=basic";
|
||||
try
|
||||
{
|
||||
string json = await client.GetStringAsync(url);
|
||||
Newtonsoft.Json.Linq.JObject root = Newtonsoft.Json.Linq.JObject.Parse(json);
|
||||
string title = root[appId]?["data"]?["name"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
return title;
|
||||
}
|
||||
catch { /* fall through to SteamCMD */ }
|
||||
|
||||
CmdAppData cmdData = await SteamCMD.GetAppInfo(appId);
|
||||
return cmdData?.Common?.Name;
|
||||
});
|
||||
|
||||
generateButton.Enabled = true;
|
||||
|
||||
if (name is not null)
|
||||
{
|
||||
gameNameTextBox.Text = name;
|
||||
SetStatus($"✓ Game name detected: {name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
SetStatus("Could not auto-detect name — enter it manually.");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Epic: search by name ─────────────────────────────────────────────────
|
||||
|
||||
private async void OnEpicSearch(object sender, EventArgs e)
|
||||
{
|
||||
string keyword = gameNameTextBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
SetStatus("Enter a game name to search.");
|
||||
return;
|
||||
}
|
||||
|
||||
SetStatus("Searching Epic store . . .");
|
||||
epicSearchButton.Enabled = false;
|
||||
generateButton.Enabled = false;
|
||||
epicResultsListBox.Items.Clear();
|
||||
epicResultsListBox.Visible = false;
|
||||
epicSearchResults.Clear();
|
||||
|
||||
List<(string ns, string name)> results = await EpicStore.QuerySearch(keyword);
|
||||
|
||||
epicSearchButton.Enabled = true;
|
||||
generateButton.Enabled = true;
|
||||
|
||||
if (results.Count == 0)
|
||||
{
|
||||
SetStatus("No results found. Try a different name.");
|
||||
return;
|
||||
}
|
||||
|
||||
epicSearchResults.AddRange(results);
|
||||
foreach ((string _, string name) in results)
|
||||
epicResultsListBox.Items.Add(name);
|
||||
|
||||
epicResultsListBox.Visible = true;
|
||||
SetStatus($"Found {results.Count} result(s). Select one to use it.");
|
||||
}
|
||||
|
||||
private void OnEpicResultSelected(object sender, EventArgs e)
|
||||
{
|
||||
int idx = epicResultsListBox.SelectedIndex;
|
||||
if (idx < 0 || idx >= epicSearchResults.Count)
|
||||
return;
|
||||
gameNameTextBox.Text = epicSearchResults[idx].name;
|
||||
SetStatus($"✓ Selected: {epicSearchResults[idx].name}");
|
||||
}
|
||||
|
||||
// ── DLC (Steam) ──────────────────────────────────────────────────────────
|
||||
|
||||
private void OnAddDlc(object sender, EventArgs e)
|
||||
{
|
||||
string dlcId = dlcIdTextBox.Text.Trim();
|
||||
string dlcName = dlcNameTextBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(dlcId) || !int.TryParse(dlcId, out _))
|
||||
{
|
||||
SetStatus("DLC ID must be a valid integer.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(dlcName))
|
||||
{
|
||||
SetStatus("DLC Name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dlcEntries.Any(d => d.id == dlcId))
|
||||
{
|
||||
SetStatus($"DLC ID {dlcId} is already in the list.");
|
||||
return;
|
||||
}
|
||||
|
||||
dlcEntries.Add((dlcId, dlcName));
|
||||
RefreshDlcList();
|
||||
dlcIdTextBox.Clear();
|
||||
dlcNameTextBox.Clear();
|
||||
SetStatus($"Added DLC: {dlcId} = {dlcName}");
|
||||
}
|
||||
|
||||
private void OnRemoveDlc(object sender, EventArgs e)
|
||||
{
|
||||
if (dlcListBox.SelectedIndex < 0)
|
||||
return;
|
||||
dlcEntries.RemoveAt(dlcListBox.SelectedIndex);
|
||||
RefreshDlcList();
|
||||
SetStatus("Removed selected DLC entry.");
|
||||
}
|
||||
|
||||
private void OnDlcListBoxSelectionChanged(object sender, EventArgs e) { }
|
||||
|
||||
private void RefreshDlcList()
|
||||
{
|
||||
dlcListBox.Items.Clear();
|
||||
foreach ((string id, string name) in dlcEntries)
|
||||
dlcListBox.Items.Add($"{id} = {name}");
|
||||
}
|
||||
|
||||
// ── Generate ────────────────────────────────────────────────────────────
|
||||
|
||||
private void OnGenerate(object sender, EventArgs e)
|
||||
{
|
||||
if (IsEpicMode)
|
||||
GenerateEpic();
|
||||
else
|
||||
GenerateSteam();
|
||||
}
|
||||
|
||||
private void GenerateSteam()
|
||||
{
|
||||
string appId = appIdTextBox.Text.Trim();
|
||||
string gameName = gameNameTextBox.Text.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(appId) || !int.TryParse(appId, out _))
|
||||
{
|
||||
SetStatus("App ID must be a valid integer.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(gameName))
|
||||
{
|
||||
SetStatus("Game Name cannot be empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SteamLibrary.TestGames.Any(g => g.appId == appId))
|
||||
{
|
||||
SetStatus($"A test game with App ID {appId} already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string gameDir = Path.Combine(TestGamesRoot, $"steam_{appId}_{SanitizeName(gameName)}");
|
||||
Directory.CreateDirectory(gameDir);
|
||||
|
||||
string dllPath = Path.Combine(gameDir, "steam_api64.dll");
|
||||
WriteSteamApiStub(dllPath);
|
||||
|
||||
CreatedDirectories.Add(gameDir);
|
||||
SteamLibrary.TestGames.Add((appId, gameName, "public", 1, gameDir));
|
||||
ProgramData.Log($"[TestGame] Steam: {gameName} ({appId}) at {gameDir}");
|
||||
SetStatus($"✓ Steam test game '{gameName}' ({appId}) generated. Press Rescan.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus($"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateEpic()
|
||||
{
|
||||
string gameName = gameNameTextBox.Text.Trim();
|
||||
if (string.IsNullOrWhiteSpace(gameName))
|
||||
{
|
||||
SetStatus("Game Name cannot be empty. Search for a game first.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the selected search result namespace if available, otherwise derive a stub
|
||||
string catalogNamespace;
|
||||
int idx = epicResultsListBox.SelectedIndex;
|
||||
if (idx >= 0 && idx < epicSearchResults.Count)
|
||||
{
|
||||
catalogNamespace = epicSearchResults[idx].ns;
|
||||
gameName = epicSearchResults[idx].name;
|
||||
}
|
||||
else
|
||||
{
|
||||
catalogNamespace = $"test_{SanitizeName(gameName).ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
if (EpicLibrary.TestManifests.Any(m => m.CatalogNamespace == catalogNamespace))
|
||||
{
|
||||
SetStatus("An Epic test game with that namespace already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string gameDir = Path.Combine(TestGamesRoot, $"epic_{SanitizeName(gameName)}");
|
||||
Directory.CreateDirectory(gameDir);
|
||||
|
||||
// Stub DLL so Epic DLL-directory scanning finds the game
|
||||
string dllPath = Path.Combine(gameDir, "EOSSDK-Win64-Shipping.dll");
|
||||
WriteSteamApiStub(dllPath);
|
||||
|
||||
CreatedDirectories.Add(gameDir);
|
||||
|
||||
EpicLibrary.TestManifests.Add(new Manifest
|
||||
{
|
||||
DisplayName = gameName,
|
||||
CatalogNamespace = catalogNamespace,
|
||||
InstallLocation = gameDir
|
||||
});
|
||||
|
||||
ProgramData.Log($"[TestGame] Epic: {gameName} ({catalogNamespace}) at {gameDir}");
|
||||
SetStatus($"✓ Epic test game '{gameName}' generated. Press Rescan.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetStatus($"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Clear / Close ────────────────────────────────────────────────────────
|
||||
|
||||
private void OnClearAll(object sender, EventArgs e)
|
||||
{
|
||||
SteamLibrary.TestGames.Clear();
|
||||
EpicLibrary.TestManifests.Clear();
|
||||
foreach (string dir in CreatedDirectories)
|
||||
try { Directory.Delete(dir, true); } catch { /* best-effort */ }
|
||||
CreatedDirectories.Clear();
|
||||
dlcEntries.Clear();
|
||||
RefreshDlcList();
|
||||
epicSearchResults.Clear();
|
||||
epicResultsListBox.Items.Clear();
|
||||
epicResultsListBox.Visible = false;
|
||||
SetStatus("All test games cleared. Press Rescan in the main window.");
|
||||
}
|
||||
|
||||
private void OnClose(object sender, EventArgs e) => Close();
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private void SetStatus(string message)
|
||||
{
|
||||
statusLabel.Text = message;
|
||||
statusLabel.ForeColor = message.StartsWith("✓", StringComparison.Ordinal)
|
||||
? System.Drawing.Color.Green
|
||||
: System.Drawing.Color.FromArgb(212, 212, 212);
|
||||
}
|
||||
|
||||
private static string SanitizeName(string name)
|
||||
{
|
||||
char[] invalid = Path.GetInvalidFileNameChars();
|
||||
return new string(name.Select(c => invalid.Contains(c) ? '_' : c).ToArray());
|
||||
}
|
||||
|
||||
private static void WriteSteamApiStub(string path)
|
||||
{
|
||||
byte[] mzStub =
|
||||
[
|
||||
0x4D, 0x5A,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
];
|
||||
File.WriteAllBytes(path, mzStub);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
||||
@@ -28,10 +28,17 @@ internal static class EpicLibrary
|
||||
}
|
||||
}
|
||||
|
||||
internal static readonly List<Manifest> TestManifests = [];
|
||||
|
||||
internal static async Task<List<Manifest>> GetGames()
|
||||
=> await Task.Run(async () =>
|
||||
{
|
||||
List<Manifest> games = new();
|
||||
|
||||
foreach (Manifest test in TestManifests)
|
||||
if (games.All(g => g.CatalogNamespace != test.CatalogNamespace))
|
||||
games.Add(test);
|
||||
|
||||
string manifests = EpicManifestsPath;
|
||||
if (manifests.DirectoryExists())
|
||||
foreach (string item in manifests.EnumerateDirectory("*.item"))
|
||||
|
||||
@@ -132,6 +132,53 @@ internal static class EpicStore
|
||||
|
||||
public static bool EpicBool = true;
|
||||
|
||||
internal static async Task<List<(string @namespace, string name)>> QuerySearch(string keyword)
|
||||
{
|
||||
List<(string, string)> results = [];
|
||||
try
|
||||
{
|
||||
string query = """
|
||||
query searchByKeyword($keywords: String!) {
|
||||
Catalog {
|
||||
searchStore(keywords: $keywords, category: "games/edition/base", count: 10, country: "US", locale: "en-US", allowCountries: "US") {
|
||||
elements {
|
||||
title
|
||||
namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
var payload = new { query, variables = new { keywords = keyword } };
|
||||
string payloadJson = JsonConvert.SerializeObject(payload);
|
||||
using HttpContent content = new StringContent(payloadJson, System.Text.Encoding.UTF8, "application/json");
|
||||
HttpClient client = HttpClientManager.HttpClient;
|
||||
if (client is null)
|
||||
return results;
|
||||
HttpResponseMessage httpResponse =
|
||||
await client.PostAsync(new Uri("https://launcher.store.epicgames.com/graphql"), content);
|
||||
_ = httpResponse.EnsureSuccessStatusCode();
|
||||
string response = await httpResponse.Content.ReadAsStringAsync();
|
||||
Newtonsoft.Json.Linq.JObject root = Newtonsoft.Json.Linq.JObject.Parse(response);
|
||||
Newtonsoft.Json.Linq.JToken elements = root["data"]?["Catalog"]?["searchStore"]?["elements"];
|
||||
if (elements is null)
|
||||
return results;
|
||||
foreach (Newtonsoft.Json.Linq.JToken el in elements)
|
||||
{
|
||||
string name = el["title"]?.ToString();
|
||||
string ns = el["namespace"]?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(ns)
|
||||
&& results.All(r => r.Item1 != ns))
|
||||
results.Add((ns, name));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
private static async Task<Response> QueryGraphQL(string categoryNamespace)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace CreamInstaller.Platforms.Steam;
|
||||
|
||||
internal static class SteamLibrary
|
||||
{
|
||||
internal static readonly List<(string appId, string name, string branch, int buildId, string gameDirectory)>
|
||||
TestGames = [];
|
||||
|
||||
private static string installPath;
|
||||
|
||||
internal static string InstallPath
|
||||
@@ -41,6 +44,11 @@ internal static class SteamLibrary
|
||||
games.Add(game);
|
||||
}
|
||||
|
||||
foreach ((string appId, string name, string branch, int buildId, string gameDirectory) testGame in
|
||||
TestGames.Where(t => games.All(g => g.appId != t.appId)))
|
||||
games.Add(testGame);
|
||||
if (TestGames.Count > 0)
|
||||
ProgramData.Log($"[Steam] Injected {TestGames.Count} test game(s).");
|
||||
ProgramData.Log($"[Steam] Total games detected: {games.Count}");
|
||||
return games;
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ internal static class ThemeManager
|
||||
private static readonly Color LightPlatform = ColorTranslator.FromHtml("#696900");
|
||||
private static readonly Color LightId = ColorTranslator.FromHtml("#006969");
|
||||
private static readonly Color LightProxy = ColorTranslator.FromHtml("#006900");
|
||||
private static readonly Color LightSelectionBack = SystemColors.Highlight;
|
||||
private static readonly Color LightSelectionBack = ColorTranslator.FromHtml("#ADD6FF");
|
||||
private static readonly Color LightComboBack = SystemColors.Control;
|
||||
private static readonly Color LightComboBorder = SystemColors.ControlDark;
|
||||
private static readonly Color LightComboText = SystemColors.ControlText;
|
||||
@@ -180,9 +180,9 @@ internal static class ThemeManager
|
||||
ll.VisitedLinkColor = DarkLink;
|
||||
break;
|
||||
|
||||
// Labels: dark background, light foreground
|
||||
// Labels: transparent so they blend with whatever container they sit in
|
||||
case Label lbl:
|
||||
lbl.BackColor = DarkBack;
|
||||
lbl.BackColor = Color.Transparent;
|
||||
lbl.ForeColor = DarkFore;
|
||||
break;
|
||||
|
||||
@@ -206,6 +206,20 @@ internal static class ThemeManager
|
||||
rtb.ForeColor = DarkFore;
|
||||
break;
|
||||
|
||||
// ListBox follows alternate dark background
|
||||
case ListBox lb:
|
||||
lb.BackColor = DarkBackAlt;
|
||||
lb.ForeColor = DarkFore;
|
||||
break;
|
||||
|
||||
// TextBox follows alternate dark background
|
||||
case TextBox tb:
|
||||
tb.BackColor = DarkBackAlt;
|
||||
tb.ForeColor = DarkFore;
|
||||
tb.BorderStyle = BorderStyle.FixedSingle;
|
||||
NativeMethods.RefreshCueBanner(tb);
|
||||
break;
|
||||
|
||||
// Layout panels set a consistent background
|
||||
case TableLayoutPanel tlp:
|
||||
tlp.BackColor = DarkBack;
|
||||
@@ -241,7 +255,7 @@ internal static class ThemeManager
|
||||
ll.VisitedLinkColor = SystemColors.HotTrack;
|
||||
break;
|
||||
case Label lbl:
|
||||
lbl.BackColor = LightBack;
|
||||
lbl.BackColor = Color.Transparent;
|
||||
lbl.ForeColor = LightFore;
|
||||
break;
|
||||
case ProgressBar pb:
|
||||
@@ -258,6 +272,16 @@ internal static class ThemeManager
|
||||
rtb.BackColor = LightBack;
|
||||
rtb.ForeColor = LightFore;
|
||||
break;
|
||||
case ListBox lb:
|
||||
lb.BackColor = LightBackAlt;
|
||||
lb.ForeColor = LightFore;
|
||||
break;
|
||||
case TextBox tb:
|
||||
tb.BackColor = LightBackAlt;
|
||||
tb.ForeColor = LightFore;
|
||||
tb.BorderStyle = BorderStyle.Fixed3D;
|
||||
NativeMethods.RefreshCueBanner(tb);
|
||||
break;
|
||||
case TableLayoutPanel tlp:
|
||||
tlp.BackColor = LightBack;
|
||||
break;
|
||||
@@ -408,6 +432,96 @@ internal static class ThemeManager
|
||||
// button is centralized here so theming resides in ThemeManager.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
// Dark checkbox colors – matched to how the system renders the "All" CheckBox control
|
||||
// in dark mode: dark fill, mid-gray border, light foreground tick.
|
||||
private static readonly Color DarkCbBorder = ColorTranslator.FromHtml("#6B6B6B");
|
||||
private static readonly Color DarkCbDisabledBorder = ColorTranslator.FromHtml("#454545");
|
||||
|
||||
/// <summary>
|
||||
/// Draws a checkbox glyph in pure GDI that matches the appearance of a dark-themed
|
||||
/// WinForms CheckBox control (same background, border, tick colors, and rounded corners).
|
||||
/// Use this in owner-draw contexts where CheckBoxRenderer always paints a white background.
|
||||
/// </summary>
|
||||
internal static void DrawDarkCheckBox(Graphics g, Point point, Size glyphSize, bool isChecked, bool enabled = true)
|
||||
{
|
||||
if (g is null) return;
|
||||
int w = glyphSize.Width;
|
||||
int h = glyphSize.Height;
|
||||
Rectangle box = new(point.X, point.Y, w - 1, h - 1);
|
||||
int radius = Math.Max(2, w / 5);
|
||||
|
||||
using System.Drawing.Drawing2D.GraphicsPath path = RoundedRect(box, radius);
|
||||
|
||||
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
||||
|
||||
if (isChecked && enabled)
|
||||
{
|
||||
// Checked + enabled: accent fill, no border, white tick — matches Windows 11 dark CheckBox
|
||||
using SolidBrush fillBrush = new(Accent);
|
||||
g.FillPath(fillBrush, path);
|
||||
|
||||
using Pen tickPen = new(Color.White, 1.7f)
|
||||
{
|
||||
StartCap = System.Drawing.Drawing2D.LineCap.Round,
|
||||
EndCap = System.Drawing.Drawing2D.LineCap.Round,
|
||||
LineJoin = System.Drawing.Drawing2D.LineJoin.Round,
|
||||
};
|
||||
float scaleX = w / 13f;
|
||||
float scaleY = h / 13f;
|
||||
g.DrawLines(tickPen, new PointF[]
|
||||
{
|
||||
new(point.X + 2 * scaleX, point.Y + 6 * scaleY),
|
||||
new(point.X + 5 * scaleX, point.Y + 9 * scaleY),
|
||||
new(point.X + 10 * scaleX, point.Y + 3 * scaleY),
|
||||
});
|
||||
}
|
||||
else if (isChecked)
|
||||
{
|
||||
// Checked + disabled: dimmed accent fill, dimmed tick
|
||||
Color dimAccent = Color.FromArgb(120, Accent);
|
||||
using SolidBrush fillBrush = new(dimAccent);
|
||||
g.FillPath(fillBrush, path);
|
||||
|
||||
using Pen tickPen = new(DarkForeDim, 1.7f)
|
||||
{
|
||||
StartCap = System.Drawing.Drawing2D.LineCap.Round,
|
||||
EndCap = System.Drawing.Drawing2D.LineCap.Round,
|
||||
LineJoin = System.Drawing.Drawing2D.LineJoin.Round,
|
||||
};
|
||||
float scaleX = w / 13f;
|
||||
float scaleY = h / 13f;
|
||||
g.DrawLines(tickPen, new PointF[]
|
||||
{
|
||||
new(point.X + 2 * scaleX, point.Y + 6 * scaleY),
|
||||
new(point.X + 5 * scaleX, point.Y + 9 * scaleY),
|
||||
new(point.X + 10 * scaleX, point.Y + 3 * scaleY),
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unchecked: dark fill, gray border, no tick
|
||||
using SolidBrush fillBrush = new(DarkBackAlt);
|
||||
g.FillPath(fillBrush, path);
|
||||
|
||||
using Pen borderPen = new(enabled ? DarkCbBorder : DarkCbDisabledBorder);
|
||||
g.DrawPath(borderPen, path);
|
||||
}
|
||||
|
||||
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.Default;
|
||||
}
|
||||
|
||||
private static System.Drawing.Drawing2D.GraphicsPath RoundedRect(Rectangle r, int radius)
|
||||
{
|
||||
int d = radius * 2;
|
||||
System.Drawing.Drawing2D.GraphicsPath path = new();
|
||||
path.AddArc(r.Left, r.Top, d, d, 180, 90);
|
||||
path.AddArc(r.Right - d, r.Top, d, d, 270, 90);
|
||||
path.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
|
||||
path.AddArc(r.Left, r.Bottom - d, d, d, 90, 90);
|
||||
path.CloseFigure();
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draws the themed combobox area (background, border and text) used in CustomTreeView.
|
||||
/// This centralizes colors and rendering for light/dark modes.
|
||||
@@ -450,15 +564,54 @@ internal static class ThemeManager
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps Win32 API calls that have no managed equivalent in WinForms.
|
||||
/// These P/Invoke declarations are required because .NET does not expose
|
||||
/// the underlying Windows messages or DWM attributes through its own APIs.
|
||||
/// </summary>
|
||||
internal static class NativeMethods
|
||||
{
|
||||
// DWM attribute index for enabling/disabling the immersive dark title bar.
|
||||
// Documented in dwmapi.h; value 20 corresponds to DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
// (Windows 10 build 19041+ / Windows 11).
|
||||
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
|
||||
|
||||
// DwmSetWindowAttribute allows setting per-window Desktop Window Manager attributes.
|
||||
// We use it here to flip the title bar to dark or light depending on the active theme,
|
||||
// since WinForms has no built-in API to control title bar coloring.
|
||||
[System.Runtime.InteropServices.DllImport("dwmapi.dll")]
|
||||
private static extern int DwmSetWindowAttribute(System.IntPtr hwnd, int attr, ref int attrValue, int attrSize);
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the dark/light title bar chrome for the given window handle.
|
||||
/// Pass <c>1</c> for dark mode, <c>0</c> for light mode.
|
||||
/// </summary>
|
||||
internal static void EnableDarkTitleBar(System.IntPtr handle, int useDark)
|
||||
{
|
||||
_ = DwmSetWindowAttribute(handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useDark, sizeof(int));
|
||||
}
|
||||
|
||||
// Win32 Edit control message that sets or updates the cue (placeholder) banner text.
|
||||
// WinForms sets PlaceholderText once at creation time via this same message internally,
|
||||
// but does not re-send it when the control's colors change. When we restyle a TextBox
|
||||
// for dark/light mode the cue banner can disappear, so we must re-send the message
|
||||
// manually to make the placeholder visible again.
|
||||
private const int EM_SETCUEBANNER = 0x1501;
|
||||
|
||||
// SendMessage is the standard Win32 mechanism for posting messages directly to a
|
||||
// window/control handle. We use the Unicode variant so the placeholder string is
|
||||
// transmitted without any ANSI conversion.
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
|
||||
private static extern System.IntPtr SendMessage(System.IntPtr hWnd, int msg, System.IntPtr wParam, string lParam);
|
||||
|
||||
/// <summary>
|
||||
/// Re-sends <c>EM_SETCUEBANNER</c> to the given TextBox so its placeholder text
|
||||
/// is redrawn after a theme change has altered the control's background or foreground colors.
|
||||
/// Does nothing if the control handle has not yet been created or the placeholder is empty.
|
||||
/// </summary>
|
||||
internal static void RefreshCueBanner(System.Windows.Forms.TextBox textBox)
|
||||
{
|
||||
if (textBox?.IsHandleCreated == true && textBox.PlaceholderText is { Length: > 0 })
|
||||
SendMessage(textBox.Handle, EM_SETCUEBANNER, (System.IntPtr)1, textBox.PlaceholderText);
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 39 KiB |
Reference in New Issue
Block a user