diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 33dec7af..162b9791 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -20,7 +20,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 ref: '${{ github.ref }}' @@ -32,7 +32,7 @@ jobs: echo "GITHUB_SHA_SHORT=$(echo $GITHUB_SHA | cut -c 1-6)" >> $GITHUB_ENV - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '6.0.x' @@ -53,7 +53,7 @@ jobs: tar -czvf win-builds.tgz --exclude='*.pdb' * - name: Upload build archive - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: win-builds path: ${{ env.PUBLISH_DIR }}/win-builds.tgz @@ -64,7 +64,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 ref: '${{ github.ref }}' @@ -76,7 +76,7 @@ jobs: echo "GITHUB_SHA_SHORT=$(echo $GITHUB_SHA | cut -c 1-6)" >> $GITHUB_ENV - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '6.0.x' @@ -97,19 +97,19 @@ jobs: tar -czvf mac-builds.tgz --exclude='*.pdb' * - name: Upload build archive - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: mac-builds path: ${{ env.PUBLISH_DIR }}/mac-builds.tgz retention-days: 5 package_release: - name: Linux builds, package and release + name: Linux/Android builds, package and release needs: [build-win, build-mac] runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 ref: '${{ github.ref }}' @@ -131,23 +131,35 @@ jobs: # nsis for windows installer run: sudo apt-get update && sudo apt-get install zip nsis - - name: Setup .NET - uses: actions/setup-dotnet@v4 + - name: Setup .NET 10 + uses: actions/setup-dotnet@v5 with: - dotnet-version: '6.0.x' + dotnet-version: '10.0.x' + + - name: Setup Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '17' + + - name: Install Android workload + shell: bash + run: | + dotnet workload install android + dotnet workload list - - name: Publish Linux RIDs + - name: Publish Linux/Android RIDs shell: bash run: ./ci/publish_all_host.sh - name: Download Windows build artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: win-builds path: ${{ env.PUBLISH_DIR }} - name: Download macOS build artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: name: mac-builds path: ${{ env.PUBLISH_DIR }} @@ -175,7 +187,10 @@ jobs: run: ${{ github.workspace }}/ci/create_appimage.sh - name: Generate Release - uses: softprops/action-gh-release@v2 - with: - draft: true - files: ${{ env.OUTPUT_DIR }}/* \ No newline at end of file + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create ${{ github.ref_name }} \ + --title "${{ github.ref_name }}" \ + --draft \ + ${{ env.OUTPUT_DIR }}/* diff --git a/.gitignore b/.gitignore index 2ad1ba86..0cd8a7b2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ mono_crash.* [Rr]elease/ [Rr]eleases/ x64/ -x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ 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..d1006f0d 100644 --- a/IonKiwi.lz4/IonKiwi.lz4.csproj +++ b/IonKiwi.lz4/IonKiwi.lz4.csproj @@ -1,7 +1,13 @@  - + + net10.0-android + + + net6.0 + + - net60 + true true IonKiwi.lz4.managed 1.0.7 diff --git a/IonKiwi.lz4/LZ4RawUtility.cs b/IonKiwi.lz4/LZ4RawUtility.cs index e3b6dad1..10d1ac8e 100644 --- a/IonKiwi.lz4/LZ4RawUtility.cs +++ b/IonKiwi.lz4/LZ4RawUtility.cs @@ -327,7 +327,9 @@ public static unsafe int LZ41_Stream_Decompress(Stream inputStream, Stream outpu /* The difference in offsets is the size of the block */ int cmpBytes = offsets[currentBlock + 1] - offsets[currentBlock]; byte[] cmpBuf = new byte[lz4.LZ4_compressBound(blockSize)]; - inputStream.Read(cmpBuf,0, cmpBytes); + var readBytes = inputStream.Read(cmpBuf,0, cmpBytes); + if (readBytes != cmpBytes) + throw new Exception("Incomplete read from stream"); fixed (byte* cmpPtr = cmpBuf) { fixed (byte* decBuf = new byte[blockSize]) 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..5dfc84e3 --- /dev/null +++ b/Knossos.NET.Android/Knossos.NET.Android.csproj @@ -0,0 +1,49 @@ + + + 3 + 3 + 1.3.4 + com.knossosnet.knossosnet + android-arm64;android-x64;android-arm;android-x86 + net10.0-android + 28 + Exe + enable + apk + false + enable + true + 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..32f85482 --- /dev/null +++ b/Knossos.NET.Android/MainActivity.cs @@ -0,0 +1,154 @@ +using Android.Content; +using Android.Content.PM; +using Android.Views; +using AndroidX.Core.Content; +using AndroidX.Core.View; +using Avalonia; +using Avalonia.Android; +using Avalonia.Threading; +using Knossos.NET.Classes; +using AndroidNet = global::Android.Net; + +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 +{ + public static MainActivity? Instance { get; private set; } + + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + return base.CustomizeAppBuilder(builder) + .WithInterFont(); + } + + private void hideSystemUI() + { + WindowCompat.SetDecorFitsSystemWindows(Window, false); + + var windowInsetsController = WindowCompat.GetInsetsController(Window, Window?.DecorView); + + if (windowInsetsController != null) + { + // Hide both navigation and status bars + windowInsetsController.Hide(WindowInsetsCompat.Type.SystemBars()); + + // Or only navigation bars: + //windowInsetsController.Hide(WindowInsetsCompat.Type.NavigationBars()); + + // Set behavior to show bars temporarily on swipe + windowInsetsController.SystemBarsBehavior = WindowInsetsControllerCompat.BehaviorShowTransientBarsBySwipe; + } + } + + public override void OnWindowFocusChanged(bool hasFocus) + { + base.OnWindowFocusChanged(hasFocus); + if(hasFocus) + hideSystemUI(); + } + + protected override void OnCreate(Bundle? savedInstanceState) + { + Window?.AddFlags(WindowManagerFlags.KeepScreenOn); + //Window?.AddFlags(WindowManagerFlags.Fullscreen); + + base.OnCreate(savedInstanceState); + Instance = this; + AndroidHelper.ShareTextAsyncFunc = ShareTextFileAndroidAsync; + AndroidHelper.ShareFileAsyncFunc = OpenFileExternalApp; + AndroidHelper.OpenUrlAsyncFunc = OpenExternalURLAsync; + hideSystemUI(); + } + + protected override void OnResume() + { + base.OnResume(); + hideSystemUI(); + } + + private static async Task ShareTextFileAndroidAsync(string texto) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + if (Instance == null) return; + + var intent = new Intent(Intent.ActionSend); + intent.SetType("text/plain"); + intent.PutExtra(Intent.ExtraText, texto); + intent.AddFlags(ActivityFlags.NewTask); + + try + { + var chooser = Intent.CreateChooser(intent, "Open text with..."); + Instance.StartActivity(chooser); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "MainActivity.ShareTextFileAndroidAsync", ex); + } + }); + } + + private static async Task OpenFileExternalApp(string fullPath, string mimetype) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + if (!System.IO.File.Exists(fullPath) || Instance == null) + return; + + try + { + var javaFile = new Java.IO.File(fullPath); + string authority = $"{Instance.PackageName}.fileprovider"; + + var uri = FileProvider.GetUriForFile(Instance, authority, javaFile); + + var intent = new Intent(Intent.ActionView); + intent.SetDataAndType(uri, mimetype); + intent.AddFlags(ActivityFlags.GrantReadUriPermission); + intent.AddFlags(ActivityFlags.NewTask); + + Instance.StartActivity(Intent.CreateChooser(intent, "Open file with...")); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "MainActivity.OpenFileExternalApp", ex); + } + }); + } + + private static async Task OpenExternalURLAsync(string url) + { + if (string.IsNullOrWhiteSpace(url)) return; + + await Dispatcher.UIThread.InvokeAsync(() => + { + if (Instance == null) return; + + try + { + if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) + url = "https://" + url.Trim(); + + var uri = AndroidNet.Uri.Parse(url); + var intent = new Intent(Intent.ActionView, uri); + intent.AddFlags(ActivityFlags.NewTask); + + Instance.StartActivity(intent); + } + catch (Exception ex) + { + Log.Add(Log.LogSeverity.Error, "MainActivity.OpenExternalURLAsync", ex); + } + }); + } +} diff --git a/Knossos.NET.Android/Properties/AndroidManifest.xml b/Knossos.NET.Android/Properties/AndroidManifest.xml new file mode 100644 index 00000000..a4306270 --- /dev/null +++ b/Knossos.NET.Android/Properties/AndroidManifest.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + 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.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.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/Templates/FreespaceModCardView.axaml b/Knossos.NET/Views/Templates/FreespaceModCardView.axaml index 5406b5c1..c846ed2a 100644 --- a/Knossos.NET/Views/Templates/FreespaceModCardView.axaml +++ b/Knossos.NET/Views/Templates/FreespaceModCardView.axaml @@ -15,8 +15,12 @@ - + + + + + - + - + - + diff --git a/Knossos.NET/Views/Windows/AddUserBuildView.axaml.cs b/Knossos.NET/Views/Windows/AddUserBuildView.axaml.cs index bcfa6177..7628c05a 100644 --- a/Knossos.NET/Views/Windows/AddUserBuildView.axaml.cs +++ b/Knossos.NET/Views/Windows/AddUserBuildView.axaml.cs @@ -1,12 +1,14 @@ using Avalonia.Controls; +using Avalonia.Markup.Xaml; namespace Knossos.NET.Views.Windows { - public partial class AddUserBuildView : Window + public partial class AddUserBuildView : KnossosWindow { public AddUserBuildView() { - InitializeComponent(); + //InitializeComponent(); + AvaloniaXamlLoader.Load(this); } } } diff --git a/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml b/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml index afc666d7..144ae0c2 100644 --- a/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml +++ b/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml @@ -1,4 +1,4 @@ - - + diff --git a/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml.cs b/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml.cs index 6269f655..67b697e8 100644 --- a/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml.cs +++ b/Knossos.NET/Views/Windows/CleanupKnossosLibraryView.axaml.cs @@ -1,19 +1,16 @@ -using System; using Avalonia.Controls; +using Avalonia.Markup.Xaml; using Knossos.NET.ViewModels; +using System; namespace Knossos.NET.Views { - public partial class CleanupKnossosLibraryView : Window + public partial class CleanupKnossosLibraryView : KnossosWindow { public CleanupKnossosLibraryView() { - InitializeComponent(); - } - - protected override void OnOpened(EventArgs e) - { - base.OnOpened(e); + //InitializeComponent(); + AvaloniaXamlLoader.Load(this); ((CleanupKnossosLibraryViewModel)DataContext!).OnRequestClose += (s, ev) => Close(); ((CleanupKnossosLibraryViewModel)DataContext!).LoadRemovableMods(); } diff --git a/Knossos.NET/Views/Windows/DebugFiltersView.axaml b/Knossos.NET/Views/Windows/DebugFiltersView.axaml index 2c3dbbed..32a00106 100644 --- a/Knossos.NET/Views/Windows/DebugFiltersView.axaml +++ b/Knossos.NET/Views/Windows/DebugFiltersView.axaml @@ -1,4 +1,4 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/Knossos.NET/Views/Windows/DebugFiltersView.axaml.cs b/Knossos.NET/Views/Windows/DebugFiltersView.axaml.cs index 833063af..fa7bc8fc 100644 --- a/Knossos.NET/Views/Windows/DebugFiltersView.axaml.cs +++ b/Knossos.NET/Views/Windows/DebugFiltersView.axaml.cs @@ -1,14 +1,16 @@ -using System; using Avalonia.Controls; +using Avalonia.Markup.Xaml; using Knossos.NET.ViewModels; +using System; namespace Knossos.NET.Views { - public partial class DebugFiltersView : Window + public partial class DebugFiltersView : KnossosWindow { public DebugFiltersView() { - InitializeComponent(); + //InitializeComponent(); + AvaloniaXamlLoader.Load(this); } } } \ No newline at end of file diff --git a/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml index 59f5361b..b9389d06 100644 --- a/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml +++ b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml @@ -1,4 +1,4 @@ -Upload to Nebula - + diff --git a/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml.cs b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml.cs index 3b8cd273..72573c62 100644 --- a/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml.cs +++ b/Knossos.NET/Views/Windows/DevModAdvancedUploadView.axaml.cs @@ -1,11 +1,13 @@ using Avalonia.Controls; +using Avalonia.Markup.Xaml; namespace Knossos.NET.Views; -public partial class DevModAdvancedUploadView : Window +public partial class DevModAdvancedUploadView : KnossosWindow { public DevModAdvancedUploadView() { - InitializeComponent(); + //InitializeComponent(); + AvaloniaXamlLoader.Load(this); } } \ No newline at end of file diff --git a/Knossos.NET/Views/Windows/DevModCreateNewView.axaml b/Knossos.NET/Views/Windows/DevModCreateNewView.axaml index 0e411773..51e6928f 100644 --- a/Knossos.NET/Views/Windows/DevModCreateNewView.axaml +++ b/Knossos.NET/Views/Windows/DevModCreateNewView.axaml @@ -1,4 +1,4 @@ - - + diff --git a/Knossos.NET/Views/Windows/DevModCreateNewView.axaml.cs b/Knossos.NET/Views/Windows/DevModCreateNewView.axaml.cs index a7235351..d79fdb33 100644 --- a/Knossos.NET/Views/Windows/DevModCreateNewView.axaml.cs +++ b/Knossos.NET/Views/Windows/DevModCreateNewView.axaml.cs @@ -4,10 +4,11 @@ namespace Knossos.NET.Views; -public partial class DevModCreateNewView : Window +public partial class DevModCreateNewView : KnossosWindow { public DevModCreateNewView() { - InitializeComponent(); + //InitializeComponent(); + AvaloniaXamlLoader.Load(this); } } \ No newline at end of file diff --git a/Knossos.NET/Views/Windows/DevModDescriptionEditorView.axaml b/Knossos.NET/Views/Windows/DevModDescriptionEditorView.axaml index 15b54a4a..9af07f92 100644 --- a/Knossos.NET/Views/Windows/DevModDescriptionEditorView.axaml +++ b/Knossos.NET/Views/Windows/DevModDescriptionEditorView.axaml @@ -1,4 +1,4 @@ - - + diff --git a/Knossos.NET/Views/Windows/DevModDescriptionEditorView.axaml.cs b/Knossos.NET/Views/Windows/DevModDescriptionEditorView.axaml.cs index 25c10da2..665d6c4c 100644 --- a/Knossos.NET/Views/Windows/DevModDescriptionEditorView.axaml.cs +++ b/Knossos.NET/Views/Windows/DevModDescriptionEditorView.axaml.cs @@ -6,11 +6,12 @@ namespace Knossos.NET.Views; -public partial class DevModDescriptionEditorView : Window +public partial class DevModDescriptionEditorView : KnossosWindow { public DevModDescriptionEditorView() { - InitializeComponent(); + //InitializeComponent(); + AvaloniaXamlLoader.Load(this); Closing += OnWindowClosing; } @@ -27,7 +28,7 @@ public void BindTextBox() } } - public void OnWindowClosing(object? sender, WindowClosingEventArgs e) + public void OnWindowClosing(object? sender, CancelEventArgs e) { if(DataContext != null) { diff --git a/Knossos.NET/Views/Windows/Fs2InstallerView.axaml b/Knossos.NET/Views/Windows/Fs2InstallerView.axaml index 36ef25fc..e356dbba 100644 --- a/Knossos.NET/Views/Windows/Fs2InstallerView.axaml +++ b/Knossos.NET/Views/Windows/Fs2InstallerView.axaml @@ -1,4 +1,4 @@ - - + diff --git a/Knossos.NET/Views/Windows/Fs2InstallerView.axaml.cs b/Knossos.NET/Views/Windows/Fs2InstallerView.axaml.cs index 185dc2df..6d6d5482 100644 --- a/Knossos.NET/Views/Windows/Fs2InstallerView.axaml.cs +++ b/Knossos.NET/Views/Windows/Fs2InstallerView.axaml.cs @@ -1,12 +1,14 @@ using Avalonia.Controls; +using Avalonia.Markup.Xaml; namespace Knossos.NET.Views { - public partial class Fs2InstallerView : Window + public partial class Fs2InstallerView : KnossosWindow { public Fs2InstallerView() { - InitializeComponent(); + //InitializeComponent(); + AvaloniaXamlLoader.Load(this); } } } diff --git a/Knossos.NET/Views/Windows/MainWindow.axaml b/Knossos.NET/Views/Windows/MainWindow.axaml index 9c2511f1..699300a4 100644 --- a/Knossos.NET/Views/Windows/MainWindow.axaml +++ b/Knossos.NET/Views/Windows/MainWindow.axaml @@ -4,13 +4,13 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:v="using:Knossos.NET.Views" - MinWidth="{Binding MinWindowWidth}" - MinHeight="{Binding MinWindowHeight}" x:DataType="vm:MainWindowViewModel" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="900" x:Class="Knossos.NET.Views.MainWindow" Icon="/Assets/knossos-icon.ico" WindowStartupLocation="CenterScreen" + MinWidth="{Binding MinWindowWidth}" + MinHeight="{Binding MinWindowHeight}" Background="{StaticResource BackgroundColorPrimary}" Title="{Binding AppTitle}" xmlns:cvt="clr-namespace:Knossos.NET.Converters;assembly=Knossos.NET"> @@ -19,49 +19,6 @@ - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Knossos.NET/Views/Windows/MainWindow.axaml.cs b/Knossos.NET/Views/Windows/MainWindow.axaml.cs index 8ec6360f..0255eb0b 100644 --- a/Knossos.NET/Views/Windows/MainWindow.axaml.cs +++ b/Knossos.NET/Views/Windows/MainWindow.axaml.cs @@ -16,24 +16,6 @@ public MainWindow() 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); - } - } - /// /// Change size of the main window /// @@ -56,7 +38,7 @@ protected override async void OnClosing(WindowClosingEventArgs e) await Dispatcher.UIThread.InvokeAsync(() => { Knossos.Tts(string.Empty); - MainWindowViewModel.Instance?.GlobalSettingsView?.CommitPendingChanges(); + MainViewModel.Instance?.GlobalSettingsView?.CommitPendingChanges(); Knossos.globalSettings.SaveSettingsOnAppClose(); canClose = true; }); diff --git a/Knossos.NET/Views/Windows/MessageBox.axaml.cs b/Knossos.NET/Views/Windows/MessageBox.axaml.cs index 9e6c7c4a..bdd25439 100644 --- a/Knossos.NET/Views/Windows/MessageBox.axaml.cs +++ b/Knossos.NET/Views/Windows/MessageBox.axaml.cs @@ -1,6 +1,14 @@ +using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; using Avalonia.Markup.Xaml; using Avalonia.Threading; +using Avalonia.VisualTree; +using Knossos.NET.ViewModels; +using System; +using System.Linq; using System.Threading.Tasks; namespace Knossos.NET.Views @@ -41,6 +49,17 @@ public MessageBox() //Messagebox is not thread safe! public static Task Show(Window? parent, string text, string title, MessageBoxButtons buttons) + { + if (KnUtils.IsAndroid || KnUtils.IsBrowser) + { + // Redirect to the non-window version + return MessageBoxView.ShowAsync(text, title, buttons).ContinueWith(t => t.Result); + } + // Open a window to show the message + return ShowWindow(parent, text, title, buttons); + } + + private static Task ShowWindow(Window? parent, string text, string title, MessageBoxButtons buttons) { var msgbox = new MessageBox() { @@ -53,20 +72,21 @@ public static Task Show(Window? parent, string text, string ti var result = MessageBoxResult.OK; - void AddButton(string caption, MessageBoxResult r, bool def = false, string classes = "", int buttonWidth = -1) - { - var button = new Button { Content = caption, Width = 100}; - button.Click += (_, __) => { + void AddButton(string caption, MessageBoxResult r, bool def = false, string? classes = null, double? buttonWidth = null) + { + var button = new Button { Content = caption, Width = 100 }; + button.Click += (_, __) => + { result = r; msgbox.Close(); }; - if(classes != "") + if (classes != null) { button.Classes.Add(classes); } - if (buttonWidth != -1) + if (buttonWidth.HasValue) { - button.Width = buttonWidth; + button.Width = buttonWidth.Value; } buttonPanel.Children.Add(button); if (def) @@ -75,56 +95,64 @@ void AddButton(string caption, MessageBoxResult r, bool def = false, string clas } } + ButtonCreation(AddButton, buttons); + + var tcs = new TaskCompletionSource(); + msgbox.Closed += delegate { tcs.TrySetResult(result); }; + + if (parent != null && parent.IsVisible) + { + msgbox.ShowDialog(parent); + } + else + { + msgbox.Show(); + } + + return tcs.Task; + } + + /// + /// Creates buttons for both versions of the messagebox system, do not call directly + /// + internal static void ButtonCreation(Action addButtonMethod, MessageBoxButtons buttons) + { if (buttons == MessageBoxButtons.OK || buttons == MessageBoxButtons.OKCancel || buttons == MessageBoxButtons.DetailsOKCancel) { - AddButton("OK", MessageBoxResult.OK, true, "Accept"); + addButtonMethod("OK", MessageBoxResult.OK, true, "Accept", null); } if (buttons == MessageBoxButtons.YesNo || buttons == MessageBoxButtons.YesNoCancel) { - AddButton("Yes", MessageBoxResult.Yes, false, "Accept"); - AddButton("No", MessageBoxResult.No, true, "Cancel"); + addButtonMethod("Yes", MessageBoxResult.Yes, false, "Accept", null); + addButtonMethod("No", MessageBoxResult.No, true, "Cancel", null); } if (buttons == MessageBoxButtons.Continue || buttons == MessageBoxButtons.ContinueCancel || buttons == MessageBoxButtons.DetailsContinueCancel || buttons == MessageBoxButtons.ContinueCancelSkipVersion) { - AddButton("Continue", MessageBoxResult.Continue, false, "Accept"); + addButtonMethod("Continue", MessageBoxResult.Continue, false, "Accept", null); } if (buttons == MessageBoxButtons.OKCancel || buttons == MessageBoxButtons.YesNoCancel || buttons == MessageBoxButtons.ContinueCancel || buttons == MessageBoxButtons.DetailsOKCancel || buttons == MessageBoxButtons.DetailsContinueCancel || buttons == MessageBoxButtons.ContinueCancelSkipVersion) { - AddButton("Cancel", MessageBoxResult.Cancel, true, "Cancel"); + addButtonMethod("Cancel", MessageBoxResult.Cancel, true, "Cancel", null); } if (buttons == MessageBoxButtons.Details || buttons == MessageBoxButtons.DetailsOKCancel || buttons == MessageBoxButtons.DetailsContinueCancel) { - AddButton("Details", MessageBoxResult.Details, false, "Option"); + addButtonMethod("Details", MessageBoxResult.Details, false, "Option", null); } if (buttons == MessageBoxButtons.DontWarnAgainOK) { - AddButton("OK", MessageBoxResult.OK, true, "Accept"); - AddButton("Don't warn again", MessageBoxResult.DontWarnAgain, false, "Option", 150); + addButtonMethod("OK", MessageBoxResult.OK, true, "Accept", null); + addButtonMethod("Don't warn again", MessageBoxResult.DontWarnAgain, false, "Option", 150); } if (buttons == MessageBoxButtons.ContinueCancelSkipVersion) { - AddButton("Skip this version", MessageBoxResult.SkipVersion, false, "Option", 150); + addButtonMethod("Skip this version", MessageBoxResult.SkipVersion, false, "Option", 150); } - - var tcs = new TaskCompletionSource(); - msgbox.Closed += delegate { tcs.TrySetResult(result); }; - - if (parent != null && parent.IsVisible) - { - msgbox.ShowDialog(parent); - } - else - { - msgbox.Show(); - } - - return tcs.Task; } } -} +} \ No newline at end of file diff --git a/Knossos.NET/Views/Windows/ModDetailsView.axaml b/Knossos.NET/Views/Windows/ModDetailsView.axaml index c0593e95..adc77cdf 100644 --- a/Knossos.NET/Views/Windows/ModDetailsView.axaml +++ b/Knossos.NET/Views/Windows/ModDetailsView.axaml @@ -1,4 +1,4 @@ - - + - + @@ -111,5 +111,5 @@ - + \ No newline at end of file diff --git a/Knossos.NET/Views/Windows/ModDetailsView.axaml.cs b/Knossos.NET/Views/Windows/ModDetailsView.axaml.cs index 0c8f9cbf..0e760545 100644 --- a/Knossos.NET/Views/Windows/ModDetailsView.axaml.cs +++ b/Knossos.NET/Views/Windows/ModDetailsView.axaml.cs @@ -1,11 +1,12 @@ using Avalonia.Controls; using Avalonia.Layout; +using Avalonia.Markup.Xaml; using System; using System.ComponentModel; namespace Knossos.NET.Views { - public partial class ModDetailsView : Window + public partial class ModDetailsView : KnossosWindow { private Carousel _carousel; private Button _left; @@ -13,7 +14,8 @@ public partial class ModDetailsView : Window public ModDetailsView() { - InitializeComponent(); + //InitializeComponent(); + AvaloniaXamlLoader.Load(this); this.Closing += ModDetailsView_StopTTS; _carousel = this.Get("carousel"); diff --git a/Knossos.NET/Views/Windows/ModInstallView.axaml b/Knossos.NET/Views/Windows/ModInstallView.axaml index 3afa519a..34fc0450 100644 --- a/Knossos.NET/Views/Windows/ModInstallView.axaml +++ b/Knossos.NET/Views/Windows/ModInstallView.axaml @@ -1,4 +1,4 @@ - + @@ -41,7 +42,7 @@ - +