a bunch of stuff (#663)
Some checks are pending
FModel QA Builder / build (push) Waiting to run

Co-authored-by: Chompster86 <chompster86@gmail.com>
Co-authored-by: Asval <asval.contactme@gmail.com>
This commit is contained in:
Krowe Moh 2026-03-31 07:01:59 +11:00 committed by GitHub
parent 2212605825
commit 7e7d6d5bc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 239 additions and 27 deletions

@ -1 +1 @@
Subproject commit 1320fd09f6373c997758ae8160d6f8035c4c8b93
Subproject commit 8fc2c250ab0e581878813127610c28e49e947239

View File

@ -92,6 +92,12 @@ public partial class App
UserSettings.Default.AudioDirectory = Path.Combine(UserSettings.Default.OutputDirectory, "Exports");
}
if (!Directory.Exists(UserSettings.Default.CodeDirectory))
{
createMe = true;
UserSettings.Default.CodeDirectory = Path.Combine(UserSettings.Default.OutputDirectory, "Exports");
}
if (!Directory.Exists(UserSettings.Default.ModelDirectory))
{
createMe = true;

View File

@ -0,0 +1,47 @@
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.Objects.UObject;
using SkiaSharp;
namespace FModel.Creator.Bases.FN;
public class BaseAssembledMesh : UCreator
{
public BaseAssembledMesh(UObject uObject, EIconStyle style) : base(uObject, style)
{
}
public override void ParseForInfo()
{
if (Object.TryGetValue(out FInstancedStruct[] additionalData, "AdditionalData"))
{
foreach (var data in additionalData)
{
if (data.NonConstStruct?.TryGetValue(out FSoftObjectPath largePreview, "LargePreviewImage", "SmallPreviewImage") == true)
{
Preview = Utils.GetBitmap(largePreview);
}
}
}
}
public override SKBitmap[] Draw()
{
var ret = new SKBitmap(Width, Height, SKColorType.Rgba8888, SKAlphaType.Premul);
using var c = new SKCanvas(ret);
switch (Style)
{
case EIconStyle.NoBackground:
DrawPreview(c);
break;
default:
DrawBackground(c);
DrawPreview(c);
break;
}
return new[] { ret };
}
}

View File

@ -87,6 +87,7 @@ public class BaseIconStats : BaseIcon
weaponRowValue.TryGetValue(out float dmgPb, "DmgPB"); //Damage at point blank
weaponRowValue.TryGetValue(out float mdpc, "MaxDamagePerCartridge"); //Max damage a weapon can do in a single hit, usually used for shotguns
weaponRowValue.TryGetValue(out float dmgCritical, "DamageZone_Critical"); //Headshot multiplier
weaponRowValue.TryGetValue(out float envDmgPb, "EnvDmgPB"); //Structure damage at point blank
weaponRowValue.TryGetValue(out int clipSize, "ClipSize"); //Item magazine size
weaponRowValue.TryGetValue(out float firingRate, "FiringRate"); //Item firing rate, value is shots per second
weaponRowValue.TryGetValue(out float swingTime, "SwingTime"); //Item swing rate, value is swing per second
@ -115,6 +116,15 @@ public class BaseIconStats : BaseIcon
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "0DEF2455463B008C4499FEA03D149EDF", "Headshot Damage"), dmgPb * dmgCritical * multiplier, 160));
}
}
{
var envdmgmultiplier = bpc != 0f ? bpc : 1;
if (envDmgPb != 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "11AF67134E0F4E27E5E588806AB475BE", "Structure Damage"), envDmgPb * envdmgmultiplier, 160));
}
}
if (clipSize > 999f || clipSize == 0f)
{
_statistics.Add(new IconStat(Utils.GetLocalizedResource("", "068239DD4327B36124498C9C5F61C038", "Magazine Size"), Utils.GetLocalizedResource("", "0FAE8E5445029F2AA209ADB0FE49B23C", "Infinite"), -1));

View File

@ -100,12 +100,14 @@ public class CreatorPackage : IDisposable
case "FortCodeTokenItemDefinition":
case "FortSchematicItemDefinition":
case "FortAlterableItemDefinition":
case "SproutHousingItemDefinition":
case "SparksKeyboardItemDefinition":
case "FortWorldMultiItemDefinition":
case "FortAlterationItemDefinition":
case "FortExpeditionItemDefinition":
case "FortIngredientItemDefinition":
case "FortConsumableItemDefinition":
case "SproutBuildingItemDefinition":
case "StWFortAccoladeItemDefinition":
case "FortAccountBuffItemDefinition":
case "FortFOBCoreDecoItemDefinition":
@ -163,6 +165,9 @@ public class CreatorPackage : IDisposable
case "JunoAthenaDanceItemOverrideDefinition":
creator = new BaseJuno(_object.Value, _style);
return true;
case "AssembledMeshSchema":
creator = new BaseAssembledMesh(_object.Value, _style);
return true;
case "FortTandemCharacterData":
creator = new BaseTandem(_object.Value, _style);
return true;

View File

@ -18,6 +18,7 @@
<Color name="BooleanConstants" foreground="#569cd6" fontWeight="bold" />
<RuleSet ignoreCase="false">
<Rule color="Comment">(\/\/.*|\/\*[\s\S]*?\*\/)</Rule>
<Span color="String" begin="&quot;" end="&quot;" />
<!-- UE Macros -->
<Keywords color="UEMacro">
@ -44,10 +45,19 @@
<Word>Int16</Word>
<Word>Int32</Word>
<Word>Int64</Word>
<Word>int8</Word>
<Word>int16</Word>
<Word>int32</Word>
<Word>int64</Word>
<Word>uint</Word>
<Word>UInt8</Word>
<Word>UInt16</Word>
<Word>UInt32</Word>
<Word>UInt64</Word>
<Word>uint8</Word>
<Word>uint16</Word>
<Word>uint32</Word>
<Word>uint64</Word>
<Word>float</Word>
<Word>double</Word>
<Word>bool</Word>
@ -83,6 +93,7 @@
<Word>inline</Word>
<Word>constexpr</Word>
<Word>default</Word>
<Word>&amp;&amp;</Word>
</Keywords>
<Keywords color="Pointer">
@ -120,8 +131,6 @@
<Rule color="Brace">[\[\]\{\}]</Rule>
<Rule color="Comment">(\/\/.*|\/\*[\s\S]*?\*\/)</Rule>
<!-- Template Functions -->
<Rule color="Function">\b[A-Za-z_][A-Za-z0-9_]*\b(?=&lt;)</Rule>

View File

@ -119,6 +119,13 @@ namespace FModel.Settings
set => SetProperty(ref _audioDirectory, value);
}
private string _codeDirectory;
public string CodeDirectory
{
get => _codeDirectory;
set => SetProperty(ref _codeDirectory, value);
}
private string _modelDirectory;
public string ModelDirectory
{

View File

@ -11,6 +11,7 @@ namespace FModel.ViewModels.ApiEndpoints;
public class DillyApiEndpoint : AbstractApiProvider
{
private Backup[] _backups;
private ManifestInfoDilly[] _manifests;
public DillyApiEndpoint(RestClient client) : base(client) { }
@ -27,6 +28,19 @@ public class DillyApiEndpoint : AbstractApiProvider
return _backups ??= GetBackupsAsync(token).GetAwaiter().GetResult();
}
public async Task<ManifestInfoDilly[]> GetManifestsAsync(CancellationToken token)
{
var request = new FRestRequest($"https://export-service-new.dillyapis.com/v1/manifests");
var response = await _client.ExecuteAsync<ManifestInfoDilly[]>(request, token).ConfigureAwait(false);
Log.Information("[{Method}] [{Status}({StatusCode})] '{Resource}'", request.Method, response.StatusDescription, (int) response.StatusCode, response.ResponseUri?.OriginalString);
return response.Data;
}
public ManifestInfoDilly[] GetManifests(CancellationToken token)
{
return _manifests ??= GetManifestsAsync(token).GetAwaiter().GetResult();
}
public async Task<IDictionary<string, IDictionary<string, string>>> GetHotfixesAsync(CancellationToken token, string language = "en")
{
var request = new FRestRequest("https://api.fortniteapi.com/v1/cloudstorage/hotfixes")

View File

@ -23,6 +23,13 @@ public class Backup
[J] public string Url { get; private set; }
}
[DebuggerDisplay("{" + nameof(AppName) + "}")]
public class ManifestInfoDilly
{
[J] public string AppName { get; private set; }
[J] public string DownloadUrl { get; private set; }
}
public class Donator
{
[J] public string Username { get; private set; }

View File

@ -104,7 +104,7 @@ public class ApplicationViewModel : ViewModel
if (UserSettings.Default.CurrentDir is null)
{
//If no game is selected, many things will break before a shutdown request is processed in the normal way.
//A hard exit is preferable to an unhandled expection in this case
//A hard exit is preferable to an unhandled exception in this case
Environment.Exit(0);
}
@ -126,7 +126,6 @@ public class ApplicationViewModel : ViewModel
if (sender is not IAesVfsReader reader) return;
CUE4Parse.GameDirectory.Disable(reader);
};
CustomDirectories = new CustomDirectoriesViewModel();
SettingsView = new SettingsViewModel();
AesManager = new AesManagerViewModel(CUE4Parse);

View File

@ -225,14 +225,12 @@ public class CUE4ParseViewModel : ViewModel
public async Task Initialize()
{
await _apiEndpointView.EpicApi.VerifyAuth(CancellationToken.None);
await _threadWorkerView.Begin(cancellationToken =>
{
Provider.OnDemandOptions = new IoStoreOnDemandOptions
{
ChunkHostUri = new Uri("https://download.epicgames.com/", UriKind.Absolute),
ChunkCacheDirectory = Directory.CreateDirectory(Path.Combine(UserSettings.Default.OutputDirectory, ".data")),
Authorization = new AuthenticationHeaderValue("Bearer", UserSettings.Default.LastAuthResponse.AccessToken),
Timeout = TimeSpan.FromSeconds(30)
};
@ -287,6 +285,20 @@ public class CUE4ParseViewModel : ViewModel
it => new FRandomAccessStreamArchive(it, manifest.FindFile(it)!.GetStream(), p.Versions));
});
var manifests = _apiEndpointView.DillyApi.GetManifests(cancellationToken);
var downloadUrl = manifests.First(x => x.AppName == "Fortnite_Studio").DownloadUrl;
using var client = new HttpClient();
var manifestBytes = client.GetByteArrayAsync(downloadUrl).GetAwaiter().GetResult();
var uefnManifest = FBuildPatchAppManifest.Deserialize(manifestBytes, manifestOptions);
Parallel.ForEach(uefnManifest.Files.Where(x => _fnLiveRegex.IsMatch(x.FileName)), fileManifest =>
{
p.RegisterVfs(fileManifest.FileName, [fileManifest.GetStream()],
it => new FRandomAccessStreamArchive(it, uefnManifest.FindFile(it)!.GetStream(), p.Versions));
});
var elapsedTime = Stopwatch.GetElapsedTime(startTs);
FLogger.Append(ELog.Information, () =>
FLogger.Text($"Fortnite [LIVE] has been loaded successfully in {elapsedTime.TotalMilliseconds:F1}ms", Constants.WHITE, true));
@ -622,6 +634,9 @@ public class CUE4ParseViewModel : ViewModel
public void AudioFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Audio | EBulkType.Auto));
public void CodeFolder(CancellationToken cancellationToken, TreeItem folder)
=> BulkFolder(cancellationToken, folder, asset => Extract(cancellationToken, asset, TabControl.HasNoTabs, EBulkType.Code | EBulkType.Auto));
public void Extract(CancellationToken cancellationToken, GameFile entry, bool addNewTab = false, EBulkType bulk = EBulkType.None)
{
ApplicationService.ApplicationView.IsAssetsExplorerVisible = false;
@ -635,6 +650,7 @@ public class CUE4ParseViewModel : ViewModel
var saveProperties = HasFlag(bulk, EBulkType.Properties);
var saveTextures = HasFlag(bulk, EBulkType.Textures);
var saveAudio = HasFlag(bulk, EBulkType.Audio);
var saveDecompiled = HasFlag(bulk, EBulkType.Code);
switch (entry.Extension)
{
case "uasset":
@ -649,6 +665,13 @@ public class CUE4ParseViewModel : ViewModel
if (saveProperties) break; // do not search for viewable exports if we are dealing with jsons
}
if (saveDecompiled)
{
if (Decompile(entry, false))
TabControl.SelectedTab.SaveDecompiled(updateUi);
break;
}
for (var i = result.InclusiveStart; i < result.ExclusiveEnd; i++)
{
if (CheckExport(cancellationToken, result.Package, i, bulk))
@ -1365,11 +1388,13 @@ public class CUE4ParseViewModel : ViewModel
}
public void Decompile(GameFile entry)
public bool Decompile(GameFile entry, bool AddTab = true)
{
ApplicationService.ApplicationView.IsAssetsExplorerVisible = false;
if (TabControl.CanAddTabs) TabControl.AddTab(entry);
if (TabControl.CanAddTabs && AddTab)
{
ApplicationService.ApplicationView.IsAssetsExplorerVisible = false;
TabControl.AddTab(entry);
}
else TabControl.SelectedTab.SoftReset(entry);
TabControl.SelectedTab.TitleExtra = "Decompiled";
@ -1398,18 +1423,21 @@ public class CUE4ParseViewModel : ViewModel
if (dummy is not UClass || pointer.Object.Value is not UClass blueprint)
continue;
cppList.Add(blueprint.DecompileBlueprintToPseudo(cookedMetaData));
cppList.Add(blueprint.DecompileBlueprintToPseudo(pkg.Mappings, cookedMetaData));
}
if (cppList.Count == 0) return false;
var cpp = cppList.Count > 1 ? string.Join("\n\n", cppList) : cppList.FirstOrDefault() ?? string.Empty;
if (entry.Path.Contains("_Verse.uasset"))
{
cpp = Regex.Replace(cpp, "__verse_0x[a-fA-F0-9]{8}_", ""); // UnmangleCasedName
}
cpp = Regex.Replace(cpp, @"CallFunc_([A-Za-z0-9_]+)_ReturnValue", "$1");
cpp = Regex.Replace(cpp, @"K2Node_DynamicCast_([A-Za-z0-9_]+)", "$1");
cpp = Regex.Replace(cpp, @"K2Node_([A-Za-z0-9_]+)", "$1");
TabControl.SelectedTab.SetDocumentText(cpp, false, false);
return true;
}
private void SaveAndPlaySound(CancellationToken cancellationToken, string fullPath, string ext, byte[] data, bool saveAudio, bool updateUi)

