mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-10-16 23:49:19 +02:00
Star favourite sceneries in the list
A right click stars or unstars the scenery under it; the starred ones are gathered in a group on top and marked where they belong. They are kept in favorites.txt beside the settings, not in eu07.ini.
This commit is contained in:
38
StarterNG.Tests/FavoriteSceneriesTests.cs
Normal file
38
StarterNG.Tests/FavoriteSceneriesTests.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
|
||||
72
StarterNG/Application/FavoriteSceneries.cs
Normal file
72
StarterNG/Application/FavoriteSceneries.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public sealed class FavoriteSceneries
|
||||
{
|
||||
private readonly IFileSystem _files;
|
||||
private readonly IDiagnosticsLog _log;
|
||||
private readonly string _path;
|
||||
private readonly HashSet<string> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,9 @@ public sealed class SettingsPaths
|
||||
/// </summary>
|
||||
public string VehicleCacheDirectory() => Path.Combine(UserConfigDirectory(), "vehicles");
|
||||
|
||||
/// <summary>The sceneries starred in the list.</summary>
|
||||
public string FavoritesPath() => Path.Combine(UserConfigDirectory(), "favorites.txt");
|
||||
|
||||
/// <summary>The day of the last update check and the release it found.</summary>
|
||||
public string UpdateCheckPath() => Path.Combine(UserConfigDirectory(), "updatecheck.txt");
|
||||
|
||||
|
||||
@@ -20,12 +20,20 @@ public sealed class SceneryTreeBuilder
|
||||
_translations = translations;
|
||||
}
|
||||
|
||||
public const string Star = "\u2605 ";
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public IReadOnlyList<SceneryTreeNode> Build(IReadOnlyList<Scenery> sceneries, bool includeArchival,
|
||||
string langCode)
|
||||
string langCode, Func<Scenery, bool>? isFavorite = null,
|
||||
string favoritesLabel = "")
|
||||
{
|
||||
var groups = new Dictionary<string, List<SceneryTreeNode>>(StringComparer.Ordinal);
|
||||
var groupLabels = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
var topLevel = new List<SceneryTreeNode>();
|
||||
var favorites = new List<SceneryTreeNode>();
|
||||
|
||||
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<SceneryTreeNode>());
|
||||
bool favorite = isFavorite?.Invoke(scenery) == true;
|
||||
var node = new SceneryTreeNode((favorite ? Star : "") + scenery.DisplayName, i,
|
||||
Array.Empty<SceneryTreeNode>());
|
||||
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<SceneryTreeNode> Sorted(IEnumerable<SceneryTreeNode> nodes) =>
|
||||
|
||||
@@ -29,9 +29,12 @@
|
||||
IsCheckedChanged="ArchivalSwitch_OnChanged"
|
||||
Content="{Binding [HideArchivalSceneries], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TreeView Name="sceneryList" Classes="Compact"
|
||||
SelectionChanged="SceneryList_OnSelectionChanged">
|
||||
SelectionChanged="SceneryList_OnSelectionChanged"
|
||||
ContextRequested="SceneryList_OnContextRequested">
|
||||
<TreeView.ContextMenu>
|
||||
<ContextMenu>
|
||||
<ContextMenu Opening="SceneryMenu_OnOpening">
|
||||
<MenuItem Name="favoriteMenuItem" Click="ToggleFavorite_OnClick" />
|
||||
<Separator />
|
||||
<MenuItem Click="ReloadSceneries_OnClick"
|
||||
Header="{Binding [ReloadSceneries], Source={x:Static starterNg:App.Loc}}" />
|
||||
<MenuItem Click="OpenSceneryDir_OnClick"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
@@ -71,8 +72,9 @@ public partial class Scenarios : UserControl
|
||||
bool includeArchival = archivalSwitch.IsChecked != true;
|
||||
bool expandGroups = AppServices.Current.Settings.AutoExpandSceneryTree;
|
||||
|
||||
var favorites = AppServices.Current.Favorites;
|
||||
var nodes = new SceneryTreeBuilder(AppServices.Current.SceneryTexts)
|
||||
.Build(Sceneries, includeArchival, App.Loc.CurrentLangCode);
|
||||
.Build(Sceneries, includeArchival, App.Loc.CurrentLangCode, favorites.Contains, App.Loc["Favorites"]);
|
||||
|
||||
foreach (var node in nodes)
|
||||
sceneryList.Items.Add(ToTreeItem(node, expandGroups));
|
||||
@@ -93,6 +95,42 @@ public partial class Scenarios : UserControl
|
||||
return item;
|
||||
}
|
||||
|
||||
private Scenery? SelectedScenery =>
|
||||
sceneryList.SelectedItem is TreeViewItem { Tag: int index } && index < Sceneries.Count
|
||||
? Sceneries[index]
|
||||
: null;
|
||||
|
||||
// A right click picks the scenery under it, so the menu speaks of that one.
|
||||
private void SceneryList_OnContextRequested(object? sender, ContextRequestedEventArgs e)
|
||||
{
|
||||
var hit = e.Source as Control;
|
||||
while (hit is not null and not TreeViewItem)
|
||||
hit = hit.Parent as Control;
|
||||
|
||||
if (hit is TreeViewItem { Tag: int } item)
|
||||
sceneryList.SelectedItem = item;
|
||||
}
|
||||
|
||||
private void SceneryMenu_OnOpening(object? sender, CancelEventArgs e)
|
||||
{
|
||||
var scenery = SelectedScenery;
|
||||
favoriteMenuItem.IsEnabled = scenery is not null;
|
||||
favoriteMenuItem.Header = App.Loc[scenery is not null && AppServices.Current.Favorites.Contains(scenery)
|
||||
? "FavoriteRemove"
|
||||
: "FavoriteAdd"];
|
||||
}
|
||||
|
||||
private void ToggleFavorite_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SelectedScenery is not { } scenery)
|
||||
return;
|
||||
|
||||
AppServices.Current.Favorites.Toggle(scenery);
|
||||
BuildSceneryTree();
|
||||
AppServices.Current.Settings.LastScenery = Path.GetFileNameWithoutExtension(scenery.Path);
|
||||
RestoreLastScenery();
|
||||
}
|
||||
|
||||
private void RestoreLastScenery()
|
||||
{
|
||||
string last = AppServices.Current.Settings.LastScenery;
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
<String key="StartNoStaffConfirm">Tato souprava nemá osádku ani cestujícího — nebude co řídit. Přesto spustit?</String>
|
||||
<String key="RestoreWeather">Obnovit počasí</String>
|
||||
<String key="ReloadSceneries">Načíst znovu</String>
|
||||
<String key="Favorites">Oblíbené</String>
|
||||
<String key="FavoriteAdd">Přidat do oblíbených</String>
|
||||
<String key="FavoriteRemove">Odebrat z oblíbených</String>
|
||||
<String key="OpenSceneryFolder">Otevřít složku</String>
|
||||
<String key="OptionalIncludes">Volitelné prvky</String>
|
||||
<String key="OpenTextureFolder">Otevřít složku s texturami</String>
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
<String key="StartNoStaffConfirm">This consist has no crew and no passenger — there will be nothing to drive. Start anyway?</String>
|
||||
<String key="RestoreWeather">Restore weather</String>
|
||||
<String key="ReloadSceneries">Reload</String>
|
||||
<String key="Favorites">Favourites</String>
|
||||
<String key="FavoriteAdd">Add to favourites</String>
|
||||
<String key="FavoriteRemove">Remove from favourites</String>
|
||||
<String key="OpenSceneryFolder">Open folder</String>
|
||||
<String key="OptionalIncludes">Optional elements</String>
|
||||
<String key="OpenTextureFolder">Open texture folder</String>
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
<String key="StartNoStaffConfirm">Ebben a szerelvényben nincs személyzet és utas sem — nem lesz mit vezetni. Mégis elindítja?</String>
|
||||
<String key="RestoreWeather">Időjárás visszaállítása</String>
|
||||
<String key="ReloadSceneries">Újratöltés</String>
|
||||
<String key="Favorites">Kedvencek</String>
|
||||
<String key="FavoriteAdd">Hozzáadás a kedvencekhez</String>
|
||||
<String key="FavoriteRemove">Eltávolítás a kedvencek közül</String>
|
||||
<String key="OpenSceneryFolder">Mappa megnyitása</String>
|
||||
<String key="OptionalIncludes">Opcionális elemek</String>
|
||||
<String key="OpenTextureFolder">Textúramappa megnyitása</String>
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
<String key="StartNoStaffConfirm">Skład nie ma obsady ani pasażera — nie będzie czym kierować. Uruchomić mimo to?</String>
|
||||
<String key="RestoreWeather">Przywróć pogodę</String>
|
||||
<String key="ReloadSceneries">Przeładuj</String>
|
||||
<String key="Favorites">Ulubione</String>
|
||||
<String key="FavoriteAdd">Dodaj do ulubionych</String>
|
||||
<String key="FavoriteRemove">Usuń z ulubionych</String>
|
||||
<String key="OpenSceneryFolder">Otwórz folder</String>
|
||||
<String key="OptionalIncludes">Elementy opcjonalne</String>
|
||||
<String key="OpenTextureFolder">Otwórz folder tekstury</String>
|
||||
|
||||
@@ -66,6 +66,9 @@
|
||||
<String key="StartNoStaffConfirm">В этом составе нет ни бригады, ни пассажира — управлять будет нечем. Всё равно запустить?</String>
|
||||
<String key="RestoreWeather">Восстановить погоду</String>
|
||||
<String key="ReloadSceneries">Перезагрузить</String>
|
||||
<String key="Favorites">Избранное</String>
|
||||
<String key="FavoriteAdd">Добавить в избранное</String>
|
||||
<String key="FavoriteRemove">Удалить из избранного</String>
|
||||
<String key="OpenSceneryFolder">Открыть папку</String>
|
||||
<String key="OptionalIncludes">Дополнительные элементы</String>
|
||||
<String key="OpenTextureFolder">Открыть папку с текстурами</String>
|
||||
|
||||
Reference in New Issue
Block a user