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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:XIVChat_Desktop"
|
||||
StartupUri="MainWindow.xaml">
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
Startup="Application_Startup">
|
||||
<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>
|
||||
</Application>
|
|
@ -1,8 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
|
@ -10,6 +7,74 @@ namespace XIVChat_Desktop {
|
|||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </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: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"
|
||||
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.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<Menu Grid.Row="1">
|
||||
<Menu Grid.Row="0">
|
||||
<MenuItem Header="XIVChat">
|
||||
<MenuItem Header="Connect"/>
|
||||
<MenuItem Header="Disonnect" IsEnabled="False"/>
|
||||
<MenuItem Header="Connect"
|
||||
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>
|
||||
</Menu>
|
||||
<ListView Grid.Row="2"></ListView>
|
||||
<TextBox Grid.Row="3" TextWrapping="Wrap"/>
|
||||
<TabControl x:Name="Tabs"
|
||||
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>
|
||||
</Window>
|
||||
</Window>
|
|
@ -1,25 +1,136 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using XIVChatCommon;
|
||||
|
||||
namespace XIVChat_Desktop {
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </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() {
|
||||
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">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RootNamespace>XIVChat_Desktop</RootNamespace>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<RootNamespace>XIVChat_Desktop</RootNamespace>
|
||||
<UseWPF>true</UseWPF>
|
||||
<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>
|
Binary file not shown.
Binary file not shown.
|
@ -12,7 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XIVChatCommon", "XIVChatCommon\XIVChatCommon.csproj", "{6F426EAB-15D4-48AC-80D2-2D2DF2CE5EE0}"
|
||||
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
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
|
|
Loading…
Reference in New Issue