View File

@ -69,6 +69,7 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
"Save_Models" => (EAction.Export, EShowAssetType.None, EBulkType.Meshes),
"Save_Animations" => (EAction.Export, EShowAssetType.None, EBulkType.Animations),
"Save_Audio" => (EAction.Export, EShowAssetType.None, EBulkType.Audio),
"Save_Code" => (EAction.Export, EShowAssetType.None, EBulkType.Code),
_ => throw new ArgumentOutOfRangeException("Unsupported asset action."),
};
@ -109,6 +110,7 @@ public class RightClickMenuCommand : ViewModelCommand<ApplicationViewModel>
EBulkType.Meshes => (UserSettings.Default.ModelDirectory, "models"),
EBulkType.Animations => (UserSettings.Default.ModelDirectory, "animations"),
EBulkType.Audio => (UserSettings.Default.AudioDirectory, "audio files"),
EBulkType.Code => (UserSettings.Default.CodeDirectory, "code files"),
_ => (null, null),
};

View File

@ -195,6 +195,7 @@ public class SettingsViewModel : ViewModel
private string _propertiesSnapshot;
private string _textureSnapshot;
private string _audioSnapshot;
private string _codeSnapshot;
private string _modelSnapshot;
private string _gameSnapshot;
private ETexturePlatform _uePlatformSnapshot;
@ -227,6 +228,7 @@ public class SettingsViewModel : ViewModel
_propertiesSnapshot = UserSettings.Default.PropertiesDirectory;
_textureSnapshot = UserSettings.Default.TextureDirectory;
_audioSnapshot = UserSettings.Default.AudioDirectory;
_codeSnapshot = UserSettings.Default.CodeDirectory;
_modelSnapshot = UserSettings.Default.ModelDirectory;
_gameSnapshot = UserSettings.Default.GameDirectory;
_uePlatformSnapshot = UserSettings.Default.CurrentDir.TexturePlatform;
@ -303,12 +305,6 @@ public class SettingsViewModel : ViewModel
if (_ueGameSnapshot != SelectedUeGame || _customVersionsSnapshot != SelectedCustomVersions ||
_uePlatformSnapshot != SelectedUePlatform || _optionsSnapshot != SelectedOptions || // combobox
_mapStructTypesSnapshot != SelectedMapStructTypes ||
_outputSnapshot != UserSettings.Default.OutputDirectory || // textbox
_rawDataSnapshot != UserSettings.Default.RawDataDirectory || // textbox
_propertiesSnapshot != UserSettings.Default.PropertiesDirectory || // textbox
_textureSnapshot != UserSettings.Default.TextureDirectory || // textbox
_audioSnapshot != UserSettings.Default.AudioDirectory || // textbox
_modelSnapshot != UserSettings.Default.ModelDirectory || // textbox
_gameSnapshot != UserSettings.Default.GameDirectory) // textbox
restart = true;

