chore(desktop): add initial code
This commit is contained in:
parent
d0c16bf655
commit
ff76997b0b
|
@ -2,8 +2,19 @@
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:XIVChat_Desktop"
|
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||||
StartupUri="MainWindow.xaml">
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
|
Startup="Application_Startup">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<local:DoubleConverter x:Key="DoubleConverter" />
|
||||||
|
<local:UShortConverter x:Key="UShortConverter" />
|
||||||
|
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ui:ThemeResources AccentColor="#02ccee" />
|
||||||
|
<ui:XamlControlsResources />
|
||||||
|
<!-- Other merged dictionaries here -->
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- Other app resources here -->
|
||||||
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
|
@ -1,8 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.ComponentModel;
|
||||||
using System.Configuration;
|
|
||||||
using System.Data;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
|
@ -10,6 +7,74 @@ namespace XIVChat_Desktop {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interaction logic for App.xaml
|
/// Interaction logic for App.xaml
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : Application {
|
public partial class App : INotifyPropertyChanged {
|
||||||
|
public MainWindow Window { get; private set; } = null!;
|
||||||
|
public Configuration Config { get; private set; } = null!;
|
||||||
|
|
||||||
|
public string? LastHost { get; set; }
|
||||||
|
|
||||||
|
private Connection? connection;
|
||||||
|
|
||||||
|
public Connection? Connection {
|
||||||
|
get => this.connection;
|
||||||
|
set {
|
||||||
|
this.connection = value;
|
||||||
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Connection)));
|
||||||
|
this.ConnectionStatusChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Connected => this.Connection != null;
|
||||||
|
public bool Disconnected => this.Connection == null;
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
private void Application_Startup(object sender, StartupEventArgs e) {
|
||||||
|
try {
|
||||||
|
this.Config = Configuration.Load() ?? new Configuration();
|
||||||
|
} catch (Exception) {
|
||||||
|
this.Config = new Configuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.Config.Save();
|
||||||
|
} catch (Exception) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
var wnd = new MainWindow();
|
||||||
|
this.Window = wnd;
|
||||||
|
|
||||||
|
// I guess this gets initialised where you call it the first time, so initialise it on the UI thread
|
||||||
|
this.Dispatcher.Invoke(() => { });
|
||||||
|
|
||||||
|
wnd.Show();
|
||||||
|
|
||||||
|
// initialise a config window to apply all our settings
|
||||||
|
_ = new ConfigWindow(wnd, this.Config);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConnectionStatusChanged() {
|
||||||
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Connected)));
|
||||||
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Disconnected)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Connect(string host, ushort port) {
|
||||||
|
if (this.Connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Connection = new Connection(this, host, port);
|
||||||
|
Task.Run(this.Connection.Connect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect() {
|
||||||
|
if (!this.Connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Connection?.Disconnect();
|
||||||
|
this.Connection = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
<Window x:Class="XIVChat_Desktop.ConfigWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||||
|
xmlns:cc="clr-namespace:XIVChat_Desktop.Controls"
|
||||||
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
|
ui:WindowHelper.UseModernWindowStyle="True"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
MinWidth="450"
|
||||||
|
MinHeight="350"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ContentRendered="ConfigWindow_OnContentRendered"
|
||||||
|
SizeToContent="WidthAndHeight"
|
||||||
|
Title="Configuration"
|
||||||
|
d:DataContext="{d:DesignInstance local:ConfigWindow}">
|
||||||
|
<TabControl>
|
||||||
|
<TabItem Header="Servers">
|
||||||
|
<cc:SavedServers ItemsSource="{Binding Config.Servers}"
|
||||||
|
ItemDoubleClick="SavedServers_ItemDoubleClick" />
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="Window">
|
||||||
|
<Grid Margin="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<CheckBox Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
Content="Always on top"
|
||||||
|
IsChecked="{Binding Config.AlwaysOnTop}"
|
||||||
|
Checked="AlwaysOnTop_Checked"
|
||||||
|
Unchecked="AlwaysOnTop_Unchecked" />
|
||||||
|
<TextBox Grid.Row="1"
|
||||||
|
Grid.Column="0"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
PreviewTextInput="NumericInputFilter"
|
||||||
|
Text="{Binding Config.FontSize, Converter={StaticResource DoubleConverter}}"
|
||||||
|
ui:ControlHelper.Header="Log font size"
|
||||||
|
Width="200" />
|
||||||
|
|
||||||
|
<Button Grid.Row="3"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Click="Save_Click">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
<TabItem Header="Connection">
|
||||||
|
<Grid Margin="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBox Grid.Row="0"
|
||||||
|
Grid.Column="0"
|
||||||
|
ui:ControlHelper.Header="Backlog messages to request"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
PreviewTextInput="NumericInputFilter"
|
||||||
|
Text="{Binding Config.BacklogMessages, Converter={StaticResource UShortConverter}}"
|
||||||
|
Width="200" />
|
||||||
|
|
||||||
|
<Button Grid.Row="2"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Click="Save_Click">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
</Window>
|
|
@ -0,0 +1,51 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for ConfigWindow.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class ConfigWindow {
|
||||||
|
public Configuration Config { get; private set; }
|
||||||
|
|
||||||
|
public ConfigWindow(Window owner, Configuration config) {
|
||||||
|
this.Owner = owner;
|
||||||
|
this.Config = config;
|
||||||
|
|
||||||
|
this.InitializeComponent();
|
||||||
|
this.DataContext = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AlwaysOnTop_Checked(object? sender, RoutedEventArgs e) {
|
||||||
|
this.SetAlwaysOnTop(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AlwaysOnTop_Unchecked(object? sender, RoutedEventArgs e) {
|
||||||
|
this.SetAlwaysOnTop(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetAlwaysOnTop(bool onTop) {
|
||||||
|
this.Owner.Topmost = onTop;
|
||||||
|
this.Config.AlwaysOnTop = onTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_Click(object? sender, RoutedEventArgs e) {
|
||||||
|
this.Config.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SavedServers_ItemDoubleClick(SavedServer? server) {
|
||||||
|
new ManageServer(this, server).ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConfigWindow_OnContentRendered(object? sender, EventArgs e) {
|
||||||
|
this.InvalidateVisual();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NumericInputFilter(object sender, TextCompositionEventArgs e) {
|
||||||
|
var allDigits = e.Text.All(c => char.IsDigit(c));
|
||||||
|
e.Handled = !allDigits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Sodium;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using XIVChatCommon;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
[JsonObject]
|
||||||
|
public class Configuration : INotifyPropertyChanged {
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
public KeyPair KeyPair { get; set; } = PublicKeyBox.GenerateKeyPair();
|
||||||
|
|
||||||
|
public ObservableCollection<SavedServer> Servers { get; set; } = new ObservableCollection<SavedServer>();
|
||||||
|
public HashSet<TrustedKey> TrustedKeys { get; set; } = new HashSet<TrustedKey>();
|
||||||
|
|
||||||
|
public ObservableCollection<Tab> Tabs { get; set; } = Tab.Defaults();
|
||||||
|
|
||||||
|
public bool AlwaysOnTop { get; set; }
|
||||||
|
|
||||||
|
private double fontSize = 14d;
|
||||||
|
|
||||||
|
public double FontSize {
|
||||||
|
get => this.fontSize;
|
||||||
|
set {
|
||||||
|
this.fontSize = value;
|
||||||
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.FontSize)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ushort BacklogMessages { get; set; } = 500;
|
||||||
|
|
||||||
|
#region io
|
||||||
|
|
||||||
|
private static string FilePath() => Path.Join(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"XIVChat for Windows",
|
||||||
|
"config.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static Configuration? Load() {
|
||||||
|
var path = FilePath();
|
||||||
|
if (!File.Exists(path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = File.OpenText(path);
|
||||||
|
using var json = new JsonTextReader(reader);
|
||||||
|
|
||||||
|
var serializer = new JsonSerializer {
|
||||||
|
ObjectCreationHandling = ObjectCreationHandling.Replace,
|
||||||
|
};
|
||||||
|
return serializer.Deserialize<Configuration>(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save() {
|
||||||
|
var path = FilePath();
|
||||||
|
if (!File.Exists(path)) {
|
||||||
|
var dir = Path.GetDirectoryName(path);
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var file = File.CreateText(path);
|
||||||
|
using var json = new JsonTextWriter(file);
|
||||||
|
|
||||||
|
var serialiser = new JsonSerializer();
|
||||||
|
serialiser.Serialize(json, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonObject]
|
||||||
|
public class SavedServer {
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Host { get; set; }
|
||||||
|
public ushort Port { get; set; }
|
||||||
|
|
||||||
|
public SavedServer(string name, string host, ushort port) {
|
||||||
|
this.Name = name;
|
||||||
|
this.Host = host;
|
||||||
|
this.Port = port;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonObject]
|
||||||
|
public class TrustedKey {
|
||||||
|
public string Name { get; set; }
|
||||||
|
public byte[] Key { get; set; }
|
||||||
|
|
||||||
|
public TrustedKey(string name, byte[] key) {
|
||||||
|
this.Name = name;
|
||||||
|
this.Key = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonObject]
|
||||||
|
public class Tab : INotifyPropertyChanged {
|
||||||
|
private string name;
|
||||||
|
|
||||||
|
public Tab(string name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Name {
|
||||||
|
get => this.name;
|
||||||
|
set {
|
||||||
|
this.name = value;
|
||||||
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Name)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Filter Filter { get; set; } = new Filter();
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public ObservableCollection<ServerMessage> Messages { get; } = new ObservableCollection<ServerMessage>();
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
public void RepopulateMessages(ConcurrentStack<ServerMessage> mainMessages) {
|
||||||
|
this.Messages.Clear();
|
||||||
|
|
||||||
|
foreach (var message in mainMessages.Where(msg => this.Filter.Allowed(msg)).Reverse()) {
|
||||||
|
this.Messages.Add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Messages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddMessage(ServerMessage message) {
|
||||||
|
if (message.Channel != 0 && !this.Filter.Allowed(message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Messages.Add(message);
|
||||||
|
|
||||||
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Messages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearMessages() {
|
||||||
|
this.Messages.Clear();
|
||||||
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.Messages)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Filter GeneralFilter() {
|
||||||
|
var generalFilters = FilterCategory.Chat.Types()
|
||||||
|
.Concat(FilterCategory.Announcements.Types())
|
||||||
|
.ToHashSet();
|
||||||
|
generalFilters.Remove(FilterType.OwnBattleSystem);
|
||||||
|
generalFilters.Remove(FilterType.OthersBattleSystem);
|
||||||
|
generalFilters.Remove(FilterType.NpcDialogue);
|
||||||
|
generalFilters.Remove(FilterType.OthersFishing);
|
||||||
|
return new Filter {
|
||||||
|
Types = generalFilters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ObservableCollection<Tab> Defaults() {
|
||||||
|
var battleFilters = FilterCategory.Battle.Types()
|
||||||
|
.Append(FilterType.OwnBattleSystem)
|
||||||
|
.Append(FilterType.OthersBattleSystem)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
return new ObservableCollection<Tab> {
|
||||||
|
new Tab("General") {
|
||||||
|
Filter = GeneralFilter(),
|
||||||
|
},
|
||||||
|
new Tab("Battle") {
|
||||||
|
Filter = new Filter {
|
||||||
|
Types = battleFilters,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonObject]
|
||||||
|
public class Filter {
|
||||||
|
public HashSet<FilterType> Types { get; set; } = new HashSet<FilterType>();
|
||||||
|
|
||||||
|
public bool Allowed(ServerMessage message) {
|
||||||
|
return this.Types
|
||||||
|
.SelectMany(type => type.Types())
|
||||||
|
.Contains(new ChatCode((ushort)message.Channel).Type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
<Window x:Class="XIVChat_Desktop.ConnectDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||||
|
xmlns:cc="clr-namespace:XIVChat_Desktop.Controls"
|
||||||
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
|
ui:WindowHelper.UseModernWindowStyle="True"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
ContentRendered="ConnectDialog_OnContentRendered"
|
||||||
|
SizeToContent="WidthAndHeight"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ResizeMode="CanResizeWithGrip"
|
||||||
|
ShowInTaskbar="False"
|
||||||
|
Title="Connect"
|
||||||
|
d:DataContext="{d:DesignInstance local:ConnectDialog}">
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<cc:SavedServers x:Name="Servers"
|
||||||
|
ItemsSource="{Binding App.Config.Servers}"
|
||||||
|
ItemDoubleClick="Servers_ItemDoubleClick" />
|
||||||
|
|
||||||
|
<WrapPanel Grid.Row="1"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Margin="0,16,0,0"
|
||||||
|
HorizontalAlignment="Right">
|
||||||
|
<Button Margin="0,0,8,0"
|
||||||
|
IsCancel="True"
|
||||||
|
Click="Cancel_Click">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button IsDefault="True"
|
||||||
|
Click="Connect_Clicked">
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for ConnectDialog.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class ConnectDialog {
|
||||||
|
public App App => (App)Application.Current;
|
||||||
|
|
||||||
|
public ConnectDialog() {
|
||||||
|
this.InitializeComponent();
|
||||||
|
this.DataContext = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Connect_Clicked(object? sender, RoutedEventArgs e) {
|
||||||
|
this.ConnectTo(this.Servers.SelectedServer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel_Click(object? sender, RoutedEventArgs e) {
|
||||||
|
this.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Servers_ItemDoubleClick(SavedServer? server) {
|
||||||
|
this.ConnectTo(server);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConnectTo(SavedServer? server) {
|
||||||
|
if (server == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.App.Connect(server.Host, server.Port);
|
||||||
|
|
||||||
|
this.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConnectDialog_OnContentRendered(object? sender, EventArgs e) {
|
||||||
|
this.InvalidateVisual();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,267 @@
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using XIVChatCommon;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
public class Connection : INotifyPropertyChanged {
|
||||||
|
private readonly App app;
|
||||||
|
|
||||||
|
private readonly string host;
|
||||||
|
private readonly ushort port;
|
||||||
|
|
||||||
|
private TcpClient? client;
|
||||||
|
|
||||||
|
private readonly Channel<string> outgoing = Channel.CreateUnbounded<string>();
|
||||||
|
private readonly Channel<byte[]> incoming = Channel.CreateUnbounded<byte[]>();
|
||||||
|
private readonly Channel<byte> cancelChannel = Channel.CreateBounded<byte>(2);
|
||||||
|
|
||||||
|
public readonly CancellationTokenSource cancel = new CancellationTokenSource();
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
public string? CurrentChannel { get; private set; }
|
||||||
|
|
||||||
|
public Connection(App app, string host, ushort port) {
|
||||||
|
this.app = app;
|
||||||
|
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendMessage(string message) {
|
||||||
|
this.outgoing.Writer.TryWrite(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disconnect() {
|
||||||
|
this.cancel.Cancel();
|
||||||
|
for (var i = 0; i < 2; i++) {
|
||||||
|
this.cancelChannel.Writer.TryWrite(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Connect() {
|
||||||
|
this.client = new TcpClient(this.host, this.port);
|
||||||
|
var stream = this.client.GetStream();
|
||||||
|
|
||||||
|
await stream.WriteAsync(new byte[] {
|
||||||
|
14, 20, 67,
|
||||||
|
});
|
||||||
|
|
||||||
|
var handshake = await KeyExchange.ClientHandshake(this.app.Config.KeyPair, stream);
|
||||||
|
|
||||||
|
if (!this.app.Config.TrustedKeys.Any(trusted => trusted.Key.SequenceEqual(handshake.RemotePublicKey))) {
|
||||||
|
var trustChannel = Channel.CreateBounded<bool>(1);
|
||||||
|
|
||||||
|
this.Dispatch(() => {
|
||||||
|
new TrustDialog(this.app.Window, trustChannel.Writer, handshake.RemotePublicKey).Show();
|
||||||
|
});
|
||||||
|
|
||||||
|
var trusted = await trustChannel.Reader.ReadAsync(this.cancel.Token);
|
||||||
|
|
||||||
|
if (!trusted) {
|
||||||
|
goto Close;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear messages if connecting to a different host
|
||||||
|
var currentHost = $"{this.host}:{this.port}";
|
||||||
|
var sameHost = this.app.LastHost == currentHost;
|
||||||
|
if (!sameHost) {
|
||||||
|
this.Dispatch(() => {
|
||||||
|
this.app.Window.ClearAllMessages();
|
||||||
|
this.app.LastHost = currentHost;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Dispatch(() => {
|
||||||
|
this.app.Window.AddSystemMessage("Connected");
|
||||||
|
});
|
||||||
|
|
||||||
|
// check if backlog or catch-up is needed
|
||||||
|
if (sameHost) {
|
||||||
|
// catch-up
|
||||||
|
var lastRealMessage = this.app.Window.Messages.LastOrDefault(msg => msg.Channel != 0);
|
||||||
|
if (lastRealMessage != null) {
|
||||||
|
var catchUp = new ClientCatchUp {
|
||||||
|
After = lastRealMessage.Timestamp,
|
||||||
|
};
|
||||||
|
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, catchUp, this.cancel.Token);
|
||||||
|
}
|
||||||
|
} else if (this.app.Config.BacklogMessages > 0) {
|
||||||
|
// backlog
|
||||||
|
var backlogReq = new ClientBacklog {
|
||||||
|
Amount = this.app.Config.BacklogMessages,
|
||||||
|
};
|
||||||
|
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, backlogReq, this.cancel.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// start a task for accepting incoming messages and sending them down the channel
|
||||||
|
_ = Task.Run(async () => {
|
||||||
|
var inc = SecretMessage.ReadSecretMessage(stream, handshake.Keys.rx, this.cancel.Token);
|
||||||
|
var cancel = this.cancelChannel.Reader.ReadAsync().AsTask();
|
||||||
|
|
||||||
|
while (!this.cancel.IsCancellationRequested) {
|
||||||
|
var result = await Task.WhenAny(inc, cancel);
|
||||||
|
if (result == inc) {
|
||||||
|
if (inc.Exception != null) {
|
||||||
|
this.Dispatch(() => {
|
||||||
|
this.app.Window.AddSystemMessage("Error reading incoming message.");
|
||||||
|
// ReSharper disable once LocalizableElement
|
||||||
|
Console.WriteLine($"Error reading incoming message: {inc.Exception.Message}");
|
||||||
|
foreach (var inner in inc.Exception.InnerExceptions) {
|
||||||
|
Console.WriteLine(inner.StackTrace);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rawMessage = await inc;
|
||||||
|
inc = SecretMessage.ReadSecretMessage(stream, handshake.Keys.rx, this.cancel.Token);
|
||||||
|
await this.incoming.Writer.WriteAsync(rawMessage);
|
||||||
|
} else if (result == cancel) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var incoming = this.incoming.Reader.ReadAsync().AsTask();
|
||||||
|
var outgoing = this.outgoing.Reader.ReadAsync().AsTask();
|
||||||
|
var cancel = this.cancelChannel.Reader.ReadAsync().AsTask();
|
||||||
|
|
||||||
|
// listen for incoming and outgoing messages and cancel requests
|
||||||
|
while (!this.cancel.IsCancellationRequested) {
|
||||||
|
var result = await Task.WhenAny(incoming, outgoing, cancel);
|
||||||
|
if (result == incoming) {
|
||||||
|
if (this.incoming.Reader.Completion.IsCompleted) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var rawMessage = await incoming;
|
||||||
|
incoming = this.incoming.Reader.ReadAsync().AsTask();
|
||||||
|
|
||||||
|
await this.HandleIncoming(rawMessage);
|
||||||
|
} else if (result == outgoing) {
|
||||||
|
var toSend = await outgoing;
|
||||||
|
outgoing = this.outgoing.Reader.ReadAsync().AsTask();
|
||||||
|
|
||||||
|
var message = new ClientMessage {
|
||||||
|
Content = toSend,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, message, this.cancel.Token);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
this.Dispatch(() => {
|
||||||
|
this.app.Window.AddSystemMessage("Error sending message.");
|
||||||
|
// ReSharper disable once LocalizableElement
|
||||||
|
Console.WriteLine($"Error sending message: {ex.Message}");
|
||||||
|
Console.WriteLine(ex.StackTrace);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (result == cancel) {
|
||||||
|
try {
|
||||||
|
await SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, ClientShutdown.Instance, this.cancel.Token);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
this.Dispatch(() => {
|
||||||
|
this.app.Window.AddSystemMessage("Error sending message.");
|
||||||
|
// ReSharper disable once LocalizableElement
|
||||||
|
Console.WriteLine($"Error sending message: {ex.Message}");
|
||||||
|
Console.WriteLine(ex.StackTrace);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point, we are disconnected, so log it
|
||||||
|
this.Dispatch(() => {
|
||||||
|
this.app.Window.AddSystemMessage("Disconnected");
|
||||||
|
});
|
||||||
|
|
||||||
|
// wait up to a second to send the shutdown packet
|
||||||
|
await Task.WhenAny(Task.Delay(1_000), SecretMessage.SendSecretMessage(stream, handshake.Keys.tx, ClientShutdown.Instance));
|
||||||
|
|
||||||
|
Close:
|
||||||
|
try {
|
||||||
|
this.client.Close();
|
||||||
|
} catch (ObjectDisposedException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleIncoming(byte[] rawMessage) {
|
||||||
|
var type = (ServerOperation)rawMessage[0];
|
||||||
|
var payload = new byte[rawMessage.Length - 1];
|
||||||
|
Array.Copy(rawMessage, 1, payload, 0, payload.Length);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case ServerOperation.Pong:
|
||||||
|
break;
|
||||||
|
case ServerOperation.Message:
|
||||||
|
var message = ServerMessage.Decode(payload);
|
||||||
|
|
||||||
|
this.Dispatch(() => {
|
||||||
|
this.app.Window.AddMessage(message);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ServerOperation.Shutdown:
|
||||||
|
this.Disconnect();
|
||||||
|
break;
|
||||||
|
case ServerOperation.PlayerData:
|
||||||
|
var playerData = payload.Length == 0 ? null : PlayerData.Decode(payload);
|
||||||
|
|
||||||
|
var visibility = playerData == null ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
|
||||||
|
this.Dispatch(() => {
|
||||||
|
var window = this.app.Window;
|
||||||
|
|
||||||
|
window.LoggedInAs.Content = playerData?.name;
|
||||||
|
window.LoggedInAs.Visibility = visibility;
|
||||||
|
|
||||||
|
window.LoggedInAsSeparator.Visibility = visibility;
|
||||||
|
|
||||||
|
window.CurrentWorld.Content = playerData?.currentWorld;
|
||||||
|
window.CurrentWorld.Visibility = visibility;
|
||||||
|
|
||||||
|
window.CurrentWorldSeparator.Visibility = visibility;
|
||||||
|
|
||||||
|
window.Location.Content = playerData?.location;
|
||||||
|
window.Location.Visibility = visibility;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ServerOperation.Availability:
|
||||||
|
break;
|
||||||
|
case ServerOperation.Channel:
|
||||||
|
var channel = ServerChannel.Decode(payload);
|
||||||
|
|
||||||
|
this.CurrentChannel = channel.name;
|
||||||
|
|
||||||
|
this.Dispatch(() => {
|
||||||
|
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(this.CurrentChannel)));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ServerOperation.Backlog:
|
||||||
|
var backlog = ServerBacklog.Decode(payload);
|
||||||
|
|
||||||
|
foreach (var msg in backlog.messages) {
|
||||||
|
this.Dispatch(() => {
|
||||||
|
this.app.Window.AddMessage(msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ServerOperation.PlayerList:
|
||||||
|
break;
|
||||||
|
case ServerOperation.LinkshellList:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Dispatch(Action action) {
|
||||||
|
this.app.Dispatcher.BeginInvoke(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
<UserControl x:Class="XIVChat_Desktop.Controls.SavedServers"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
x:Name="SavedServersControl"
|
||||||
|
d:DesignHeight="450"
|
||||||
|
d:DesignWidth="800">
|
||||||
|
<Grid Margin="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ListView x:Name="Servers"
|
||||||
|
Grid.Column="0"
|
||||||
|
DataContext="{Binding ElementName=SavedServersControl}"
|
||||||
|
ItemsSource="{Binding ItemsSource}"
|
||||||
|
MouseDoubleClick="Item_MouseDoubleClick">
|
||||||
|
<ListView.View>
|
||||||
|
<GridView>
|
||||||
|
<GridViewColumn Header="Name"
|
||||||
|
DisplayMemberBinding="{Binding Name}" />
|
||||||
|
<GridViewColumn Header="Host"
|
||||||
|
DisplayMemberBinding="{Binding Host}" />
|
||||||
|
<GridViewColumn Header="Port"
|
||||||
|
DisplayMemberBinding="{Binding Port}" />
|
||||||
|
</GridView>
|
||||||
|
</ListView.View>
|
||||||
|
</ListView>
|
||||||
|
<WrapPanel Grid.Column="1"
|
||||||
|
Orientation="Vertical"
|
||||||
|
Margin="8,0,0,0">
|
||||||
|
<Button HorizontalAlignment="Stretch"
|
||||||
|
Margin="0,0,0,4"
|
||||||
|
Click="AddServer_Click">
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
<Button HorizontalAlignment="Stretch"
|
||||||
|
Margin="0,0,0,4"
|
||||||
|
Click="EditServer_Click">
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button HorizontalAlignment="Stretch"
|
||||||
|
Click="DeleteServer_Click">
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
|
@ -0,0 +1,73 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop.Controls {
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for SavedServers.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class SavedServers {
|
||||||
|
public App App => (App)Application.Current;
|
||||||
|
private Configuration Config => this.App.Config;
|
||||||
|
private Window Window => Window.GetWindow(this)!;
|
||||||
|
|
||||||
|
public IEnumerable<SavedServer> ItemsSource {
|
||||||
|
get { return (IEnumerable<SavedServer>)this.GetValue(ItemsSourceProperty); }
|
||||||
|
set { this.SetValue(ItemsSourceProperty, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public SavedServer? SelectedServer {
|
||||||
|
get {
|
||||||
|
var item = this.Servers.SelectedItem;
|
||||||
|
|
||||||
|
return item as SavedServer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
|
||||||
|
"ItemsSource",
|
||||||
|
typeof(IEnumerable<SavedServer>),
|
||||||
|
typeof(SavedServers),
|
||||||
|
new PropertyMetadata(null)
|
||||||
|
);
|
||||||
|
|
||||||
|
public SavedServers() {
|
||||||
|
this.InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddServer_Click(object sender, RoutedEventArgs e) {
|
||||||
|
new ManageServer(this.Window, null).ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteServer_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var server = this.SelectedServer;
|
||||||
|
if (server == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Config.Servers.Remove(server);
|
||||||
|
this.Config.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditServer_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var server = this.SelectedServer;
|
||||||
|
if (server == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new ManageServer(this.Window, server).ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Item_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) {
|
||||||
|
var server = ((FrameworkElement)e.OriginalSource).DataContext;
|
||||||
|
if (!(server is SavedServer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ItemDoubleClick?.Invoke((SavedServer)server);
|
||||||
|
}
|
||||||
|
|
||||||
|
public delegate void MouseDoubleClickHandler(SavedServer server);
|
||||||
|
|
||||||
|
public event MouseDoubleClickHandler? ItemDoubleClick;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows.Data;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
public class DoubleConverter : IValueConverter {
|
||||||
|
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||||
|
if (double.TryParse(value.ToString(), out var res)) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UShortConverter : IValueConverter {
|
||||||
|
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||||
|
return value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||||
|
if (ushort.TryParse(value.ToString(), out var res)) {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,347 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using XIVChatCommon;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
public enum FilterCategory {
|
||||||
|
[Category("Chat",
|
||||||
|
FilterType.Say,
|
||||||
|
FilterType.Yell,
|
||||||
|
FilterType.Shout,
|
||||||
|
FilterType.Tell,
|
||||||
|
FilterType.Party,
|
||||||
|
FilterType.Alliance,
|
||||||
|
FilterType.FreeCompany,
|
||||||
|
FilterType.PvpTeam,
|
||||||
|
FilterType.CrossLinkshell1,
|
||||||
|
FilterType.CrossLinkshell2,
|
||||||
|
FilterType.CrossLinkshell3,
|
||||||
|
FilterType.CrossLinkshell4,
|
||||||
|
FilterType.CrossLinkshell5,
|
||||||
|
FilterType.CrossLinkshell6,
|
||||||
|
FilterType.CrossLinkshell7,
|
||||||
|
FilterType.CrossLinkshell8,
|
||||||
|
FilterType.Linkshell1,
|
||||||
|
FilterType.Linkshell2,
|
||||||
|
FilterType.Linkshell3,
|
||||||
|
FilterType.Linkshell4,
|
||||||
|
FilterType.Linkshell5,
|
||||||
|
FilterType.Linkshell6,
|
||||||
|
FilterType.Linkshell7,
|
||||||
|
FilterType.Linkshell8,
|
||||||
|
FilterType.NoviceNetwork,
|
||||||
|
FilterType.StandardEmote,
|
||||||
|
FilterType.CustomEmote,
|
||||||
|
FilterType.Gm
|
||||||
|
)]
|
||||||
|
Chat,
|
||||||
|
|
||||||
|
[Category("Battle", FilterType.Battle)]
|
||||||
|
Battle,
|
||||||
|
|
||||||
|
[Category("Announcements",
|
||||||
|
FilterType.Debug,
|
||||||
|
FilterType.Urgent,
|
||||||
|
FilterType.Notice,
|
||||||
|
FilterType.SystemMessages,
|
||||||
|
FilterType.OwnBattleSystem,
|
||||||
|
FilterType.OthersBattleSystem,
|
||||||
|
FilterType.GatheringSystem,
|
||||||
|
FilterType.ErrorMessages,
|
||||||
|
FilterType.Echo,
|
||||||
|
FilterType.NoviceNetworkAnnouncements,
|
||||||
|
FilterType.FreeCompanyAnnouncements,
|
||||||
|
FilterType.PvpTeamAnnouncements,
|
||||||
|
FilterType.FreeCompanyLoginLogout,
|
||||||
|
FilterType.PvpTeamLoginLogout,
|
||||||
|
FilterType.RetainerSale,
|
||||||
|
FilterType.NpcDialogue,
|
||||||
|
FilterType.NpcAnnouncement,
|
||||||
|
FilterType.Loot,
|
||||||
|
FilterType.OwnProgression,
|
||||||
|
FilterType.PartyProgression,
|
||||||
|
FilterType.OthersProgression,
|
||||||
|
FilterType.OwnLoot,
|
||||||
|
FilterType.OthersLoot,
|
||||||
|
FilterType.OwnCrafting,
|
||||||
|
FilterType.OthersCrafting,
|
||||||
|
FilterType.OwnGathering,
|
||||||
|
FilterType.OthersFishing,
|
||||||
|
FilterType.PeriodicRecruitment,
|
||||||
|
FilterType.Sign,
|
||||||
|
FilterType.Random,
|
||||||
|
FilterType.Orchestrion,
|
||||||
|
FilterType.MessageBook,
|
||||||
|
FilterType.Alarm
|
||||||
|
)]
|
||||||
|
Announcements,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CategoryAttribute : Attribute {
|
||||||
|
public string Name { get; }
|
||||||
|
public FilterType[] Types { get; }
|
||||||
|
|
||||||
|
public CategoryAttribute(string name, params FilterType[] types) {
|
||||||
|
this.Name = name;
|
||||||
|
this.Types = types;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FilterCategoryExtensions {
|
||||||
|
private static CategoryAttribute? Info(this FilterCategory filter) => filter
|
||||||
|
.GetType()
|
||||||
|
.GetField(filter.ToString())
|
||||||
|
?.GetCustomAttribute<CategoryAttribute>(false);
|
||||||
|
|
||||||
|
public static string? Name(this FilterCategory category) => category.Info()?.Name;
|
||||||
|
|
||||||
|
public static IEnumerable<FilterType> Types(this FilterCategory category) => category.Info()?.Types ?? new FilterType[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Changing the order of these is a breaking change
|
||||||
|
public enum FilterType {
|
||||||
|
[Filter("Say", ChatType.Say)] Say,
|
||||||
|
[Filter("Shout", ChatType.Shout)] Shout,
|
||||||
|
[Filter("Yell", ChatType.Yell)] Yell,
|
||||||
|
|
||||||
|
[Filter("Tell", ChatType.TellOutgoing, ChatType.TellIncoming)]
|
||||||
|
Tell,
|
||||||
|
|
||||||
|
[Filter("Party", ChatType.Party, ChatType.CrossParty)]
|
||||||
|
Party,
|
||||||
|
|
||||||
|
[Filter("Alliance", ChatType.Alliance)]
|
||||||
|
Alliance,
|
||||||
|
|
||||||
|
[Filter("Free Company", ChatType.FreeCompany)]
|
||||||
|
FreeCompany,
|
||||||
|
[Filter("PvP Team", ChatType.PvpTeam)] PvpTeam,
|
||||||
|
|
||||||
|
[Filter("Cross-world Linkshell [1]", ChatType.CrossLinkshell1)]
|
||||||
|
CrossLinkshell1,
|
||||||
|
|
||||||
|
[Filter("Cross-world Linkshell [2]", ChatType.CrossLinkshell2)]
|
||||||
|
CrossLinkshell2,
|
||||||
|
|
||||||
|
[Filter("Cross-world Linkshell [3]", ChatType.CrossLinkshell3)]
|
||||||
|
CrossLinkshell3,
|
||||||
|
|
||||||
|
[Filter("Cross-world Linkshell [4]", ChatType.CrossLinkshell4)]
|
||||||
|
CrossLinkshell4,
|
||||||
|
|
||||||
|
[Filter("Cross-world Linkshell [5]", ChatType.CrossLinkshell5)]
|
||||||
|
CrossLinkshell5,
|
||||||
|
|
||||||
|
[Filter("Cross-world Linkshell [6]", ChatType.CrossLinkshell6)]
|
||||||
|
CrossLinkshell6,
|
||||||
|
|
||||||
|
[Filter("Cross-world Linkshell [7]", ChatType.CrossLinkshell7)]
|
||||||
|
CrossLinkshell7,
|
||||||
|
|
||||||
|
[Filter("Cross-world Linkshell [8]", ChatType.CrossLinkshell8)]
|
||||||
|
CrossLinkshell8,
|
||||||
|
|
||||||
|
[Filter("Linkshell [1]", ChatType.Linkshell1)]
|
||||||
|
Linkshell1,
|
||||||
|
|
||||||
|
[Filter("Linkshell [2]", ChatType.Linkshell2)]
|
||||||
|
Linkshell2,
|
||||||
|
|
||||||
|
[Filter("Linkshell [3]", ChatType.Linkshell3)]
|
||||||
|
Linkshell3,
|
||||||
|
|
||||||
|
[Filter("Linkshell [4]", ChatType.Linkshell4)]
|
||||||
|
Linkshell4,
|
||||||
|
|
||||||
|
[Filter("Linkshell [5]", ChatType.Linkshell5)]
|
||||||
|
Linkshell5,
|
||||||
|
|
||||||
|
[Filter("Linkshell [6]", ChatType.Linkshell6)]
|
||||||
|
Linkshell6,
|
||||||
|
|
||||||
|
[Filter("Linkshell [7]", ChatType.Linkshell7)]
|
||||||
|
Linkshell7,
|
||||||
|
|
||||||
|
[Filter("Linkshell [8]", ChatType.Linkshell8)]
|
||||||
|
Linkshell8,
|
||||||
|
|
||||||
|
[Filter("Novice Network", ChatType.NoviceNetwork)]
|
||||||
|
NoviceNetwork,
|
||||||
|
|
||||||
|
[Filter("Standard Emotes", ChatType.StandardEmote)]
|
||||||
|
StandardEmote,
|
||||||
|
|
||||||
|
[Filter("Custom Emotes", ChatType.CustomEmote)]
|
||||||
|
CustomEmote,
|
||||||
|
|
||||||
|
[Filter("Battle",
|
||||||
|
ChatType.Damage,
|
||||||
|
ChatType.Miss,
|
||||||
|
ChatType.Action,
|
||||||
|
ChatType.Item,
|
||||||
|
ChatType.Healing,
|
||||||
|
ChatType.GainBuff,
|
||||||
|
ChatType.LoseBuff,
|
||||||
|
ChatType.GainDebuff,
|
||||||
|
ChatType.LoseDebuff,
|
||||||
|
ChatType.BattleSystem
|
||||||
|
)]
|
||||||
|
Battle,
|
||||||
|
|
||||||
|
[Filter("Debug", ChatType.Debug)] Debug,
|
||||||
|
[Filter("Urgent", ChatType.Urgent)] Urgent,
|
||||||
|
[Filter("Notice", ChatType.Notice)] Notice,
|
||||||
|
|
||||||
|
[Filter("System Messages", ChatType.System)]
|
||||||
|
SystemMessages,
|
||||||
|
|
||||||
|
[Filter("Own Battle System Messages", ChatType.BattleSystem, Source = FilterSource.Self)]
|
||||||
|
OwnBattleSystem,
|
||||||
|
|
||||||
|
[Filter("Others' Battle System Messages", ChatType.BattleSystem, Source = FilterSource.Others)]
|
||||||
|
OthersBattleSystem,
|
||||||
|
|
||||||
|
[Filter("Gathering System Messages", ChatType.GatheringSystem)]
|
||||||
|
GatheringSystem,
|
||||||
|
|
||||||
|
[Filter("Error Messages", ChatType.Error)]
|
||||||
|
ErrorMessages,
|
||||||
|
[Filter("Echo", ChatType.Echo)] Echo,
|
||||||
|
|
||||||
|
[Filter("Novice Network Notifications", ChatType.NoviceNetworkSystem)]
|
||||||
|
NoviceNetworkAnnouncements,
|
||||||
|
|
||||||
|
[Filter("Free Company Announcements", ChatType.FreeCompanyAnnouncement)]
|
||||||
|
FreeCompanyAnnouncements,
|
||||||
|
|
||||||
|
[Filter("PvP Team Announcements", ChatType.PvpTeamAnnouncement)]
|
||||||
|
PvpTeamAnnouncements,
|
||||||
|
|
||||||
|
[Filter("Free Company Member Login Notifications", ChatType.FreeCompanyLoginLogout)]
|
||||||
|
FreeCompanyLoginLogout,
|
||||||
|
|
||||||
|
[Filter("PvP Team Member Login Notifications", ChatType.PvpTeamLoginLogout)]
|
||||||
|
PvpTeamLoginLogout,
|
||||||
|
|
||||||
|
[Filter("Retainer Sale Notifications", ChatType.RetainerSale)]
|
||||||
|
RetainerSale,
|
||||||
|
|
||||||
|
[Filter("NPC Dialogue", ChatType.NpcDialogue)]
|
||||||
|
NpcDialogue,
|
||||||
|
|
||||||
|
[Filter("NPC Dialogue (Announcements)", ChatType.NpcAnnouncement)]
|
||||||
|
NpcAnnouncement,
|
||||||
|
|
||||||
|
[Filter("Loot Notices", ChatType.LootNotice)]
|
||||||
|
Loot,
|
||||||
|
|
||||||
|
[Filter("Own Progression Messages", ChatType.Progress, Source = FilterSource.Self)]
|
||||||
|
OwnProgression,
|
||||||
|
|
||||||
|
[Filter("Party Members' Progression Messages", ChatType.Progress, Source = FilterSource.Party)]
|
||||||
|
PartyProgression,
|
||||||
|
|
||||||
|
[Filter("Others' Progression Messages", ChatType.Progress, Source = FilterSource.Others)]
|
||||||
|
OthersProgression,
|
||||||
|
|
||||||
|
[Filter("Own Loot Messages", ChatType.LootRoll, Source = FilterSource.Self)]
|
||||||
|
OwnLoot,
|
||||||
|
|
||||||
|
[Filter("Others' Loot Messages", ChatType.LootRoll, Source = FilterSource.Others)]
|
||||||
|
OthersLoot,
|
||||||
|
|
||||||
|
[Filter("Own Synthesis Messages", ChatType.Crafting, Source = FilterSource.Self)]
|
||||||
|
OwnCrafting,
|
||||||
|
|
||||||
|
[Filter("Others' Synthesis Messages", ChatType.Crafting, Source = FilterSource.Self)]
|
||||||
|
OthersCrafting,
|
||||||
|
|
||||||
|
[Filter("Own Gathering Messages", ChatType.Gathering, Source = FilterSource.Self)]
|
||||||
|
OwnGathering,
|
||||||
|
|
||||||
|
[Filter("Others' Fishing Messages", ChatType.Gathering, Source = FilterSource.Others)]
|
||||||
|
OthersFishing,
|
||||||
|
|
||||||
|
[Filter("Periodic Recruitment Notifications", ChatType.PeriodicRecruitmentNotification)]
|
||||||
|
PeriodicRecruitment,
|
||||||
|
|
||||||
|
[Filter("Sign Messages for PC Targets", ChatType.Sign)]
|
||||||
|
Sign,
|
||||||
|
|
||||||
|
[Filter("Random Number Messages", ChatType.RandomNumber)]
|
||||||
|
Random,
|
||||||
|
|
||||||
|
[Filter("Current Orchestrion Track Messages", ChatType.Orchestrion)]
|
||||||
|
Orchestrion,
|
||||||
|
|
||||||
|
[Filter("Message Book Alert", ChatType.MessageBook)]
|
||||||
|
MessageBook,
|
||||||
|
|
||||||
|
[Filter("Alarm Notifications", ChatType.Alarm)]
|
||||||
|
Alarm,
|
||||||
|
|
||||||
|
[Filter("GM Messages",
|
||||||
|
ChatType.GmTell,
|
||||||
|
ChatType.GmSay,
|
||||||
|
ChatType.GmShout,
|
||||||
|
ChatType.GmYell,
|
||||||
|
ChatType.GmParty,
|
||||||
|
ChatType.GmFreeCompany,
|
||||||
|
ChatType.GmLinkshell1,
|
||||||
|
ChatType.GmLinkshell2,
|
||||||
|
ChatType.GmLinkshell3,
|
||||||
|
ChatType.GmLinkshell4,
|
||||||
|
ChatType.GmLinkshell5,
|
||||||
|
ChatType.GmLinkshell6,
|
||||||
|
ChatType.GmLinkshell7,
|
||||||
|
ChatType.GmLinkshell8,
|
||||||
|
ChatType.GmNoviceNetwork
|
||||||
|
)]
|
||||||
|
Gm,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FilterSource {
|
||||||
|
None,
|
||||||
|
Self,
|
||||||
|
Party,
|
||||||
|
Others,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FilterAttribute : Attribute {
|
||||||
|
public string Name { get; }
|
||||||
|
public ChatType[] Types { get; }
|
||||||
|
public FilterSource Source { get; set; } = FilterSource.None;
|
||||||
|
|
||||||
|
public FilterAttribute(string name, params ChatType[] types) {
|
||||||
|
this.Name = name;
|
||||||
|
this.Types = types;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FilterTypeExtensions {
|
||||||
|
private static readonly ChatSource[] Others = {
|
||||||
|
ChatSource.PartyMember, ChatSource.AllianceMember, ChatSource.Other, ChatSource.EngagedEnemy, ChatSource.UnengagedEnemy, ChatSource.FriendlyNpc, ChatSource.PartyPet, ChatSource.AlliancePet, ChatSource.OtherPet,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static FilterAttribute? Info(this FilterType filter) => filter
|
||||||
|
.GetType()
|
||||||
|
.GetField(filter.ToString())
|
||||||
|
?.GetCustomAttribute<FilterAttribute>(false);
|
||||||
|
|
||||||
|
public static string? Name(this FilterType filter) => filter.Info()?.Name;
|
||||||
|
|
||||||
|
public static IEnumerable<ChatType> Types(this FilterType filter) => filter.Info()?.Types ?? new ChatType[0];
|
||||||
|
|
||||||
|
public static ChatSource[] Sources(this FilterType filter) => filter.Info()?.Source switch {
|
||||||
|
FilterSource.Self => new[] {
|
||||||
|
ChatSource.Self,
|
||||||
|
},
|
||||||
|
FilterSource.Party => new[] {
|
||||||
|
ChatSource.PartyMember,
|
||||||
|
},
|
||||||
|
FilterSource.Others => Others,
|
||||||
|
_ => new ChatSource[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,22 +4,110 @@
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:XIVChat_Desktop"
|
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||||
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
|
ui:WindowHelper.UseModernWindowStyle="True"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Height="450" Width="800">
|
Title="XIVChat for Windows"
|
||||||
|
Height="450"
|
||||||
|
Width="800"
|
||||||
|
x:Name="Main"
|
||||||
|
Icon="/ic_launcher-playstore.png"
|
||||||
|
d:DataContext="{d:DesignInstance local:MainWindow}">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Menu Grid.Row="1">
|
<Menu Grid.Row="0">
|
||||||
<MenuItem Header="XIVChat">
|
<MenuItem Header="XIVChat">
|
||||||
<MenuItem Header="Connect"/>
|
<MenuItem Header="Connect"
|
||||||
<MenuItem Header="Disonnect" IsEnabled="False"/>
|
Click="Connect_Click"
|
||||||
|
IsEnabled="{Binding App.Disconnected, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<MenuItem Header="Disconnect"
|
||||||
|
Click="Disconnect_Click"
|
||||||
|
IsEnabled="{Binding App.Connected, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Header="Configuration"
|
||||||
|
Click="Configuration_Click" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Header="Tabs">
|
||||||
|
<MenuItem Header="Manage"
|
||||||
|
Click="ManageTabs_Click" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
<ListView Grid.Row="2"></ListView>
|
<TabControl x:Name="Tabs"
|
||||||
<TextBox Grid.Row="3" TextWrapping="Wrap"/>
|
Margin="8,0,8,8"
|
||||||
|
TabStripPlacement="Bottom"
|
||||||
|
Grid.Row="1"
|
||||||
|
Loaded="Tabs_Loaded"
|
||||||
|
SelectionChanged="Tabs_SelectionChanged"
|
||||||
|
ItemsSource="{Binding App.Config.Tabs}">
|
||||||
|
<TabControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding Name}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</TabControl.ItemTemplate>
|
||||||
|
|
||||||
|
<TabControl.ContentTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid d:DataContext="{d:DesignInstance local:Tab}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Row="0"
|
||||||
|
x:Name="scroller">
|
||||||
|
<ItemsControl Background="#333"
|
||||||
|
x:Name="items"
|
||||||
|
Padding="4"
|
||||||
|
ItemsSource="{Binding Messages, UpdateSourceTrigger=PropertyChanged}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<VirtualizingStackPanel IsVirtualizing="True"
|
||||||
|
VirtualizationMode="Recycling"
|
||||||
|
VerticalAlignment="Bottom" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock FontFamily="Global User Interface, /fonts/#XIV AXIS Std ATK"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontSize="{Binding App.Config.FontSize, ElementName=Main, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
local:MessageFormatter.FormattedText="{Binding .}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<TextBlock Margin="8,4,0,0"
|
||||||
|
Grid.Row="1"
|
||||||
|
Text="{Binding App.Connection.CurrentChannel, ElementName=Main, UpdateSourceTrigger=PropertyChanged}" />
|
||||||
|
|
||||||
|
<TextBox ui:ControlHelper.PlaceholderText="Send a message..."
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0,0,0,8"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
SpellCheck.IsEnabled="True"
|
||||||
|
KeyDown="Input_Submit" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</TabControl.ContentTemplate>
|
||||||
|
</TabControl>
|
||||||
|
<StatusBar Grid.Row="2">
|
||||||
|
<StatusBarItem x:Name="LoggedInAs"
|
||||||
|
Content="Not logged in" />
|
||||||
|
<Separator x:Name="LoggedInAsSeparator"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
<StatusBarItem x:Name="CurrentWorld"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
<Separator x:Name="CurrentWorldSeparator"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
<StatusBarItem x:Name="Location"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
</StatusBar>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
|
@ -1,25 +1,136 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using XIVChatCommon;
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace XIVChat_Desktop {
|
namespace XIVChat_Desktop {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interaction logic for MainWindow.xaml
|
/// Interaction logic for MainWindow.xaml
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow : Window {
|
public partial class MainWindow {
|
||||||
|
public App App => (App)Application.Current;
|
||||||
|
|
||||||
|
public ConcurrentStack<ServerMessage> Messages { get; } = new ConcurrentStack<ServerMessage>();
|
||||||
|
|
||||||
public MainWindow() {
|
public MainWindow() {
|
||||||
InitializeComponent();
|
this.InitializeComponent();
|
||||||
|
this.DataContext = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? FindElementByName<T>(DependencyObject element, string sChildName) where T : FrameworkElement {
|
||||||
|
T? childElement = null;
|
||||||
|
var nChildCount = VisualTreeHelper.GetChildrenCount(element);
|
||||||
|
for (int i = 0; i < nChildCount; i++) {
|
||||||
|
if (!(VisualTreeHelper.GetChild(element, i) is FrameworkElement child)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is T t && child.Name.Equals(sChildName)) {
|
||||||
|
childElement = t;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
childElement = this.FindElementByName<T>(child, sChildName);
|
||||||
|
|
||||||
|
if (childElement != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return childElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearAllMessages() {
|
||||||
|
this.Messages.Clear();
|
||||||
|
foreach (var tab in this.App.Config.Tabs) {
|
||||||
|
tab.ClearMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddSystemMessage(string content) {
|
||||||
|
var message = new ServerMessage {
|
||||||
|
Channel = 0,
|
||||||
|
Content = Encoding.UTF8.GetBytes(content),
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
Chunks = new List<Chunk> {
|
||||||
|
new TextChunk {
|
||||||
|
Foreground = 0xb38cffff,
|
||||||
|
Content = content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.AddMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddMessage(ServerMessage message) {
|
||||||
|
// detect if scroller is at the bottom
|
||||||
|
var scroller = this.FindElementByName<ScrollViewer>(this.Tabs, "scroller");
|
||||||
|
var wasAtBottom = Math.Abs(scroller!.VerticalOffset - scroller.ScrollableHeight) < .0001;
|
||||||
|
|
||||||
|
// add message to main list
|
||||||
|
this.Messages.Push(message);
|
||||||
|
// add message to each tab if the filter allows for it
|
||||||
|
foreach (var tab in this.App.Config.Tabs) {
|
||||||
|
tab.AddMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// scroll to the bottom if previously at the bottom
|
||||||
|
if (wasAtBottom) {
|
||||||
|
scroller.ScrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Connect_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var dialog = new ConnectDialog {
|
||||||
|
Owner = this,
|
||||||
|
};
|
||||||
|
dialog.ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Disconnect_Click(object sender, RoutedEventArgs e) {
|
||||||
|
this.App.Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Input_Submit(object sender, KeyEventArgs e) {
|
||||||
|
if (e.Key != Key.Return) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var conn = this.App.Connection;
|
||||||
|
if (conn == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(sender is TextBox)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textBox = (TextBox)sender;
|
||||||
|
|
||||||
|
conn.SendMessage(textBox.Text);
|
||||||
|
textBox.Text = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Configuration_Click(object sender, RoutedEventArgs e) {
|
||||||
|
new ConfigWindow(this, this.App.Config).Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tabs_Loaded(object sender, RoutedEventArgs e) {
|
||||||
|
this.Tabs.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tabs_SelectionChanged(object sender, SelectionChangedEventArgs e) {
|
||||||
|
var scroller = this.FindElementByName<ScrollViewer>(this.Tabs, "scroller");
|
||||||
|
scroller?.ScrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ManageTabs_Click(object sender, RoutedEventArgs e) {
|
||||||
|
new ManageTabs(this).Show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<Window x:Class="XIVChat_Desktop.ManageServer"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
|
ui:WindowHelper.UseModernWindowStyle="True"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
SizeToContent="WidthAndHeight"
|
||||||
|
ContentRendered="ManageServer_OnContentRendered"
|
||||||
|
Title="Manage server"
|
||||||
|
d:DataContext="{d:DesignInstance local:ManageServer}">
|
||||||
|
<Grid Margin="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition />
|
||||||
|
<ColumnDefinition MinWidth="200" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Label VerticalAlignment="Center"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="0">
|
||||||
|
Name
|
||||||
|
</Label>
|
||||||
|
<TextBox Margin="4,0,0,0"
|
||||||
|
Grid.Row="0"
|
||||||
|
Grid.Column="1"
|
||||||
|
x:Name="ServerName"
|
||||||
|
Text="{Binding Server.Name}" />
|
||||||
|
|
||||||
|
<Label VerticalAlignment="Center"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="0">
|
||||||
|
IP Address
|
||||||
|
</Label>
|
||||||
|
<TextBox Margin="4,4,0,0"
|
||||||
|
Grid.Row="1"
|
||||||
|
Grid.Column="1"
|
||||||
|
x:Name="ServerHost"
|
||||||
|
Text="{Binding Server.Host}" />
|
||||||
|
|
||||||
|
<Label VerticalAlignment="Center"
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="0">
|
||||||
|
Port
|
||||||
|
</Label>
|
||||||
|
<TextBox Margin="4,4,0,0"
|
||||||
|
Grid.Row="2"
|
||||||
|
Grid.Column="1"
|
||||||
|
x:Name="ServerPort"
|
||||||
|
Text="{Binding Server.Port}"
|
||||||
|
ui:ControlHelper.PlaceholderText="14777" />
|
||||||
|
|
||||||
|
<WrapPanel Margin="0,8,0,0"
|
||||||
|
Grid.Row="3"
|
||||||
|
Grid.ColumnSpan="2"
|
||||||
|
Grid.Column="0"
|
||||||
|
HorizontalAlignment="Right">
|
||||||
|
<Button IsCancel="True"
|
||||||
|
Click="Cancel_Click">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button Margin="4,0,0,0"
|
||||||
|
IsDefault="True"
|
||||||
|
Click="Save_Click">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
|
@ -0,0 +1,64 @@
|
||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for ManageServer.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class ManageServer {
|
||||||
|
public App App => (App)Application.Current;
|
||||||
|
public SavedServer? Server { get; private set; }
|
||||||
|
|
||||||
|
private readonly bool isNewServer;
|
||||||
|
|
||||||
|
public ManageServer(Window owner, SavedServer? server) {
|
||||||
|
this.Owner = owner;
|
||||||
|
this.Server = server;
|
||||||
|
this.isNewServer = server == null;
|
||||||
|
|
||||||
|
this.InitializeComponent();
|
||||||
|
this.DataContext = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var serverName = this.ServerName.Text;
|
||||||
|
var serverHost = this.ServerHost.Text;
|
||||||
|
|
||||||
|
if (serverName.Length == 0 || serverHost.Length == 0) {
|
||||||
|
MessageBox.Show("Server must have a name and host.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ushort port;
|
||||||
|
if (this.ServerPort.Text.Length == 0) {
|
||||||
|
port = 14777;
|
||||||
|
} else {
|
||||||
|
if (!ushort.TryParse(this.ServerPort.Text, out port) || port < 1) {
|
||||||
|
MessageBox.Show("Port was not valid. It must be a number between 1 and 65535.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNewServer) {
|
||||||
|
this.Server = new SavedServer(
|
||||||
|
serverName,
|
||||||
|
serverHost,
|
||||||
|
port
|
||||||
|
);
|
||||||
|
this.App.Config.Servers.Add(this.Server);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.App.Config.Save();
|
||||||
|
|
||||||
|
this.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Cancel_Click(object sender, RoutedEventArgs e) {
|
||||||
|
this.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ManageServer_OnContentRendered(object? sender, EventArgs e) {
|
||||||
|
this.InvalidateVisual();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
<Window x:Class="XIVChat_Desktop.FiltersSelection"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||||
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
|
ui:WindowHelper.UseModernWindowStyle="True"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Manage tab"
|
||||||
|
Height="450"
|
||||||
|
Width="400"
|
||||||
|
d:DataContext="{d:DesignInstance local:FiltersSelection}">
|
||||||
|
<Grid Margin="8">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBox Grid.Row="0"
|
||||||
|
Margin="0,0,0,8"
|
||||||
|
ui:ControlHelper.PlaceholderText="Name"
|
||||||
|
ui:ControlHelper.Header="Name"
|
||||||
|
Text="{Binding Tab.Name}" />
|
||||||
|
<TabControl Grid.Row="1"
|
||||||
|
x:Name="Tabs" />
|
||||||
|
<Button Margin="0,8,0,0"
|
||||||
|
Grid.Row="2"
|
||||||
|
Content="Save"
|
||||||
|
Click="Save_Click" />
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
|
@ -0,0 +1,116 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for FiltersSelection.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class FiltersSelection {
|
||||||
|
public App App => (App)Application.Current;
|
||||||
|
|
||||||
|
public Tab Tab { get; }
|
||||||
|
|
||||||
|
private readonly bool isNewTab;
|
||||||
|
private readonly IImmutableSet<FilterType> oldFilters;
|
||||||
|
|
||||||
|
public FiltersSelection(Window owner, Tab? tab) {
|
||||||
|
this.Owner = owner;
|
||||||
|
this.isNewTab = tab == null;
|
||||||
|
this.Tab = tab ?? new Tab("") {
|
||||||
|
Filter = Tab.GeneralFilter(),
|
||||||
|
};
|
||||||
|
this.oldFilters = this.Tab.Filter.Types.ToImmutableHashSet();
|
||||||
|
|
||||||
|
this.InitializeComponent();
|
||||||
|
this.DataContext = this;
|
||||||
|
|
||||||
|
foreach (var category in (FilterCategory[])Enum.GetValues(typeof(FilterCategory))) {
|
||||||
|
var panel = new WrapPanel {
|
||||||
|
Margin = new Thickness(8),
|
||||||
|
Orientation = Orientation.Vertical,
|
||||||
|
};
|
||||||
|
|
||||||
|
var tabContent = new ScrollViewer {
|
||||||
|
Content = panel,
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||||
|
};
|
||||||
|
|
||||||
|
var buttonsPanel = new WrapPanel {
|
||||||
|
Margin = new Thickness(0, 0, 0, 4),
|
||||||
|
};
|
||||||
|
|
||||||
|
var selectButton = new Button {
|
||||||
|
Content = "Select all",
|
||||||
|
};
|
||||||
|
selectButton.Click += (sender, e) => SetAllChecked(true);
|
||||||
|
|
||||||
|
var deselectButton = new Button {
|
||||||
|
Content = "Deselect all",
|
||||||
|
Margin = new Thickness(4, 0, 0, 0),
|
||||||
|
};
|
||||||
|
deselectButton.Click += (sender, e) => SetAllChecked(false);
|
||||||
|
|
||||||
|
void SetAllChecked(bool isChecked) {
|
||||||
|
foreach (var child in panel.Children) {
|
||||||
|
if (!(child is CheckBox)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var check = (CheckBox)child;
|
||||||
|
check.IsChecked = isChecked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonsPanel.Children.Add(selectButton);
|
||||||
|
buttonsPanel.Children.Add(deselectButton);
|
||||||
|
|
||||||
|
panel.Children.Add(buttonsPanel);
|
||||||
|
panel.Children.Add(new Separator());
|
||||||
|
|
||||||
|
foreach (var type in category.Types()) {
|
||||||
|
var check = new CheckBox {
|
||||||
|
Content = type.Name(),
|
||||||
|
IsChecked = this.Tab.Filter.Types.Contains(type),
|
||||||
|
};
|
||||||
|
|
||||||
|
check.Checked += (sender, e) => {
|
||||||
|
this.Tab.Filter.Types.Add(type);
|
||||||
|
};
|
||||||
|
check.Unchecked += (sender, e) => {
|
||||||
|
this.Tab.Filter.Types.Remove(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.Children.Add(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tabItem = new TabItem {
|
||||||
|
Header = new TextBlock(new Run(category.Name())),
|
||||||
|
Content = tabContent,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.Tabs.Items.Add(tabItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_Click(object sender, RoutedEventArgs e) {
|
||||||
|
if (this.Tab.Name.Length == 0) {
|
||||||
|
MessageBox.Show("Tab must have a name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNewTab) {
|
||||||
|
this.App.Config.Tabs.Add(this.Tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNewTab || !this.oldFilters.SetEquals(this.Tab.Filter.Types)) {
|
||||||
|
this.Tab.RepopulateMessages(this.App.Window.Messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.App.Config.Save();
|
||||||
|
this.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<Window x:Class="XIVChat_Desktop.ManageTabs"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||||
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
|
ui:WindowHelper.UseModernWindowStyle="True"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Manage tabs" Height="250" Width="400"
|
||||||
|
d:DataContext="{d:DesignInstance local:ManageTabs}">
|
||||||
|
<Grid Margin="8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ListView x:Name="Tabs" Grid.Column="0" ItemsSource="{Binding App.Config.Tabs}" MouseDoubleClick="Tab_MouseDoubleClick">
|
||||||
|
<ListView.View>
|
||||||
|
<GridView>
|
||||||
|
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Name}"/>
|
||||||
|
</GridView>
|
||||||
|
</ListView.View>
|
||||||
|
</ListView>
|
||||||
|
<WrapPanel Grid.Column="1" Orientation="Vertical" Margin="8,0,0,0">
|
||||||
|
<Button HorizontalAlignment="Stretch" Margin="0,0,0,4" Click="AddTab_Click">Add</Button>
|
||||||
|
<Button HorizontalAlignment="Stretch" Margin="0,0,0,4" Click="EditTab_Click">Edit</Button>
|
||||||
|
<Button HorizontalAlignment="Stretch" Click="DeleteTab_Click">Delete</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
|
@ -0,0 +1,59 @@
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for ManageTabs.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class ManageTabs {
|
||||||
|
public App App => (App)Application.Current;
|
||||||
|
|
||||||
|
private Tab? SelectedTab {
|
||||||
|
get {
|
||||||
|
var item = this.Tabs.SelectedItem;
|
||||||
|
|
||||||
|
return item as Tab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ManageTabs(Window owner) {
|
||||||
|
this.Owner = owner;
|
||||||
|
this.InitializeComponent();
|
||||||
|
this.DataContext = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddTab_Click(object sender, RoutedEventArgs e) {
|
||||||
|
new FiltersSelection(this, null).ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EditTab_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var tab = this.SelectedTab;
|
||||||
|
if (tab == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new FiltersSelection(this, tab).ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteTab_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var tab = this.SelectedTab;
|
||||||
|
if (tab == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.App.Config.Tabs.Remove(tab);
|
||||||
|
this.App.Config.Save();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Tab_MouseDoubleClick(object sender, MouseButtonEventArgs e) {
|
||||||
|
var item = ((FrameworkElement)e.OriginalSource).DataContext;
|
||||||
|
if (!(item is Tab)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tab = item as Tab;
|
||||||
|
|
||||||
|
new FiltersSelection(this, tab).ShowDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using XIVChatCommon;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
public class MessageFormatter {
|
||||||
|
private static readonly BitmapFrame FontIcon =
|
||||||
|
BitmapFrame.Create(new Uri("pack://application:,,,/Resources/fonticon_ps4.tex.png"));
|
||||||
|
|
||||||
|
public static readonly DependencyProperty FormattedTextProperty = DependencyProperty.RegisterAttached(
|
||||||
|
"FormattedText",
|
||||||
|
typeof(ServerMessage),
|
||||||
|
typeof(MessageFormatter),
|
||||||
|
new PropertyMetadata(null, FormattedTextPropertyChanged)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static void SetFormattedText(DependencyObject textBlock, ServerMessage value) {
|
||||||
|
textBlock.SetValue(FormattedTextProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetFormattedText(DependencyObject textBlock) {
|
||||||
|
return (string)textBlock.GetValue(FormattedTextProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FormattedTextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||||
|
// Clear current textBlock
|
||||||
|
if (!(d is TextBlock textBlock)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textBlock.ClearValue(TextBlock.TextProperty);
|
||||||
|
textBlock.Inlines.Clear();
|
||||||
|
|
||||||
|
// Create new formatted text
|
||||||
|
var lineHeight = textBlock.FontFamily.LineSpacing * textBlock.FontSize;
|
||||||
|
foreach (var inline in ChunksToTextBlock(lineHeight, (ServerMessage)e.NewValue)) {
|
||||||
|
textBlock.Inlines.Add(inline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<Inline> ChunksToTextBlock(double lineHeight, ServerMessage message) {
|
||||||
|
var elements = new List<Inline>();
|
||||||
|
|
||||||
|
var timestampString = message.Timestamp.ToLocalTime().ToString("t", CultureInfo.CurrentUICulture);
|
||||||
|
elements.Add(new Run($"[{timestampString}]") {
|
||||||
|
Foreground = new SolidColorBrush(Colors.White),
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var chunk in message.Chunks) {
|
||||||
|
switch (chunk) {
|
||||||
|
case TextChunk textChunk:
|
||||||
|
var colour = textChunk.Foreground ?? textChunk.FallbackColour ?? 0;
|
||||||
|
|
||||||
|
var r = (byte)((colour >> 24) & 0xFF);
|
||||||
|
var g = (byte)((colour >> 16) & 0xFF);
|
||||||
|
var b = (byte)((colour >> 8) & 0xFF);
|
||||||
|
var a = (byte)(colour & 0xFF);
|
||||||
|
|
||||||
|
var brush = new SolidColorBrush(Color.FromArgb(a, r, g, b));
|
||||||
|
var style = textChunk.Italic ? FontStyles.Italic : FontStyles.Normal;
|
||||||
|
|
||||||
|
//var part = string.Empty;
|
||||||
|
//foreach (char c in textChunk.Content) {
|
||||||
|
// if (c >= '\ue000' && c <= '\uf8ff') {
|
||||||
|
// // private use
|
||||||
|
|
||||||
|
// // add existing text if necessary
|
||||||
|
// if (part.Length != 0) {
|
||||||
|
// elements.Add(new Run(part) {
|
||||||
|
// Foreground = brush,
|
||||||
|
// FontStyle = style,
|
||||||
|
// });
|
||||||
|
// part = string.Empty;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // add private use segment with font
|
||||||
|
// elements.Add(new Run(c.ToString()) {
|
||||||
|
// Foreground = brush,
|
||||||
|
// FontStyle = style,
|
||||||
|
// FontFamily = new FontFamily(new Uri("pack://application:,,,/"), "/fonts/#XIV AXIS Std ATK"),
|
||||||
|
// });
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// part += c;
|
||||||
|
//}
|
||||||
|
|
||||||
|
elements.Add(new Run(textChunk.Content) {
|
||||||
|
Foreground = brush,
|
||||||
|
FontStyle = style,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case IconChunk iconChunk:
|
||||||
|
var bounds = GetBounds(iconChunk.Index);
|
||||||
|
if (bounds == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var width = lineHeight / bounds.Value.Height * bounds.Value.Width;
|
||||||
|
|
||||||
|
var cropped = new CroppedBitmap(FontIcon, bounds.Value);
|
||||||
|
var image = new Image {
|
||||||
|
Source = cropped,
|
||||||
|
Width = width,
|
||||||
|
Height = lineHeight,
|
||||||
|
};
|
||||||
|
elements.Add(new InlineUIContainer(image) {
|
||||||
|
BaselineAlignment = BaselineAlignment.Bottom,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Int32Rect? GetBounds(byte id) => id switch {
|
||||||
|
1 => new Int32Rect(0, 0, 20, 20),
|
||||||
|
2 => new Int32Rect(20, 0, 20, 20),
|
||||||
|
3 => new Int32Rect(40, 0, 20, 20),
|
||||||
|
4 => new Int32Rect(60, 0, 20, 20),
|
||||||
|
5 => new Int32Rect(80, 0, 20, 20),
|
||||||
|
6 => new Int32Rect(0, 20, 20, 20),
|
||||||
|
7 => new Int32Rect(20, 20, 20, 20),
|
||||||
|
8 => new Int32Rect(40, 20, 20, 20),
|
||||||
|
9 => new Int32Rect(60, 20, 20, 20),
|
||||||
|
10 => new Int32Rect(80, 20, 20, 20),
|
||||||
|
11 => new Int32Rect(0, 40, 20, 20),
|
||||||
|
12 => new Int32Rect(20, 40, 20, 20),
|
||||||
|
13 => new Int32Rect(40, 40, 20, 20),
|
||||||
|
14 => new Int32Rect(60, 40, 20, 20),
|
||||||
|
15 => new Int32Rect(80, 40, 20, 20),
|
||||||
|
16 => new Int32Rect(60, 100, 20, 20),
|
||||||
|
17 => new Int32Rect(80, 100, 20, 20),
|
||||||
|
18 => new Int32Rect(0, 60, 54, 20),
|
||||||
|
19 => new Int32Rect(54, 60, 54, 20),
|
||||||
|
20 => new Int32Rect(60, 80, 20, 20),
|
||||||
|
21 => new Int32Rect(0, 80, 28, 20),
|
||||||
|
22 => new Int32Rect(28, 80, 32, 20),
|
||||||
|
23 => new Int32Rect(80, 80, 20, 20),
|
||||||
|
24 => new Int32Rect(0, 100, 28, 20),
|
||||||
|
25 => new Int32Rect(28, 100, 32, 20),
|
||||||
|
51 => new Int32Rect(124, 0, 20, 20),
|
||||||
|
52 => new Int32Rect(144, 0, 20, 20),
|
||||||
|
53 => new Int32Rect(164, 0, 20, 20),
|
||||||
|
54 => new Int32Rect(100, 0, 12, 20),
|
||||||
|
55 => new Int32Rect(112, 0, 12, 20),
|
||||||
|
56 => new Int32Rect(100, 20, 20, 20),
|
||||||
|
57 => new Int32Rect(120, 20, 20, 20),
|
||||||
|
58 => new Int32Rect(140, 20, 20, 20),
|
||||||
|
59 => new Int32Rect(100, 40, 20, 20),
|
||||||
|
60 => new Int32Rect(120, 40, 20, 20),
|
||||||
|
61 => new Int32Rect(140, 40, 20, 20),
|
||||||
|
62 => new Int32Rect(160, 20, 20, 20),
|
||||||
|
63 => new Int32Rect(160, 40, 20, 20),
|
||||||
|
64 => new Int32Rect(184, 0, 20, 20),
|
||||||
|
65 => new Int32Rect(204, 0, 20, 20),
|
||||||
|
66 => new Int32Rect(224, 0, 20, 20),
|
||||||
|
67 => new Int32Rect(180, 20, 20, 20),
|
||||||
|
68 => new Int32Rect(200, 20, 20, 20),
|
||||||
|
69 => new Int32Rect(236, 236, 20, 20),
|
||||||
|
70 => new Int32Rect(180, 40, 20, 20),
|
||||||
|
71 => new Int32Rect(200, 40, 20, 20),
|
||||||
|
72 => new Int32Rect(220, 40, 20, 20),
|
||||||
|
73 => new Int32Rect(220, 20, 20, 20),
|
||||||
|
74 => new Int32Rect(108, 60, 20, 20),
|
||||||
|
75 => new Int32Rect(128, 60, 20, 20),
|
||||||
|
76 => new Int32Rect(148, 60, 20, 20),
|
||||||
|
77 => new Int32Rect(168, 60, 20, 20),
|
||||||
|
78 => new Int32Rect(188, 60, 20, 20),
|
||||||
|
79 => new Int32Rect(208, 60, 20, 20),
|
||||||
|
80 => new Int32Rect(228, 60, 20, 20),
|
||||||
|
81 => new Int32Rect(100, 80, 20, 20),
|
||||||
|
82 => new Int32Rect(120, 80, 20, 20),
|
||||||
|
83 => new Int32Rect(140, 80, 20, 20),
|
||||||
|
84 => new Int32Rect(160, 80, 20, 20),
|
||||||
|
85 => new Int32Rect(180, 80, 20, 20),
|
||||||
|
86 => new Int32Rect(200, 80, 20, 20),
|
||||||
|
87 => new Int32Rect(220, 80, 20, 20),
|
||||||
|
88 => new Int32Rect(100, 100, 20, 20),
|
||||||
|
89 => new Int32Rect(120, 100, 20, 20),
|
||||||
|
90 => new Int32Rect(140, 100, 20, 20),
|
||||||
|
91 => new Int32Rect(160, 100, 20, 20),
|
||||||
|
92 => new Int32Rect(180, 100, 20, 20),
|
||||||
|
93 => new Int32Rect(200, 100, 20, 20),
|
||||||
|
94 => new Int32Rect(220, 100, 20, 20),
|
||||||
|
95 => new Int32Rect(0, 120, 20, 20),
|
||||||
|
96 => new Int32Rect(20, 120, 20, 20),
|
||||||
|
97 => new Int32Rect(40, 120, 20, 20),
|
||||||
|
98 => new Int32Rect(60, 120, 20, 20),
|
||||||
|
99 => new Int32Rect(80, 120, 20, 20),
|
||||||
|
100 => new Int32Rect(100, 120, 20, 20),
|
||||||
|
101 => new Int32Rect(120, 120, 20, 20),
|
||||||
|
102 => new Int32Rect(140, 120, 20, 20),
|
||||||
|
103 => new Int32Rect(160, 120, 20, 20),
|
||||||
|
104 => new Int32Rect(180, 120, 20, 20),
|
||||||
|
105 => new Int32Rect(200, 120, 20, 20),
|
||||||
|
106 => new Int32Rect(220, 120, 20, 20),
|
||||||
|
107 => new Int32Rect(0, 140, 20, 20),
|
||||||
|
108 => new Int32Rect(20, 140, 20, 20),
|
||||||
|
109 => new Int32Rect(40, 140, 20, 20),
|
||||||
|
110 => new Int32Rect(60, 140, 20, 20),
|
||||||
|
111 => new Int32Rect(80, 140, 20, 20),
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:ui="http://schemas.modernwpf.com/2019">
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ui:IntellisenseResources Source="/ModernWpf;component/DesignTime/DesignTimeResources.xaml" />
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
</ResourceDictionary>
|
|
@ -0,0 +1,72 @@
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop.Properties {
|
||||||
|
using System;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||||
|
// class via a tool like ResGen or Visual Studio.
|
||||||
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
|
// with the /str option, or rebuild your VS project.
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
public class Resources {
|
||||||
|
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal Resources() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
public static global::System.Resources.ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("XIVChat_Desktop.Properties.Resources", typeof(Resources).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
public static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get {
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized string similar to XIVChat for Windows.
|
||||||
|
/// </summary>
|
||||||
|
public static string AppName {
|
||||||
|
get {
|
||||||
|
return ResourceManager.GetString("AppName", resourceCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="AppName" xml:space="preserve">
|
||||||
|
<value>XIVChat for Windows</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
Binary file not shown.
|
@ -0,0 +1,116 @@
|
||||||
|
<Window x:Class="XIVChat_Desktop.TrustDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||||
|
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||||
|
ui:WindowHelper.UseModernWindowStyle="True"
|
||||||
|
SizeToContent="WidthAndHeight"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Key verification">
|
||||||
|
<Grid Margin="8">
|
||||||
|
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Column="0" Grid.Row="0">
|
||||||
|
You are attempting to connect to a server you have never connected to before. Please check the server and ensure the two keys below match.
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<Grid Grid.Column="0" Grid.Row="1">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0">Server:</TextBlock>
|
||||||
|
<TextBlock x:Name="ServerPublicKey" Grid.Row="1" Grid.Column="0"/>
|
||||||
|
<Grid Grid.Row="2" Grid.Column="0" Height="24" x:Name="ServerPublicKeyColours">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Rectangle Grid.Column="0"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="1"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="2"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="3"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="4"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="5"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="6"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="7"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="8"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="9"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="10"></Rectangle>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Grid.Column="0" Grid.Row="2">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
<RowDefinition/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0">Client:</TextBlock>
|
||||||
|
<TextBlock x:Name="ClientPublicKey" Grid.Row="1" Grid.Column="0"/>
|
||||||
|
<Grid Grid.Row="2" Grid.Column="0" Height="24" x:Name="ClientPublicKeyColours">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
<ColumnDefinition/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Rectangle Grid.Column="0"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="1"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="2"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="3"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="4"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="5"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="6"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="7"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="8"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="9"></Rectangle>
|
||||||
|
<Rectangle Grid.Column="10"></Rectangle>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<WrapPanel Grid.Row="3" Grid.Column="0" Orientation="Vertical">
|
||||||
|
<TextBlock>Give the key a name to remember it.</TextBlock>
|
||||||
|
<TextBox x:Name="KeyName">No name</TextBox>
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0">Do both keys match?</TextBlock>
|
||||||
|
|
||||||
|
<WrapPanel Grid.Row="5" Grid.Column="0">
|
||||||
|
<Button Click="Yes_Click">Yes</Button>
|
||||||
|
<Button Click="No_Click">No</Button>
|
||||||
|
</WrapPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
|
@ -0,0 +1,85 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Shapes;
|
||||||
|
|
||||||
|
namespace XIVChat_Desktop {
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for TrustDialog.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class TrustDialog {
|
||||||
|
private readonly ChannelWriter<bool> trustChannel;
|
||||||
|
private readonly byte[] remoteKey;
|
||||||
|
|
||||||
|
private App App => (App)Application.Current;
|
||||||
|
|
||||||
|
public TrustDialog(Window owner, ChannelWriter<bool> trustChannel, byte[] remoteKey) {
|
||||||
|
this.Owner = owner;
|
||||||
|
this.trustChannel = trustChannel;
|
||||||
|
this.remoteKey = remoteKey;
|
||||||
|
|
||||||
|
this.InitializeComponent();
|
||||||
|
|
||||||
|
this.ClientPublicKey.Text = ToHexString(this.App.Config.KeyPair.PublicKey);
|
||||||
|
var clientColours = BreakIntoColours(this.App.Config.KeyPair.PublicKey);
|
||||||
|
for (int i = 0; i < this.ClientPublicKeyColours.Children.Count; i++) {
|
||||||
|
var rect = (Rectangle)this.ClientPublicKeyColours.Children[i];
|
||||||
|
rect.Fill = new SolidColorBrush(clientColours[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ServerPublicKey.Text = ToHexString(remoteKey);
|
||||||
|
var serverColours = BreakIntoColours(remoteKey);
|
||||||
|
for (int i = 0; i < this.ServerPublicKeyColours.Children.Count; i++) {
|
||||||
|
var rect = (Rectangle)this.ServerPublicKeyColours.Children[i];
|
||||||
|
rect.Fill = new SolidColorBrush(serverColours[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Color> BreakIntoColours(IEnumerable<byte> key) {
|
||||||
|
var colours = new List<Color>();
|
||||||
|
|
||||||
|
// ReSharper disable once LoopCanBeConvertedToQuery
|
||||||
|
foreach (var chunk in SplitList(key.ToList(), 3)) {
|
||||||
|
var r = chunk[0];
|
||||||
|
var g = chunk.Count > 1 ? chunk[1] : (byte)0;
|
||||||
|
var b = chunk.Count > 2 ? chunk[2] : (byte)0;
|
||||||
|
|
||||||
|
colours.Add(Color.FromRgb(r, g, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
return colours;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<List<T>> SplitList<T>(List<T> locations, int nSize) {
|
||||||
|
for (int i = 0; i < locations.Count; i += nSize) {
|
||||||
|
yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ToHexString(IEnumerable<byte> bytes) {
|
||||||
|
return string.Join("", bytes.Select(b => b.ToString("X2")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Yes_Click(object sender, RoutedEventArgs e) {
|
||||||
|
var keyName = this.KeyName.Text;
|
||||||
|
if (keyName.Length == 0) {
|
||||||
|
MessageBox.Show("You must give this key a name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var trustedKey = new TrustedKey(keyName, this.remoteKey);
|
||||||
|
this.App.Config.TrustedKeys.Add(trustedKey);
|
||||||
|
this.App.Config.Save();
|
||||||
|
await this.trustChannel.WriteAsync(true);
|
||||||
|
this.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void No_Click(object sender, RoutedEventArgs e) {
|
||||||
|
await this.trustChannel.WriteAsync(false);
|
||||||
|
this.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,59 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||||
<RootNamespace>XIVChat_Desktop</RootNamespace>
|
<RootNamespace>XIVChat_Desktop</RootNamespace>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
</PropertyGroup>
|
<AssemblyName>XIVChat Desktop</AssemblyName>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Company>XIVChat</Company>
|
||||||
|
<AssemblyVersion>1.0.0</AssemblyVersion>
|
||||||
|
<FileVersion>1.0.0</FileVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="fonts\ffxiv.ttf"/>
|
||||||
|
<None Remove="ic_launcher-playstore.png"/>
|
||||||
|
<None Remove="Resources\fonticon_ps4.tex.png"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ModernWpfUI" Version="0.9.2"/>
|
||||||
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3"/>
|
||||||
|
<PackageReference Include="Sodium.Core" Version="1.2.3"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\XIVChatCommon\XIVChatCommon.csproj"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Resource Include="fonts\ffxiv.ttf"/>
|
||||||
|
<Resource Include="ic_launcher-playstore.png"/>
|
||||||
|
<Resource Include="Resources\fonticon_ps4.tex.png"/>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Properties\Resources.Designer.cs">
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Update="Properties\Resources.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Properties\DesignTimeResources.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
<ContainsDesignTimeResources>true</ContainsDesignTimeResources>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
Binary file not shown.
Binary file not shown.
|
@ -12,7 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVChatCommon", "XIVChatCommon\XIVChatCommon.csproj", "{6F426EAB-15D4-48AC-80D2-2D2DF2CE5EE0}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVChatCommon", "XIVChatCommon\XIVChatCommon.csproj", "{6F426EAB-15D4-48AC-80D2-2D2DF2CE5EE0}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVChat Desktop", "XIVChat Desktop\XIVChat Desktop.csproj", "{D2773EAE-6B9E-4017-9681-585AAB5A99F4}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XIVChat Desktop", "XIVChat Desktop\XIVChat Desktop.csproj", "{D2773EAE-6B9E-4017-9681-585AAB5A99F4}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|
Loading…
Reference in New Issue