mirror of
https://github.com/MaSzyna-EU07/StarterNG.git
synced 2026-07-17 17:09:19 +02:00
Fix loads, some UI improvements
This commit is contained in:
@@ -29,13 +29,25 @@
|
||||
|
||||
<!-- Row 0: title / caption strip. Transparent background makes the
|
||||
whole strip a drag handle (PointerPressed -> BeginMoveDrag),
|
||||
and the OS buttons render at its top-right. -->
|
||||
<Border Grid.Row="0" Background="Transparent"
|
||||
PointerPressed="TitleBar_OnPointerPressed">
|
||||
<TextBlock Text="Starter MaSzyna" VerticalAlignment="Center"
|
||||
Margin="14,0,0,0" FontSize="11"
|
||||
Foreground="{StaticResource CarbonFgDim}" />
|
||||
</Border>
|
||||
and the OS buttons render at its top-right. The log button sits
|
||||
on top of the strip, just left of the OS min/max/close buttons. -->
|
||||
<StackPanel Orientation="Horizontal" Spacing="16" Grid.Row="0">
|
||||
<Border Background="Transparent"
|
||||
PointerPressed="TitleBar_OnPointerPressed">
|
||||
<TextBlock Text="Starter MaSzyna" VerticalAlignment="Center"
|
||||
Margin="14,0,0,0" FontSize="11"
|
||||
Foreground="{StaticResource CarbonFgDim}" />
|
||||
</Border>
|
||||
|
||||
<!-- Reserve ~140px on the right for the OS caption buttons so the
|
||||
log button lands immediately to their left. -->
|
||||
<Button Classes="Basic" Click="openLastLogClick"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Stretch"
|
||||
Margin="0,0,0,0" Padding="10,0" Cursor="Hand"
|
||||
ToolTip.Tip="{Binding [ShowLatestLogTooltip], Source={x:Static starterNg:App.Loc}}">
|
||||
<materialIcons:MaterialIcon Kind="FileDocument" Height="16" Width="16" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Row 1: logo + navigation (left) and the right cluster (right) -->
|
||||
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
@@ -65,7 +77,7 @@
|
||||
HorizontalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right cluster: version, language, log, social links.
|
||||
<!-- Right cluster: version, language, social links.
|
||||
In the toolbar row, so it sits below the OS caption buttons. -->
|
||||
<StackPanel Grid.Column="3" Orientation="Horizontal" VerticalAlignment="Center"
|
||||
Margin="0,0,8,0" Spacing="4">
|
||||
@@ -82,10 +94,6 @@
|
||||
<ComboBoxItem>English</ComboBoxItem>
|
||||
</ComboBox>
|
||||
|
||||
<Button Classes="Basic" Click="openLastLogClick"
|
||||
ToolTip.Tip="{Binding [ShowLatestLogTooltip], Source={x:Static starterNg:App.Loc}}">
|
||||
<materialIcons:MaterialIcon Kind="FileDocument" />
|
||||
</Button>
|
||||
<Button Classes="Basic" Cursor="Hand" Tag="http://eu07.pl/" Click="linkClick">
|
||||
<materialIcons:MaterialIcon Kind="Web" />
|
||||
</Button>
|
||||
|
||||
@@ -181,8 +181,12 @@
|
||||
</Style>
|
||||
|
||||
<!-- ── Compact list (tight rows that fill the available space) ─────────── -->
|
||||
<!-- Transparent surface so the list blends into the GroupBox panel instead
|
||||
of showing the Fluent theme's default light-grey ListBox background. -->
|
||||
<Style Selector="ListBox.Compact">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.Compact ListBoxItem">
|
||||
<Setter Property="Padding" Value="6,2" />
|
||||
|
||||
@@ -27,9 +27,11 @@
|
||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,*" ColumnSpacing="6" Margin="0,0,0,6">
|
||||
<ComboBox Name="categoryCombo" Grid.Column="0" HorizontalAlignment="Stretch"
|
||||
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
|
||||
PointerWheelChanged="Combo_OnPointerWheel"
|
||||
SelectionChanged="CategoryCombo_OnSelectionChanged" />
|
||||
<ComboBox Name="classCombo" Grid.Column="1" HorizontalAlignment="Stretch"
|
||||
PlaceholderText="{Binding [Class], Source={x:Static starterNg:App.Loc}}"
|
||||
PointerWheelChanged="Combo_OnPointerWheel"
|
||||
SelectionChanged="ClassCombo_OnSelectionChanged" />
|
||||
</Grid>
|
||||
<TextBox Name="searchBox" DockPanel.Dock="Top" Margin="0,0,0,8"
|
||||
|
||||
@@ -337,6 +337,30 @@ public partial class Depot : UserControl
|
||||
BuildList();
|
||||
}
|
||||
|
||||
// Hovering over the Type/Class combos and scrolling cycles the selection
|
||||
// without having to open the dropdown (scroll up = previous, down = next).
|
||||
// When the dropdown is open the wheel scrolls the list as usual.
|
||||
private void Combo_OnPointerWheel(object? sender, PointerWheelEventArgs e)
|
||||
{
|
||||
if (sender is not ComboBox combo || combo.IsDropDownOpen)
|
||||
return;
|
||||
|
||||
int count = combo.ItemCount;
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
int index = combo.SelectedIndex;
|
||||
if (e.Delta.Y > 0) // scroll up -> previous
|
||||
index = index <= 0 ? 0 : index - 1;
|
||||
else if (e.Delta.Y < 0) // scroll down -> next
|
||||
index = index < 0 ? 0 : Math.Min(index + 1, count - 1);
|
||||
else
|
||||
return;
|
||||
|
||||
combo.SelectedIndex = index;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- vehicle browser
|
||||
|
||||
// Flat list of plain-text vehicle entries (no expanders, like the original
|
||||
@@ -1270,20 +1294,33 @@ public partial class Depot : UserControl
|
||||
var phys = PhysicsFor(lead);
|
||||
|
||||
// Prefer the cargo this vehicle actually accepts (.fiz LoadAccepted);
|
||||
// otherwise suggest everything from data/load_weights.txt. Free text is
|
||||
// still allowed.
|
||||
var accepted = phys != null && !string.IsNullOrWhiteSpace(phys.LoadAccepted)
|
||||
// otherwise offer everything from data/load_weights.txt.
|
||||
var available = phys != null && !string.IsNullOrWhiteSpace(phys.LoadAccepted)
|
||||
? phys.LoadAccepted.Split(',', ';').Select(s => s.Trim()).Where(s => s.Length > 0).ToList()
|
||||
: null;
|
||||
: LoadTypes().ToList();
|
||||
|
||||
var typeBox = new AutoCompleteBox
|
||||
{
|
||||
FontSize = 12,
|
||||
MinWidth = 200,
|
||||
Text = lead.LoadType ?? "",
|
||||
ItemsSource = accepted ?? (IEnumerable<string>)LoadTypes(),
|
||||
FilterMode = AutoCompleteFilterMode.ContainsOrdinal
|
||||
};
|
||||
// Keep a load already set on the vehicle selectable even when it is not in
|
||||
// the accepted/known list (e.g. a custom load read from the scenery).
|
||||
if (!string.IsNullOrEmpty(lead.LoadType) &&
|
||||
!available.Contains(lead.LoadType!, StringComparer.OrdinalIgnoreCase))
|
||||
available.Insert(0, lead.LoadType!);
|
||||
|
||||
// Dropdown of available cargo: the leading "(none)" entry clears the load.
|
||||
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 200 };
|
||||
typeCombo.Items.Add(new ComboBoxItem { Content = App.Loc["None"], Tag = null });
|
||||
foreach (var name in available)
|
||||
typeCombo.Items.Add(new ComboBoxItem { Content = name, Tag = name });
|
||||
|
||||
// Pre-select the vehicle's current load (or "(none)").
|
||||
typeCombo.SelectedIndex = 0;
|
||||
if (!string.IsNullOrEmpty(lead.LoadType))
|
||||
foreach (var obj in typeCombo.Items)
|
||||
if (obj is ComboBoxItem ci && ci.Tag is string t &&
|
||||
string.Equals(t, lead.LoadType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
typeCombo.SelectedItem = ci;
|
||||
break;
|
||||
}
|
||||
|
||||
// Cap the amount at the .fiz MaxLoad when it is defined.
|
||||
int maxLoad = phys != null && phys.MaxLoad > 0 ? phys.MaxLoad : 1000;
|
||||
@@ -1295,22 +1332,25 @@ public partial class Depot : UserControl
|
||||
|
||||
void Apply()
|
||||
{
|
||||
string type = typeBox.Text?.Trim() ?? "";
|
||||
string? type = (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
|
||||
int count = (int)(countBox.Value ?? 0);
|
||||
foreach (var c in item.Cars)
|
||||
{
|
||||
c.LoadCount = count;
|
||||
c.LoadType = count > 0 && !string.IsNullOrEmpty(type) ? type : c.LoadType;
|
||||
if (count > 0)
|
||||
// "(none)" -> no cargo at all; otherwise store the chosen type/amount.
|
||||
c.LoadType = string.IsNullOrEmpty(type) ? null : type;
|
||||
c.LoadCount = string.IsNullOrEmpty(type) ? 0 : count;
|
||||
if (c.LoadCount > 0)
|
||||
c.HasVelocity = true; // velocity token must precede loadcount
|
||||
}
|
||||
}
|
||||
|
||||
typeBox.LostFocus += (_, _) => Apply();
|
||||
// Wire the handlers only after the initial selection so we don't apply
|
||||
// (and mark the consist dirty) while merely populating the editor.
|
||||
typeCombo.SelectionChanged += (_, _) => Apply();
|
||||
countBox.ValueChanged += (_, _) => Apply();
|
||||
|
||||
loadsPanel.Children.Add(UnitTitle(item));
|
||||
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeBox));
|
||||
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeCombo));
|
||||
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
|
||||
loadsPanel.Children.Add(new TextBlock
|
||||
{
|
||||
|
||||
@@ -59,9 +59,15 @@
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
|
||||
Text="{Binding [Time], Source={x:Static starterNg:App.Loc}}" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Name="weatherTime" Width="90"
|
||||
HorizontalAlignment="Left" Watermark="hh:mm"
|
||||
LostFocus="Weather_OnChanged" />
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||
<TimePicker Name="weatherTime" ClockIdentifier="24HourClock"
|
||||
MinuteIncrement="1" VerticalAlignment="Center"
|
||||
SelectedTimeChanged="WeatherTime_OnChanged" />
|
||||
<Button Name="weatherTimeNow" Classes="Flat" Cursor="Hand"
|
||||
VerticalAlignment="Center" Padding="10,4"
|
||||
Click="WeatherTimeNow_OnClick"
|
||||
Content="{Binding [CurrentTime], Source={x:Static starterNg:App.Loc}}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"
|
||||
Text="{Binding [Season], Source={x:Static starterNg:App.Loc}}" />
|
||||
@@ -94,8 +100,8 @@
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center"
|
||||
Text="{Binding [Visibility], Source={x:Static starterNg:App.Loc}}" />
|
||||
<StackPanel Grid.Row="4" Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||
<Slider Name="weatherFog" Minimum="50" Maximum="2500" Width="220"
|
||||
TickFrequency="50" IsSnapToTickEnabled="True"
|
||||
<Slider Name="weatherFog" Minimum="0" Maximum="10000" Width="220"
|
||||
TickFrequency="250" IsSnapToTickEnabled="True"
|
||||
ValueChanged="Weather_OnSliderChanged" />
|
||||
<TextBlock Name="weatherFogLabel" VerticalAlignment="Center" MinWidth="56" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
@@ -21,6 +23,9 @@ public partial class Scenarios : UserControl
|
||||
private Scenery? _currentScenery;
|
||||
// suppresses weather change handlers while the form is being populated
|
||||
private bool _loadingWeather;
|
||||
// "Today" season: the day field only previews the current day-of-year; the
|
||||
// stored Day stays 0 so the scenery keeps using the live date.
|
||||
private bool _todaySeason;
|
||||
|
||||
public Scenarios()
|
||||
{
|
||||
@@ -30,6 +35,7 @@ public partial class Scenarios : UserControl
|
||||
Sceneries = GameData.Instance.Sceneries;
|
||||
|
||||
var groupNodes = new Dictionary<string, TreeViewItem>();
|
||||
var topLevel = new List<TreeViewItem>();
|
||||
|
||||
for (int i = 0; i < Sceneries.Count; i++)
|
||||
{
|
||||
@@ -38,7 +44,7 @@ public partial class Scenarios : UserControl
|
||||
// case 1: no group - put without parent
|
||||
if (string.IsNullOrEmpty(scenery.Group))
|
||||
{
|
||||
sceneryList.Items.Add(new TreeViewItem
|
||||
topLevel.Add(new TreeViewItem
|
||||
{
|
||||
Header = Path.GetFileNameWithoutExtension(scenery.Path),
|
||||
Tag = i
|
||||
@@ -57,7 +63,7 @@ public partial class Scenarios : UserControl
|
||||
};
|
||||
|
||||
groupNodes[scenery.Group] = groupNode;
|
||||
sceneryList.Items.Add(groupNode);
|
||||
topLevel.Add(groupNode);
|
||||
}
|
||||
|
||||
groupNode.Items.Add(new TreeViewItem
|
||||
@@ -67,17 +73,59 @@ public partial class Scenarios : UserControl
|
||||
});
|
||||
}
|
||||
|
||||
// Sort the scenery tree alphabetically: group folders' inner sceneries
|
||||
// first, then the top-level (group folders + ungrouped sceneries). The Tag
|
||||
// keeps each node's original scenery index, so sorting only reorders the
|
||||
// display - the vehicles/trainsets list stays in scenery order.
|
||||
foreach (var node in groupNodes.Values)
|
||||
{
|
||||
var children = node.Items.Cast<TreeViewItem>()
|
||||
.OrderBy(c => c.Header as string, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
node.Items.Clear();
|
||||
foreach (var child in children)
|
||||
node.Items.Add(child);
|
||||
}
|
||||
|
||||
foreach (var item in topLevel.OrderBy(t => t.Header as string, StringComparer.OrdinalIgnoreCase))
|
||||
sceneryList.Items.Add(item);
|
||||
|
||||
// refresh the consist preview when the view is shown again (e.g. after
|
||||
// editing the consist in the depot). Switching tabs only toggles IsVisible
|
||||
// (the view stays in the tree), so listen for that too.
|
||||
AttachedToVisualTree += (_, _) => RefreshSelectedConsist();
|
||||
AttachedToVisualTree += (_, _) => OnReentered();
|
||||
PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.Property == IsVisibleProperty && IsVisible)
|
||||
RefreshSelectedConsist();
|
||||
OnReentered();
|
||||
};
|
||||
}
|
||||
|
||||
// Called whenever this view is shown again (tab switch / re-attach). Edits made
|
||||
// in the depot mutate the same Trainset objects, so refresh both the consist
|
||||
// preview and the vehicle-list captions that depend on them.
|
||||
private void OnReentered()
|
||||
{
|
||||
RefreshVehicleLabels();
|
||||
RefreshSelectedConsist();
|
||||
}
|
||||
|
||||
// Rebuilds the caption of each entry in the Vehicles list from its trainset's
|
||||
// current vehicles, so a consist edited in the depot is reflected here too.
|
||||
// Selection and order are preserved (only the text is updated).
|
||||
private void RefreshVehicleLabels()
|
||||
{
|
||||
if (sceneryList.SelectedItem is not TreeViewItem { Tag: int tag } || tag >= Sceneries.Count)
|
||||
return;
|
||||
|
||||
var scn = Sceneries[tag];
|
||||
foreach (var obj in vehicleList.Items)
|
||||
{
|
||||
if (obj is ListBoxItem { Tag: int vi } lbi && vi < scn.Trainsets.Count)
|
||||
lbi.Content = string.Join(" + ", scn.Trainsets[vi].Vehicles.Select(d => d.Name));
|
||||
}
|
||||
}
|
||||
|
||||
private void SceneryList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
var item = sceneryList.SelectedItem as TreeViewItem;
|
||||
@@ -183,12 +231,14 @@ public partial class Scenarios : UserControl
|
||||
try
|
||||
{
|
||||
weatherForm.IsEnabled = true;
|
||||
weatherTime.Text = scenery.WeatherTime;
|
||||
weatherTime.SelectedTime = ParseTime(scenery.WeatherTime);
|
||||
weatherDay.Value = scenery.Day;
|
||||
weatherTemp.Value = scenery.Temperature;
|
||||
weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 50, 2500);
|
||||
weatherFog.Value = System.Math.Clamp(scenery.FogEnd, 0, 10000);
|
||||
SelectByTag(weatherSeason, scenery.Day.ToString());
|
||||
SelectByTag(weatherOvercast, scenery.Overcast.ToString(CultureInfo.InvariantCulture));
|
||||
// Day 0 maps to the "Today" season entry: preview the live day-of-year.
|
||||
ApplyTodayPreview(scenery.Day == 0);
|
||||
UpdateWeatherLabels();
|
||||
}
|
||||
finally
|
||||
@@ -204,9 +254,11 @@ public partial class Scenarios : UserControl
|
||||
return;
|
||||
|
||||
var scn = _currentScenery;
|
||||
if (!string.IsNullOrWhiteSpace(weatherTime.Text))
|
||||
scn.WeatherTime = weatherTime.Text!.Trim();
|
||||
scn.Day = (int)(weatherDay.Value ?? 0);
|
||||
if (weatherTime.SelectedTime is { } t)
|
||||
scn.WeatherTime = $"{t.Hours:D2}:{t.Minutes:D2}";
|
||||
// In "Today" mode the day field is only a preview, so keep the stored day
|
||||
// at 0 (the scenery then follows the live date).
|
||||
scn.Day = _todaySeason ? 0 : (int)(weatherDay.Value ?? 0);
|
||||
scn.Temperature = weatherTemp.Value;
|
||||
scn.FogEnd = (int)weatherFog.Value;
|
||||
if ((weatherOvercast.SelectedItem as ComboBoxItem)?.Tag is string ov &&
|
||||
@@ -235,7 +287,44 @@ public partial class Scenarios : UserControl
|
||||
}
|
||||
|
||||
// --- weather form event handlers ---
|
||||
private void Weather_OnChanged(object? sender, RoutedEventArgs e) => CaptureWeather();
|
||||
private void WeatherTime_OnChanged(object? sender, TimePickerSelectedValueChangedEventArgs e) => CaptureWeather();
|
||||
|
||||
// "Current time" button: drop the live wall-clock time into the picker (the
|
||||
// SelectedTimeChanged handler then captures it onto the scenery).
|
||||
private void WeatherTimeNow_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
weatherTime.SelectedTime = new TimeSpan(now.Hour, now.Minute, 0);
|
||||
}
|
||||
|
||||
// Parses a stored "hh:mm" string into a TimeSpan, or null when it is missing
|
||||
// or malformed (so the picker simply shows no selection).
|
||||
private static TimeSpan? ParseTime(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return null;
|
||||
var m = Regex.Match(text, @"^\s*(\d{1,2})[:.](\d{2})");
|
||||
if (m.Success &&
|
||||
int.TryParse(m.Groups[1].Value, out int h) && h is >= 0 and <= 23 &&
|
||||
int.TryParse(m.Groups[2].Value, out int min) && min is >= 0 and <= 59)
|
||||
return new TimeSpan(h, min, 0);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Toggles "Today" preview: the day field shows the live day-of-year (read-only)
|
||||
// and CaptureWeather keeps the stored Day at 0; any other season re-enables it.
|
||||
private void ApplyTodayPreview(bool today)
|
||||
{
|
||||
_todaySeason = today;
|
||||
weatherDay.IsEnabled = !today;
|
||||
if (today)
|
||||
{
|
||||
bool prev = _loadingWeather;
|
||||
_loadingWeather = true;
|
||||
weatherDay.Value = DateTime.Now.DayOfYear;
|
||||
_loadingWeather = prev;
|
||||
}
|
||||
}
|
||||
|
||||
private void Weather_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) => CaptureWeather();
|
||||
|
||||
@@ -251,9 +340,17 @@ public partial class Scenarios : UserControl
|
||||
if ((weatherSeason.SelectedItem as ComboBoxItem)?.Tag is string tag &&
|
||||
int.TryParse(tag, out int day))
|
||||
{
|
||||
_loadingWeather = true;
|
||||
weatherDay.Value = day;
|
||||
_loadingWeather = false;
|
||||
if (day == 0) // "Today" - preview the live day-of-year, keep stored day at 0
|
||||
{
|
||||
ApplyTodayPreview(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyTodayPreview(false);
|
||||
_loadingWeather = true;
|
||||
weatherDay.Value = day;
|
||||
_loadingWeather = false;
|
||||
}
|
||||
}
|
||||
CaptureWeather();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<String key="NoWeatherData">No weather data in the scenery.</String>
|
||||
<String key="NoTimetable">No timetable for the selected consist.</String>
|
||||
<String key="Time">Time</String>
|
||||
<String key="CurrentTime">Now</String>
|
||||
<String key="Day">Day of year</String>
|
||||
<String key="Sky">Sky</String>
|
||||
<String key="Fog">Fog / visibility</String>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
<String key="NoWeatherData">Brak danych o pogodzie w scenerii.</String>
|
||||
<String key="NoTimetable">Brak rozkładu jazdy dla wybranego składu.</String>
|
||||
<String key="Time">Godzina</String>
|
||||
<String key="CurrentTime">Czas aktualny</String>
|
||||
<String key="Day">Dzień roku</String>
|
||||
<String key="Sky">Niebo</String>
|
||||
<String key="Fog">Mgła / widoczność</String>
|
||||
|
||||
Reference in New Issue
Block a user