View File

@ -409,7 +409,17 @@ public class TabItem : ViewModel
Application.Current.Dispatcher.Invoke(() => File.WriteAllText(directory, Document.Text));
SaveCheck(directory, fileName, updateUi);
}
public void SaveDecompiled(bool updateUi)
{
var fileName = Path.ChangeExtension(Entry.Name, ".cpp");
var directory = Path.Combine(UserSettings.Default.PropertiesDirectory,
UserSettings.Default.KeepDirectoryStructure ? Entry.Directory : "", fileName).Replace('\\', '/');
Directory.CreateDirectory(directory.SubstringBeforeLast('/'));
Application.Current.Dispatcher.Invoke(() => File.WriteAllText(directory, Document.Text));
SaveCheck(directory, fileName, updateUi);
}
private void SaveCheck(string path, string fileName, bool updateUi)
{
if (File.Exists(path))

View File

@ -81,6 +81,9 @@ public partial class UpdateViewModel : ViewModel
if (username.Equals("Asval", StringComparison.OrdinalIgnoreCase))
{
username = "4sval"; // found out the hard way co-authored usernames can't be trusted
} else if (username.Equals("Krowe Moh", StringComparison.OrdinalIgnoreCase))
{
username = "Krowe-moh";
}
coAuthorMap[commit].Add(username);
@ -101,7 +104,7 @@ public partial class UpdateViewModel : ViewModel
}
catch
{
//
// Ignore
}
}

