diff --git a/Core/HelpURLs.cs b/Core/HelpURLs.cs index 787bc95034..2f8019cd9f 100644 --- a/Core/HelpURLs.cs +++ b/Core/HelpURLs.cs @@ -14,6 +14,7 @@ public static class HelpURLs public const string CloneFakeInstances = "https://github.com/KSP-CKAN/CKAN/pull/2627"; public const string DeleteDirectories = "https://github.com/KSP-CKAN/CKAN/pull/2962"; + public const string PreferredHosts = "https://github.com/KSP-CKAN/CKAN/pull/3877"; public const string Filters = "https://github.com/KSP-CKAN/CKAN/pull/3458"; public const string Labels = "https://github.com/KSP-CKAN/CKAN/pull/2936"; public const string PlayTime = "https://github.com/KSP-CKAN/CKAN/pull/3543"; diff --git a/Core/Registry/Registry.cs b/Core/Registry/Registry.cs index 6968a64d1c..390d8626bf 100644 --- a/Core/Registry/Registry.cs +++ b/Core/Registry/Registry.cs @@ -1284,6 +1284,28 @@ public Dictionary> GetDownloadHashIndex() return index; } + /// + /// Return all hosts from latest versions of all available modules, + /// sorted by number of occurrences, most common first + /// + /// Host strings without duplicates + public IEnumerable GetAllHosts() + => available_modules.Values + // Pick all modules where download is not null + .Where(availMod => availMod?.Latest()?.download != null) + // Merge all the URLs into one sequence + .SelectMany(availMod => availMod.Latest().download) + // Skip relative URLs because they don't have hosts + .Where(dlUri => dlUri.IsAbsoluteUri) + // Group the URLs by host + .GroupBy(dlUri => dlUri.Host) + // Put most commonly used hosts first + .OrderByDescending(grp => grp.Count()) + // Alphanumeric sort if same number of usages + .ThenBy(grp => grp.Key) + // Return the host from each group + .Select(grp => grp.Key); + /// /// Partition all CkanModules in available_modules into /// compatible and incompatible groups. diff --git a/GUI/CKAN-GUI.csproj b/GUI/CKAN-GUI.csproj index 2d9673272e..681a127d2d 100644 --- a/GUI/CKAN-GUI.csproj +++ b/GUI/CKAN-GUI.csproj @@ -112,6 +112,12 @@ InstallFiltersDialog.cs + + Form + + + PreferredHostsDialog.cs + Form @@ -816,6 +822,12 @@ ..\..\Dialogs\InstallFiltersDialog.cs + + PreferredHostsDialog.cs + + + ..\..\Dialogs\PreferredHostsDialog.cs + Main.cs Designer diff --git a/GUI/Controls/EditModpack.cs b/GUI/Controls/EditModpack.cs index e82997bb81..33dfc0b3fb 100644 --- a/GUI/Controls/EditModpack.cs +++ b/GUI/Controls/EditModpack.cs @@ -20,8 +20,8 @@ public EditModpack() this.ToolTip.SetToolTip(NameTextBox, Properties.Resources.EditModpackTooltipName); this.ToolTip.SetToolTip(AbstractTextBox, Properties.Resources.EditModpackTooltipAbstract); this.ToolTip.SetToolTip(VersionTextBox, Properties.Resources.EditModpackTooltipVersion); - this.ToolTip.SetToolTip(GameVersionMinComboBox, Properties.Resources.EditModpackTooltipGameVersionMin); - this.ToolTip.SetToolTip(GameVersionMaxComboBox, Properties.Resources.EditModpackTooltipGameVersionMax); + this.ToolTip.SetToolTip(GameVersionMinComboBox, Properties.Resources.EditModpackTooltipGameVersionMin); + this.ToolTip.SetToolTip(GameVersionMaxComboBox, Properties.Resources.EditModpackTooltipGameVersionMax); this.ToolTip.SetToolTip(LicenseComboBox, Properties.Resources.EditModpackTooltipLicense); this.ToolTip.SetToolTip(IncludeVersionsCheckbox, Properties.Resources.EditModpackTooltipIncludeVersions); this.ToolTip.SetToolTip(DependsRadioButton, Properties.Resources.EditModpackTooltipDepends); diff --git a/GUI/Dialogs/PreferredHostsDialog.Designer.cs b/GUI/Dialogs/PreferredHostsDialog.Designer.cs new file mode 100644 index 0000000000..287cc78890 --- /dev/null +++ b/GUI/Dialogs/PreferredHostsDialog.Designer.cs @@ -0,0 +1,234 @@ +namespace CKAN.GUI +{ + partial class PreferredHostsDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(PreferredHostsDialog)); + this.ToolTip = new System.Windows.Forms.ToolTip(); + this.ExplanationLabel = new System.Windows.Forms.Label(); + this.Splitter = new System.Windows.Forms.SplitContainer(); + this.AvailableHostsLabel = new System.Windows.Forms.Label(); + this.AvailableHostsListBox = new System.Windows.Forms.ListBox(); + this.MoveRightButton = new System.Windows.Forms.Button(); + this.MoveLeftButton = new System.Windows.Forms.Button(); + this.PreferredHostsLabel = new System.Windows.Forms.Label(); + this.PreferredHostsListBox = new System.Windows.Forms.ListBox(); + this.MoveUpButton = new System.Windows.Forms.Button(); + this.MoveDownButton = new System.Windows.Forms.Button(); + ((System.ComponentModel.ISupportInitialize)(this.Splitter)).BeginInit(); + this.Splitter.Panel1.SuspendLayout(); + this.Splitter.Panel2.SuspendLayout(); + this.Splitter.SuspendLayout(); + this.SuspendLayout(); + // + // ToolTip + // + this.ToolTip.AutoPopDelay = 10000; + this.ToolTip.InitialDelay = 250; + this.ToolTip.ReshowDelay = 250; + this.ToolTip.ShowAlways = true; + // + // ExplanationLabel + // + this.ExplanationLabel.Dock = System.Windows.Forms.DockStyle.Top; + this.ExplanationLabel.Font = new System.Drawing.Font(System.Drawing.SystemFonts.DefaultFont.Name, 12, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel); + this.ExplanationLabel.Location = new System.Drawing.Point(5, 0); + this.ExplanationLabel.Name = "ExplanationLabel"; + this.ExplanationLabel.Padding = new System.Windows.Forms.Padding(10, 10, 10, 10); + this.ExplanationLabel.Size = new System.Drawing.Size(490, 60); + resources.ApplyResources(this.ExplanationLabel, "ExplanationLabel"); + // + // Splitter + // + this.Splitter.Dock = System.Windows.Forms.DockStyle.Fill; + this.Splitter.Location = new System.Drawing.Point(0, 0); + this.Splitter.Name = "Splitter"; + this.Splitter.Size = new System.Drawing.Size(534, 300); + this.Splitter.SplitterDistance = 262; + this.Splitter.SplitterWidth = 10; + this.Splitter.TabIndex = 0; + // + // Splitter.Panel1 + // + this.Splitter.Panel1.Controls.Add(this.AvailableHostsLabel); + this.Splitter.Panel1.Controls.Add(this.AvailableHostsListBox); + this.Splitter.Panel1.Controls.Add(this.MoveRightButton); + this.Splitter.Panel1.Controls.Add(this.MoveLeftButton); + this.Splitter.Panel1MinSize = 200; + // + // Splitter.Panel2 + // + this.Splitter.Panel2.Controls.Add(this.PreferredHostsLabel); + this.Splitter.Panel2.Controls.Add(this.PreferredHostsListBox); + this.Splitter.Panel2.Controls.Add(this.MoveUpButton); + this.Splitter.Panel2.Controls.Add(this.MoveDownButton); + this.Splitter.Panel2MinSize = 200; + // + // AvailableHostsLabel + // + this.AvailableHostsLabel.AutoSize = true; + this.AvailableHostsLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.AvailableHostsLabel.Font = new System.Drawing.Font(System.Drawing.SystemFonts.DefaultFont.Name, 12, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel); + this.AvailableHostsLabel.Location = new System.Drawing.Point(10, 10); + this.AvailableHostsLabel.Name = "AvailableHostsLabel"; + this.AvailableHostsLabel.Size = new System.Drawing.Size(75, 23); + this.AvailableHostsLabel.TabIndex = 1; + resources.ApplyResources(this.AvailableHostsLabel, "AvailableHostsLabel"); + // + // AvailableHostsListBox + // + this.AvailableHostsListBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left | System.Windows.Forms.AnchorStyles.Right)))); + this.AvailableHostsListBox.FormattingEnabled = true; + this.AvailableHostsListBox.Location = new System.Drawing.Point(10, 35); + this.AvailableHostsListBox.Name = "AvailableHostsListBox"; + this.AvailableHostsListBox.Size = new System.Drawing.Size(210, 255); + this.AvailableHostsListBox.TabIndex = 2; + this.AvailableHostsListBox.SelectedIndexChanged += new System.EventHandler(this.AvailableHostsListBox_SelectedIndexChanged); + this.AvailableHostsListBox.DoubleClick += new System.EventHandler(this.AvailableHostsListBox_DoubleClick); + // + // MoveRightButton + // + this.MoveRightButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top + | System.Windows.Forms.AnchorStyles.Right)); + this.MoveRightButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.MoveRightButton.Location = new System.Drawing.Point(230, 35); + this.MoveRightButton.Name = "MoveRightButton"; + this.MoveRightButton.Size = new System.Drawing.Size(32, 32); + this.MoveRightButton.TabIndex = 3; + this.MoveRightButton.Text = "▸"; + this.MoveRightButton.UseVisualStyleBackColor = true; + this.MoveRightButton.Click += new System.EventHandler(this.MoveRightButton_Click); + resources.ApplyResources(this.MoveRightButton, "MoveRightButton"); + // + // MoveLeftButton + // + this.MoveLeftButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top + | System.Windows.Forms.AnchorStyles.Right)); + this.MoveLeftButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.MoveLeftButton.Location = new System.Drawing.Point(230, 72); + this.MoveLeftButton.Name = "MoveLeftButton"; + this.MoveLeftButton.Size = new System.Drawing.Size(32, 32); + this.MoveLeftButton.TabIndex = 4; + this.MoveLeftButton.Text = "◂"; + this.MoveLeftButton.UseVisualStyleBackColor = true; + this.MoveLeftButton.Click += new System.EventHandler(this.MoveLeftButton_Click); + resources.ApplyResources(this.MoveLeftButton, "MoveLeftButton"); + // + // PreferredHostsLabel + // + this.PreferredHostsLabel.AutoSize = true; + this.PreferredHostsLabel.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)); + this.PreferredHostsLabel.Font = new System.Drawing.Font(System.Drawing.SystemFonts.DefaultFont.Name, 12F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Pixel); + this.PreferredHostsLabel.Location = new System.Drawing.Point(0, 10); + this.PreferredHostsLabel.Name = "PreferredHostsLabel"; + this.PreferredHostsLabel.Size = new System.Drawing.Size(75, 23); + this.PreferredHostsLabel.TabIndex = 5; + resources.ApplyResources(this.PreferredHostsLabel, "PreferredHostsLabel"); + // + // PreferredHostsListBox + // + this.PreferredHostsListBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.PreferredHostsListBox.FormattingEnabled = true; + this.PreferredHostsListBox.Location = new System.Drawing.Point(0, 35); + this.PreferredHostsListBox.Name = "PreferredHostsListBox"; + this.PreferredHostsListBox.Size = new System.Drawing.Size(216, 255); + this.PreferredHostsListBox.TabIndex = 6; + this.PreferredHostsListBox.SelectedIndexChanged += new System.EventHandler(this.PreferredHostsListBox_SelectedIndexChanged); + this.PreferredHostsListBox.DoubleClick += new System.EventHandler(this.PreferredHostsListBox_DoubleClick); + // + // MoveUpButton + // + this.MoveUpButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top + | System.Windows.Forms.AnchorStyles.Right)); + this.MoveUpButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.MoveUpButton.Location = new System.Drawing.Point(226, 35); + this.MoveUpButton.Name = "MoveUpButton"; + this.MoveUpButton.Size = new System.Drawing.Size(32, 32); + this.MoveUpButton.TabIndex = 7; + this.MoveUpButton.Text = "▴"; + this.MoveUpButton.UseVisualStyleBackColor = true; + this.MoveUpButton.Click += new System.EventHandler(this.MoveUpButton_Click); + resources.ApplyResources(this.MoveUpButton, "MoveUpButton"); + // + // MoveDownButton + // + this.MoveDownButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Top + | System.Windows.Forms.AnchorStyles.Right)); + this.MoveDownButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.MoveDownButton.Location = new System.Drawing.Point(226, 72); + this.MoveDownButton.Name = "MoveDownButton"; + this.MoveDownButton.Size = new System.Drawing.Size(32, 32); + this.MoveDownButton.TabIndex = 8; + this.MoveDownButton.Text = "▾"; + this.MoveDownButton.UseVisualStyleBackColor = true; + this.MoveDownButton.Click += new System.EventHandler(this.MoveDownButton_Click); + resources.ApplyResources(this.MoveDownButton, "MoveDownButton"); + // + // PreferredHostsDialog + // + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; + this.ClientSize = new System.Drawing.Size(534, 360); + this.Controls.Add(this.Splitter); + this.Controls.Add(this.ExplanationLabel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable; + this.Icon = Properties.Resources.AppIcon; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.MinimumSize = new System.Drawing.Size(480, 300); + this.HelpButton = true; + this.Name = "PreferredHostsDialog"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Load += new System.EventHandler(this.PreferredHostsDialog_Load); + this.Closing += new System.ComponentModel.CancelEventHandler(this.PreferredHostsDialog_Closing); + resources.ApplyResources(this, "$this"); + this.Splitter.Panel1.ResumeLayout(false); + this.Splitter.Panel2.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.Splitter)).EndInit(); + this.Splitter.ResumeLayout(false); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.ToolTip ToolTip; + private System.Windows.Forms.Label ExplanationLabel; + private System.Windows.Forms.SplitContainer Splitter; + private System.Windows.Forms.Label AvailableHostsLabel; + private System.Windows.Forms.ListBox AvailableHostsListBox; + private System.Windows.Forms.Button MoveRightButton; + private System.Windows.Forms.Button MoveLeftButton; + private System.Windows.Forms.Label PreferredHostsLabel; + private System.Windows.Forms.ListBox PreferredHostsListBox; + private System.Windows.Forms.Button MoveUpButton; + private System.Windows.Forms.Button MoveDownButton; + } +} diff --git a/GUI/Dialogs/PreferredHostsDialog.cs b/GUI/Dialogs/PreferredHostsDialog.cs new file mode 100644 index 0000000000..9c1f2e5896 --- /dev/null +++ b/GUI/Dialogs/PreferredHostsDialog.cs @@ -0,0 +1,176 @@ +using System; +using System.Linq; +using System.ComponentModel; +using System.Windows.Forms; + +using CKAN.Configuration; + +namespace CKAN.GUI +{ + public partial class PreferredHostsDialog : Form + { + public PreferredHostsDialog(IConfiguration config, Registry registry) + { + InitializeComponent(); + this.config = config; + allHosts = registry.GetAllHosts().ToArray(); + placeholder = Properties.Resources.PreferredHostsPlaceholder; + + ToolTip.SetToolTip(MoveRightButton, Properties.Resources.PreferredHostsTooltipMoveRight); + ToolTip.SetToolTip(MoveLeftButton, Properties.Resources.PreferredHostsTooltipMoveLeft); + ToolTip.SetToolTip(MoveUpButton, Properties.Resources.PreferredHostsTooltipMoveUp); + ToolTip.SetToolTip(MoveDownButton, Properties.Resources.PreferredHostsTooltipMoveDown); + } + + /// + /// Open the user guide when the user presses F1 + /// + protected override void OnHelpRequested(HelpEventArgs evt) + { + evt.Handled = Util.TryOpenWebPage(HelpURLs.PreferredHosts); + } + + /// + /// Open the user guide when the user clicks the help button + /// + protected override void OnHelpButtonClicked(CancelEventArgs evt) + { + evt.Cancel = Util.TryOpenWebPage(HelpURLs.PreferredHosts); + } + + private void PreferredHostsDialog_Load(object sender, EventArgs e) + { + AvailableHostsListBox.Items.AddRange(allHosts + .Except(config.PreferredHosts) + .ToArray()); + PreferredHostsListBox.Items.AddRange(config.PreferredHosts + .Select(host => host ?? placeholder) + .ToArray()); + AvailableHostsListBox_SelectedIndexChanged(null, null); + PreferredHostsListBox_SelectedIndexChanged(null, null); + } + + private void PreferredHostsDialog_Closing(object sender, CancelEventArgs e) + { + config.PreferredHosts = PreferredHostsListBox.Items.Cast() + .Select(h => h == placeholder ? null : h) + .ToArray(); + } + + private void AvailableHostsListBox_SelectedIndexChanged(object sender, EventArgs e) + { + MoveRightButton.Enabled = AvailableHostsListBox.SelectedIndex > -1; + } + + private void PreferredHostsListBox_SelectedIndexChanged(object sender, EventArgs e) + { + var haveSelection = PreferredHostsListBox.SelectedIndex > -1; + MoveLeftButton.Enabled = haveSelection + && PreferredHostsListBox.SelectedItem != placeholder; + MoveUpButton.Enabled = PreferredHostsListBox.SelectedIndex > 0; + MoveDownButton.Enabled = haveSelection + && PreferredHostsListBox.SelectedIndex < PreferredHostsListBox.Items.Count - 1; + } + + private void AvailableHostsListBox_DoubleClick(object sender, EventArgs r) + { + MoveRightButton_Click(null, null); + } + + private void PreferredHostsListBox_DoubleClick(object sender, EventArgs r) + { + MoveLeftButton_Click(null, null); + } + + private void MoveRightButton_Click(object sender, EventArgs e) + { + if (AvailableHostsListBox.SelectedIndex > -1) + { + if (PreferredHostsListBox.Items.Count == 0) + { + PreferredHostsListBox.Items.Add(placeholder); + } + var fromWhere = AvailableHostsListBox.SelectedIndex; + var selected = AvailableHostsListBox.SelectedItem; + var toWhere = PreferredHostsListBox.Items.IndexOf(placeholder); + AvailableHostsListBox.Items.Remove(selected); + PreferredHostsListBox.Items.Insert(toWhere, selected); + // Preserve selection on same line + if (AvailableHostsListBox.Items.Count > 0) + { + AvailableHostsListBox.SetSelected(Math.Min(fromWhere, + AvailableHostsListBox.Items.Count - 1), + true); + } + else + { + // ListBox doesn't notify of selection changes that happen via removal + AvailableHostsListBox_SelectedIndexChanged(null, null); + } + } + } + + private void MoveLeftButton_Click(object sender, EventArgs e) + { + if (PreferredHostsListBox.SelectedIndex > -1) + { + var fromWhere = PreferredHostsListBox.SelectedIndex; + var selected = PreferredHostsListBox.SelectedItem; + if (selected != placeholder) + { + PreferredHostsListBox.Items.Remove(selected); + // Regenerate the list to put the item back in the original order + AvailableHostsListBox.Items.Clear(); + AvailableHostsListBox.Items.AddRange(allHosts + .Except(PreferredHostsListBox.Items.Cast()) + .ToArray()); + if (PreferredHostsListBox.Items.Count == 1) + { + PreferredHostsListBox.Items.Remove(placeholder); + } + // Preserve selection on same line + if (PreferredHostsListBox.Items.Count > 0) + { + PreferredHostsListBox.SetSelected(Math.Min(fromWhere, + PreferredHostsListBox.Items.Count - 1), + true); + } + } + } + } + + private void MoveUpButton_Click(object sender, EventArgs e) + { + if (PreferredHostsListBox.SelectedIndex > 0) + { + MoveItem(PreferredHostsListBox.SelectedIndex - 1, + PreferredHostsListBox.SelectedIndex); + // ListBox doesn't notify of selection changes that happen via removal + PreferredHostsListBox_SelectedIndexChanged(null, null); + } + } + + private void MoveDownButton_Click(object sender, EventArgs e) + { + if (PreferredHostsListBox.SelectedIndex > -1 + && PreferredHostsListBox.SelectedIndex < PreferredHostsListBox.Items.Count - 1) + { + MoveItem(PreferredHostsListBox.SelectedIndex + 1, + PreferredHostsListBox.SelectedIndex); + // ListBox doesn't notify of selection changes that happen via insertion + PreferredHostsListBox_SelectedIndexChanged(null, null); + } + } + + private void MoveItem(int from, int to) + { + var item = PreferredHostsListBox.Items[from]; + PreferredHostsListBox.Items.RemoveAt(from); + PreferredHostsListBox.Items.Insert(to, item); + } + + private readonly IConfiguration config; + private readonly string[] allHosts; + private readonly string placeholder; + } +} diff --git a/GUI/Dialogs/PreferredHostsDialog.resx b/GUI/Dialogs/PreferredHostsDialog.resx new file mode 100644 index 0000000000..334b026bd6 --- /dev/null +++ b/GUI/Dialogs/PreferredHostsDialog.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + If a module has multiple download URLs, they will be prioritised by host according to the list on the right. + Available hosts (by popularity): + Preferred hosts (by priority): + Preferred Hosts + diff --git a/GUI/Localization/en-US/PreferredHostsDialog.en-US.resx b/GUI/Localization/en-US/PreferredHostsDialog.en-US.resx new file mode 100644 index 0000000000..48918f45af --- /dev/null +++ b/GUI/Localization/en-US/PreferredHostsDialog.en-US.resx @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + If a module has multiple download URLs, they will be prioritized by host according to the list on the right. + diff --git a/GUI/Main/Main.Designer.cs b/GUI/Main/Main.Designer.cs index ad9784fe62..b1df4e1eca 100644 --- a/GUI/Main/Main.Designer.cs +++ b/GUI/Main/Main.Designer.cs @@ -46,6 +46,7 @@ private void InitializeComponent() this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.cKANSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.pluginsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); + this.preferredHostsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.installFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.GameCommandlineToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.compatibleGameVersionsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -252,10 +253,11 @@ private void InitializeComponent() // this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.cKANSettingsToolStripMenuItem, - this.pluginsToolStripMenuItem, this.GameCommandlineToolStripMenuItem, this.compatibleGameVersionsToolStripMenuItem, - this.installFiltersToolStripMenuItem}); + this.preferredHostsToolStripMenuItem, + this.installFiltersToolStripMenuItem, + this.pluginsToolStripMenuItem}); this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem"; this.settingsToolStripMenuItem.Size = new System.Drawing.Size(88, 29); resources.ApplyResources(this.settingsToolStripMenuItem, "settingsToolStripMenuItem"); @@ -274,6 +276,13 @@ private void InitializeComponent() this.pluginsToolStripMenuItem.Click += new System.EventHandler(this.pluginsToolStripMenuItem_Click); resources.ApplyResources(this.pluginsToolStripMenuItem, "pluginsToolStripMenuItem"); // + // preferredHostsToolStripMenuItem + // + this.preferredHostsToolStripMenuItem.Name = "preferredHostsToolStripMenuItem"; + this.preferredHostsToolStripMenuItem.Size = new System.Drawing.Size(247, 30); + this.preferredHostsToolStripMenuItem.Click += new System.EventHandler(this.preferredHostsToolStripMenuItem_Click); + resources.ApplyResources(this.preferredHostsToolStripMenuItem, "preferredHostsToolStripMenuItem"); + // // installFiltersToolStripMenuItem // this.installFiltersToolStripMenuItem.Name = "installFiltersToolStripMenuItem"; @@ -878,6 +887,7 @@ private void InitializeComponent() public System.Windows.Forms.ToolStripMenuItem settingsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem cKANSettingsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem pluginsToolStripMenuItem; + private System.Windows.Forms.ToolStripMenuItem preferredHostsToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem installFiltersToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem GameCommandlineToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem compatibleGameVersionsToolStripMenuItem; diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index dadcfc535f..8f97edafba 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -580,6 +580,16 @@ private void pluginsToolStripMenuItem_Click(object sender, EventArgs e) Enabled = true; } + private void preferredHostsToolStripMenuItem_Click(object sender, EventArgs e) + { + Enabled = false; + var dlg = new PreferredHostsDialog( + ServiceLocator.Container.Resolve(), + RegistryManager.Instance(CurrentInstance).registry); + dlg.ShowDialog(this); + Enabled = true; + } + private void installFiltersToolStripMenuItem_Click(object sender, EventArgs e) { Enabled = false; diff --git a/GUI/Main/Main.resx b/GUI/Main/Main.resx index 9a8d9c15f4..99d60b5d46 100644 --- a/GUI/Main/Main.resx +++ b/GUI/Main/Main.resx @@ -135,6 +135,7 @@ &Settings CKAN &settings CKAN &plugins + Preferred &hosts Installation &filters &Game command-line &Compatible game versions diff --git a/GUI/Properties/Resources.Designer.cs b/GUI/Properties/Resources.Designer.cs index c1f9ffa1e8..04b82c95a4 100644 --- a/GUI/Properties/Resources.Designer.cs +++ b/GUI/Properties/Resources.Designer.cs @@ -1221,5 +1221,21 @@ internal static string DeleteUnmanagedFileDelete { internal static string DeleteUnmanagedFileCancel { get { return (string)(ResourceManager.GetObject("DeleteUnmanagedFileCancel", resourceCulture)); } } + + internal static string PreferredHostsPlaceholder { + get { return (string)(ResourceManager.GetObject("PreferredHostsPlaceholder", resourceCulture)); } + } + internal static string PreferredHostsTooltipMoveRight { + get { return (string)(ResourceManager.GetObject("PreferredHostsTooltipMoveRight", resourceCulture)); } + } + internal static string PreferredHostsTooltipMoveLeft { + get { return (string)(ResourceManager.GetObject("PreferredHostsTooltipMoveLeft", resourceCulture)); } + } + internal static string PreferredHostsTooltipMoveUp { + get { return (string)(ResourceManager.GetObject("PreferredHostsTooltipMoveUp", resourceCulture)); } + } + internal static string PreferredHostsTooltipMoveDown { + get { return (string)(ResourceManager.GetObject("PreferredHostsTooltipMoveDown", resourceCulture)); } + } } } diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index 4db456ba9c..9888513f71 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -479,4 +479,9 @@ If you wish to remove this folder, uninstall these mods normally. This action cannot be undone! Delete Cancel + <ALL OTHER HOSTS>This string should not be valid as a hostname + Add selected host to preferences list + Remove selected host from preferences list + Make selected host higher priority + Make selected host lower priority diff --git a/Tests/Core/Registry/Registry.cs b/Tests/Core/Registry/Registry.cs index cf7730a29f..6d28b70c1c 100644 --- a/Tests/Core/Registry/Registry.cs +++ b/Tests/Core/Registry/Registry.cs @@ -297,6 +297,76 @@ public void HasUpdate_OtherModDependsOnCurrent_ReturnsFalse() } } + [Test, + // Empty registry, return nothing + TestCase(new string[] { }, + new string[] { }), + // One per host, sort by alphanumeric + TestCase(new string[] + { + @"{ + ""identifier"": ""ModA"", + ""version"": ""1.0"", + ""download"": [ + ""https://archive.org/"", + ""https://spacedock.info/"", + ""https://github.com/"" + ] + }", + }, + new string[] + { + "archive.org", "github.com", "spacedock.info" + }), + // Multiple per host, sort by frequency + TestCase(new string[] + { + @"{ + ""identifier"": ""ModA"", + ""version"": ""1.0"", + ""download"": [ + ""https://archive.org/"", + ""https://spacedock.info/"", + ""https://github.com/"" + ] + }", + @"{ + ""identifier"": ""ModB"", + ""version"": ""1.0"", + ""download"": [ + ""https://spacedock.info/"", + ""https://github.com/"" + ] + }", + @"{ + ""identifier"": ""ModC"", + ""version"": ""1.0"", + ""download"": [ + ""https://github.com/"" + ] + }", + }, + new string[] + { + "github.com", "spacedock.info", "archive.org" + }), + ] + public void GetAllHosts_WithModules_ReturnsCorrectList(string[] modules, + string[] correctAnswer) + { + // Arrange + foreach (var module in modules) + { + registry.AddAvailable(CkanModule.FromJson(module)); + } + + // Act + var allHosts = registry.GetAllHosts().ToArray(); + + // Assert + Assert.AreEqual(correctAnswer, allHosts); + } + [Test] public void TxEmbeddedCommit() { diff --git a/Tests/GUI/ResourcesTests.cs b/Tests/GUI/ResourcesTests.cs index b37c476484..12a5ab4db3 100644 --- a/Tests/GUI/ResourcesTests.cs +++ b/Tests/GUI/ResourcesTests.cs @@ -48,8 +48,11 @@ public void PropertiesResources_LanguageResource_NotSet() TestCase(typeof(CKAN.GUI.DeleteDirectories)), TestCase(typeof(CKAN.GUI.EditModpack)), TestCase(typeof(CKAN.GUI.EditModSearch)), + TestCase(typeof(CKAN.GUI.InstallationHistory)), TestCase(typeof(CKAN.GUI.ManageMods)), TestCase(typeof(CKAN.GUI.ModInfo)), + TestCase(typeof(CKAN.GUI.PlayTime)), + TestCase(typeof(CKAN.GUI.UnmanagedFiles)), TestCase(typeof(CKAN.GUI.Wait)), // Dialogs @@ -58,13 +61,16 @@ public void PropertiesResources_LanguageResource_NotSet() TestCase(typeof(CKAN.GUI.AskUserForAutoUpdatesDialog)), TestCase(typeof(CKAN.GUI.CloneGameInstanceDialog)), TestCase(typeof(CKAN.GUI.CompatibleGameVersionsDialog)), + TestCase(typeof(CKAN.GUI.DownloadsFailedDialog)), TestCase(typeof(CKAN.GUI.EditLabelsDialog)), TestCase(typeof(CKAN.GUI.ErrorDialog)), TestCase(typeof(CKAN.GUI.GameCommandLineOptionsDialog)), + TestCase(typeof(CKAN.GUI.InstallFiltersDialog)), TestCase(typeof(CKAN.GUI.ManageGameInstancesDialog)), TestCase(typeof(CKAN.GUI.NewRepoDialog)), TestCase(typeof(CKAN.GUI.NewUpdateDialog)), TestCase(typeof(CKAN.GUI.PluginsDialog)), + TestCase(typeof(CKAN.GUI.PreferredHostsDialog)), TestCase(typeof(CKAN.GUI.RenameInstanceDialog)), TestCase(typeof(CKAN.GUI.SelectionDialog)), TestCase(typeof(CKAN.GUI.YesNoDialog)),