CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,453 @@
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace CVRManager.Views;
|
||||
|
||||
public class LogView : UserControl
|
||||
{
|
||||
public static readonly StyledProperty<string?> TextProperty = AvaloniaProperty.Register<LogView, string?>(nameof(Text));
|
||||
|
||||
readonly ScrollViewer scroll;
|
||||
readonly LogSurface surface = new();
|
||||
|
||||
static LogView() => TextProperty.Changed.AddClassHandler<LogView>((c, _) => c.OnTextChanged());
|
||||
|
||||
public LogView()
|
||||
{
|
||||
scroll = new ScrollViewer
|
||||
{
|
||||
Content = surface,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
};
|
||||
scroll.ScrollChanged += (_, _) => surface.UpdateView(scroll.Offset, scroll.Viewport);
|
||||
Content = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse("#0B0D10")),
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#232833")),
|
||||
BorderThickness = new Thickness(1),
|
||||
CornerRadius = new CornerRadius(8),
|
||||
ClipToBounds = true,
|
||||
Child = scroll,
|
||||
};
|
||||
}
|
||||
|
||||
public string? Text
|
||||
{
|
||||
get => GetValue(TextProperty);
|
||||
set => SetValue(TextProperty, value);
|
||||
}
|
||||
|
||||
void OnTextChanged()
|
||||
{
|
||||
var atEnd = scroll.Extent.Height <= 0 || scroll.Offset.Y + scroll.Viewport.Height >= scroll.Extent.Height - 24;
|
||||
if (!surface.SetText(Text ?? "")) return;
|
||||
if (atEnd) Dispatcher.UIThread.Post(() => scroll.ScrollToEnd(), DispatcherPriority.Background);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LogSurface : Control
|
||||
{
|
||||
const int MaxLines = 20000;
|
||||
const int KeepLines = 15000;
|
||||
|
||||
static readonly IBrush DefaultBrush = new SolidColorBrush(Color.Parse("#E6E8EC"));
|
||||
static readonly IBrush TimeBrush = new SolidColorBrush(Color.Parse("#6B7585"));
|
||||
static readonly IBrush TagBrush = new SolidColorBrush(Color.Parse("#7AA2FF"));
|
||||
static readonly IBrush ErrorBrush = new SolidColorBrush(Color.Parse("#F0646A"));
|
||||
static readonly IBrush WarnBrush = new SolidColorBrush(Color.Parse("#F5B13D"));
|
||||
static readonly IBrush SelectionBrush = new SolidColorBrush(Color.Parse("#553A5BA8"));
|
||||
|
||||
static readonly Regex TimeRx = new(@"^(\[\d{1,2}:\d{2}:\d{2}(?:[.,]\d+)?\]|\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?)(\s*)(.*)$", RegexOptions.Compiled);
|
||||
static readonly Regex TagRx = new(@"^(\s*(?:\[[^\]]{1,40}\]|\([^)]{1,100}\)))(.*)$", RegexOptions.Compiled);
|
||||
static readonly Regex StackRx = new(@"^\s+at\s", RegexOptions.Compiled);
|
||||
static readonly Regex FileLineRx = new(@"^\(Filename:", RegexOptions.Compiled);
|
||||
static readonly Regex PipeLevelRx = new(@"\|\s*(?<l>error|exception|fatal|warn|warning|info|debug|trace)\s*\)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
static readonly Regex BracketLevelRx = new(@"\[(?<l>error|fatal|fail|exception|warn|warning)\]", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
static readonly Regex LeadingLevelRx = new(@"^\s*(?<l>error|fatal|unhandled exception|exception|failed|failure|warning|warn)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
static readonly Regex ExceptionRx = new(@"(?:^|\s)(?:[A-Za-z_]\w*\.)*[A-Za-z_]\w*Exception:", RegexOptions.Compiled);
|
||||
|
||||
readonly record struct Seg(string Text, IBrush Brush);
|
||||
|
||||
static readonly Regex AnsiRx = new("\u001b\\[([0-9;]*)m", RegexOptions.Compiled);
|
||||
static readonly Regex OtherEscRx = new("\u001b\\[[0-9;?]*[A-Za-ln-z]", RegexOptions.Compiled);
|
||||
static readonly IBrush[] AnsiNormal = new[] { "#8B93A1", "#F0646A", "#4ADE9B", "#F5B13D", "#7AA2FF", "#C792EA", "#5FD3E0", "#E6E8EC" }
|
||||
.Select(c => (IBrush)new SolidColorBrush(Color.Parse(c))).ToArray();
|
||||
static readonly IBrush[] AnsiBright = new[] { "#A0A8B5", "#FF8087", "#6EF0B5", "#FFD166", "#9AB8FF", "#DDA8FF", "#8FEAF5", "#FFFFFF" }
|
||||
.Select(c => (IBrush)new SolidColorBrush(Color.Parse(c))).ToArray();
|
||||
|
||||
static IBrush Rgb(int r, int g, int b) => new SolidColorBrush(Color.FromRgb((byte)Math.Clamp(r, 0, 255), (byte)Math.Clamp(g, 0, 255), (byte)Math.Clamp(b, 0, 255)));
|
||||
|
||||
static IBrush? ApplySgr(string codes, IBrush? current)
|
||||
{
|
||||
var parts = codes.Length == 0 ? new[] { "0" } : codes.Split(';');
|
||||
for (var i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (!int.TryParse(parts[i], out var n)) continue;
|
||||
if (n == 0 || n == 39) current = null;
|
||||
else if (n >= 30 && n <= 37) current = AnsiNormal[n - 30];
|
||||
else if (n >= 90 && n <= 97) current = AnsiBright[n - 90];
|
||||
else if (n == 38 && i + 1 < parts.Length)
|
||||
{
|
||||
if (parts[i + 1] == "2" && i + 4 < parts.Length && int.TryParse(parts[i + 2], out var r) && int.TryParse(parts[i + 3], out var g) && int.TryParse(parts[i + 4], out var b))
|
||||
{
|
||||
current = Rgb(r, g, b);
|
||||
i += 4;
|
||||
}
|
||||
else if (parts[i + 1] == "5" && i + 2 < parts.Length && int.TryParse(parts[i + 2], out var idx))
|
||||
{
|
||||
if (idx < 8) current = AnsiNormal[idx];
|
||||
else if (idx < 16) current = AnsiBright[idx - 8];
|
||||
else if (idx < 232)
|
||||
{
|
||||
var c = idx - 16;
|
||||
int Level(int v) => v == 0 ? 0 : 55 + v * 40;
|
||||
current = Rgb(Level(c / 36), Level(c / 6 % 6), Level(c % 6));
|
||||
}
|
||||
else { var v = 8 + (idx - 232) * 10; current = Rgb(v, v, v); }
|
||||
i += 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
static Line ParseAnsi(string raw, IBrush? carried)
|
||||
{
|
||||
raw = OtherEscRx.Replace(raw, "");
|
||||
var segs = new List<Seg>(4);
|
||||
var brush = carried;
|
||||
var pos = 0;
|
||||
foreach (Match m in AnsiRx.Matches(raw))
|
||||
{
|
||||
if (m.Index > pos) segs.Add(new Seg(raw[pos..m.Index], brush ?? DefaultBrush));
|
||||
brush = ApplySgr(m.Groups[1].Value, brush);
|
||||
pos = m.Index + m.Length;
|
||||
}
|
||||
if (pos < raw.Length) segs.Add(new Seg(raw[pos..], brush ?? DefaultBrush));
|
||||
return new Line { Text = string.Concat(segs.Select(s => s.Text)), Segs = segs.ToArray(), AnsiEnd = brush };
|
||||
}
|
||||
|
||||
sealed class Line
|
||||
{
|
||||
public IBrush? AnsiEnd;
|
||||
public string Text = "";
|
||||
public Seg[] Segs = Array.Empty<Seg>();
|
||||
public IBrush? Level;
|
||||
}
|
||||
|
||||
readonly List<Line> lines = new();
|
||||
readonly Dictionary<Line, FormattedText[]> formatted = new();
|
||||
readonly Typeface typeface = new(new FontFamily("Cascadia Mono,Consolas,monospace"));
|
||||
readonly double fontSize = 12;
|
||||
readonly Thickness pad = new(12, 8);
|
||||
readonly double charWidth;
|
||||
readonly double lineHeight;
|
||||
|
||||
string shown = "";
|
||||
int maxCols;
|
||||
Vector offset;
|
||||
Size viewport;
|
||||
(int Line, int Col)? anchor;
|
||||
(int Line, int Col)? caret;
|
||||
bool dragging;
|
||||
|
||||
public LogSurface()
|
||||
{
|
||||
var probe = Format("0000000000", DefaultBrush);
|
||||
charWidth = probe.Width / 10;
|
||||
lineHeight = Math.Ceiling(probe.Height) + 1;
|
||||
Focusable = true;
|
||||
Cursor = new Cursor(StandardCursorType.Ibeam);
|
||||
ClipToBounds = true;
|
||||
}
|
||||
|
||||
FormattedText Format(string text, IBrush brush) =>
|
||||
new(text, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, fontSize, brush);
|
||||
|
||||
static IBrush? FromWord(string word) => word.ToLowerInvariant() switch
|
||||
{
|
||||
"error" or "fatal" or "fail" or "exception" or "failed" or "failure" or "unhandled exception" => ErrorBrush,
|
||||
"warn" or "warning" => WarnBrush,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
static IBrush? LevelOfLine(string line)
|
||||
{
|
||||
var pipe = PipeLevelRx.Match(line);
|
||||
if (pipe.Success) return FromWord(pipe.Groups["l"].Value);
|
||||
|
||||
var head = line.Length > 160 ? line[..160] : line;
|
||||
var bracket = BracketLevelRx.Match(head);
|
||||
if (bracket.Success) return FromWord(bracket.Groups["l"].Value);
|
||||
|
||||
var message = line;
|
||||
var time = TimeRx.Match(message);
|
||||
if (time.Success) message = time.Groups[3].Value;
|
||||
var tag = TagRx.Match(message);
|
||||
while (tag.Success)
|
||||
{
|
||||
message = tag.Groups[2].Value;
|
||||
tag = TagRx.Match(message);
|
||||
}
|
||||
|
||||
var leading = LeadingLevelRx.Match(message);
|
||||
if (leading.Success) return FromWord(leading.Groups["l"].Value);
|
||||
return ExceptionRx.IsMatch(message) ? ErrorBrush : null;
|
||||
}
|
||||
|
||||
static Line Parse(string text, IBrush? inherited)
|
||||
{
|
||||
var line = new Line { Text = text, Level = inherited };
|
||||
var segs = new List<Seg>(4);
|
||||
if (StackRx.IsMatch(text)) segs.Add(new Seg(text, inherited ?? DefaultBrush));
|
||||
else if (FileLineRx.IsMatch(text)) segs.Add(new Seg(text, TimeBrush));
|
||||
else
|
||||
{
|
||||
var level = LevelOfLine(text);
|
||||
line.Level = level;
|
||||
var rest = text;
|
||||
var time = TimeRx.Match(rest);
|
||||
if (time.Success)
|
||||
{
|
||||
segs.Add(new Seg(time.Groups[1].Value, TimeBrush));
|
||||
if (time.Groups[2].Length > 0) segs.Add(new Seg(time.Groups[2].Value, DefaultBrush));
|
||||
rest = time.Groups[3].Value;
|
||||
}
|
||||
var tag = TagRx.Match(rest);
|
||||
if (tag.Success)
|
||||
{
|
||||
segs.Add(new Seg(tag.Groups[1].Value, level ?? TagBrush));
|
||||
rest = tag.Groups[2].Value;
|
||||
}
|
||||
if (rest.Length > 0) segs.Add(new Seg(rest, level ?? DefaultBrush));
|
||||
}
|
||||
line.Segs = segs.ToArray();
|
||||
return line;
|
||||
}
|
||||
|
||||
void AppendLines(string chunk)
|
||||
{
|
||||
var parts = chunk.Split('\n');
|
||||
var count = parts.Length;
|
||||
if (count > 0 && parts[^1].Length == 0) count--;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var text = parts[i].TrimEnd('\r');
|
||||
var carried = lines.Count > 0 ? lines[^1].AnsiEnd : null;
|
||||
var line = carried != null || text.IndexOf('\u001b') >= 0
|
||||
? ParseAnsi(text, carried)
|
||||
: Parse(text, lines.Count > 0 ? lines[^1].Level : null);
|
||||
text = line.Text;
|
||||
lines.Add(line);
|
||||
if (text.Length > maxCols) maxCols = text.Length;
|
||||
}
|
||||
}
|
||||
|
||||
void TrimHead()
|
||||
{
|
||||
if (lines.Count <= MaxLines) return;
|
||||
var drop = lines.Count - KeepLines;
|
||||
foreach (var l in lines.Take(drop)) formatted.Remove(l);
|
||||
lines.RemoveRange(0, drop);
|
||||
maxCols = lines.Count == 0 ? 0 : lines.Max(l => l.Text.Length);
|
||||
anchor = anchor is { } a ? (Math.Max(0, a.Line - drop), a.Line - drop < 0 ? 0 : a.Col) : null;
|
||||
caret = caret is { } c ? (Math.Max(0, c.Line - drop), c.Line - drop < 0 ? 0 : c.Col) : null;
|
||||
}
|
||||
|
||||
public bool SetText(string text)
|
||||
{
|
||||
if (text == shown) return false;
|
||||
|
||||
string? appended = null;
|
||||
var endsWithNewline = shown.Length == 0 || shown[^1] == '\n';
|
||||
if (shown.Length > 0 && text.StartsWith(shown, StringComparison.Ordinal)) appended = text[shown.Length..];
|
||||
else if (shown.Length > 0)
|
||||
{
|
||||
var key = shown.Length > 400 ? shown[^400..] : shown;
|
||||
var at = text.LastIndexOf(key, StringComparison.Ordinal);
|
||||
if (at >= 0) appended = text[(at + key.Length)..];
|
||||
}
|
||||
|
||||
if (appended == null)
|
||||
{
|
||||
lines.Clear();
|
||||
formatted.Clear();
|
||||
maxCols = 0;
|
||||
anchor = caret = null;
|
||||
AppendLines(text);
|
||||
}
|
||||
else
|
||||
{
|
||||
var partial = "";
|
||||
if (!endsWithNewline && lines.Count > 0)
|
||||
{
|
||||
partial = lines[^1].Text;
|
||||
formatted.Remove(lines[^1]);
|
||||
lines.RemoveAt(lines.Count - 1);
|
||||
}
|
||||
AppendLines(partial + appended);
|
||||
}
|
||||
|
||||
TrimHead();
|
||||
shown = text;
|
||||
InvalidateMeasure();
|
||||
InvalidateVisual();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdateView(Vector newOffset, Size newViewport)
|
||||
{
|
||||
var resized = newViewport != viewport;
|
||||
offset = newOffset;
|
||||
viewport = newViewport;
|
||||
if (resized) InvalidateMeasure();
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
var width = pad.Left + pad.Right + maxCols * charWidth;
|
||||
var height = pad.Top + pad.Bottom + lines.Count * lineHeight;
|
||||
return new Size(Math.Max(width, viewport.Width), Math.Max(height, viewport.Height));
|
||||
}
|
||||
|
||||
public override void Render(DrawingContext context)
|
||||
{
|
||||
context.FillRectangle(Brushes.Transparent, new Rect(Bounds.Size));
|
||||
if (lines.Count == 0) return;
|
||||
|
||||
var first = Math.Max(0, (int)((offset.Y - pad.Top) / lineHeight));
|
||||
var last = Math.Min(lines.Count - 1, (int)((offset.Y + viewport.Height - pad.Top) / lineHeight) + 1);
|
||||
var (selStart, selEnd) = Selection();
|
||||
|
||||
if (formatted.Count > 600) formatted.Clear();
|
||||
for (var i = first; i <= last; i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
var y = pad.Top + i * lineHeight;
|
||||
|
||||
if (selStart is { } s && selEnd is { } e && i >= s.Line && i <= e.Line)
|
||||
{
|
||||
var c0 = i == s.Line ? s.Col : 0;
|
||||
var c1 = i == e.Line ? e.Col : line.Text.Length + 1;
|
||||
if (c1 > c0) context.FillRectangle(SelectionBrush, new Rect(pad.Left + c0 * charWidth, y, (c1 - c0) * charWidth, lineHeight));
|
||||
}
|
||||
|
||||
if (!formatted.TryGetValue(line, out var fts))
|
||||
formatted[line] = fts = line.Segs.Select(sg => Format(sg.Text, sg.Brush)).ToArray();
|
||||
|
||||
var x = pad.Left;
|
||||
for (var k = 0; k < line.Segs.Length; k++)
|
||||
{
|
||||
var width = line.Segs[k].Text.Length * charWidth;
|
||||
if (x + width >= offset.X && x <= offset.X + viewport.Width)
|
||||
context.DrawText(fts[k], new Point(x, y));
|
||||
x += width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(int Line, int Col) HitTest(Point p)
|
||||
{
|
||||
if (lines.Count == 0) return (0, 0);
|
||||
var line = Math.Clamp((int)Math.Floor((p.Y - pad.Top) / lineHeight), 0, lines.Count - 1);
|
||||
var col = Math.Clamp((int)Math.Round((p.X - pad.Left) / charWidth), 0, lines[line].Text.Length);
|
||||
return (line, col);
|
||||
}
|
||||
|
||||
((int Line, int Col)? Start, (int Line, int Col)? End) Selection()
|
||||
{
|
||||
if (anchor is not { } a || caret is not { } c || a == c) return (null, null);
|
||||
var forward = a.Line < c.Line || (a.Line == c.Line && a.Col <= c.Col);
|
||||
return forward ? (a, c) : (c, a);
|
||||
}
|
||||
|
||||
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
||||
{
|
||||
base.OnPointerPressed(e);
|
||||
Focus();
|
||||
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
|
||||
var hit = HitTest(e.GetPosition(this));
|
||||
if (e.ClickCount == 2 && lines.Count > 0)
|
||||
{
|
||||
var text = lines[hit.Line].Text;
|
||||
int start = hit.Col, end = hit.Col;
|
||||
while (start > 0 && !char.IsWhiteSpace(text[start - 1])) start--;
|
||||
while (end < text.Length && !char.IsWhiteSpace(text[end])) end++;
|
||||
anchor = (hit.Line, start);
|
||||
caret = (hit.Line, end);
|
||||
}
|
||||
else if (e.ClickCount >= 3 && lines.Count > 0)
|
||||
{
|
||||
anchor = (hit.Line, 0);
|
||||
caret = (hit.Line, lines[hit.Line].Text.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
anchor = hit;
|
||||
caret = hit;
|
||||
dragging = true;
|
||||
}
|
||||
e.Pointer.Capture(this);
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
protected override void OnPointerMoved(PointerEventArgs e)
|
||||
{
|
||||
base.OnPointerMoved(e);
|
||||
if (!dragging) return;
|
||||
caret = HitTest(e.GetPosition(this));
|
||||
InvalidateVisual();
|
||||
}
|
||||
|
||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||
{
|
||||
base.OnPointerReleased(e);
|
||||
dragging = false;
|
||||
e.Pointer.Capture(null);
|
||||
}
|
||||
|
||||
string SelectedText()
|
||||
{
|
||||
var (start, end) = Selection();
|
||||
if (start is not { } s || end is not { } e) return "";
|
||||
var sb = new StringBuilder();
|
||||
for (var i = s.Line; i <= e.Line && i < lines.Count; i++)
|
||||
{
|
||||
var text = lines[i].Text;
|
||||
var c0 = i == s.Line ? Math.Min(s.Col, text.Length) : 0;
|
||||
var c1 = i == e.Line ? Math.Min(e.Col, text.Length) : text.Length;
|
||||
sb.Append(text, c0, Math.Max(0, c1 - c0));
|
||||
if (i < e.Line) sb.Append('\n');
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
protected override async void OnKeyDown(KeyEventArgs e)
|
||||
{
|
||||
base.OnKeyDown(e);
|
||||
if (!e.KeyModifiers.HasFlag(KeyModifiers.Control)) return;
|
||||
if (e.Key == Key.A && lines.Count > 0)
|
||||
{
|
||||
anchor = (0, 0);
|
||||
caret = (lines.Count - 1, lines[^1].Text.Length);
|
||||
InvalidateVisual();
|
||||
e.Handled = true;
|
||||
}
|
||||
else if (e.Key == Key.C)
|
||||
{
|
||||
var text = SelectedText();
|
||||
if (text.Length > 0 && TopLevel.GetTopLevel(this)?.Clipboard is { } clipboard) await clipboard.SetTextAsync(text);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:CVRManager.ViewModels"
|
||||
xmlns:m="using:CVRManager.Models"
|
||||
xmlns:v="using:CVRManager.Views"
|
||||
x:Class="CVRManager.Views.MainWindow"
|
||||
Icon="/Assets/app.ico" Title="CVR Manager" Width="1240" Height="820" MinWidth="1000" MinHeight="620">
|
||||
|
||||
<Grid>
|
||||
<DockPanel>
|
||||
<Border DockPanel.Dock="Top" Background="{DynamicResource SidebarBg}" BorderBrush="{DynamicResource CardBorder}" BorderThickness="0,0,0,1" Padding="24,0">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<Path Data="{StaticResource CvrMark}" Fill="{DynamicResource AccentBrush}" Stretch="Uniform" Height="17" VerticalAlignment="Center" Margin="0,0,12,0"/>
|
||||
<TextBlock Text="CVR Manager" FontSize="16" FontWeight="Bold" VerticalAlignment="Center" Margin="0,0,28,0"/>
|
||||
<RadioButton Classes="tab" GroupName="top" Content="Installs" IsChecked="{Binding IsInstallsPage}"/>
|
||||
<RadioButton Classes="tab" GroupName="top" Content="Sources" IsChecked="{Binding IsModsPage}"/>
|
||||
<RadioButton Classes="tab" GroupName="top" Content="Gallery" IsChecked="{Binding IsGalleryPage}"/>
|
||||
<RadioButton Classes="tab" GroupName="top" Content="Settings" IsChecked="{Binding IsSettingsPage}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Panel>
|
||||
<Grid ColumnDefinitions="300,*" Margin="24,20,24,20" IsVisible="{Binding IsInstallsPage}">
|
||||
<DockPanel Margin="0,0,20,0">
|
||||
<StackPanel DockPanel.Dock="Bottom" Spacing="8" Margin="0,8,0,0">
|
||||
<Button Content="+ New install" Command="{Binding ShowNewCommand}" Classes="accent" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
||||
<Button Content="Refresh branches" Command="{Binding RefreshBranchesCommand}" Classes="ghost" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<ListBox x:Name="InstallList" Classes="cards" ItemsSource="{Binding Installs}" SelectedItem="{Binding SelectedInstall}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="vm:InstallVM">
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="16,12,12,12" ColumnSpacing="8">
|
||||
<StackPanel Spacing="3">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="15" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding Branch}" Classes="dim" FontSize="12" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding CardStatus}" Classes="dim" FontSize="12" IsVisible="{Binding !UpdateAvailable}"/>
|
||||
<TextBlock Text="{Binding CardStatus}" Foreground="{DynamicResource WarnFg}" FontWeight="SemiBold" FontSize="12" IsVisible="{Binding UpdateAvailable}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" VerticalAlignment="Stretch">
|
||||
<StackPanel Spacing="4" VerticalAlignment="Center">
|
||||
<Button Content="▶ Desktop" Command="{Binding LaunchDesktopCommand}" IsEnabled="{Binding HasGame}" Classes="accent" Padding="10,6" FontSize="12" HorizontalAlignment="Stretch" ToolTip.Tip="Launch in desktop mode (again, if it is already running)"/>
|
||||
<Button Content="▶ VR" Command="{Binding LaunchVrCommand}" IsEnabled="{Binding HasGame}" Padding="10,6" FontSize="12" HorizontalAlignment="Stretch" ToolTip.Tip="Launch in VR mode (again, if it is already running)"/>
|
||||
</StackPanel>
|
||||
<Button Content="{Binding StopShortText}" Command="{Binding StopCommand}" IsVisible="{Binding IsRunning}" Classes="stop" Padding="9,6" FontSize="12" VerticalAlignment="Stretch" VerticalContentAlignment="Center" ToolTip.Tip="{Binding StopTip}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
|
||||
<Panel Grid.Column="1">
|
||||
<StackPanel IsVisible="{Binding NoInstalls}" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="10">
|
||||
<TextBlock Text="No installs yet" Classes="h1" HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
|
||||
<ContentControl Content="{Binding SelectedInstall}">
|
||||
<ContentControl.ContentTemplate>
|
||||
<DataTemplate DataType="vm:InstallVM">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Spacing="12" Margin="0,0,0,12">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="{Binding Name}" Classes="h1"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border Classes="chip"><TextBlock Text="{Binding Branch}"/></Border>
|
||||
<Border Classes="chip"><TextBlock Text="{Binding BuildText}"/></Border>
|
||||
<Border Classes="chip good" IsVisible="{Binding UpToDate}"><TextBlock Text="Up to date"/></Border>
|
||||
<Border Classes="chip warn" IsVisible="{Binding UpdateAvailable}"><TextBlock Text="{Binding UpdateChipText}"/></Border>
|
||||
<Border Classes="chip" IsVisible="{Binding UpdateUnknown}"><TextBlock Text="Latest build unknown"/></Border>
|
||||
<Border Classes="chip good" IsVisible="{Binding IsRunning}"><TextBlock Text="{Binding RunningText}"/></Border>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Top">
|
||||
<ComboBox ItemsSource="{Binding Main.AccountChoices}" SelectedItem="{Binding AccountChoice}" IsVisible="{Binding Main.HasAccounts}" MinWidth="150" MinHeight="40" Padding="12,8" ToolTip.Tip="Account">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="m:AccountChoice"><TextBlock Text="{Binding Name}"/></DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<ComboBox ItemsSource="{Binding LaunchModes}" SelectedItem="{Binding LaunchModeChoice}" MinWidth="130" MinHeight="40" Padding="12,8">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="m:LaunchModeOption"><TextBlock Text="{Binding Label}"/></DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<Button Content="▶ Launch" Command="{Binding LaunchCommand}" IsEnabled="{Binding HasGame}" Classes="accent" MinWidth="132" HorizontalContentAlignment="Center" ToolTip.Tip="{Binding LaunchTip}"/>
|
||||
<Button Content="{Binding StopText}" Command="{Binding StopCommand}" IsVisible="{Binding IsRunning}" Classes="stop" HorizontalContentAlignment="Center" ToolTip.Tip="{Binding StopTip}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Content="Update" Command="{Binding UpdateCommand}"/>
|
||||
<Button Content="Verify files" Command="{Binding VerifyCommand}"/>
|
||||
<Button Content="Open folder" Command="{Binding OpenFolderCommand}"/>
|
||||
<Button Content="Duplicate" Command="{Binding DuplicateCommand}"/>
|
||||
<Button Content="Delete install" Command="{Binding DeleteCommand}" Classes="ghost danger"/>
|
||||
</StackPanel>
|
||||
<Border BorderBrush="{DynamicResource CardBorder}" BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<RadioButton Classes="tab" GroupName="sub" Content="Overview" IsChecked="{Binding ShowOverview}"/>
|
||||
<RadioButton Classes="tab" GroupName="sub" Content="Mods" IsChecked="{Binding ShowModsTab}"/>
|
||||
<RadioButton Classes="tab" GroupName="sub" Content="Logs" IsChecked="{Binding ShowLogs}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Panel>
|
||||
<ScrollViewer IsVisible="{Binding ShowOverview}">
|
||||
<StackPanel Spacing="14" Margin="0,0,10,16" MaxWidth="900" HorizontalAlignment="Stretch">
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Branch and launch" Classes="h2"/>
|
||||
<TextBlock Text="Name" Classes="label"/>
|
||||
<TextBox Text="{Binding Name}"/>
|
||||
<TextBlock Text="Branch" Classes="label"/>
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
|
||||
<ComboBox ItemsSource="{Binding Main.BranchChoices}" SelectedItem="{Binding BranchHint}" HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="m:BranchChoice"><TextBlock Text="{Binding Display}"/></DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<Button Grid.Column="1" Content="+ Beta code" Command="{Binding EnterCodeCommand}" VerticalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
<TextBlock Text="Launch arguments" Classes="label"/>
|
||||
<Grid ColumnDefinitions="*,260" ColumnSpacing="8">
|
||||
<TextBox Text="{Binding LaunchArgs}" PlaceholderText="Extra arguments"/>
|
||||
<ComboBox Grid.Column="1" ItemsSource="{x:Static m:LaunchArgOption.All}" SelectedItem="{Binding ArgChoice}" PlaceholderText="Add an argument..." HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="m:LaunchArgOption">
|
||||
<StackPanel ToolTip.Tip="{Binding Description}">
|
||||
<TextBlock Text="{Binding Flag}" FontWeight="Medium"/>
|
||||
<TextBlock Text="{Binding Description}" Classes="dim" FontSize="11" MaxWidth="360" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="MelonLoader (version.dll)" Classes="h2"/>
|
||||
<TextBlock Text="{Binding MlText}" Classes="dim"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<ComboBox ItemsSource="{Binding MlVersions}" SelectedItem="{Binding MlVersionChoice}" PlaceholderText="Version" MinWidth="150"/>
|
||||
<Button Content="Install this version" Command="{Binding SetMlVersionCommand}"/>
|
||||
<Button Content="{Binding MlToggleText}" Command="{Binding ToggleMlCommand}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Hooks" Classes="h2"/>
|
||||
<ItemsControl ItemsSource="{Binding Hooks}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:InstallHookVM">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto" ColumnSpacing="12" Margin="0,3">
|
||||
<CheckBox IsChecked="{Binding Enabled}" MinWidth="0"/>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Medium"/>
|
||||
<TextBlock Text="{Binding When}" Classes="dim" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2" Content="Run now" Command="{Binding RunCommand}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Shared cache" Classes="h2"/>
|
||||
<TextBlock Text="{Binding CacheText}" Classes="dim"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="Link to shared cache" Command="{Binding LinkCacheCommand}" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid IsVisible="{Binding ShowModsTab}" RowDefinitions="Auto,Auto,*,Auto" Margin="0,0,0,4">
|
||||
<Grid ColumnDefinitions="*,200" ColumnSpacing="8" Margin="0,0,0,8">
|
||||
<TextBox Text="{Binding Main.ModSearch}" PlaceholderText="Search all available mods"/>
|
||||
<ComboBox Grid.Column="1" ItemsSource="{x:Static vm:MainViewModel.ModFilters}" SelectedItem="{Binding Main.ModFilter}" HorizontalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="1" ColumnDefinitions="34,250,90,90,*,96" Margin="12,0,12,4">
|
||||
<TextBlock Grid.Column="1" Text="Name" Classes="dim" FontSize="12"/>
|
||||
<TextBlock Grid.Column="2" Text="Installed" Classes="dim" FontSize="12"/>
|
||||
<TextBlock Grid.Column="3" Text="Latest" Classes="dim" FontSize="12"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding Main.AvailableStatus}" Classes="dim" FontSize="12"/>
|
||||
</Grid>
|
||||
<ListBox Grid.Row="2" Classes="table" ItemsSource="{Binding Main.ModRows}" SelectedItem="{Binding Main.SelectedRow}" SelectionChanged="OnTableSelection">
|
||||
<ListBox.DataTemplates>
|
||||
<DataTemplate DataType="vm:GroupHeader">
|
||||
<Border Background="{DynamicResource GroupHeaderBg}" Padding="12,9" Margin="0,6,0,0" CornerRadius="6" Cursor="Hand">
|
||||
<TextBlock Text="{Binding Text}" FontWeight="SemiBold"/>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
<DataTemplate DataType="vm:ModVM">
|
||||
<Grid ColumnDefinitions="34,250,90,90,*,96" Margin="12,6" ToolTip.Tip="{Binding Description}">
|
||||
<CheckBox IsChecked="{Binding InstallUsed}" IsEnabled="{Binding HasInstall}" MinWidth="0" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="0,0,10,0">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Medium" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding AuthorLine}" Classes="dim" FontSize="11" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center" Margin="0,0,8,0">
|
||||
<TextBlock Text="{Binding InstalledText}" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="update" FontSize="11" Foreground="{DynamicResource WarnFg}" IsVisible="{Binding UpdateAvailable}"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="3" Text="{Binding LatestText}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Margin="0,0,8,0"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding ShortDescription}" Classes="dim" VerticalAlignment="Center" TextWrapping="Wrap" MaxHeight="34" TextTrimming="CharacterEllipsis" Margin="0,0,10,0"/>
|
||||
<Button Grid.Column="5" Content="Manage" Command="{Binding ManageCommand}" Padding="10,5" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
|
||||
<Grid Grid.Row="3" ColumnDefinitions="Auto,Auto,*,Auto,Auto" ColumnSpacing="8" Margin="0,12,0,0">
|
||||
<StackPanel Grid.ColumnSpan="2" Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="Scan Steam install" Command="{Binding Main.ScanSteamInstallCommand}" IsVisible="{Binding Main.HasDetected}"/>
|
||||
<Button Content="Scan this install's folder" Command="{Binding ImportModsCommand}"/>
|
||||
<Button Content="Refresh sources" Command="{Binding Main.RefreshAllModsCommand}" Classes="ghost"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="2" Text="{Binding PendingText}" Foreground="{DynamicResource WarnFg}" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0,0,12,0"/>
|
||||
<Button Grid.Column="3" Content="Add local file(s)..." Command="{Binding Main.AddLocalNewCommand}"/>
|
||||
<Button Grid.Column="4" Content="Install / Update" Command="{Binding ApplyCommand}" Classes="accent"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid IsVisible="{Binding ShowLogs}" RowDefinitions="Auto,*" Margin="0,0,0,4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8" Spacing="4">
|
||||
<RadioButton Classes="tab" GroupName="logtab" Content="MelonLoader" IsChecked="{Binding ShowMlLog}" Padding="14,8"/>
|
||||
<RadioButton Classes="tab" GroupName="logtab" Content="Unity" IsChecked="{Binding ShowUnityLog}" Padding="14,8"/>
|
||||
<TextBlock Text="Shared by all installs" Classes="dim" FontSize="12" VerticalAlignment="Center" Margin="12,0,0,0" IsVisible="{Binding ShowUnityLog}"/>
|
||||
</StackPanel>
|
||||
<v:LogView Grid.Row="1" Text="{Binding MlLogText}" IsVisible="{Binding ShowMlLog}"/>
|
||||
<v:LogView Grid.Row="1" Text="{Binding UnityLogText}" IsVisible="{Binding ShowUnityLog}"/>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</ContentControl.ContentTemplate>
|
||||
</ContentControl>
|
||||
</Panel>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="320,*" Margin="24,20,24,20" IsVisible="{Binding IsModsPage}">
|
||||
<DockPanel Margin="0,0,20,0">
|
||||
<TextBlock DockPanel.Dock="Top" Text="Sources" Classes="h1" Margin="0,0,0,14"/>
|
||||
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="*,Auto" ColumnSpacing="8" Margin="0,8,0,0">
|
||||
<TextBox Text="{Binding AddSourceRepo}" PlaceholderText="owner/repo or URL"/>
|
||||
<Button Grid.Column="1" Content="Add" Command="{Binding AddSourceCommand}" Classes="accent" Padding="16,8"/>
|
||||
</Grid>
|
||||
<ListBox Classes="cards" ItemsSource="{Binding Sources}" SelectedItem="{Binding SelectedSource}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="vm:SourceVM">
|
||||
<StackPanel Spacing="2" Margin="16,12">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding Detail}" Classes="dim" FontSize="12" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding CountText}" Classes="dim" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
<ContentControl Grid.Column="1" Content="{Binding SelectedSource}">
|
||||
<ContentControl.ContentTemplate>
|
||||
<DataTemplate DataType="vm:SourceVM">
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Spacing="10" Margin="0,0,0,12">
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
|
||||
<TextBlock Text="{Binding Name}" Classes="h1" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Refresh" Command="{Binding Main.RefreshAllModsCommand}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="2" Content="Remove source" Command="{Binding RemoveCommand}" IsVisible="{Binding CanRemove}" Classes="ghost danger" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Border Classes="chip"><TextBlock Text="{Binding Kind}"/></Border>
|
||||
<Border Classes="chip"><TextBlock Text="{Binding CountText}"/></Border>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Link}" Classes="dim"/>
|
||||
<Grid ColumnDefinitions="*,Auto,260" ColumnSpacing="8">
|
||||
<TextBox Text="{Binding Filter}" PlaceholderText="Search this source"/>
|
||||
<TextBlock Grid.Column="1" Text="Add to" VerticalAlignment="Center" Classes="dim"/>
|
||||
<ComboBox Grid.Column="2" ItemsSource="{Binding Main.Installs}" SelectedItem="{Binding Main.SelectedInstall}" HorizontalAlignment="Stretch" PlaceholderText="Pick an install">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="vm:InstallVM"><TextBlock Text="{Binding Name}"/></DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="*,Auto" Margin="0,12,0,0" IsVisible="{Binding Main.HasSelectedInstall}">
|
||||
<TextBlock Text="{Binding Main.SelectedInstall.PendingText}" Foreground="{DynamicResource WarnFg}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="1" Content="Install / Update" Command="{Binding Main.SelectedInstall.ApplyCommand}" Classes="accent"/>
|
||||
</Grid>
|
||||
<ListBox Classes="plain" ItemsSource="{Binding VisibleMods}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="vm:ModVM">
|
||||
<Grid ColumnDefinitions="*,110,110,Auto,Auto" ColumnSpacing="10" Margin="8,6" ToolTip.Tip="{Binding Description}">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Name}" FontWeight="Medium" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Text="{Binding ShortDescription}" Classes="dim" FontSize="11" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="1" Text="{Binding InstalledText}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding LatestText}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
||||
<Button Grid.Column="3" Content="{Binding ToggleInstallText}" Command="{Binding ToggleInstallCommand}" IsEnabled="{Binding HasInstall}" Padding="10,5" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="4" Content="Manage" Command="{Binding ManageCommand}" Classes="ghost" Padding="10,5" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</ContentControl.ContentTemplate>
|
||||
</ContentControl>
|
||||
</Grid>
|
||||
|
||||
<DockPanel Margin="24,20,24,20" IsVisible="{Binding IsGalleryPage}">
|
||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8" Margin="0,0,0,14">
|
||||
<StackPanel>
|
||||
<TextBlock Text="Gallery" Classes="h1"/>
|
||||
<TextBlock Text="{Binding Gallery.FolderText}" Classes="dim" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Content="Refresh" Command="{Binding Gallery.RefreshCommand}" VerticalAlignment="Center"/>
|
||||
<Button Grid.Column="2" Content="Open folder" Command="{Binding Gallery.OpenFolderCommand}" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<TextBlock DockPanel.Dock="Bottom" Text="{Binding Gallery.Status}" Classes="dim" Margin="0,8,0,0"/>
|
||||
<ScrollViewer>
|
||||
<StackPanel>
|
||||
<TextBlock Text="No screenshots yet." Classes="dim" IsVisible="{Binding Gallery.IsEmpty}" Margin="0,20"/>
|
||||
<ItemsControl ItemsSource="{Binding Gallery.Items}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:GalleryItemVM">
|
||||
<Border Classes="card" Width="276" Margin="0,0,12,12" Padding="10">
|
||||
<StackPanel Spacing="8">
|
||||
<Border Background="{DynamicResource AppBg}" CornerRadius="6" ClipToBounds="True" Height="150">
|
||||
<Image Source="{Binding Thumb}" Stretch="UniformToFill"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding DateText}" FontWeight="Medium"/>
|
||||
<TextBlock Text="{Binding Name}" Classes="dim" FontSize="11" TextTrimming="CharacterEllipsis"/>
|
||||
<Grid ColumnDefinitions="*,*,Auto,Auto" ColumnSpacing="6">
|
||||
<Button Content="Copy" Command="{Binding CopyCommand}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Padding="6,5"/>
|
||||
<Button Grid.Column="1" Content="Open" Command="{Binding OpenCommand}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Padding="6,5"/>
|
||||
<Button Grid.Column="2" Content="Show" Command="{Binding ShowCommand}" Classes="ghost" Padding="8,5"/>
|
||||
<Button Grid.Column="3" Content="Delete" Command="{Binding DeleteCommand}" Classes="ghost danger" Padding="8,5"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button Content="Load more" Command="{Binding Gallery.MoreCommand}" IsVisible="{Binding Gallery.HasMore}" HorizontalAlignment="Center" Margin="0,8,0,16"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
|
||||
<ScrollViewer IsVisible="{Binding IsSettingsPage}">
|
||||
<StackPanel Spacing="14" Margin="24,20,24,20" MaxWidth="780" HorizontalAlignment="Stretch">
|
||||
<TextBlock Text="Settings" Classes="h1"/>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="{Binding VersionText}" Classes="h2"/>
|
||||
<TextBlock Text="{Binding UpdateStatus}" Classes="dim" FontSize="12" IsVisible="{Binding UpdateStatus, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="Check for updates" Command="{Binding CheckUpdateCommand}"/>
|
||||
<Button Content="{Binding InstallUpdateText}" Command="{Binding InstallUpdateCommand}" IsVisible="{Binding UpdateReady}" Classes="accent"/>
|
||||
</StackPanel>
|
||||
<CheckBox IsChecked="{Binding CheckUpdates}" Content="Check on startup"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Steam account" Classes="h2"/>
|
||||
<TextBlock Text="{Binding SteamAccountText}" Classes="dim" FontSize="12" TextWrapping="Wrap"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="Sign in..." Command="{Binding SteamSignInCommand}"/>
|
||||
<Button Content="Sign out" Command="{Binding SteamSignOutCommand}" IsVisible="{Binding SteamSignedIn}" Classes="ghost"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding SteamClientText}" Classes="dim" FontSize="12" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Game accounts" Classes="h2"/>
|
||||
<ItemsControl ItemsSource="{Binding Accounts}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:AccountVM">
|
||||
<Grid ColumnDefinitions="220,*,Auto" ColumnSpacing="10" Margin="0,0,0,6">
|
||||
<TextBox Text="{Binding Name}"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Username}" Classes="dim" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
||||
<Button Grid.Column="2" Content="✕" Command="{Binding RemoveCommand}" Classes="ghost" ToolTip.Tip="Remove"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Grid ColumnDefinitions="220,*,Auto,Auto" ColumnSpacing="10">
|
||||
<TextBox Text="{Binding NewAccountUser}" PlaceholderText="Username"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding NewAccountKey}" PlaceholderText="Access key" PasswordChar="•"/>
|
||||
<Button Grid.Column="2" Content="Add" Command="{Binding AddAccountCommand}"/>
|
||||
<Button Grid.Column="3" Content="Import from installs" Command="{Binding ImportAccountsCommand}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Appearance" Classes="h2"/>
|
||||
<TextBlock Text="{Binding ThemeNote}" IsVisible="{Binding ThemeNote, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" Classes="dim" TextWrapping="Wrap"/>
|
||||
<ComboBox ItemsSource="{Binding ThemeNames}" SelectedItem="{Binding Theme}" MinWidth="260" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Library location" Classes="h2"/>
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
|
||||
<TextBox Text="{Binding LibraryRoot}" IsEnabled="{Binding CanChangeRoot}"/>
|
||||
<Button Grid.Column="1" Content="Browse..." Command="{Binding BrowseLibraryRootCommand}" IsEnabled="{Binding CanChangeRoot}"/>
|
||||
<Button Grid.Column="2" Content="Apply" Command="{Binding ApplyLibraryRootCommand}" IsEnabled="{Binding CanChangeRoot}"/>
|
||||
</Grid>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Content="Open library" Command="{Binding OpenLibraryCommand}"/>
|
||||
<Button Content="Open shared cache" Command="{Binding OpenCacheCommand}"/>
|
||||
<Button Content="Open mod store" Command="{Binding OpenModStoreCommand}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Cache" Classes="h2"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="12">
|
||||
<Button Content="Deduplicate and relink all caches" Command="{Binding DedupeCommand}"/>
|
||||
<TextBlock Text="{Binding CacheSize}" Classes="dim" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<CheckBox IsChecked="{Binding CacheGuard}" Content="Multi-instance cache guard"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Hooks" Classes="h2"/>
|
||||
<TextBlock Text="{}{game} {install} {branch} {build} work in arguments." TextWrapping="Wrap" Classes="dim"/>
|
||||
<ItemsControl ItemsSource="{Binding Hooks}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:HookVM">
|
||||
<Border BorderBrush="{DynamicResource CardBorder}" BorderThickness="1" CornerRadius="8" Padding="14" Margin="0,0,0,10">
|
||||
<StackPanel Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
|
||||
<TextBox Text="{Binding Name}" FontWeight="SemiBold"/>
|
||||
<Button Grid.Column="1" Content="Remove" Command="{Binding RemoveCommand}" Classes="ghost danger"/>
|
||||
</Grid>
|
||||
<WrapPanel>
|
||||
<CheckBox IsChecked="{Binding BeforeUpdate}" Content="Before update" Margin="0,0,18,0"/>
|
||||
<CheckBox IsChecked="{Binding AfterUpdate}" Content="After install / update" Margin="0,0,18,0"/>
|
||||
<CheckBox IsChecked="{Binding BeforeLaunch}" Content="Before launch"/>
|
||||
</WrapPanel>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBox Text="{Binding Command}" PlaceholderText="Script or program"/>
|
||||
<TextBox Text="{Binding Arguments}" PlaceholderText="Arguments, e.g. --target "{game}""/>
|
||||
</StackPanel>
|
||||
<CheckBox IsChecked="{Binding DefaultOn}" Content="Enable on new installs"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<Button Content="+ Add command hook" Command="{Binding AddHookCommand}" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Activity log" Classes="h2"/>
|
||||
<v:LogView Text="{Binding LogText}" Height="260"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="GitHub" Classes="h2"/>
|
||||
<TextBox Text="{Binding GitHubToken}" PasswordChar="●" PlaceholderText="Personal access token (optional)"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Panel>
|
||||
</DockPanel>
|
||||
|
||||
<Border IsVisible="{Binding ShowNew}" Background="#B0000000">
|
||||
<Border Classes="card" Width="540" VerticalAlignment="Center" HorizontalAlignment="Center" Background="{DynamicResource OverlayBg}">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="New install" Classes="h1"/>
|
||||
<TextBlock Text="Name" Classes="label"/>
|
||||
<TextBox Text="{Binding NewName}" PlaceholderText="Name"/>
|
||||
<TextBlock Text="Branch" Classes="label"/>
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
|
||||
<ComboBox ItemsSource="{Binding BranchChoices}" SelectedItem="{Binding NewChoice}" HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate DataType="m:BranchChoice"><TextBlock Text="{Binding Display}"/></DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<Button Grid.Column="1" Content="+ Beta code" Command="{Binding EnterCodeNewCommand}" VerticalAlignment="Stretch"/>
|
||||
</Grid>
|
||||
<CheckBox IsChecked="{Binding NewImport}" Content="Mirror an existing install" Margin="0,8,0,0"/>
|
||||
<StackPanel Spacing="8" IsVisible="{Binding NewImport}">
|
||||
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="8">
|
||||
<ComboBox ItemsSource="{Binding DetectedInstalls}" SelectedItem="{Binding NewDetected}" PlaceholderText="Detected Steam installs" HorizontalAlignment="Stretch"/>
|
||||
<Button Grid.Column="1" Content="Browse..." Command="{Binding BrowseImportCommand}"/>
|
||||
</Grid>
|
||||
<TextBox Text="{Binding NewImportPath}" PlaceholderText="Folder with ChilloutVR.exe"/>
|
||||
<CheckBox IsChecked="{Binding NewMoveCache}" Content="Copy its cache too"/>
|
||||
<TextBlock Text="The original is not modified." Classes="dim" FontSize="12" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||
<Button Content="Cancel" Command="{Binding CancelNewCommand}" Classes="ghost"/>
|
||||
<Button Content="Create install" Command="{Binding CreateInstallCommand}" Classes="accent"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<Border IsVisible="{Binding ShowManage}" Background="#B0000000">
|
||||
<Border Classes="card" Width="500" MaxHeight="680" VerticalAlignment="Center" HorizontalAlignment="Center" Background="{DynamicResource OverlayBg}" DataContext="{Binding ManagedMod}">
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="12">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Details}" Classes="dim" FontSize="12" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="Version" Classes="label"/>
|
||||
<ComboBox ItemsSource="{Binding VersionChoices}" SelectedItem="{Binding InstallVersion}" IsEnabled="{Binding HasInstall}" HorizontalAlignment="Stretch"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" IsVisible="{Binding HasSourceChoice}">
|
||||
<TextBlock Text="Source" VerticalAlignment="Center" Classes="dim"/>
|
||||
<ComboBox ItemsSource="{Binding SourceOptions}" SelectedItem="{Binding SelectedSource}" MinHeight="30" Padding="10,4"/>
|
||||
</StackPanel>
|
||||
<Grid ColumnDefinitions="150,*" ColumnSpacing="10">
|
||||
<ComboBox ItemsSource="{x:Static vm:ModVM.Types}" SelectedItem="{Binding Type}" HorizontalAlignment="Stretch"/>
|
||||
<TextBox Grid.Column="1" Text="{Binding AssetFilter}" PlaceholderText="Asset filter (optional)" IsVisible="{Binding !IsLocal}"/>
|
||||
</Grid>
|
||||
<TextBlock Text="Available versions" Classes="label"/>
|
||||
<ListBox Classes="plain" ItemsSource="{Binding Releases}" SelectedItem="{Binding SelectedRelease}" MaxHeight="170">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="vm:ReleaseRow"><TextBlock Text="{Binding Text}" FontSize="12"/></DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<TextBlock Text="{Binding UsedBy}" Classes="dim" FontSize="12"/>
|
||||
<WrapPanel>
|
||||
<Button Content="Download selected" Command="{Binding DownloadCommand}" IsVisible="{Binding !IsLocal}" Margin="0,0,6,6"/>
|
||||
<Button Content="Replace local files..." Command="{Binding AddLocalCommand}" IsVisible="{Binding IsLocal}" Margin="0,0,6,6"/>
|
||||
<Button Content="Delete download" Command="{Binding DeleteDownloadCommand}" Margin="0,0,6,6"/>
|
||||
<Button Content="Remove from catalog" Command="{Binding RemoveCommand}" IsEnabled="{Binding InCatalog}" Classes="ghost danger" Margin="0,0,6,6"/>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
<Button Content="Close" Command="{Binding Main.CloseManageCommand}" Classes="accent" HorizontalAlignment="Right"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<Border IsVisible="{Binding ShowSignIn}" Background="#B0000000">
|
||||
<Border Classes="card" Width="440" VerticalAlignment="Center" HorizontalAlignment="Center" Background="{DynamicResource OverlayBg}">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Sign in to Steam" Classes="h1" FontSize="20"/>
|
||||
<TextBlock Text="{Binding SignInPrompt}" Classes="dim" TextWrapping="Wrap"/>
|
||||
<TextBox Text="{Binding SignInUser}" PlaceholderText="Steam username" IsVisible="{Binding SignInNeedsPassword}"/>
|
||||
<TextBox Text="{Binding SignInPassword}" PasswordChar="●" PlaceholderText="Password" IsVisible="{Binding SignInNeedsPassword}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding SubmitSignInCommand}"/>
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<TextBox Text="{Binding SignInCode}" PlaceholderText="Steam Guard code" IsVisible="{Binding SignInNeedsCode}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding SubmitSignInCommand}"/>
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<TextBlock Text="The password is not saved." Classes="dim" FontSize="12" TextWrapping="Wrap"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,8,0,0">
|
||||
<Button Content="Cancel" Command="{Binding CancelSignInCommand}" Classes="ghost"/>
|
||||
<Button Content="Continue" Command="{Binding SubmitSignInCommand}" Classes="accent" IsVisible="{Binding SignInCanContinue}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<Border IsVisible="{Binding ShowCodeEntry}" Background="#B0000000">
|
||||
<Border Classes="card" Width="440" VerticalAlignment="Center" HorizontalAlignment="Center" Background="{DynamicResource OverlayBg}">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="Enter beta code" Classes="h1" FontSize="20"/>
|
||||
<TextBlock Text="{Binding CodeIntro}" Classes="dim" TextWrapping="Wrap"/>
|
||||
<TextBox Text="{Binding CodeText}" PlaceholderText="Beta access code" IsEnabled="{Binding CodeIdle}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding SubmitCodeCommand}"/>
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<TextBlock Text="{Binding CodeError}" IsVisible="{Binding HasCodeError}" Foreground="{DynamicResource DangerFg}" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="Checking..." IsVisible="{Binding CodeBusy}" Classes="dim"/>
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,8,0,0">
|
||||
<Button Content="Cancel" Command="{Binding CancelCodeCommand}" IsEnabled="{Binding CodeIdle}" Classes="ghost"/>
|
||||
<Button Content="Check code" Command="{Binding SubmitCodeCommand}" IsEnabled="{Binding CodeIdle}" Classes="accent"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
<Border IsVisible="{Binding ToastVisible}" Classes="card" Background="{DynamicResource ToastBg}" Padding="0" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="24" MinWidth="340" MaxWidth="480">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
<Border Width="4" CornerRadius="10,0,0,10" Background="{DynamicResource AccentBrush}" IsVisible="{Binding !ToastError}"/>
|
||||
<Border Width="4" CornerRadius="10,0,0,10" Background="{DynamicResource DangerFg}" IsVisible="{Binding ToastError}"/>
|
||||
<StackPanel Grid.Column="1" Spacing="8" Margin="14,12">
|
||||
<TextBlock Text="{Binding Status}" TextWrapping="Wrap"/>
|
||||
<ProgressBar Value="{Binding Progress}" Maximum="100" IsIndeterminate="{Binding Indeterminate}" Height="4" IsVisible="{Binding IsBusy}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" VerticalAlignment="Center" Margin="0,0,8,0">
|
||||
<Button Content="Cancel" Command="{Binding CancelCommand}" Classes="ghost" IsVisible="{Binding IsBusy}"/>
|
||||
<Button Content="✕" Command="{Binding DismissToastCommand}" Classes="ghost" IsVisible="{Binding IsIdle}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,148 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Input.Platform;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.VisualTree;
|
||||
using CVRManager.ViewModels;
|
||||
|
||||
namespace CVRManager.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
var vm = new MainViewModel
|
||||
{
|
||||
PickFolder = async () =>
|
||||
{
|
||||
var r = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { AllowMultiple = false });
|
||||
return r.Count > 0 ? r[0].TryGetLocalPath() : null;
|
||||
},
|
||||
PickFiles = async () =>
|
||||
{
|
||||
var r = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
AllowMultiple = true,
|
||||
FileTypeFilter = new[] { new FilePickerFileType("Mod files") { Patterns = new[] { "*.dll", "*.zip" } } },
|
||||
});
|
||||
return (IReadOnlyList<string>)r.Select(f => f.TryGetLocalPath()).Where(p => p != null).Select(p => p!).ToList();
|
||||
},
|
||||
Confirm = ConfirmAsync,
|
||||
CopyImage = async path =>
|
||||
{
|
||||
if (Clipboard == null) return;
|
||||
using var bmp = new Avalonia.Media.Imaging.Bitmap(path);
|
||||
await Clipboard.SetBitmapAsync(bmp);
|
||||
},
|
||||
};
|
||||
DataContext = vm;
|
||||
Opened += (_, _) => WindowIcons.Apply(this);
|
||||
HookInstallDrag(vm);
|
||||
}
|
||||
|
||||
InstallVM? dragItem;
|
||||
Point dragStart;
|
||||
bool dragging;
|
||||
|
||||
void HookInstallDrag(MainViewModel vm)
|
||||
{
|
||||
var list = InstallList;
|
||||
list.AddHandler(PointerPressedEvent, (_, e) =>
|
||||
{
|
||||
if (!e.GetCurrentPoint(list).Properties.IsLeftButtonPressed) return;
|
||||
if ((e.Source as Visual)?.FindAncestorOfType<Button>(true) != null) return;
|
||||
if ((e.Source as StyledElement)?.DataContext is not InstallVM item) return;
|
||||
dragItem = item;
|
||||
dragStart = e.GetPosition(list);
|
||||
dragging = false;
|
||||
}, RoutingStrategies.Bubble, true);
|
||||
|
||||
list.AddHandler(PointerMovedEvent, (_, e) =>
|
||||
{
|
||||
if (dragItem == null) return;
|
||||
var p = e.GetPosition(list);
|
||||
if (!dragging)
|
||||
{
|
||||
if (Math.Abs(p.Y - dragStart.Y) < 6) return;
|
||||
dragging = true;
|
||||
e.Pointer.Capture(list);
|
||||
list.Cursor = new Cursor(StandardCursorType.SizeNorthSouth);
|
||||
}
|
||||
var from = vm.Installs.IndexOf(dragItem);
|
||||
for (var i = 0; i < list.ItemCount; i++)
|
||||
{
|
||||
if (i == from || list.ContainerFromIndex(i) is not Control c || c.TranslatePoint(default, list) is not { } top) continue;
|
||||
var mid = top.Y + c.Bounds.Height / 2;
|
||||
if ((i > from && p.Y > mid && p.Y < top.Y + c.Bounds.Height) || (i < from && p.Y < mid && p.Y > top.Y))
|
||||
{
|
||||
vm.MoveInstall(dragItem, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
MarkDragged(list);
|
||||
}, RoutingStrategies.Bubble, true);
|
||||
|
||||
void End()
|
||||
{
|
||||
if (dragItem == null) return;
|
||||
var moved = dragging;
|
||||
dragItem = null;
|
||||
dragging = false;
|
||||
list.Cursor = null;
|
||||
MarkDragged(list);
|
||||
if (moved) vm.Save();
|
||||
}
|
||||
list.AddHandler(PointerReleasedEvent, (_, e) => { if (dragging) e.Pointer.Capture(null); End(); }, RoutingStrategies.Bubble, true);
|
||||
list.AddHandler(PointerCaptureLostEvent, (_, _) => End(), RoutingStrategies.Bubble, true);
|
||||
}
|
||||
|
||||
void MarkDragged(ListBox list)
|
||||
{
|
||||
for (var i = 0; i < list.ItemCount; i++)
|
||||
if (list.ContainerFromIndex(i) is Control c)
|
||||
c.Opacity = dragging && c.DataContext == dragItem ? 0.6 : 1;
|
||||
}
|
||||
|
||||
void OnTableSelection(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (e.AddedItems.Count > 0 && e.AddedItems[0] is GroupHeader header)
|
||||
header.ToggleCommand.Execute(null);
|
||||
}
|
||||
|
||||
async Task<bool> ConfirmAsync(string message)
|
||||
{
|
||||
var dlg = new Window
|
||||
{
|
||||
Title = "Confirm",
|
||||
Width = 460,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
CanResize = false,
|
||||
};
|
||||
var ok = new Button { Content = "OK", Padding = new Thickness(18, 6) };
|
||||
var cancel = new Button { Content = "Cancel", Padding = new Thickness(18, 6) };
|
||||
ok.Click += (_, _) => dlg.Close(true);
|
||||
cancel.Click += (_, _) => dlg.Close(false);
|
||||
dlg.Content = new StackPanel
|
||||
{
|
||||
Margin = new Thickness(20),
|
||||
Spacing = 16,
|
||||
Children =
|
||||
{
|
||||
new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
|
||||
new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Spacing = 8,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Children = { ok, cancel },
|
||||
},
|
||||
},
|
||||
};
|
||||
return await dlg.ShowDialog<bool>(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace CVRManager.Views;
|
||||
|
||||
public static class WindowIcons
|
||||
{
|
||||
const uint WM_SETICON = 0x0080;
|
||||
const int ICON_SMALL = 0;
|
||||
const int ICON_BIG = 1;
|
||||
const int SM_CXICON = 11;
|
||||
const int SM_CXSMICON = 49;
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||
static extern uint PrivateExtractIcons(string file, int index, int cx, int cy, IntPtr[] icons, uint[] ids, uint count, uint flags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
static extern IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
static extern int GetSystemMetrics(int index);
|
||||
|
||||
public static void Apply(Window window)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
var exe = Environment.ProcessPath;
|
||||
var hwnd = window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
|
||||
if (exe == null || hwnd == IntPtr.Zero || !Path.GetFileName(exe).Equals("CVRManager.exe", StringComparison.OrdinalIgnoreCase)) return;
|
||||
|
||||
Set(hwnd, exe, GetSystemMetrics(SM_CXSMICON), ICON_SMALL);
|
||||
Set(hwnd, exe, GetSystemMetrics(SM_CXICON), ICON_BIG);
|
||||
}
|
||||
|
||||
static void Set(IntPtr hwnd, string exe, int size, int kind)
|
||||
{
|
||||
var handles = new IntPtr[1];
|
||||
var ids = new uint[1];
|
||||
if (PrivateExtractIcons(exe, 0, size, size, handles, ids, 1, 0) > 0 && handles[0] != IntPtr.Zero)
|
||||
SendMessage(hwnd, WM_SETICON, (IntPtr)kind, handles[0]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user