View File

@ -65,6 +65,26 @@
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Decompiled Blueprints"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Save_Code" />
<Binding Path="Tag"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Data="{StaticResource CodeIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Folder's Packages Models"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>

View File

@ -93,7 +93,7 @@ public partial class EndpointEditor
private void OnEvaluator(object sender, RoutedEventArgs e)
{
Process.Start(new ProcessStartInfo { FileName = "https://jsonpath.herokuapp.com/", UseShellExecute = true });
Process.Start(new ProcessStartInfo { FileName = "https://jsonpath.com/", UseShellExecute = true });
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Globalization;
using System.IO;
using System.Windows.Data;
namespace FModel.Views.Resources.Converters
{
public class FileNameWithoutExtensionConverter : IValueConverter
{
public static readonly FileNameWithoutExtensionConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value is string s ? Path.GetFileNameWithoutExtension(s) : value;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Globalization;
using System.Windows.Data;
using FModel.Extensions;
namespace FModel.Views.Resources.Converters;

View File

@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace FModel.Views.Resources.Converters;
public class TextToRefreshConverter : IValueConverter
{
public static readonly TextToRefreshConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is DateTime dt && dt != DateTime.MaxValue)
return $"Next Refresh: {dt:MMM d, yyyy}";
return "Next Refresh: Never";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}

View File

@ -771,9 +771,15 @@
</Image.ContextMenu>
</Image>
</Border>
<TextBlock Grid.Column="2" VerticalAlignment="Bottom" HorizontalAlignment="Center"
Visibility="{Binding SelectedItem.HasMultipleImages, ElementName=TabControlName, Converter={StaticResource BoolToVisibilityConverter}}"
Text="{Binding SelectedItem.Page, ElementName=TabControlName}" />
<StackPanel Grid.Column="2" VerticalAlignment="Bottom" HorizontalAlignment="Center">
<TextBlock HorizontalAlignment="Center"
Text="{Binding SelectedItem.SelectedImage.ExportName, Converter={x:Static converters:FileNameWithoutExtensionConverter.Instance}, ElementName=TabControlName}"
FontSize="10" FontWeight="SemiBold"
Visibility="{Binding SelectedItem.HasImage, ElementName=TabControlName, Converter={StaticResource BoolToVisibilityConverter}}" />
<TextBlock HorizontalAlignment="Center"
Visibility="{Binding SelectedItem.HasMultipleImages, ElementName=TabControlName, Converter={StaticResource BoolToVisibilityConverter}}"
Text="{Binding SelectedItem.Page, ElementName=TabControlName}" />
</StackPanel>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding SelectedItem.HasImage, ElementName=TabControlName}" Value="False">

View File

@ -695,7 +695,7 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="* Require a restart for changes to take effect"
<TextBlock Grid.Column="0" Text="* May Require a restart for changes to take effect"
HorizontalAlignment="Right" VerticalAlignment="Center" FontSize="11" Margin="0 0 10 0"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.Layer1InteractionForegroundBrush}}" />
<Button Grid.Column="1" MinWidth="78" Margin="0 0 12 0" IsDefault="True" IsCancel="False"

