feat(desktop): add export feature
This commit is contained in:
parent
a48beae786
commit
900fa642a0
|
@ -8,6 +8,7 @@
|
|||
<ResourceDictionary>
|
||||
<local:DoubleConverter x:Key="DoubleConverter" />
|
||||
<local:UShortConverter x:Key="UShortConverter" />
|
||||
<local:SenderPlayerConverter x:Key="SenderPlayerConverter" />
|
||||
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemeResources AccentColor="#02ccee" />
|
||||
|
@ -17,4 +18,4 @@
|
|||
<!-- Other app resources here -->
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
</Application>
|
||||
|
|
|
@ -139,6 +139,8 @@ namespace XIVChat_Desktop {
|
|||
|
||||
public Filter Filter { get; set; } = new Filter();
|
||||
|
||||
public bool ProcessMarkdown { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public List<ServerMessage> Messages { get; } = new List<ServerMessage>();
|
||||
|
||||
|
@ -243,7 +245,7 @@ namespace XIVChat_Desktop {
|
|||
public class Filter {
|
||||
public HashSet<FilterType> Types { get; set; } = new HashSet<FilterType>();
|
||||
|
||||
public bool Allowed(ServerMessage message) {
|
||||
public virtual bool Allowed(ServerMessage message) {
|
||||
var code = new ChatCode((ushort)message.Channel);
|
||||
return this.Types.Any(type => type.Allowed(code));
|
||||
}
|
||||
|
|
|
@ -28,6 +28,18 @@ namespace XIVChat_Desktop.Controls {
|
|||
set => this.SetValue(TabProperty, value);
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty ShowTimestampsProperty = DependencyProperty.Register(
|
||||
"ShowTimestamps",
|
||||
typeof(bool),
|
||||
typeof(MessageTextBlock),
|
||||
new PropertyMetadata(true, PropertyChanged)
|
||||
);
|
||||
|
||||
public bool ShowTimestamps {
|
||||
get => (bool)this.GetValue(ShowTimestampsProperty);
|
||||
set => this.SetValue(ShowTimestampsProperty, value);
|
||||
}
|
||||
|
||||
public static void PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
// Clear current textBlock
|
||||
if (!(d is MessageTextBlock textBlock)) {
|
||||
|
@ -41,12 +53,12 @@ namespace XIVChat_Desktop.Controls {
|
|||
return;
|
||||
}
|
||||
|
||||
textBlock.ClearValue(TextBlock.TextProperty);
|
||||
textBlock.ClearValue(TextProperty);
|
||||
textBlock.Inlines.Clear();
|
||||
|
||||
// Create new formatted text
|
||||
var lineHeight = textBlock.FontFamily.LineSpacing * textBlock.FontSize;
|
||||
foreach (var inline in MessageFormatter.ChunksToTextBlock(lineHeight, message, tab)) {
|
||||
foreach (var inline in MessageFormatter.ChunksToTextBlock(lineHeight, message, tab.ProcessMarkdown, textBlock.ShowTimestamps)) {
|
||||
textBlock.Inlines.Add(inline);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Windows.Data;
|
||||
using XIVChatCommon;
|
||||
|
||||
namespace XIVChat_Desktop {
|
||||
public class DoubleConverter : IValueConverter {
|
||||
|
@ -30,4 +32,29 @@ namespace XIVChat_Desktop {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class SenderPlayerConverter : IValueConverter {
|
||||
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (!(value is ServerMessage.SenderPlayer sender)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var s = new StringBuilder();
|
||||
|
||||
s.Append(sender.Name);
|
||||
|
||||
var worldName = Util.WorldName(sender.Server);
|
||||
if (worldName != null) {
|
||||
s.Append(" (");
|
||||
s.Append(worldName);
|
||||
s.Append(")");
|
||||
}
|
||||
|
||||
return s.ToString();
|
||||
}
|
||||
|
||||
public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
<Window x:Class="XIVChat_Desktop.Export"
|
||||
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"
|
||||
xmlns:cc="clr-namespace:XIVChat_Desktop.Controls"
|
||||
xmlns:xiv="clr-namespace:XIVChatCommon;assembly=XIVChatCommon"
|
||||
xmlns:ui="http://schemas.modernwpf.com/2019"
|
||||
xmlns:sdl="http://schemas.sdl.com/xaml"
|
||||
ui:WindowHelper.UseModernWindowStyle="True"
|
||||
mc:Ignorable="d"
|
||||
Title="Export"
|
||||
x:Name="Main"
|
||||
d:DataContext="{d:DesignInstance local:Export}"
|
||||
Height="600"
|
||||
Width="800">
|
||||
<Grid Margin="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ItemsControl Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Padding="4"
|
||||
VirtualizingPanel.ScrollUnit="Pixel"
|
||||
ItemsSource="{Binding ExportTab}">
|
||||
<ItemsControl.Template>
|
||||
<ControlTemplate TargetType="ItemsControl">
|
||||
<ScrollViewer x:Name="scroller"
|
||||
CanContentScroll="True"
|
||||
Background="#333">
|
||||
<ItemsPresenter Margin="0,0,16,0" />
|
||||
</ScrollViewer>
|
||||
</ControlTemplate>
|
||||
</ItemsControl.Template>
|
||||
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel IsVirtualizing="True"
|
||||
VirtualizationMode="Recycling"
|
||||
VerticalAlignment="Bottom" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<cc:MessageTextBlock FontFamily="Global User Interface, /fonts/#XIV AXIS Std ATK"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="{Binding App.Config.FontSize, ElementName=Main, UpdateSourceTrigger=PropertyChanged}"
|
||||
Tab="{Binding ExportTab, ElementName=Main}"
|
||||
Message="{Binding .}"
|
||||
ShowTimestamps="{Binding ShowTimestamps, ElementName=Main}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Grid Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Margin="8, 0, 0, 0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ScrollViewer Grid.Row="0"
|
||||
Margin="8,0,0, 8">
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<CheckBox Content="Process messages as Markdown"
|
||||
Checked="Markdown_Checked"
|
||||
Unchecked="Markdown_Unchecked"
|
||||
IsChecked="{Binding ExportTab.ProcessMarkdown}" />
|
||||
|
||||
<CheckBox Content="Show timestamps"
|
||||
IsChecked="{Binding ShowTimestamps}" />
|
||||
|
||||
<DatePicker x:Name="AfterDatePicker"
|
||||
ui:ControlHelper.Header="On or after"
|
||||
SelectedDateChanged="AfterDatePicker_OnSelectedDateChanged" />
|
||||
<ui:SimpleTimePicker x:Name="AfterTimePicker"
|
||||
SelectedDateTimeChanged="AfterTimePicker_OnSelectedDateTimeChanged" />
|
||||
<Button Margin="0,4,0,8"
|
||||
Content="Clear"
|
||||
Click="AfterClear_Click" />
|
||||
|
||||
<DatePicker x:Name="BeforeDatePicker"
|
||||
ui:ControlHelper.Header="On or before"
|
||||
SelectedDateChanged="BeforeDatePicker_OnSelectedDateChanged" />
|
||||
<ui:SimpleTimePicker x:Name="BeforeTimePicker"
|
||||
SelectedDateTimeChanged="BeforeTimePicker_OnSelectedDateTimeChanged" />
|
||||
<Button Margin="0,4,0,8"
|
||||
Content="Clear"
|
||||
Click="BeforeClear_Click" />
|
||||
|
||||
<Button Margin="0,8,0,8"
|
||||
Content="Configure senders">
|
||||
<ui:FlyoutService.Flyout>
|
||||
<ui:Flyout>
|
||||
<Grid Height="300">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ListBox MinWidth="100"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
x:Name="SendersFilterSource"
|
||||
ItemsSource="{Binding Senders, Mode=OneWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Margin="0,0,16,0"
|
||||
Text="{Binding ., Converter={StaticResource SenderPlayerConverter}}" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<StackPanel Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Margin="8,0,8,0">
|
||||
<Button Click="RightArrow_Click"
|
||||
Content="→" />
|
||||
<Button Click="LeftArrow_Click"
|
||||
Margin="0,4,0,0"
|
||||
Content="←" />
|
||||
</StackPanel>
|
||||
<ListBox Grid.Column="2"
|
||||
MinWidth="100"
|
||||
ScrollViewer.VerticalScrollBarVisibility="Visible"
|
||||
x:Name="SenderFiltersDest"
|
||||
ItemsSource="{Binding ExportTab.Filter.Senders, Mode=OneWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Margin="0,0,16,0"
|
||||
Text="{Binding ., Converter={StaticResource SenderPlayerConverter}}" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</ui:Flyout>
|
||||
</ui:FlyoutService.Flyout>
|
||||
</Button>
|
||||
|
||||
<TabControl x:Name="Tabs" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Button Grid.Row="1"
|
||||
Content="Save"
|
||||
Click="Save_Click"
|
||||
HorizontalAlignment="Right" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
|
@ -0,0 +1,395 @@
|
|||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Threading;
|
||||
using Microsoft.Win32;
|
||||
using XIVChatCommon;
|
||||
|
||||
namespace XIVChat_Desktop {
|
||||
public partial class Export : INotifyPropertyChanged {
|
||||
public App App => (App)Application.Current;
|
||||
|
||||
public Tab ExportTab { get; }
|
||||
|
||||
public ExportFilter Filter => (ExportFilter)this.ExportTab.Filter;
|
||||
|
||||
public ObservableCollection<ServerMessage.SenderPlayer> Senders { get; } = new ObservableCollection<ServerMessage.SenderPlayer>();
|
||||
|
||||
private bool showTimestamps = true;
|
||||
|
||||
public bool ShowTimestamps {
|
||||
get => this.showTimestamps;
|
||||
set {
|
||||
this.showTimestamps = value;
|
||||
this.OnPropertyChanged(nameof(this.ShowTimestamps));
|
||||
}
|
||||
}
|
||||
|
||||
public Export(Window owner) {
|
||||
this.Owner = owner;
|
||||
|
||||
this.ExportTab = new Tab("Export") {
|
||||
Filter = new ExportFilter {
|
||||
Types = Tab.GeneralFilter().Types,
|
||||
},
|
||||
};
|
||||
|
||||
this.Repopulate();
|
||||
|
||||
this.InitializeComponent();
|
||||
this.DataContext = this;
|
||||
|
||||
this.SetUpFilters();
|
||||
}
|
||||
|
||||
private void SetUpFilters() {
|
||||
foreach (var category in (FilterCategory[])Enum.GetValues(typeof(FilterCategory))) {
|
||||
var tabContent = new WrapPanel {
|
||||
Margin = new Thickness(8),
|
||||
Orientation = Orientation.Vertical,
|
||||
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);
|
||||
|
||||
var doingMultiple = false;
|
||||
|
||||
void SetAllChecked(bool isChecked) {
|
||||
doingMultiple = true;
|
||||
|
||||
foreach (var child in tabContent.Children) {
|
||||
if (!(child is CheckBox)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var check = (CheckBox)child;
|
||||
check.IsChecked = isChecked;
|
||||
}
|
||||
|
||||
this.Repopulate();
|
||||
|
||||
doingMultiple = false;
|
||||
}
|
||||
|
||||
buttonsPanel.Children.Add(selectButton);
|
||||
buttonsPanel.Children.Add(deselectButton);
|
||||
|
||||
tabContent.Children.Add(buttonsPanel);
|
||||
tabContent.Children.Add(new Separator());
|
||||
|
||||
foreach (var type in category.Types()) {
|
||||
var check = new CheckBox {
|
||||
Content = type.Name(),
|
||||
IsChecked = this.ExportTab.Filter.Types.Contains(type),
|
||||
};
|
||||
|
||||
check.Checked += (sender, e) => {
|
||||
this.ExportTab.Filter.Types.Add(type);
|
||||
|
||||
if (!doingMultiple) {
|
||||
this.Repopulate();
|
||||
}
|
||||
};
|
||||
check.Unchecked += (sender, e) => {
|
||||
this.ExportTab.Filter.Types.Remove(type);
|
||||
|
||||
if (!doingMultiple) {
|
||||
this.Repopulate();
|
||||
}
|
||||
};
|
||||
|
||||
tabContent.Children.Add(check);
|
||||
}
|
||||
|
||||
var tabItem = new TabItem {
|
||||
Header = new TextBlock(new Run(category.Name())),
|
||||
Content = tabContent,
|
||||
};
|
||||
|
||||
this.Tabs.Items.Add(tabItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void Repopulate() {
|
||||
this.ExportTab.RepopulateMessages(this.App.Window.Messages);
|
||||
this.SetUpSenders();
|
||||
}
|
||||
|
||||
private void SetUpSenders() {
|
||||
// var senders = this.ExportTab.Messages
|
||||
var senders = this.App.Window.Messages
|
||||
.Where(msg => ((ExportFilter)this.ExportTab.Filter).AllowedMinusSenders(msg))
|
||||
.Select(msg => msg.GetSenderPlayer())
|
||||
.Where(sender => sender != null)
|
||||
.ToImmutableSortedSet();
|
||||
|
||||
this.Senders.Clear();
|
||||
|
||||
foreach (var sender in senders) {
|
||||
this.Senders.Add(sender!);
|
||||
}
|
||||
}
|
||||
|
||||
public class ExportFilter : Filter {
|
||||
public ObservableCollection<ServerMessage.SenderPlayer> Senders { get; } = new ObservableCollection<ServerMessage.SenderPlayer>();
|
||||
|
||||
public DateTime? Before { get; set; }
|
||||
public DateTime? After { get; set; }
|
||||
|
||||
public void AddSender(ServerMessage.SenderPlayer sender) {
|
||||
if (this.Senders.Contains(sender)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.Senders.Add(sender);
|
||||
}
|
||||
|
||||
public bool AllowedMinusSenders(ServerMessage message) {
|
||||
if (!base.Allowed(message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.Before != null && message.Timestamp > this.Before) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.After != null && message.Timestamp < this.After) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool Allowed(ServerMessage message) {
|
||||
if (!this.AllowedMinusSenders(message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check sender if any senders are selected
|
||||
var sender = message.GetSenderPlayer();
|
||||
if (this.Senders.Count != 0 && sender != null && !this.Senders.Contains(sender)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// our stuff
|
||||
return true;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected void OnPropertyChanged1([CallerMemberName] string? propertyName = null) {
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
private void Markdown_Checked(object sender, RoutedEventArgs e) => this.SetMarkdownProcessing(true);
|
||||
|
||||
private void Markdown_Unchecked(object sender, RoutedEventArgs e) => this.SetMarkdownProcessing(false);
|
||||
|
||||
private void SetMarkdownProcessing(bool on) {
|
||||
this.ExportTab.ProcessMarkdown = on;
|
||||
this.Repopulate();
|
||||
}
|
||||
|
||||
private void RightArrow_Click(object sender, RoutedEventArgs e) {
|
||||
var idx = this.SendersFilterSource.SelectedIndex;
|
||||
if (idx == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var player = this.Senders[idx];
|
||||
|
||||
var filter = (ExportFilter)this.ExportTab.Filter;
|
||||
filter.AddSender(player);
|
||||
|
||||
this.Repopulate();
|
||||
}
|
||||
|
||||
private void LeftArrow_Click(object sender, RoutedEventArgs e) {
|
||||
var idx = this.SenderFiltersDest.SelectedIndex;
|
||||
if (idx == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var filter = (ExportFilter)this.ExportTab.Filter;
|
||||
|
||||
var player = filter.Senders.ElementAt(idx);
|
||||
|
||||
filter.Senders.Remove(player);
|
||||
|
||||
this.Repopulate();
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private async void Save_Click(object sender, RoutedEventArgs e) {
|
||||
// create a new flowdocument for saving
|
||||
var flow = new FlowDocument();
|
||||
|
||||
// turn every message in the tab into a paragraph in the flowdocument
|
||||
foreach (var message in this.ExportTab.Messages) {
|
||||
this.Dispatch(DispatcherPriority.Background, () => {
|
||||
var paragraph = new Paragraph();
|
||||
// this has to be done on the main thread
|
||||
var inlines = MessageFormatter.ChunksToTextBlock(this.App.Config.FontSize, message, this.ExportTab.ProcessMarkdown, this.ShowTimestamps);
|
||||
paragraph.Inlines.AddRange(inlines);
|
||||
flow.Blocks.Add(paragraph);
|
||||
});
|
||||
}
|
||||
|
||||
// ask the user where to save
|
||||
var saveDialog = new SaveFileDialog {
|
||||
Filter = $"{DataFormats.Text} (*.txt)|*.txt|{DataFormats.Rtf} (*.rtf)|*.rtf",
|
||||
};
|
||||
|
||||
if (saveDialog.ShowDialog(this) != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
var ext = saveDialog.FileName.Split('.').Last().ToLowerInvariant();
|
||||
string dataFormat = ext switch {
|
||||
"rtf" => DataFormats.Rtf,
|
||||
_ => DataFormats.Text,
|
||||
};
|
||||
// save the data into memory (this apparently has to happen on the main thread or we'd save directly into a
|
||||
// file)
|
||||
await using var memoryStream = new MemoryStream();
|
||||
new TextRange(flow.ContentStart, flow.ContentEnd).Save(memoryStream, dataFormat);
|
||||
|
||||
// write the saved data to a file on another thread
|
||||
await Task.Run(async () => {
|
||||
await using var stream = new FileStream(saveDialog.FileName, FileMode.Create);
|
||||
memoryStream.Position = 0;
|
||||
await memoryStream.CopyToAsync(stream);
|
||||
});
|
||||
|
||||
// show completion box
|
||||
MessageBox.Show("Exported successfully.");
|
||||
}
|
||||
|
||||
private bool ignoreDateChanges;
|
||||
|
||||
private void AfterDatePicker_OnSelectedDateChanged(object? sender, SelectionChangedEventArgs e) {
|
||||
if (this.ignoreDateChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
var datePicker = (DatePicker)sender!;
|
||||
this.Filter.After = UpdateDate(this.Filter.After?.ToLocalTime(), datePicker.SelectedDate)?.ToUniversalTime();
|
||||
|
||||
this.ignoreDateChanges = true;
|
||||
this.AfterTimePicker.SelectedDateTime = this.Filter.After?.ToLocalTime();
|
||||
this.ignoreDateChanges = false;
|
||||
|
||||
this.Repopulate();
|
||||
}
|
||||
|
||||
private void AfterTimePicker_OnSelectedDateTimeChanged(object sender, RoutedPropertyChangedEventArgs<DateTime?> e) {
|
||||
if (this.ignoreDateChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.Filter.After = UpdateTime(this.Filter.After?.ToLocalTime(), e.NewValue)?.ToUniversalTime();
|
||||
|
||||
this.AfterDatePicker.SelectedDate = this.Filter.After?.ToLocalTime();
|
||||
}
|
||||
|
||||
private void BeforeDatePicker_OnSelectedDateChanged(object? sender, SelectionChangedEventArgs e) {
|
||||
if (this.ignoreDateChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
var datePicker = (DatePicker)sender!;
|
||||
this.Filter.Before = UpdateDate(this.Filter.Before?.ToLocalTime(), datePicker.SelectedDate)?.ToUniversalTime();
|
||||
|
||||
this.ignoreDateChanges = true;
|
||||
this.BeforeTimePicker.SelectedDateTime = this.Filter.Before?.ToLocalTime();
|
||||
this.ignoreDateChanges = false;
|
||||
|
||||
this.Repopulate();
|
||||
}
|
||||
|
||||
private void BeforeTimePicker_OnSelectedDateTimeChanged(object sender, RoutedPropertyChangedEventArgs<DateTime?> e) {
|
||||
if (this.ignoreDateChanges) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.Filter.Before = UpdateTime(this.Filter.Before?.ToLocalTime(), e.NewValue)?.ToUniversalTime();
|
||||
|
||||
this.BeforeDatePicker.SelectedDate = this.Filter.Before?.ToLocalTime();
|
||||
}
|
||||
|
||||
private static DateTime? UpdateTime(DateTime? dest, DateTime? source) {
|
||||
switch (dest) {
|
||||
case null when source == null:
|
||||
return null;
|
||||
case null:
|
||||
return source;
|
||||
}
|
||||
|
||||
if (source == null) {
|
||||
return dest;
|
||||
}
|
||||
|
||||
var newValue = source.Value;
|
||||
|
||||
return new DateTime(dest.Value.Year, dest.Value.Month, dest.Value.Day, newValue.Hour, newValue.Minute, newValue.Second);
|
||||
}
|
||||
|
||||
private static DateTime? UpdateDate(DateTime? dest, DateTime? source) {
|
||||
switch (dest) {
|
||||
case null when source == null:
|
||||
return null;
|
||||
case null:
|
||||
return source;
|
||||
}
|
||||
|
||||
if (source == null) {
|
||||
return dest;
|
||||
}
|
||||
|
||||
var newValue = source.Value;
|
||||
|
||||
return new DateTime(newValue.Year, newValue.Month, newValue.Day, dest.Value.Hour, dest.Value.Minute, dest.Value.Second);
|
||||
}
|
||||
|
||||
private void BeforeClear_Click(object sender, RoutedEventArgs e) {
|
||||
this.ignoreDateChanges = true;
|
||||
this.BeforeDatePicker.SelectedDate = null;
|
||||
this.ignoreDateChanges = false;
|
||||
this.BeforeTimePicker.SelectedDateTime = null;
|
||||
}
|
||||
|
||||
private void AfterClear_Click(object sender, RoutedEventArgs e) {
|
||||
this.ignoreDateChanges = true;
|
||||
this.AfterDatePicker.SelectedDate = null;
|
||||
this.ignoreDateChanges = false;
|
||||
this.AfterTimePicker.SelectedDateTime = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,6 +31,9 @@
|
|||
<MenuItem Header="Scan"
|
||||
Click="Scan_Click" />
|
||||
<Separator />
|
||||
<MenuItem Header="Export"
|
||||
Click="Export_Click" />
|
||||
<Separator />
|
||||
<MenuItem Header="Configuration"
|
||||
Click="Configuration_Click" />
|
||||
</MenuItem>
|
||||
|
|
|
@ -171,5 +171,9 @@ namespace XIVChat_Desktop {
|
|||
internal virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private void Export_Click(object sender, RoutedEventArgs e) {
|
||||
new Export(this).Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<Window x:Class="XIVChat_Desktop.FiltersSelection"
|
||||
<Window x:Class="XIVChat_Desktop.ManageTab"
|
||||
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"
|
||||
|
@ -11,7 +11,7 @@
|
|||
Title="Manage tab"
|
||||
Height="450"
|
||||
Width="400"
|
||||
d:DataContext="{d:DesignInstance local:FiltersSelection}">
|
||||
d:DataContext="{d:DesignInstance local:ManageTab}">
|
||||
<Grid Margin="8">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace XIVChat_Desktop {
|
|||
/// <summary>
|
||||
/// Interaction logic for FiltersSelection.xaml
|
||||
/// </summary>
|
||||
public partial class FiltersSelection {
|
||||
public partial class ManageTab {
|
||||
public App App => (App)Application.Current;
|
||||
|
||||
public Tab Tab { get; }
|
||||
|
@ -16,7 +16,7 @@ namespace XIVChat_Desktop {
|
|||
private readonly bool isNewTab;
|
||||
private readonly IImmutableSet<FilterType> oldFilters;
|
||||
|
||||
public FiltersSelection(Window owner, Tab? tab) {
|
||||
public ManageTab(Window owner, Tab? tab) {
|
||||
this.Owner = owner;
|
||||
this.isNewTab = tab == null;
|
||||
this.Tab = tab ?? new Tab("") {
|
||||
|
@ -100,7 +100,7 @@ namespace XIVChat_Desktop {
|
|||
MessageBox.Show("Tab must have a name.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (this.isNewTab) {
|
||||
this.App.Config.Tabs.Add(this.Tab);
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace XIVChat_Desktop {
|
|||
}
|
||||
|
||||
private void AddTab_Click(object sender, RoutedEventArgs e) {
|
||||
new FiltersSelection(this, null).ShowDialog();
|
||||
new ManageTab(this, null).ShowDialog();
|
||||
}
|
||||
|
||||
private void EditTab_Click(object sender, RoutedEventArgs e) {
|
||||
|
@ -31,7 +31,7 @@ namespace XIVChat_Desktop {
|
|||
if (tab == null) {
|
||||
return;
|
||||
}
|
||||
new FiltersSelection(this, tab).ShowDialog();
|
||||
new ManageTab(this, tab).ShowDialog();
|
||||
}
|
||||
|
||||
private void DeleteTab_Click(object sender, RoutedEventArgs e) {
|
||||
|
@ -53,7 +53,7 @@ namespace XIVChat_Desktop {
|
|||
|
||||
var tab = item as Tab;
|
||||
|
||||
new FiltersSelection(this, tab).ShowDialog();
|
||||
new ManageTab(this, tab).ShowDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,13 +45,15 @@ namespace XIVChat_Desktop {
|
|||
// }
|
||||
// }
|
||||
|
||||
public static IEnumerable<Inline> ChunksToTextBlock(double lineHeight, ServerMessage message, Tab tab) {
|
||||
public static IEnumerable<Inline> ChunksToTextBlock(double lineHeight, ServerMessage message, bool processMarkdown, bool showTimestamp) {
|
||||
var elements = new List<Inline>();
|
||||
|
||||
var timestampString = message.Timestamp.ToLocalTime().ToString("t", CultureInfo.CurrentUICulture);
|
||||
elements.Add(new Run($"[{timestampString}]") {
|
||||
Foreground = new SolidColorBrush(Colors.White),
|
||||
});
|
||||
if (showTimestamp) {
|
||||
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) {
|
||||
|
@ -66,7 +68,7 @@ namespace XIVChat_Desktop {
|
|||
var brush = new SolidColorBrush(Color.FromArgb(a, r, g, b));
|
||||
var style = textChunk.Italic ? FontStyles.Italic : FontStyles.Normal;
|
||||
|
||||
if (tab.ProcessMarkdown) {
|
||||
if (processMarkdown) {
|
||||
var inlines = Markdown.MarkdownToInlines(textChunk.Content);
|
||||
|
||||
foreach (var inline in inlines) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Threading;
|
||||
using Windows.UI.Xaml.Data;
|
||||
|
||||
namespace XIVChat_Desktop {
|
||||
public static class Util {
|
||||
|
@ -65,5 +66,613 @@ namespace XIVChat_Desktop {
|
|||
// A whitespace character is a space(U + 0020), tab(U + 0009), newline(U + 000A), line tabulation (U + 000B), form feed (U + 000C), or carriage return (U + 000D).
|
||||
return c <= ' ' && (c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r');
|
||||
}
|
||||
|
||||
public static string? WorldName(ushort id) {
|
||||
return id switch {
|
||||
1 => "reserved1",
|
||||
2 => "c-contents",
|
||||
3 => "c-whiteae",
|
||||
4 => "c-baudinii",
|
||||
5 => "c-contents2",
|
||||
6 => "c-funereus",
|
||||
16 => "konconv",
|
||||
23 => "Asura",
|
||||
24 => "Belias",
|
||||
25 => "Chaos",
|
||||
26 => "Hecatoncheir",
|
||||
27 => "Moomba",
|
||||
28 => "Pandaemonium",
|
||||
29 => "Shinryu",
|
||||
30 => "Unicorn",
|
||||
31 => "Yojimbo",
|
||||
32 => "Zeromus",
|
||||
33 => "Twintania",
|
||||
34 => "Brynhildr",
|
||||
35 => "Famfrit",
|
||||
36 => "Lich",
|
||||
37 => "Mateus",
|
||||
38 => "Shemhazai",
|
||||
39 => "Omega",
|
||||
40 => "Jenova",
|
||||
41 => "Zalera",
|
||||
42 => "Zodiark",
|
||||
43 => "Alexander",
|
||||
44 => "Anima",
|
||||
45 => "Carbuncle",
|
||||
46 => "Fenrir",
|
||||
47 => "Hades",
|
||||
48 => "Ixion",
|
||||
49 => "Kujata",
|
||||
50 => "Typhon",
|
||||
51 => "Ultima",
|
||||
52 => "Valefor",
|
||||
53 => "Exodus",
|
||||
54 => "Faerie",
|
||||
55 => "Lamia",
|
||||
56 => "Phoenix",
|
||||
57 => "Siren",
|
||||
58 => "Garuda",
|
||||
59 => "Ifrit",
|
||||
60 => "Ramuh",
|
||||
61 => "Titan",
|
||||
62 => "Diabolos",
|
||||
63 => "Gilgamesh",
|
||||
64 => "Leviathan",
|
||||
65 => "Midgardsormr",
|
||||
66 => "Odin",
|
||||
67 => "Shiva",
|
||||
68 => "Atomos",
|
||||
69 => "Bahamut",
|
||||
70 => "Chocobo",
|
||||
71 => "Moogle",
|
||||
72 => "Tonberry",
|
||||
73 => "Adamantoise",
|
||||
74 => "Coeurl",
|
||||
75 => "Malboro",
|
||||
76 => "Tiamat",
|
||||
77 => "Ultros",
|
||||
78 => "Behemoth",
|
||||
79 => "Cactuar",
|
||||
80 => "Cerberus",
|
||||
81 => "Goblin",
|
||||
82 => "Mandragora",
|
||||
83 => "Louisoix",
|
||||
84 => "Syldra",
|
||||
85 => "Spriggan",
|
||||
90 => "Aegis",
|
||||
91 => "Balmung",
|
||||
92 => "Durandal",
|
||||
93 => "Excalibur",
|
||||
94 => "Gungnir",
|
||||
95 => "Hyperion",
|
||||
96 => "Masamune",
|
||||
97 => "Ragnarok",
|
||||
98 => "Ridill",
|
||||
99 => "Sargatanas",
|
||||
100 => "dev_test",
|
||||
101 => "zone_test",
|
||||
102 => "trs_test",
|
||||
103 => "contents_test",
|
||||
110 => "b-tirica",
|
||||
111 => "b-contents",
|
||||
112 => "b-chiriri",
|
||||
113 => "b-contents2",
|
||||
114 => "b-jugularis",
|
||||
115 => "e-regia",
|
||||
116 => "e-pialii",
|
||||
117 => "e-contents",
|
||||
118 => "e-contents2",
|
||||
119 => "e-coloria",
|
||||
120 => "e-gouldiae",
|
||||
121 => "e-contents3",
|
||||
123 => "e-trichroa",
|
||||
124 => "e-hyperion",
|
||||
126 => "e-ragnarok",
|
||||
127 => "e-ridill",
|
||||
128 => "e-sargatanas",
|
||||
129 => "a-militaris",
|
||||
130 => "a-aegis",
|
||||
131 => "a-balmung",
|
||||
132 => "a-durandal",
|
||||
133 => "a-excalibur",
|
||||
134 => "a-gungnir",
|
||||
135 => "a-hyperion",
|
||||
136 => "a-masamune",
|
||||
137 => "a-ragnarok",
|
||||
138 => "a-ridill",
|
||||
139 => "a-sargatanas",
|
||||
140 => "a-contents",
|
||||
141 => "b-contents3",
|
||||
142 => "b-cyanoptera",
|
||||
144 => "a-contentsx",
|
||||
145 => "a-contents2",
|
||||
156 => "z-japonicus",
|
||||
157 => "z-dauma",
|
||||
158 => "z-contents",
|
||||
159 => "z-contents2",
|
||||
160 => "s-bella",
|
||||
161 => "Chocobo",
|
||||
162 => "s-contents",
|
||||
163 => "chs-contents",
|
||||
164 => "s-contents2",
|
||||
165 => "s-guttata",
|
||||
166 => "Moogle",
|
||||
167 => "chs-contents2",
|
||||
168 => "Namazu",
|
||||
169 => "chs-contents3",
|
||||
170 => "s-poliogenys",
|
||||
171 => "s-haematina",
|
||||
172 => "s-contents3",
|
||||
173 => "s-contents4",
|
||||
174 => "s-public1",
|
||||
175 => "s-public2",
|
||||
176 => "s-public3",
|
||||
177 => "s-public4",
|
||||
178 => "s-public5",
|
||||
179 => "s-public6",
|
||||
180 => "s-public7",
|
||||
181 => "s-public8",
|
||||
182 => "s-macropus",
|
||||
183 => "s-contents7",
|
||||
184 => "s-latrans",
|
||||
185 => "s-contents8",
|
||||
186 => "s-cucullatus",
|
||||
187 => "s-bicolor",
|
||||
188 => "s-contents5",
|
||||
189 => "s-contents6",
|
||||
201 => "Nacontents01",
|
||||
202 => "Nacontents02",
|
||||
203 => "Nacontents03",
|
||||
204 => "Nacontents04",
|
||||
205 => "Nacontents05",
|
||||
206 => "Nacontents06",
|
||||
207 => "Nacontents07",
|
||||
208 => "Nacontents08",
|
||||
209 => "Nacontents09",
|
||||
210 => "Nacontents10",
|
||||
211 => "Nacontents11",
|
||||
212 => "Nacontents12",
|
||||
213 => "Nacontents13",
|
||||
214 => "Nacontents14",
|
||||
215 => "Nacontents15",
|
||||
216 => "Nacontents16",
|
||||
217 => "Nacontents17",
|
||||
218 => "Nacontents18",
|
||||
219 => "Nacontents19",
|
||||
220 => "Nacontents20",
|
||||
221 => "Nacontents21",
|
||||
222 => "Nacontents22",
|
||||
223 => "Nacontents23",
|
||||
224 => "Nacontents24",
|
||||
225 => "Jpcontents01",
|
||||
226 => "Jpcontents02",
|
||||
227 => "Jpcontents03",
|
||||
228 => "Jpcontents04",
|
||||
229 => "Jpcontents05",
|
||||
230 => "Jpcontents06",
|
||||
231 => "Jpcontents07",
|
||||
232 => "Jpcontents08",
|
||||
233 => "Jpcontents09",
|
||||
234 => "Jpcontents10",
|
||||
235 => "Jpcontents11",
|
||||
236 => "Jpcontents12",
|
||||
237 => "Jpcontents13",
|
||||
238 => "Jpcontents14",
|
||||
239 => "Jpcontents15",
|
||||
240 => "Jpcontents16",
|
||||
241 => "Jpcontents17",
|
||||
242 => "Jpcontents18",
|
||||
243 => "Jpcontents19",
|
||||
244 => "Jpcontents20",
|
||||
245 => "Jpcontents21",
|
||||
246 => "Jpcontents22",
|
||||
247 => "Jpcontents23",
|
||||
248 => "Jpcontents24",
|
||||
249 => "Jpcontents25",
|
||||
250 => "Jpcontents26",
|
||||
251 => "Jpcontents27",
|
||||
252 => "Jpcontents28",
|
||||
253 => "Jpcontents29",
|
||||
254 => "Jpcontents30",
|
||||
255 => "Nacontents25",
|
||||
256 => "Nacontents26",
|
||||
257 => "Nacontents27",
|
||||
258 => "Nacontents28",
|
||||
259 => "Nacontents29",
|
||||
260 => "Nacontents30",
|
||||
261 => "Nacontents31",
|
||||
262 => "Nacontents32",
|
||||
263 => "Nacontents33",
|
||||
264 => "Nacontents34",
|
||||
265 => "Jpcontents31",
|
||||
266 => "Jpcontents32",
|
||||
267 => "Jpcontents33",
|
||||
268 => "Jpcontents34",
|
||||
269 => "Jpcontents35",
|
||||
270 => "Jpcontents36",
|
||||
271 => "Nacontents36",
|
||||
272 => "Nacontents37",
|
||||
273 => "Nacontents38",
|
||||
274 => "Nacontents39",
|
||||
275 => "Nacontents40",
|
||||
276 => "Nacontents41",
|
||||
277 => "Nacontents42",
|
||||
278 => "Nacontents43",
|
||||
279 => "Nacontents44",
|
||||
280 => "Nacontents45",
|
||||
576 => "h-collaris",
|
||||
577 => "h-metallica",
|
||||
578 => "h-contents",
|
||||
579 => "h-contents2",
|
||||
580 => "h-cauta",
|
||||
581 => "h-contents3",
|
||||
582 => "h-picatus",
|
||||
583 => "h-contents4",
|
||||
584 => "h-sordidus",
|
||||
585 => "h-canente",
|
||||
586 => "h-contents5",
|
||||
630 => "a-contents3",
|
||||
631 => "a-contents4",
|
||||
632 => "a-contents5",
|
||||
633 => "a-contents6",
|
||||
634 => "a-contents7",
|
||||
635 => "a-contents8",
|
||||
636 => "a-contents9",
|
||||
637 => "a-contents10",
|
||||
638 => "a-contents20",
|
||||
640 => "a-public1",
|
||||
641 => "a-public2",
|
||||
642 => "a-public3",
|
||||
643 => "a-public4",
|
||||
644 => "a-public5",
|
||||
645 => "a-public6",
|
||||
646 => "a-public7",
|
||||
647 => "a-public8",
|
||||
648 => "a-public9",
|
||||
649 => "a-public10",
|
||||
650 => "a-public11",
|
||||
651 => "a-public12",
|
||||
652 => "a-public13",
|
||||
653 => "a-public14",
|
||||
654 => "a-public15",
|
||||
655 => "a-public16",
|
||||
656 => "a-public17",
|
||||
657 => "a-public18",
|
||||
1024 => "test1",
|
||||
1025 => "test2",
|
||||
1040 => "DiPingGuan",
|
||||
1041 => "MiWuShiDi",
|
||||
1042 => "LaNuoXiYa",
|
||||
1043 => "ZiShuiZhanQiao",
|
||||
1044 => "HuanYingQunDao",
|
||||
1045 => "MoDuNa",
|
||||
1046 => "MoShouLingYu",
|
||||
1047 => "FengMoDong",
|
||||
1048 => "TaiyangHaiAn",
|
||||
1049 => "XiaoMaiJiuGang",
|
||||
1050 => "YinLeiHu",
|
||||
1051 => "ShengXiaTan",
|
||||
1052 => "PuTaoJiuGang",
|
||||
1053 => "HeiYiSenLin",
|
||||
1054 => "QingLinQuan",
|
||||
1055 => "JinChuiTaiDi",
|
||||
1056 => "HongChaChuan",
|
||||
1057 => "YiXiuJiaDe",
|
||||
1058 => "XueSongYuan",
|
||||
1059 => "YaoJingLing",
|
||||
1060 => "MengYaChi",
|
||||
1061 => "ZhiZhangXiaGu",
|
||||
1062 => "MiYueZhiTa",
|
||||
1063 => "MoLaBiWan",
|
||||
1064 => "YueYaWan",
|
||||
1065 => "YaoLanShu",
|
||||
1066 => "QiaoMingDong",
|
||||
1067 => "NiMuHe",
|
||||
1068 => "HuangJinGu",
|
||||
1069 => "BaiLingTi",
|
||||
1070 => "TianLangXingDengTa",
|
||||
1071 => "ZhuoReZouLang",
|
||||
1072 => "SiYuJuMu",
|
||||
1073 => "YueYingDao",
|
||||
1074 => "ShuiJingTa",
|
||||
1075 => "MengXiangGong",
|
||||
1076 => "BaiJinHuanXiang",
|
||||
1077 => "HeiJinHu",
|
||||
1078 => "LongXiPuBu",
|
||||
1079 => "ShiWeiTa",
|
||||
1080 => "TongLingTongShan",
|
||||
1081 => "ShenYiZhiDi",
|
||||
1082 => "ShiJiuDaQiao",
|
||||
1083 => "YongHengChuan",
|
||||
1084 => "HaiWuShaTan",
|
||||
1085 => "HeFengLiuDi",
|
||||
1086 => "ZeMeiErYaoSai",
|
||||
1087 => "JuShiQiu",
|
||||
1088 => "JianDouLingYu",
|
||||
1089 => "HeiChenYiZhan",
|
||||
1090 => "ShuiLianYan",
|
||||
1091 => "LingHangMingDeng",
|
||||
1092 => "HaiCiShiKu",
|
||||
1093 => "FeiCuiHu",
|
||||
1094 => "XiongXinGuangChang",
|
||||
1095 => "KuErZhaSi",
|
||||
1096 => "LiuShaMiGong",
|
||||
1097 => "FangXiangTang",
|
||||
1098 => "HuaMiZhanQiao",
|
||||
1099 => "LanWuYongQuan",
|
||||
1100 => "ShenLingShengYu",
|
||||
1101 => "BaiYunYa",
|
||||
1102 => "HaiJiangShuiQu",
|
||||
1103 => "YuanLingYouShu",
|
||||
1104 => "ShaZhongLuTing",
|
||||
1105 => "JieMeiQiu",
|
||||
1106 => "JingYuZhuangYuan",
|
||||
1107 => "ZuJiGu",
|
||||
1108 => "ShanHuTa",
|
||||
1109 => "HengDuanYa",
|
||||
1110 => "ShuiShangTingYuan",
|
||||
1111 => "WuXianHuiLang",
|
||||
1112 => "JinDingChi",
|
||||
1113 => "LvRenZhanQiao",
|
||||
1114 => "LongWenYan",
|
||||
1115 => "HaiManQiaoLang",
|
||||
1116 => "YuanQuanZhiTi",
|
||||
1117 => "MiShiTa",
|
||||
1118 => "RiShengMen",
|
||||
1119 => "XiFengJia",
|
||||
1120 => "ShenLiZhiMen",
|
||||
1121 => "FuXiaoZhiJian",
|
||||
1122 => "HaiLangDong",
|
||||
1123 => "XiangShuYuan",
|
||||
1124 => "MoNvKaFeiGuan",
|
||||
1125 => "JuLongShouYingDi",
|
||||
1126 => "DiYuHeGu",
|
||||
1127 => "FuRongYuanZhuo",
|
||||
1128 => "ShenWoJiao",
|
||||
1129 => "HuangJinGuangChang",
|
||||
1130 => "WanZhiMuChang",
|
||||
1131 => "QiMuQuan",
|
||||
1132 => "JingChiZhanQiao",
|
||||
1133 => "BaiOuTA",
|
||||
1134 => "XiaoShiWangDu",
|
||||
1135 => "KuaTianQiao",
|
||||
1136 => "ShengRenLei",
|
||||
1137 => "JianFeng",
|
||||
1138 => "HouWeiTa",
|
||||
1139 => "BaiYinJiShi",
|
||||
1140 => "LaiShengHuiLang",
|
||||
1141 => "BaoFengLuMen",
|
||||
1142 => "YouLingHu",
|
||||
1143 => "ShiLvHu",
|
||||
1144 => "HuangHunWan",
|
||||
1145 => "XiaoALaMiGe",
|
||||
1146 => "FangLangShenLiTang",
|
||||
1147 => "JingJiSen",
|
||||
1148 => "LangYanQiu",
|
||||
1149 => "ShengRenLvDao",
|
||||
1150 => "BuHuiZhanQuan",
|
||||
1151 => "JiuTeng",
|
||||
1152 => "RongYaoXi",
|
||||
1153 => "JingShu",
|
||||
1154 => "YuMenYiXue",
|
||||
1155 => "YiWangLvZhou",
|
||||
1156 => "YanDiLing",
|
||||
1157 => "GeYongLieGu",
|
||||
1158 => "LiuShaWu",
|
||||
1159 => "BaJianShiQianTing",
|
||||
1160 => "Bahamute",
|
||||
1161 => "Zhushenhuanghun",
|
||||
1162 => "Wangzhezhijian",
|
||||
1163 => "Luxingniao",
|
||||
1164 => "Shenshengcaipansuo",
|
||||
1165 => "Bingtiangong",
|
||||
1166 => "Longchaoshendian",
|
||||
1167 => "HongYuHai",
|
||||
1168 => "HuangJinGang",
|
||||
1169 => "YanXia",
|
||||
1170 => "ChaoFengTing",
|
||||
1171 => "ShenQuanHen",
|
||||
1172 => "BaiYinXiang",
|
||||
1173 => "YuZhouHeYin",
|
||||
1174 => "WoXianXiRan",
|
||||
1175 => "ChenXiWangZuo",
|
||||
1176 => "MengYuBaoJing",
|
||||
1177 => "HaiMaoChaWu",
|
||||
1178 => "RouFengHaiWan",
|
||||
1179 => "HuPoYuan",
|
||||
1536 => "contentstest1",
|
||||
1537 => "contentstest2",
|
||||
1552 => "sdocontents1",
|
||||
1553 => "sdocontents2",
|
||||
1554 => "sdocontents3",
|
||||
1555 => "sdocontents4",
|
||||
1556 => "sdocontents5",
|
||||
1557 => "sdocontents6",
|
||||
1558 => "sdocontents7",
|
||||
1559 => "sdocontents8",
|
||||
1560 => "sdocontents9",
|
||||
1561 => "sdocontents10",
|
||||
1562 => "sdocontents11",
|
||||
1563 => "sdocontents12",
|
||||
1564 => "sdocontents13",
|
||||
1565 => "sdocontents14",
|
||||
1566 => "sdocontents15",
|
||||
1567 => "sdocontents16",
|
||||
1568 => "sdocontents17",
|
||||
1569 => "sdocontents18",
|
||||
1570 => "sdocontents19",
|
||||
1571 => "sdocontents20",
|
||||
1572 => "sdocontents21",
|
||||
1573 => "sdocontents22",
|
||||
1574 => "sdocontents23",
|
||||
1575 => "sdocontents24",
|
||||
1576 => "sdocontents25",
|
||||
1577 => "sdocontents26",
|
||||
1578 => "sdocontents27",
|
||||
1579 => "sdocontents28",
|
||||
1580 => "sdocontents29",
|
||||
1581 => "sdocontents30",
|
||||
1582 => "sdocontents31",
|
||||
1583 => "sdocontents32",
|
||||
1584 => "sdocontents33",
|
||||
1585 => "sdocontents34",
|
||||
1586 => "sdocontents35",
|
||||
1587 => "sdocontents36",
|
||||
1588 => "sdocontents37",
|
||||
1589 => "sdocontents38",
|
||||
1590 => "sdocontents39",
|
||||
1591 => "sdocontents40",
|
||||
1592 => "sdocontents41",
|
||||
1593 => "sdocontents42",
|
||||
1594 => "sdocontents43",
|
||||
1595 => "sdocontents44",
|
||||
1596 => "sdocontents45",
|
||||
1597 => "sdocontents46",
|
||||
1598 => "sdocontents47",
|
||||
1599 => "sdocontents48",
|
||||
1600 => "sdocontents49",
|
||||
1601 => "sdocontents50",
|
||||
1602 => "sdocontents51",
|
||||
1603 => "sdocontents52",
|
||||
1604 => "sdocontents53",
|
||||
1605 => "sdocontents54",
|
||||
1606 => "sdocontents55",
|
||||
1607 => "sdocontents56",
|
||||
1608 => "sdocontents57",
|
||||
1609 => "sdocontents58",
|
||||
1610 => "sdocontents59",
|
||||
1611 => "sdocontents60",
|
||||
1612 => "sdocontents61",
|
||||
1613 => "sdocontents62",
|
||||
1614 => "sdocontents63",
|
||||
1615 => "sdocontents64",
|
||||
1616 => "sdocontents65",
|
||||
1617 => "sdocontents66",
|
||||
1618 => "sdocontents67",
|
||||
1619 => "sdocontents68",
|
||||
1620 => "sdocontents69",
|
||||
1621 => "sdocontents70",
|
||||
1622 => "sdocontents71",
|
||||
1623 => "sdocontents72",
|
||||
1624 => "sdocontents73",
|
||||
1625 => "sdocontents74",
|
||||
1626 => "sdocontents75",
|
||||
1627 => "sdocontents76",
|
||||
1628 => "sdocontents77",
|
||||
1629 => "sdocontents78",
|
||||
1630 => "sdocontents79",
|
||||
1631 => "sdocontents80",
|
||||
1632 => "sdocontents81",
|
||||
1633 => "sdocontents82",
|
||||
1634 => "sdocontents83",
|
||||
1635 => "sdocontents84",
|
||||
1636 => "sdocontents85",
|
||||
1637 => "sdocontents86",
|
||||
1638 => "sdocontents87",
|
||||
1639 => "sdocontents88",
|
||||
1640 => "sdocontents89",
|
||||
1641 => "sdocontents90",
|
||||
1642 => "sdocontents91",
|
||||
1643 => "sdocontents92",
|
||||
1644 => "sdocontents93",
|
||||
1645 => "sdocontents94",
|
||||
1646 => "sdocontents95",
|
||||
1647 => "sdocontents96",
|
||||
1648 => "sdocontents97",
|
||||
1649 => "sdocontents98",
|
||||
1650 => "sdocontents99",
|
||||
1651 => "sdocontents100",
|
||||
1652 => "sdocontents101",
|
||||
1653 => "sdocontents102",
|
||||
1654 => "sdocontents103",
|
||||
1655 => "sdocontents104",
|
||||
1656 => "sdocontents105",
|
||||
1657 => "sdocontents106",
|
||||
1658 => "sdocontents107",
|
||||
1659 => "sdocontents108",
|
||||
1660 => "sdocontents109",
|
||||
1661 => "sdocontents110",
|
||||
1662 => "sdocontents111",
|
||||
1663 => "sdocontents112",
|
||||
1664 => "sdocontents113",
|
||||
1665 => "sdocontents114",
|
||||
1666 => "sdocontents115",
|
||||
1667 => "sdocontents116",
|
||||
1668 => "sdocontents117",
|
||||
1669 => "sdocontents118",
|
||||
1670 => "sdocontents119",
|
||||
1671 => "sdocontents120",
|
||||
2048 => "Unei",
|
||||
2049 => "Doga",
|
||||
2050 => "KrBahamut",
|
||||
2051 => "KrIfrit",
|
||||
2052 => "KrGaruda",
|
||||
2053 => "KrRamuh",
|
||||
2054 => "KrOdin",
|
||||
2055 => "KrUltima",
|
||||
2056 => "KrMandragora",
|
||||
2057 => "KrTonberry2",
|
||||
2058 => "KrExcalibur",
|
||||
2059 => "KrPhoenix",
|
||||
2060 => "KrAlexander",
|
||||
2061 => "KrTitan",
|
||||
2062 => "KrLeviathan",
|
||||
2063 => "KrShiva",
|
||||
2064 => "KrBehemoth",
|
||||
2065 => "KrGilgamesh",
|
||||
2066 => "KrSabotender",
|
||||
2067 => "KrUnicorn",
|
||||
2068 => "KrRagnarok",
|
||||
2069 => "KrRamia",
|
||||
2070 => "KrNewPublic1",
|
||||
2071 => "KrNewPublic2",
|
||||
2072 => "KrNewPublic3",
|
||||
2073 => "KrNewPublic4",
|
||||
2074 => "KrNewPublic5",
|
||||
2075 => "KrCarbuncle",
|
||||
2076 => "KrChocobo",
|
||||
2077 => "KrMoogle",
|
||||
2078 => "KrTonberry",
|
||||
2079 => "KrCaitsith",
|
||||
2560 => "koreacontents1",
|
||||
2561 => "koreacontents2",
|
||||
2562 => "krcontents1",
|
||||
2563 => "krcontents2",
|
||||
2564 => "krcontents3",
|
||||
2565 => "krcontents4",
|
||||
2566 => "krcontents5",
|
||||
2567 => "krcontents6",
|
||||
2568 => "krcontents7",
|
||||
2569 => "krcontents8",
|
||||
2570 => "krcontents9",
|
||||
2571 => "krcontents10",
|
||||
2572 => "krcontents11",
|
||||
2573 => "krcontents12",
|
||||
2574 => "krcontents13",
|
||||
2575 => "krcontents14",
|
||||
2576 => "krcontents15",
|
||||
2577 => "krcontents16",
|
||||
2578 => "krcontents17",
|
||||
2579 => "krcontents18",
|
||||
2580 => "krcontents19",
|
||||
2581 => "krcontents20",
|
||||
2582 => "krcontents21",
|
||||
2583 => "krcontents22",
|
||||
10000 => "crossworld_range",
|
||||
10001 => "crossworld",
|
||||
10002 => "crossworld",
|
||||
10003 => "crossworld",
|
||||
10004 => "crossworld",
|
||||
10005 => "crossworld",
|
||||
10006 => "crossworld",
|
||||
10007 => "crossworld",
|
||||
10008 => "crossworld",
|
||||
10009 => "crossworld",
|
||||
10010 => "crossworld",
|
||||
65534 => "outofrange1",
|
||||
65535 => "outofrange2",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ModernWpfUI" Version="0.9.2" />
|
||||
<PackageReference Include="ModernWpfUI.MahApps" Version="0.9.2" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="Sodium.Core" Version="1.2.3" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
using MessagePack.Formatters;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
// ReSharper disable UnusedAutoPropertyAccessor.Global
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
@ -54,6 +57,124 @@ namespace XIVChatCommon {
|
|||
protected override byte[] PayloadEncode() {
|
||||
return MessagePackSerializer.Serialize(this);
|
||||
}
|
||||
|
||||
public SenderPlayer? GetSenderPlayer() {
|
||||
using var stream = new MemoryStream(this.Sender);
|
||||
using var reader = new BinaryReader(stream);
|
||||
|
||||
var text = new List<byte>();
|
||||
|
||||
while (reader.BaseStream.Position < reader.BaseStream.Length) {
|
||||
var b = reader.ReadByte();
|
||||
|
||||
// read payloads
|
||||
if (b == 2) {
|
||||
var chunkType = reader.ReadByte();
|
||||
var chunkLen = XivString.GetInteger(reader);
|
||||
|
||||
// interactive
|
||||
if (chunkType == 0x27) {
|
||||
var subType = reader.ReadByte();
|
||||
// player name
|
||||
if (subType == 0x01) {
|
||||
// unk
|
||||
reader.ReadByte();
|
||||
|
||||
var serverId = (ushort)XivString.GetInteger(reader);
|
||||
|
||||
// unk
|
||||
reader.ReadBytes(2);
|
||||
|
||||
var nameLen = (int)XivString.GetInteger(reader);
|
||||
var playerName = Encoding.UTF8.GetString(reader.ReadBytes(nameLen));
|
||||
|
||||
return new SenderPlayer(playerName, serverId);
|
||||
}
|
||||
}
|
||||
|
||||
reader.ReadBytes((int)chunkLen);
|
||||
continue;
|
||||
}
|
||||
|
||||
// read text
|
||||
text.Add(b);
|
||||
}
|
||||
|
||||
if (text.Count == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = Encoding.UTF8.GetString(text.ToArray());
|
||||
|
||||
// remove the party position if present
|
||||
var chars = name.ToCharArray();
|
||||
if (chars.Length > 0 && PartyChars.Contains((chars[0]))) {
|
||||
name = name.Substring(1);
|
||||
}
|
||||
|
||||
return new SenderPlayer(name, 0);
|
||||
}
|
||||
|
||||
private static readonly char[] PartyChars = {
|
||||
'\ue090', '\ue091', '\ue092', '\ue093', '\ue094', '\ue095', '\ue096', '\ue097',
|
||||
};
|
||||
|
||||
public class SenderPlayer : IComparable<SenderPlayer>, IComparable {
|
||||
public string Name { get; }
|
||||
public ushort Server { get; }
|
||||
|
||||
public SenderPlayer(string name, ushort server) {
|
||||
this.Name = name;
|
||||
this.Server = server;
|
||||
}
|
||||
|
||||
protected bool Equals(SenderPlayer other) {
|
||||
return this.Name == other.Name && this.Server == other.Server;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) {
|
||||
if (ReferenceEquals(null, obj)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, obj)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return obj.GetType() == this.GetType() && this.Equals((SenderPlayer)obj);
|
||||
}
|
||||
|
||||
public override int GetHashCode() {
|
||||
unchecked {
|
||||
return (this.Name.GetHashCode() * 397) ^ (int)this.Server;
|
||||
}
|
||||
}
|
||||
|
||||
public int CompareTo(SenderPlayer? other) {
|
||||
if (ReferenceEquals(this, other)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(null, other)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
var nameComparison = string.Compare(this.Name, other.Name, StringComparison.Ordinal);
|
||||
return nameComparison != 0 ? nameComparison : this.Server.CompareTo(other.Server);
|
||||
}
|
||||
|
||||
public int CompareTo(object? obj) {
|
||||
if (ReferenceEquals(null, obj)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, obj)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return obj is SenderPlayer other ? this.CompareTo(other) : throw new ArgumentException($"Object must be of type {nameof(SenderPlayer)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Union(1, typeof(TextChunk))]
|
||||
|
@ -528,6 +649,7 @@ namespace XIVChatCommon {
|
|||
CustomEmote = 28,
|
||||
StandardEmote = 29,
|
||||
Yell = 30,
|
||||
|
||||
// 31 - also party?
|
||||
CrossParty = 32,
|
||||
PvpTeam = 36,
|
||||
|
@ -630,10 +752,12 @@ namespace XIVChatCommon {
|
|||
/// Sent in response to a client ping. Has no payload.
|
||||
/// </summary>
|
||||
Pong = 1,
|
||||
|
||||
/// <summary>
|
||||
/// A message was sent in game and is being relayed to the client.
|
||||
/// </summary>
|
||||
Message = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The server is shutting down. Clients should send no response and close their sockets. Has no payload.
|
||||
/// </summary>
|
||||
|
@ -747,6 +871,7 @@ namespace XIVChatCommon {
|
|||
CrossLinkshell6 = 14,
|
||||
CrossLinkshell7 = 15,
|
||||
CrossLinkshell8 = 16,
|
||||
|
||||
// 17 - unused?
|
||||
// 18 - unused?
|
||||
Linkshell1 = 19,
|
||||
|
@ -971,8 +1096,7 @@ namespace XIVChatCommon {
|
|||
}
|
||||
|
||||
public enum ClientPreference {
|
||||
[Preference(typeof(bool))]
|
||||
BacklogNewestMessagesFirst,
|
||||
[Preference(typeof(bool))] BacklogNewestMessagesFirst,
|
||||
}
|
||||
|
||||
public class PreferenceAttribute : Attribute {
|
||||
|
@ -1030,10 +1154,12 @@ namespace XIVChatCommon {
|
|||
/// The client is sending data to the server to keep the socket alive. Has no payload.
|
||||
/// </summary>
|
||||
Ping = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The client has a message to be sent in the game and is relaying it to the server.
|
||||
/// </summary>
|
||||
Message = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The client is shutting down. Clients should send this and close their socket for a clean shutdown.
|
||||
/// </summary>
|
||||
|
|
|
@ -121,7 +121,7 @@ namespace XIVChatCommon {
|
|||
Int32 = 0xFE,
|
||||
}
|
||||
|
||||
private static uint GetInteger(BinaryReader input) {
|
||||
public static uint GetInteger(BinaryReader input) {
|
||||
var t = input.ReadByte();
|
||||
var type = (IntegerType)t;
|
||||
return GetInteger(input, type);
|
||||
|
|
Loading…
Reference in New Issue