diff --git a/StarterNG.Tests/FavoriteSceneriesTests.cs b/StarterNG.Tests/FavoriteSceneriesTests.cs
new file mode 100644
index 0000000..098fb8c
--- /dev/null
+++ b/StarterNG.Tests/FavoriteSceneriesTests.cs
@@ -0,0 +1,38 @@
+using StarterNG.Application;
+using StarterNG.Domain.Sceneries;
+using StarterNG.Tests.Fakes;
+
+namespace StarterNG.Tests;
+
+public class FavoriteSceneriesTests
+{
+ private const string FavoritesPath = "/home/user/.config/MaSzyna/favorites.txt";
+
+ [Fact]
+ public void A_starred_scenery_stays_starred_after_a_restart()
+ {
+ var installation = new TestInstallation(new InMemoryFileSystem()
+ .WithLegacyFile(TestInstallation.At("scenery", "td.scn"), "//$n Testowa"));
+ var scenery = Assert.Single(installation.Sceneries.LoadAll());
+
+ new FavoriteSceneries(installation.Files, installation.Log, FavoritesPath).Toggle(scenery);
+ var restarted = new FavoriteSceneries(installation.Files, installation.Log, FavoritesPath);
+
+ Assert.True(restarted.Contains(scenery));
+ Assert.Equal("td.scn", installation.Files.ReadAllText(FavoritesPath));
+ }
+
+ [Fact]
+ public void Starring_again_unstars()
+ {
+ var installation = new TestInstallation(new InMemoryFileSystem()
+ .WithLegacyFile(TestInstallation.At("scenery", "td.scn"), "//$n Testowa"));
+ var scenery = Assert.Single(installation.Sceneries.LoadAll());
+ var favorites = new FavoriteSceneries(installation.Files, installation.Log, FavoritesPath);
+
+ favorites.Toggle(scenery);
+ favorites.Toggle(scenery);
+
+ Assert.False(favorites.Contains(scenery));
+ }
+}
diff --git a/StarterNG.Tests/ScenarioPresentationTests.cs b/StarterNG.Tests/ScenarioPresentationTests.cs
index b3f9ce5..70b3c2f 100644
--- a/StarterNG.Tests/ScenarioPresentationTests.cs
+++ b/StarterNG.Tests/ScenarioPresentationTests.cs
@@ -80,6 +80,36 @@ public class SceneryTreeBuilderTests
return (new SceneryTreeBuilder(new SceneryTranslations(files, installation.Log)), sceneries);
}
+ [Fact]
+ public void Starred_sceneries_are_gathered_on_top_and_starred_where_they_belong()
+ {
+ var (builder, sceneries) = Build("//$l Śląsk", "//$l Śląsk", "//$n Aaa luzem");
+ string starred = SceneryTreeBuilder.Star + sceneries.Single(scenery => scenery.Path.EndsWith("s1.scn")).DisplayName;
+ string plain = sceneries.Single(scenery => scenery.Path.EndsWith("s0.scn")).DisplayName;
+
+ var nodes = builder.Build(sceneries, includeArchival: true, "en",
+ scenery => scenery.Path.EndsWith("s1.scn"), "Ulubione");
+
+ var top = nodes[0];
+ Assert.True(top.IsGroup);
+ Assert.Equal(SceneryTreeBuilder.Star + "Ulubione", top.Label);
+ Assert.Equal(starred, Assert.Single(top.Children).Label);
+
+ var group = Assert.Single(nodes, node => node.Label == "Śląsk");
+ Assert.Contains(group.Children, node => node.Label == starred);
+ Assert.Contains(group.Children, node => node.Label == plain);
+ }
+
+ [Fact]
+ public void Without_favourites_there_is_no_favourites_group()
+ {
+ var (builder, sceneries) = Build("//$n Bytom");
+
+ var nodes = builder.Build(sceneries, includeArchival: true, "en", _ => false, "Ulubione");
+
+ Assert.DoesNotContain(nodes, node => node.Label.StartsWith(SceneryTreeBuilder.Star));
+ }
+
[Fact]
public void Scenarios_declaring_a_group_are_gathered_under_it()
{
diff --git a/StarterNG/Application/AppServices.cs b/StarterNG/Application/AppServices.cs
index 82fb036..77fd3f4 100644
--- a/StarterNG/Application/AppServices.cs
+++ b/StarterNG/Application/AppServices.cs
@@ -53,6 +53,7 @@ public sealed class AppServices
Executables = new ExecutableLocator(files, paths, environment, log);
SettingsStore = new SettingsStore(files, clock, log, settingsPaths, new SettingsSerializer(), Executables);
MissingVehicleLog = new MissingVehicleLog(SettingsStore.Settings, MissingAssets, Library, log);
+ Favorites = new FavoriteSceneries(files, log, settingsPaths.FavoritesPath());
var assembly = typeof(AppServices).Assembly;
var version = assembly.GetName().Version ?? new Version(0, 0);
string? commit = UpdateCheck.CommitOf(
@@ -134,6 +135,8 @@ public sealed class AppServices
public UpdateCheck Updates { get; }
+ public FavoriteSceneries Favorites { get; }
+
public StartSimulation StartSimulation { get; }
public LocalizationService Localization { get; }
diff --git a/StarterNG/Application/FavoriteSceneries.cs b/StarterNG/Application/FavoriteSceneries.cs
new file mode 100644
index 0000000..4a00831
--- /dev/null
+++ b/StarterNG/Application/FavoriteSceneries.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using StarterNG.Application.Abstractions;
+using StarterNG.Domain.Sceneries;
+
+namespace StarterNG.Application;
+
+///
+/// The sceneries starred in the list, by file name, one per line in a file of the
+/// starter's own - eu07.ini is the simulator's as much as ours.
+///
+public sealed class FavoriteSceneries
+{
+ private readonly IFileSystem _files;
+ private readonly IDiagnosticsLog _log;
+ private readonly string _path;
+ private readonly HashSet _names = new(StringComparer.OrdinalIgnoreCase);
+
+ public FavoriteSceneries(IFileSystem files, IDiagnosticsLog log, string path)
+ {
+ _files = files;
+ _log = log;
+ _path = path;
+ Load();
+ }
+
+ public bool Contains(Scenery scenery) => _names.Contains(Key(scenery));
+
+ public void Toggle(Scenery scenery)
+ {
+ string key = Key(scenery);
+ if (!_names.Remove(key))
+ _names.Add(key);
+ Save();
+ }
+
+ private static string Key(Scenery scenery) => Path.GetFileName(scenery.Path);
+
+ private void Load()
+ {
+ try
+ {
+ if (!_files.FileExists(_path))
+ return;
+
+ foreach (string line in _files.ReadAllText(_path).Split('\n'))
+ if (line.Trim() is { Length: > 0 } name)
+ _names.Add(name);
+ }
+ catch (Exception ex)
+ {
+ _log.Log($"reading {_path}", ex);
+ }
+ }
+
+ private void Save()
+ {
+ try
+ {
+ if (Path.GetDirectoryName(_path) is { Length: > 0 } directory)
+ _files.CreateDirectory(directory);
+
+ _files.WriteAllText(_path, string.Join('\n', _names.Order(StringComparer.OrdinalIgnoreCase)));
+ }
+ catch (Exception ex)
+ {
+ _log.Log($"writing {_path}", ex);
+ }
+ }
+}
diff --git a/StarterNG/Infrastructure/Settings/SettingsPaths.cs b/StarterNG/Infrastructure/Settings/SettingsPaths.cs
index dbf1418..0864b19 100644
--- a/StarterNG/Infrastructure/Settings/SettingsPaths.cs
+++ b/StarterNG/Infrastructure/Settings/SettingsPaths.cs
@@ -50,6 +50,9 @@ public sealed class SettingsPaths
///
public string VehicleCacheDirectory() => Path.Combine(UserConfigDirectory(), "vehicles");
+ /// The sceneries starred in the list.
+ public string FavoritesPath() => Path.Combine(UserConfigDirectory(), "favorites.txt");
+
/// The day of the last update check and the release it found.
public string UpdateCheckPath() => Path.Combine(UserConfigDirectory(), "updatecheck.txt");
diff --git a/StarterNG/Presentation/Scenarios/SceneryTree.cs b/StarterNG/Presentation/Scenarios/SceneryTree.cs
index 507809a..ff3d969 100644
--- a/StarterNG/Presentation/Scenarios/SceneryTree.cs
+++ b/StarterNG/Presentation/Scenarios/SceneryTree.cs
@@ -20,12 +20,20 @@ public sealed class SceneryTreeBuilder
_translations = translations;
}
+ public const string Star = "\u2605 ";
+
+ ///
+ /// The tree of the list: groups and loose sceneries by name, and the starred ones
+ /// gathered once more in a group of their own on top, starred wherever they appear.
+ ///
public IReadOnlyList Build(IReadOnlyList sceneries, bool includeArchival,
- string langCode)
+ string langCode, Func? isFavorite = null,
+ string favoritesLabel = "")
{
var groups = new Dictionary>(StringComparer.Ordinal);
var groupLabels = new Dictionary(StringComparer.Ordinal);
var topLevel = new List();
+ var favorites = new List();
for (int i = 0; i < sceneries.Count; i++)
{
@@ -35,7 +43,11 @@ public sealed class SceneryTreeBuilder
_translations.LoadFor(scenery, langCode);
- var node = new SceneryTreeNode(scenery.DisplayName, i, Array.Empty());
+ bool favorite = isFavorite?.Invoke(scenery) == true;
+ var node = new SceneryTreeNode((favorite ? Star : "") + scenery.DisplayName, i,
+ Array.Empty());
+ if (favorite)
+ favorites.Add(node);
if (string.IsNullOrEmpty(scenery.Group))
{
@@ -55,7 +67,10 @@ public sealed class SceneryTreeBuilder
foreach (var (group, members) in groups)
topLevel.Add(new SceneryTreeNode(groupLabels[group], -1, Sorted(members)));
- return Sorted(topLevel);
+ var tree = Sorted(topLevel);
+ if (favorites.Count > 0)
+ tree.Insert(0, new SceneryTreeNode(Star + favoritesLabel, -1, Sorted(favorites)));
+ return tree;
}
private static List Sorted(IEnumerable nodes) =>
diff --git a/StarterNG/Views/Scenarios.axaml b/StarterNG/Views/Scenarios.axaml
index 8b809cd..c8580a4 100644
--- a/StarterNG/Views/Scenarios.axaml
+++ b/StarterNG/Views/Scenarios.axaml
@@ -29,9 +29,12 @@
IsCheckedChanged="ArchivalSwitch_OnChanged"
Content="{Binding [HideArchivalSceneries], Source={x:Static starterNg:App.Loc}}" />
+ SelectionChanged="SceneryList_OnSelectionChanged"
+ ContextRequested="SceneryList_OnContextRequested">
-
+
+
+