diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..a80ab938 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,30 @@ + + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/IonKiwi.lz4/IonKiwi.lz4.csproj b/IonKiwi.lz4/IonKiwi.lz4.csproj index 312984fb..28bacc25 100644 --- a/IonKiwi.lz4/IonKiwi.lz4.csproj +++ b/IonKiwi.lz4/IonKiwi.lz4.csproj @@ -1,7 +1,7 @@  - net60 + net6.0;net8.0;net10.0-android true IonKiwi.lz4.managed 1.0.7 diff --git a/Knossos.NET.Android/Icon.png b/Knossos.NET.Android/Icon.png new file mode 100644 index 00000000..e0313cfc Binary files /dev/null and b/Knossos.NET.Android/Icon.png differ diff --git a/Knossos.NET.Android/Knossos.NET.Android.csproj b/Knossos.NET.Android/Knossos.NET.Android.csproj new file mode 100644 index 00000000..4535fe52 --- /dev/null +++ b/Knossos.NET.Android/Knossos.NET.Android.csproj @@ -0,0 +1,53 @@ + + + Exe + net10.0-android + 28 + enable + apk + false + enable + true + + com.knossosnet.knossosnet + 2 + 2 + 1.3.2 + true + false + + + + + + + + + + + + + + + + + + + + Resources\drawable\Icon.png + + + + + + + + + + + + + + + + diff --git a/Knossos.NET.Android/MainActivity.cs b/Knossos.NET.Android/MainActivity.cs new file mode 100644 index 00000000..7246f14d --- /dev/null +++ b/Knossos.NET.Android/MainActivity.cs @@ -0,0 +1,31 @@ +using Android.Content.PM; +using Android.Views; +using Avalonia; +using Avalonia.Android; + +namespace Knossos.NET.Android; + +[Activity( + Label = "Knossos.NET", + Theme = "@style/MyTheme.NoActionBar", + Icon = "@drawable/icon", + MainLauncher = true, + ConfigurationChanges = ConfigChanges.Orientation + | ConfigChanges.ScreenSize + | ConfigChanges.UiMode, + ScreenOrientation = ScreenOrientation.SensorLandscape)] +public class MainActivity : AvaloniaMainActivity +{ + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .WithInterFont(); + } + + protected override void OnCreate(Bundle? savedInstanceState) + { + Window?.AddFlags(WindowManagerFlags.KeepScreenOn); + Window?.AddFlags(WindowManagerFlags.Fullscreen); + base.OnCreate(savedInstanceState); + } +} diff --git a/Knossos.NET.Android/Properties/AndroidManifest.xml b/Knossos.NET.Android/Properties/AndroidManifest.xml new file mode 100644 index 00000000..606edbb0 --- /dev/null +++ b/Knossos.NET.Android/Properties/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Knossos.NET.Android/Resources/AboutResources.txt b/Knossos.NET.Android/Resources/AboutResources.txt new file mode 100644 index 00000000..c2bca974 --- /dev/null +++ b/Knossos.NET.Android/Resources/AboutResources.txt @@ -0,0 +1,44 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.axml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable/ + icon.png + + layout/ + main.axml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called "R" +(this is an Android convention) that contains the tokens for each one of the resources +included. For example, for the above Resources layout, this is what the R class would expose: + +public class R { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main +to reference the layout/main.axml file, or R.strings.first_string to reference the first +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/Knossos.NET.Android/Resources/drawable-night-v31/avalonia_anim.xml b/Knossos.NET.Android/Resources/drawable-night-v31/avalonia_anim.xml new file mode 100644 index 00000000..dde4b5a7 --- /dev/null +++ b/Knossos.NET.Android/Resources/drawable-night-v31/avalonia_anim.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Knossos.NET.Android/Resources/drawable-v31/avalonia_anim.xml b/Knossos.NET.Android/Resources/drawable-v31/avalonia_anim.xml new file mode 100644 index 00000000..94f27d9e --- /dev/null +++ b/Knossos.NET.Android/Resources/drawable-v31/avalonia_anim.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Knossos.NET.Android/Resources/drawable/splash_screen.xml b/Knossos.NET.Android/Resources/drawable/splash_screen.xml new file mode 100644 index 00000000..2e920b4b --- /dev/null +++ b/Knossos.NET.Android/Resources/drawable/splash_screen.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/Knossos.NET.Android/Resources/layout/overlay_controls.xml b/Knossos.NET.Android/Resources/layout/overlay_controls.xml new file mode 100644 index 00000000..00cbea5f --- /dev/null +++ b/Knossos.NET.Android/Resources/layout/overlay_controls.xml @@ -0,0 +1,439 @@ + + + + + - + - diff --git a/Knossos.NET/Views/DialogHost.cs b/Knossos.NET/Views/DialogHost.cs new file mode 100644 index 00000000..45e15966 --- /dev/null +++ b/Knossos.NET/Views/DialogHost.cs @@ -0,0 +1,67 @@ +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; +using System; +using System.Linq; + +namespace Knossos.NET.Views +{ + /// + /// Handles displaying a view over an overlay on the MainView + /// to display windows or messages + /// + public static class DialogHost + { + public static ContentPresenter? Show(Control dialog, Action? onDismiss = null) + { + var overlayHost = MainView.instance?.GetVisualDescendants().OfType().FirstOrDefault(x => x.Name == "DialogOverlay"); + if (overlayHost == null) + { + Log.Add(Log.LogSeverity.Error, "DialogHost.Show()", "Unable to find the dialog overlay."); + return null; + } + EnsureHost(overlayHost); + overlayHost.IsHitTestVisible = true; + overlayHost.Content = Wrap(dialog, onDismiss); + return overlayHost; + } + + + public static void Hide(Control dialog, ContentPresenter? overlayHost = null) + { + if(overlayHost == null) + overlayHost = MainView.instance?.GetVisualDescendants().OfType().FirstOrDefault(x => x.Name == "DialogOverlay"); + if (overlayHost == null) + { + Log.Add(Log.LogSeverity.Error, "DialogHost.Show()", "Unable to find the dialog overlay."); + return; + } + overlayHost.Content = null; + overlayHost.IsHitTestVisible = false; + } + + + private static Control Wrap(Control content, Action? onDismiss) + { + var root = new Border + { + Background = new SolidColorBrush(Color.FromArgb(0x80, 0x00, 0x00, 0x00)), + Child = content, + HorizontalAlignment = HorizontalAlignment.Stretch, + VerticalAlignment = VerticalAlignment.Stretch + }; + //root.PointerPressed += (_, __) => onDismiss?.Invoke(); // dismiss outside card + return root; + } + + + private static void EnsureHost(ContentPresenter overlayHost) + { + // Make sure overlayHost stretches over the window + overlayHost.HorizontalAlignment = HorizontalAlignment.Stretch; + overlayHost.VerticalAlignment = VerticalAlignment.Stretch; + } + } +} \ No newline at end of file diff --git a/Knossos.NET/Views/GlobalSettingsView.axaml b/Knossos.NET/Views/GlobalSettingsView.axaml index 90cda015..d5b4903d 100644 --- a/Knossos.NET/Views/GlobalSettingsView.axaml +++ b/Knossos.NET/Views/GlobalSettingsView.axaml @@ -18,7 +18,7 @@ - + @@ -29,11 +29,15 @@ - + Library Folder + + Library Folder + + Stats diff --git a/Knossos.NET/Views/KnossosWindow.cs b/Knossos.NET/Views/KnossosWindow.cs new file mode 100644 index 00000000..a60826e0 --- /dev/null +++ b/Knossos.NET/Views/KnossosWindow.cs @@ -0,0 +1,244 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Media; +using System; +using System.ComponentModel; +using System.Threading.Tasks; + +namespace Knossos.NET.Views +{ + /// + /// Window wrapper system + /// For opening new windows both on desktop OS and single view systems like Android + /// Instead of using Avalonia Window, use this, this will create a new window in runtime for desktop os + /// and display the view on a overlay over the MainView in single view OS. + /// + public partial class KnossosWindow : UserControl + { + public KnossosWindow() + { + } + + //Add some properties from Window missing on Usercontrol + public static readonly StyledProperty TitleProperty = AvaloniaProperty.Register(nameof(Title)); + public string? Title + { + get => GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + public static readonly StyledProperty CanCloseProperty = AvaloniaProperty.Register(nameof(CanClose), defaultValue: true); + public bool CanClose + { + get => GetValue(CanCloseProperty); + set => SetValue(CanCloseProperty, value); + } + + public static readonly StyledProperty CanResizeProperty = AvaloniaProperty.Register(nameof(CanResize), defaultValue: true); + public bool CanResize + { + get => GetValue(CanResizeProperty); + set => SetValue(CanResizeProperty, value); + } + + public static readonly StyledProperty IconProperty = AvaloniaProperty.Register(nameof(Icon)); + public WindowIcon? Icon + { + get => GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + public static readonly StyledProperty WindowStartupLocationProperty = AvaloniaProperty.Register(nameof(WindowStartupLocation), WindowStartupLocation.CenterOwner); + public WindowStartupLocation WindowStartupLocation + { + get => GetValue(WindowStartupLocationProperty); + set => SetValue(WindowStartupLocationProperty, value); + } + + public static readonly StyledProperty SizeToContentProperty = AvaloniaProperty.Register(nameof(SizeToContent), SizeToContent.Manual); + public SizeToContent SizeToContent + { + get => GetValue(SizeToContentProperty); + set => SetValue(SizeToContentProperty, value); + } + + public static readonly StyledProperty TopmostProperty = AvaloniaProperty.Register(nameof(Topmost), false); + public bool Topmost + { + get => GetValue(TopmostProperty); + set => SetValue(TopmostProperty, value); + } + + // Dialog result + public object? DialogResult { get; set; } + + // Window events + public event EventHandler? Opened; + public event EventHandler? Closing; + public event EventHandler? Closed; + + // Window-like API + public void Show() => _ = ShowInternalAsync(KnUtils.GetTopLevel(), isDialog: false); + public void Show(Window owner) => _ = ShowInternalAsync(owner, isDialog: false); + public Task ShowDialog() => ShowInternalAsync(KnUtils.GetTopLevel(), isDialog: true); + public Task ShowDialog(Window? owner) => ShowInternalAsync(owner, isDialog: true); + protected virtual Task OnClosingAsync() => Task.FromResult(true); + + private Window? _hostWindow; // desktop host + private ContentPresenter? _overlayHost; // android host + private TaskCompletionSource? _tcs; // ShowDialog() + + //Close the window and run OnClosing + public async void Close() + { + var ce = new CancelEventArgs(); + if (!CanClose) ce.Cancel = true; + Closing?.Invoke(this, ce); + if (ce.Cancel) return; + + var ok = await OnClosingAsync().ConfigureAwait(true); + if (!ok) return; + + //desktop + if (_hostWindow != null) + { + _hostWindow.Close(); + return; + } + + //android + if (_overlayHost != null) + { + DialogHost.Hide(this, _overlayHost); + _overlayHost.IsHitTestVisible = false; + _overlayHost = null; + _tcs?.TrySetResult(DialogResult); + _tcs = null; + Closed?.Invoke(this, EventArgs.Empty); + } + } + + // Add Window-like title and close button to the supplied view + private Control BuildOverlayChrome(Control body) + { + var card = new Border + { + Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Colors.Black), + CornerRadius = new CornerRadius(10), + Padding = new Thickness(0), + Margin = new Thickness(20), + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Stretch, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Stretch + }; + + var layout = new Grid + { + RowDefinitions = new RowDefinitions("35, *") + }; + + var titleBar = new Grid + { + Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.FromRgb(240, 240, 240)), + Height = 35 + }; + titleBar.ColumnDefinitions = new ColumnDefinitions("*, Auto"); + + // title + var titleText = new TextBlock + { + Margin = new Thickness(8), + FontWeight = Avalonia.Media.FontWeight.SemiBold, + Foreground = SolidColorBrush.Parse("Black"), + Text = Title + }; + + // close button + var closeBtn = new Button + { + Content = "✕", + Width = 60, + Height = 30, + Background = SolidColorBrush.Parse("Red"), + Margin = new Thickness(0, 3, 6, 6) + }; + closeBtn.Click += (_, __) => Close(); + + titleBar.Children.Add(titleText); + Grid.SetColumn(closeBtn, 1); + titleBar.Children.Add(closeBtn); + + // title bar + Grid.SetRow(titleBar, 0); + layout.Children.Add(titleBar); + + // view + Grid.SetRow(body, 1); + if (MainView.instance != null) + { + body.MaxHeight = MainView.instance.Bounds.Height - 80; + body.MaxWidth = MainView.instance.Bounds.Width - 40; + } + layout.Children.Add(body); + + card.Child = layout; + return card; + } + + // Open the window, creates a window in runtime and attaches the view to it + // or display it on the mainview via dialoghost + private async Task ShowInternalAsync(TopLevel? owner, bool isDialog) + { + if (KnUtils.IsAndroid || KnUtils.IsBrowser) + { + _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var chrome = BuildOverlayChrome(this); + _overlayHost = DialogHost.Show(chrome, onDismiss: () => Close()); + Opened?.Invoke(this, EventArgs.Empty); + + if (isDialog) + await _tcs.Task.ConfigureAwait(false); + + return; + } + + // Desktop, create a window + var w = new Window + { + Content = this, + Title = Title ?? string.Empty, + SizeToContent = SizeToContent, + Topmost = Topmost, + }; + + if (!double.IsNaN(Width) && Width > 0) w.Width = Width; + if (!double.IsNaN(Height) && Height > 0) w.Height = Height; + if (MinWidth > 0) w.MinWidth = MinWidth; + if (MinHeight > 0) w.MinHeight = MinHeight; + w.WindowStartupLocation = WindowStartupLocation; + + w.Closing += async (_, e) => + { + var ce = new CancelEventArgs(); + if (!CanClose) ce.Cancel = true; + Closing?.Invoke(this, ce); + if (ce.Cancel) { e.Cancel = true; return; } + + var ok = await OnClosingAsync().ConfigureAwait(true); + if (!ok) { e.Cancel = true; return; } + }; + + w.Opened += (_, __) => Opened?.Invoke(this, EventArgs.Empty); + w.Closed += (_, __) => { Closed?.Invoke(this, EventArgs.Empty); _hostWindow = null; }; + + _hostWindow = w; + + if (isDialog && owner is Window ownerWin) + await w.ShowDialog(ownerWin); + else if (isDialog && MainWindow.instance != null) + await w.ShowDialog(MainWindow.instance); + else + w.Show(); + } + } +} diff --git a/Knossos.NET/Views/MainView.axaml b/Knossos.NET/Views/MainView.axaml new file mode 100644 index 00000000..1aaffaa3 --- /dev/null +++ b/Knossos.NET/Views/MainView.axaml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Knossos.NET/Views/MainView.axaml.cs b/Knossos.NET/Views/MainView.axaml.cs new file mode 100644 index 00000000..c1738d38 --- /dev/null +++ b/Knossos.NET/Views/MainView.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Knossos.NET.Views; + +public partial class MainView : UserControl +{ + public static MainView? instance; + + public MainView() + { + instance = this; + InitializeComponent(); + } + + + /// + /// For custom mode, for setting the task buttom at the last place in the menu + /// we need to change the margins so it is displayed properly. + /// + public void FixMarginButtomTasks() + { + var tasks = this.FindControl("TaskButtom"); + if (tasks != null) + { + tasks.Margin = new Thickness(9, -45, 0, 0); + } + var list = this.FindControl("ButtomList"); + if (list != null) + { + list.Margin = new Thickness(2, 0, -100, 0); + } + } +} \ No newline at end of file diff --git a/Knossos.NET/Views/MessageBoxView.axaml b/Knossos.NET/Views/MessageBoxView.axaml new file mode 100644 index 00000000..bc66f9b1 --- /dev/null +++ b/Knossos.NET/Views/MessageBoxView.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Knossos.NET/Views/MessageBoxView.axaml.cs b/Knossos.NET/Views/MessageBoxView.axaml.cs new file mode 100644 index 00000000..f350351a --- /dev/null +++ b/Knossos.NET/Views/MessageBoxView.axaml.cs @@ -0,0 +1,60 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using System; +using System.Threading.Tasks; +using static Knossos.NET.Views.MessageBox; + +namespace Knossos.NET.Views +{ + /// + /// Single view version of the Knossos Messagebox system + /// Used to display a message on a overlay rather than a window + /// + public partial class MessageBoxView : UserControl + { + public string Title { get => _title.Text!; set => _title.Text = value; } + public string Text { get => _body.Text!; set => _body.Text = value; } + + private readonly TextBlock _title; + private readonly TextBlock _body; + private readonly StackPanel _buttonsHostPanel; + + public MessageBoxView() + { + AvaloniaXamlLoader.Load(this); + _title = this.FindControl("TitleText")!; + _body = this.FindControl("BodyText")!; + _buttonsHostPanel = this.FindControl("ButtonsHostPanel")!; + } + + /// + /// Displays a message on a overlay over the main view + /// Usefull for single view OS like Android or WASM + /// Normally you dont want to call this directly unless you really want a message + /// on the overlay. Use MessageBox.Show() instead. + /// + /// + /// + /// + /// + public static Task ShowAsync(string text, string title, MessageBoxButtons buttons) + { + var tcs = new TaskCompletionSource(); + var view = new MessageBoxView { Title = title, Text = text }; + + void AddButton(string caption, MessageBoxResult r, bool isDefault = false, string? classes = null, double? width = null) + { + var b = new Button { Content = caption, MinWidth = width ?? 100 }; + if (!string.IsNullOrEmpty(classes)) b.Classes.Add(classes!); + b.Click += (_, __) => { tcs.TrySetResult(r); DialogHost.Hide(view); }; + view._buttonsHostPanel.Children.Add(b); + if (isDefault) view.AttachedToVisualTree += (_, __) => b.Focus(); + }; + + MessageBox.ButtonCreation(AddButton, buttons); + + DialogHost.Show(view, onDismiss: () => tcs.TrySetResult(MessageBoxResult.Cancel)); + return tcs.Task; + } + } +} \ No newline at end of file diff --git a/Knossos.NET/Views/ModListView.axaml b/Knossos.NET/Views/ModListView.axaml index 8e5abfbb..9358764d 100644 --- a/Knossos.NET/Views/ModListView.axaml +++ b/Knossos.NET/Views/ModListView.axaml @@ -80,7 +80,7 @@ - + diff --git a/Knossos.NET/Views/ModListView.axaml.cs b/Knossos.NET/Views/ModListView.axaml.cs index 0f89d28f..dec65c81 100644 --- a/Knossos.NET/Views/ModListView.axaml.cs +++ b/Knossos.NET/Views/ModListView.axaml.cs @@ -67,16 +67,16 @@ private void ApplyFilterButtonsStyle() { try { - if (filterPanel != null && MainWindowViewModel.Instance != null) + if (filterPanel != null && MainViewModel.Instance != null) { - if (MainWindowViewModel.Instance.tagFilter.Any()) + if (MainViewModel.Instance.tagFilter.Any()) { var tags = ModTags.GetListAllFilters(); foreach (var item in filterPanel.Children) { if (item is Button button && button.Tag is int tagIndex) { - if (tags.Count() > tagIndex && MainWindowViewModel.Instance.tagFilter.Contains(tags[tagIndex])) + if (tags.Count() > tagIndex && MainViewModel.Instance.tagFilter.Contains(tags[tagIndex])) { button.Classes.Add("Secondary"); } diff --git a/Knossos.NET/Views/NebulaModListView.axaml b/Knossos.NET/Views/NebulaModListView.axaml index 72488c4a..6b45a7b1 100644 --- a/Knossos.NET/Views/NebulaModListView.axaml +++ b/Knossos.NET/Views/NebulaModListView.axaml @@ -65,7 +65,7 @@ - + diff --git a/Knossos.NET/Views/NebulaModListView.axaml.cs b/Knossos.NET/Views/NebulaModListView.axaml.cs index 72bf6fca..952d1d88 100644 --- a/Knossos.NET/Views/NebulaModListView.axaml.cs +++ b/Knossos.NET/Views/NebulaModListView.axaml.cs @@ -66,16 +66,16 @@ private void ApplyFilterButtonsStyle() { try { - if (filterPanel != null && MainWindowViewModel.Instance != null) + if (filterPanel != null && MainViewModel.Instance != null) { - if (MainWindowViewModel.Instance.tagFilter.Any()) + if (MainViewModel.Instance.tagFilter.Any()) { var tags = ModTags.GetListAllFilters(); foreach (var item in filterPanel.Children) { if (item is Button button && button.Tag is int tagIndex) { - if (tags.Count() > tagIndex && MainWindowViewModel.Instance.tagFilter.Contains(tags[tagIndex])) + if (tags.Count() > tagIndex && MainViewModel.Instance.tagFilter.Contains(tags[tagIndex])) { button.Classes.Add("Secondary"); } diff --git a/Knossos.NET/Views/PxoView.axaml b/Knossos.NET/Views/PxoView.axaml index 195193d1..d8fca4ef 100644 --- a/Knossos.NET/Views/PxoView.axaml +++ b/Knossos.NET/Views/PxoView.axaml @@ -12,7 +12,7 @@ - + diff --git a/Knossos.NET/Views/TaskView.axaml b/Knossos.NET/Views/TaskView.axaml index 5c4eb4bc..975b922e 100644 --- a/Knossos.NET/Views/TaskView.axaml +++ b/Knossos.NET/Views/TaskView.axaml @@ -14,7 +14,7 @@ - + @@ -57,7 +57,7 @@ One video link per line - + diff --git a/Knossos.NET/Views/Templates/DevModMembersMgrView.cs.axaml b/Knossos.NET/Views/Templates/DevModMembersMgrView.cs.axaml index bcb48eea..73a04d9e 100644 --- a/Knossos.NET/Views/Templates/DevModMembersMgrView.cs.axaml +++ b/Knossos.NET/Views/Templates/DevModMembersMgrView.cs.axaml @@ -31,7 +31,7 @@ - + You must be logged in to Nebula to manage your mod development team. diff --git a/Knossos.NET/Views/Templates/ModCardView.axaml b/Knossos.NET/Views/Templates/ModCardView.axaml index 5542fb96..9de4e8d5 100644 --- a/Knossos.NET/Views/Templates/ModCardView.axaml +++ b/Knossos.NET/Views/Templates/ModCardView.axaml @@ -33,7 +33,7 @@ - +