View File

@ -61,6 +61,8 @@ public partial class SettingsView
_applicationView.CUE4Parse.Provider.ReadScriptData = UserSettings.Default.ReadScriptData;
_applicationView.CUE4Parse.Provider.ReadShaderMaps = UserSettings.Default.ReadShaderMaps;
UserSettings.Save();
}
private void OnBrowseOutput(object sender, RoutedEventArgs e)
@ -74,6 +76,7 @@ public partial class SettingsView
UserSettings.Default.PropertiesDirectory = path;
UserSettings.Default.TextureDirectory = path;
UserSettings.Default.AudioDirectory = path;
UserSettings.Default.CodeDirectory = path;
}
private void OnBrowseDirectories(object sender, RoutedEventArgs e)

View File

@ -63,7 +63,7 @@
</adonisControls:SplitButton.SplitMenu>
</adonisControls:SplitButton>
<TextBlock VerticalAlignment="Bottom" HorizontalAlignment="Right" FontSize="10" Margin="0 2.5 0 0"
Text="{Binding NextUpdateCheck, Source={x:Static local:Settings.UserSettings.Default}, StringFormat=Next Refresh: {0:MMM d, yyyy}}" />
Text="{Binding NextUpdateCheck, Source={x:Static local:Settings.UserSettings.Default}, Converter={x:Static converters:TextToRefreshConverter.Instance}}" />
</StackPanel>
</Grid>