Browse Source

Why there are more errors than previously

Krzysztof Krysiński 2 years ago
parent
commit
a56b87c03c
18 changed files with 1039 additions and 22 deletions
  1. 64 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/Converters/JsonConverters/DefaultUnknownEnumConverter.cs
  2. 0 13
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/ProcessHelper.cs
  3. 9 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/CustomDialog.cs
  4. 78 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/OptionsDialog.cs
  5. 94 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/ResizeDocumentDialog.cs
  6. 3 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/SizeUnit.cs
  7. 1 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/Toolbars/IBasicShapeToolbar.cs
  8. 7 1
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/Toolbars/IToolbar.cs
  9. 33 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Services/CommandProvider.cs
  10. 47 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Services/NewsFeed/News.cs
  11. 66 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Services/NewsFeed/NewsProvider.cs
  12. 2 2
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/ColorsViewModel.cs
  13. 2 2
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/FileViewModel.cs
  14. 1 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/WindowViewModel.cs
  15. 3 3
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/Tools/Tools/ColorPickerToolViewModel.cs
  16. 341 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Windows/HelloTherePopup.axaml
  17. 237 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Windows/HelloTherePopup.axaml.cs
  18. 51 0
      src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Windows/ResizeablePopup.cs

+ 64 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/Converters/JsonConverters/DefaultUnknownEnumConverter.cs

@@ -0,0 +1,64 @@
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace PixiEditor.Helpers.Converters;
+
+public class DefaultUnknownEnumConverter : StringEnumConverter
+{
+    /// <summary>
+    /// The default value used to fallback on when a enum is not convertable.
+    /// </summary>
+    private readonly int defaultValue;
+
+    /// <inheritdoc />
+    /// <summary>
+    /// Default constructor. Defaults the default value to 0.
+    /// </summary>
+    public DefaultUnknownEnumConverter()
+    {
+
+    }
+
+    /// <inheritdoc />
+    /// <summary>
+    /// Sets the default value for the enum value.
+    /// </summary>
+    /// <param name="defaultValue">The default value to use.</param>
+    public DefaultUnknownEnumConverter(int defaultValue)
+    {
+        this.defaultValue = defaultValue;
+    }
+
+    /// <inheritdoc />
+    /// <summary>
+    /// Reads the provided JSON and attempts to convert using StringEnumConverter. If that fails set the value to the default value.
+    /// </summary>
+    /// <param name="reader">Reads the JSON value.</param>
+    /// <param name="objectType">Current type that is being converted.</param>
+    /// <param name="existingValue">The existing value being read.</param>
+    /// <param name="serializer">Instance of the JSON Serializer.</param>
+    /// <returns>The deserialized value of the enum if it exists or the default value if it does not.</returns>
+    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+    {
+        try
+        {
+            return base.ReadJson(reader, objectType, existingValue, serializer);
+        }
+        catch
+        {
+            return Enum.Parse(objectType, $"{defaultValue}");
+        }
+    }
+
+    /// <inheritdoc />
+    /// <summary>
+    /// Validates that this converter can handle the type that is being provided.
+    /// </summary>
+    /// <param name="objectType">The type of the object being converted.</param>
+    /// <returns>True if the base class says so, and if the value is an enum and has a default value to fall on.</returns>
+    public override bool CanConvert(Type objectType)
+    {
+        return base.CanConvert(objectType) && objectType.GetTypeInfo().IsEnum && Enum.IsDefined(objectType, defaultValue);
+    }
+}

+ 0 - 13
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Helpers/ProcessHelper.cs

@@ -19,17 +19,4 @@ internal static class ProcessHelper
     {
         return IOperatingSystem.Current.ProcessUtility.IsRunningAsAdministrator();
     }
-
-    public static void OpenInExplorer(string path)
-    {
-        try
-        {
-            string fixedPath = Path.GetFullPath(path);
-            var process = Process.Start("explorer.exe", $"/select,\"{fixedPath}\"");
-
-            // Explorer might need a second to show up
-            process.WaitForExit(500);
-        }
-        finally{}
-    }
 }

+ 9 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/CustomDialog.cs

@@ -0,0 +1,9 @@
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace PixiEditor.Models.Dialogs;
+
+internal abstract class CustomDialog : ObservableObject
+{
+    public abstract Task<bool> ShowDialog();
+}

+ 78 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/OptionsDialog.cs

@@ -0,0 +1,78 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+using PixiEditor.Extensions.Common.Localization;
+
+namespace PixiEditor.Models.Dialogs;
+
+internal class OptionsDialog<T> : CustomDialog, IEnumerable<T>
+{
+    private Dictionary<T, Action<T>> _results = new();
+
+    public string Title { get; set; }
+
+    public object Content { get; set; }
+
+    public T Result { get; private set; }
+
+    public OptionsDialog(LocalizedString title, object content)
+    {
+        Title = title;
+
+        if (content is not Visual)
+        {
+            Content = new TextBlock()
+            {
+                Text = content.ToString(),
+                FontSize = 15,
+                TextAlignment = TextAlignment.Center,
+                TextTrimming = TextTrimming.WordEllipsis,
+                TextWrapping = TextWrapping.WrapWithOverflow,
+                HorizontalAlignment = HorizontalAlignment.Center,
+                VerticalAlignment = VerticalAlignment.Center,
+            };
+        }
+        else
+        {
+            Content = content;
+        }
+    }
+
+    public OptionsDialog(string title, object content, IEnumerable<KeyValuePair<T, Action<T>>> options) : this(title, content)
+    {
+        _results = new(options);
+    }
+
+    public Action<T> this[T name]
+    {
+        get => _results[name];
+        set => _results.Add(name, value);
+    }
+
+    public override async Task<bool> ShowDialog()
+    {
+        var popup = new OptionPopup(Title, Content, new(_results.Keys.Select(x => (object)x)));
+        var popupResult = popup.ShowDialog();
+
+        Result = (T)popup.Result;
+        if (Result != null)
+        {
+            _results[Result]?.Invoke(Result);
+        }
+
+        return popupResult.GetValueOrDefault(false);
+    }
+
+    public void Add(T name) => _results.Add(name, null);
+
+    public void Add(T name, Action<T> action) => _results.Add(name, action);
+
+    public IEnumerator<T> GetEnumerator() => _results.Keys.GetEnumerator();
+
+    IEnumerator IEnumerable.GetEnumerator() => _results.Keys.GetEnumerator();
+}

+ 94 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/ResizeDocumentDialog.cs

@@ -0,0 +1,94 @@
+using System.Threading.Tasks;
+using Avalonia;
+using PixiEditor.Avalonia.Helpers.Extensions;
+using PixiEditor.ChangeableDocument.Enums;
+using PixiEditor.Models.Enums;
+using PixiEditor.Views.Dialogs;
+
+namespace PixiEditor.Models.Dialogs;
+
+internal class ResizeDocumentDialog : CustomDialog
+{
+    private int height;
+    private int width;
+
+    public ResizeDocumentDialog(int currentWidth, int currentHeight, bool openResizeCanvas = false)
+    {
+        Width = currentWidth;
+        Height = currentHeight;
+        OpenResizeCanvas = openResizeCanvas;
+    }
+
+    public bool OpenResizeCanvas { get; set; }
+
+    public ResizeAnchor ResizeAnchor { get; set; }
+
+    public int Width
+    {
+        get => width;
+        set
+        {
+            if (width != value)
+            {
+                width = value;
+                OnPropertyChanged(nameof(Width));
+            }
+        }
+    }
+
+    public int Height
+    {
+        get => height;
+        set
+        {
+            if (height != value)
+            {
+                height = value;
+                OnPropertyChanged(nameof(Height));
+            }
+        }
+    }
+
+    public override async Task<bool> ShowDialog()
+    {
+        return OpenResizeCanvas ? await ShowResizeCanvasDialog() : await ShowResizeDocumentCanvas();
+    }
+
+    async Task<bool> ShowDialog<T>()
+        where T : ResizeablePopup, new()
+    {
+        T popup = new T()
+        {
+            NewAbsoluteHeight = Height,
+            NewAbsoluteWidth = Width,
+            NewPercentageSize = 100,
+            NewSelectedUnit = SizeUnit.Pixel
+        };
+
+        await Application.Current.ForDesktopMainWindowAsync(async window =>
+        {
+            var result = await popup.ShowDialog<bool>(window);
+            if (result)
+            {
+                Width = popup.NewAbsoluteWidth;
+                Height = popup.NewAbsoluteHeight;
+                if (popup is ResizeCanvasPopup resizeCanvas)
+                {
+                    ResizeAnchor = resizeCanvas.SelectedAnchorPoint;
+                }
+            }
+        });
+
+        return false;
+    }
+
+    private async Task<bool> ShowResizeDocumentCanvas()
+    {
+        return await ShowDialog<ResizeDocumentPopup>();
+    }
+
+    private async Task<bool> ShowResizeCanvasDialog()
+    {
+        return await ShowDialog<ResizeCanvasPopup>();
+    }
+}

+ 3 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Dialogs/SizeUnit.cs

@@ -0,0 +1,3 @@
+namespace PixiEditor.Models.Enums;
+
+public enum SizeUnit { Pixel, Percentage }

+ 1 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/Toolbars/IBasicShapeToolbar.cs

@@ -2,7 +2,7 @@
 
 namespace PixiEditor.Models.Containers.Toolbars;
 
-public interface IBasicShapeToolbar : IBasicToolbar
+internal interface IBasicShapeToolbar : IBasicToolbar
 {
     public bool Fill { get; }
     public Color FillColor { get; }

+ 7 - 1
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Handlers/Toolbars/IToolbar.cs

@@ -1,8 +1,14 @@
-using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Settings;
+using System.Collections.ObjectModel;
+using PixiEditor.ViewModels.SubViewModels.Tools.ToolSettings.Settings;
 
 namespace PixiEditor.Models.Containers.Toolbars;
 
 internal interface IToolbar : IHandler
 {
     public Setting GetSetting(string name);
+    public ObservableCollection<Setting> Settings { get; set; }
+    public bool SettingsGenerated { get; }
+    public void GenerateSettings();
+    public void SaveToolbarSettings();
+    public void LoadSharedSettings();
 }

+ 33 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Services/CommandProvider.cs

@@ -0,0 +1,33 @@
+using System.Windows.Input;
+using System.Windows.Media;
+using Avalonia.Media;
+using PixiEditor.Models.Commands;
+using PixiEditor.Models.Commands.Commands;
+using PixiEditor.Models.Commands.Evaluators;
+using XAMLCommand = PixiEditor.Models.Commands.XAML.Command;
+
+namespace PixiEditor.Models.Services;
+
+internal class CommandProvider
+{
+    private readonly CommandController _controller;
+
+    public CommandProvider(CommandController controller)
+    {
+        _controller = controller;
+    }
+
+    public Command GetCommand(string name) => _controller.Commands[name];
+
+    public CanExecuteEvaluator GetCanExecute(string name) => _controller.CanExecuteEvaluators[name];
+
+    public bool CanExecute(string name, Command command, object argument) =>
+        _controller.CanExecuteEvaluators[name].CallEvaluate(command, argument);
+
+    public IconEvaluator GetIconEvaluator(string name) => _controller.IconEvaluators[name];
+
+    public IImage GetIcon(string name, Command command, object argument) =>
+        _controller.IconEvaluators[name].CallEvaluate(command, argument);
+
+    public ICommand GetICommand(string name, bool useProvidedArgument = false) => XAMLCommand.GetICommand(_controller.Commands[name], useProvidedArgument);
+}

+ 47 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Services/NewsFeed/News.cs

@@ -0,0 +1,47 @@
+using System.ComponentModel;
+using System.Globalization;
+using System.Security.Cryptography;
+using System.Text;
+using Newtonsoft.Json;
+using PixiEditor.Extensions.Helpers;
+using PixiEditor.Helpers.Converters;
+
+namespace PixiEditor.Models.Services.NewsFeed;
+
+[JsonConverter(typeof(DefaultUnknownEnumConverter), (int)Misc)]
+internal enum NewsType
+{
+    [Description("NewVersion.png")]
+    NewVersion,
+    [Description("YouTube.png")]
+    YtVideo,
+    [Description("Article.png")]
+    BlogPost,
+    [Description("OfficialAnnouncement.png")]
+    OfficialAnnouncement,
+    [Description("Misc.png")]
+    Misc
+}
+
+internal record News
+{
+    public string Title { get; init; } = string.Empty;
+    public NewsType NewsType { get; init; } = NewsType.Misc;
+    public string Url { get; init; }
+    public DateTime Date { get; init; }
+    public string CoverImageUrl { get; init; } = string.Empty;
+
+    [JsonIgnore]
+    public string ResolvedIconUrl => $"/Images/News/{NewsType.GetDescription()}";
+
+    [JsonIgnore]
+    public bool IsNew { get; set; } = false;
+
+    public int GetIdentifyingNumber()
+    {
+        MD5 md5Hasher = MD5.Create();
+        string data = Title + Url + Date.ToString(CultureInfo.InvariantCulture) + CoverImageUrl;
+        var hashed = md5Hasher.ComputeHash(Encoding.UTF8.GetBytes(data));
+        return BitConverter.ToInt32(hashed, 0);
+    }
+}

+ 66 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Models/Services/NewsFeed/NewsProvider.cs

@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Platform;
+using PixiEditor.UpdateModule;
+
+namespace PixiEditor.Models.Services.NewsFeed;
+
+internal class NewsProvider
+{
+    private const int MaxNewsCount = 20;
+    private const string FeedUrl = "https://raw.githubusercontent.com/PixiEditor/news-feed/main/";
+
+    private List<int> _lastCheckedIds = new List<int>();
+
+    public NewsProvider()
+    {
+        _lastCheckedIds = IPreferences.Current.GetPreference(PreferencesConstants.LastCheckedNewsIds, new List<int>());
+    }
+
+    public async Task<List<News>?> FetchNewsAsync()
+    {
+        List<News> allNews = new List<News>();
+        await FetchFrom(allNews, "shared.json");
+        await FetchFrom(allNews, $"{IPlatform.Current.Id}.json");
+
+        var sorted = allNews.OrderByDescending(x => x.Date).Take(MaxNewsCount).ToList();
+        MarkNewOnes(sorted);
+        return sorted;
+    }
+
+    private async Task FetchFrom(List<News> output, string fileName)
+    {
+        using HttpClient client = new HttpClient();
+        client.DefaultRequestHeaders.Add("User-Agent", "PixiEditor");
+        HttpResponseMessage response = await client.GetAsync($"{FeedUrl}{fileName}");
+        if (response.StatusCode == HttpStatusCode.OK)
+        {
+            string content = await response.Content.ReadAsStringAsync();
+            var list = JsonConvert.DeserializeObject<List<News>>(content);
+            output.AddRange(list);
+        }
+    }
+
+    private void MarkNewOnes(List<News> list)
+    {
+        foreach (var news in list)
+        {
+            if (news.GetIdentifyingNumber() is var num && !_lastCheckedIds.Contains(num))
+            {
+                news.IsNew = true;
+                _lastCheckedIds.Add(num);
+                if (_lastCheckedIds.Count > MaxNewsCount)
+                {
+                    _lastCheckedIds.RemoveAt(0);
+                }
+            }
+        }
+
+        IPreferences.Current.UpdatePreference(PreferencesConstants.LastCheckedNewsIds, _lastCheckedIds);
+    }
+}

+ 2 - 2
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/ColorsViewModel.cs

@@ -36,7 +36,7 @@ namespace PixiEditor.ViewModels.SubViewModels.Main;
 [Command.Group("PixiEditor.Colors", "PALETTE_COLORS")]
 internal class ColorsViewModel : SubViewModel<ViewModelMain>
 {
-    public RelayCommand<List<PaletteColor>> ImportPaletteCommand { get; set; }
+    public AsyncRelayCommand<List<PaletteColor>> ImportPaletteCommand { get; set; }
     private PaletteProvider paletteProvider;
 
     public PaletteProvider PaletteProvider
@@ -82,7 +82,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>
     public ColorsViewModel(ViewModelMain owner)
         : base(owner)
     {
-        ImportPaletteCommand = new RelayCommand<List<PaletteColor>>(ImportPalette, CanImportPalette);
+        ImportPaletteCommand = new AsyncRelayCommand<List<PaletteColor>>(ImportPalette, CanImportPalette);
         Owner.OnStartupEvent += OwnerOnStartupEvent;
     }
 

+ 2 - 2
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/FileViewModel.cs

@@ -112,9 +112,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Internal("PixiEditor.File.OpenRecent")]
-    public void OpenRecent(object parameter)
+    public void OpenRecent(string parameter)
     {
-        string path = (string)parameter;
+        string path = parameter;
         if (!File.Exists(path))
         {
             NoticeDialog.Show("FILE_NOT_FOUND", "FAILED_TO_OPEN_FILE");

+ 1 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/SubViewModels/WindowViewModel.cs

@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Avalonia.Views;
 using PixiEditor.Models.Commands;
 using PixiEditor.ViewModels.SubViewModels.Document;
+using PixiEditor.Views.Dialogs;
 using Command = PixiEditor.Models.Commands.Attributes.Commands.Command;
 
 namespace PixiEditor.ViewModels.SubViewModels.Main;

+ 3 - 3
src/PixiEditor.Avalonia/PixiEditor.Avalonia/ViewModels/Tools/Tools/ColorPickerToolViewModel.cs

@@ -105,10 +105,10 @@ internal class ColorPickerToolViewModel : ToolViewModel, IColorPickerHandler
     private void UpdateActionDisplay()
     {
         // TODO: We probably need to create keyboard service to handle this
-        bool ctrlDown = (Keyboard.Modifiers & KeyModifiers.Control) != 0;
-        bool shiftDown = (Keyboard.Modifiers & KeyModifiers.Shift) != 0;
+        /*bool ctrlDown = (Keyboard.Modifiers & KeyModifiers.Control) != 0;
+        bool shiftDown = (Keyboard.Modifiers & KeyModifiers.Shift) != 0;*/
         
-        UpdateActionDisplay(ctrlDown, shiftDown);
+        UpdateActionDisplay(false, false);
     }
     
     private void UpdateActionDisplay(bool ctrlIsDown, bool shiftIsDown)

+ 341 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Windows/HelloTherePopup.axaml

@@ -0,0 +1,341 @@
+<Window x:Class="PixiEditor.Views.Dialogs.HelloTherePopup"
+        x:ClassModifier="internal"
+        xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+        xmlns:uc="clr-namespace:PixiEditor.Views.UserControls"
+        xmlns:cmds="clr-namespace:PixiEditor.Models.Commands.XAML"
+        xmlns:local="clr-namespace:PixiEditor.Views.Dialogs"
+        xmlns:models="clr-namespace:PixiEditor.Models"
+        xmlns:views="clr-namespace:PixiEditor.Views"
+        xmlns:helpers="clr-namespace:PixiEditor.Helpers"
+        xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+        xmlns:newsFeed="clr-namespace:PixiEditor.Views.UserControls.NewsFeed"
+        xmlns:gif="http://wpfanimatedgif.codeplex.com"
+        xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+        mc:Ignorable="d" ShowInTaskbar="False"
+        Title="Hello there!" Height="662" Width="982" MinHeight="500" MinWidth="500"
+        d:DataContext="{d:DesignInstance local:HelloTherePopup}"
+        WindowStartupLocation="CenterScreen" Loaded="HelloTherePopup_OnLoaded"
+        FlowDirection="{helpers:Localization FlowDirection}">
+
+    <Window.Resources>
+        <Style TargetType="TextBlock">
+            <Setter Property="Foreground" Value="White"/>
+            <Setter Property="FontSize" Value="16"/>
+        </Style>
+    </Window.Resources>
+
+    <WindowChrome.WindowChrome>
+        <WindowChrome CaptionHeight="35"  GlassFrameThickness="0.1"
+                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}"/>
+    </WindowChrome.WindowChrome>
+
+    <Window.CommandBindings>
+        <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
+                        Executed="CommandBinding_Executed_Close" />
+    </Window.CommandBindings>
+
+    <Grid Background="{StaticResource AccentColor}">
+        <Grid.RowDefinitions>
+            <RowDefinition Height="35" />
+            <RowDefinition Height="*"/>
+        </Grid.RowDefinitions>
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="*"/>
+            <ColumnDefinition Width="300" x:Name="newsColumn"/>
+        </Grid.ColumnDefinitions>
+
+        <DockPanel Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource MainColor}">
+            <Button DockPanel.Dock="Right" HorizontalAlignment="Right" Style="{StaticResource CloseButtonStyle}"
+                    WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Close"
+                    Command="{x:Static SystemCommands.CloseWindowCommand}" />
+        </DockPanel>
+
+        <ScrollViewer Grid.Column="0" Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="3,0">
+            <Grid Grid.Row="1" Margin="0,30,0,0">
+                <Grid.RowDefinitions>
+                    <RowDefinition Height="90"/>
+                    <RowDefinition Height="Auto"/>
+                    <RowDefinition MinHeight="120"/>
+                    <RowDefinition Height="Auto"/>
+                </Grid.RowDefinitions>
+
+                <Grid Grid.RowSpan="3" HorizontalAlignment="Right" VerticalAlignment="Center">
+                    <CheckBox Visibility="{Binding NewsDisabled, Converter={converters:InverseBoolToVisibilityConverter}}"
+                              Width="40" Height="40" IsChecked="{Binding NewsPanelCollapsed}">
+                    <CheckBox.Template>
+                        <ControlTemplate TargetType="{x:Type CheckBox}">
+                            <StackPanel Orientation="Horizontal" Focusable="False">
+                                <StackPanel.Background>
+                                    <VisualBrush>
+                                        <VisualBrush.Visual>
+                                            <Ellipse Fill="{StaticResource MainColor}" Width="20" Height="20"/>
+                                        </VisualBrush.Visual>
+                                    </VisualBrush>
+                                </StackPanel.Background>
+                                <Image Focusable="False" Cursor="Hand" x:Name="checkboxImage" Source="/Images/Chevron-right.png">
+                                    <Image.RenderTransform>
+                                        <RotateTransform Angle="180" CenterX="19" CenterY="20"/>
+                                    </Image.RenderTransform>
+                                </Image>
+                                <ContentPresenter Focusable="False"/>
+                            </StackPanel>
+                            <ControlTemplate.Triggers>
+                                <Trigger Property="IsChecked" Value="True">
+                                    <Setter TargetName="checkboxImage" Property="RenderTransform">
+                                        <Setter.Value>
+                                            <RotateTransform Angle="0" CenterX="1" CenterY="0"/>
+                                        </Setter.Value>
+                                    </Setter>
+                                </Trigger>
+                            </ControlTemplate.Triggers>
+                        </ControlTemplate>
+                    </CheckBox.Template>
+                </CheckBox>
+                </Grid>
+                <StackPanel Grid.Row="0" HorizontalAlignment="Center">
+                    <StackPanel Orientation="Horizontal">
+                        <Image Source="avares://PixiEditor.UI.Common/Assets/PixiEditorLogo.png" Height="40" VerticalAlignment="Center"/>
+                        <TextBlock FontSize="40" FontWeight="SemiBold" VerticalAlignment="Center" Margin="10,0,0,0">PixiEditor</TextBlock>
+                    </StackPanel>
+                    <TextBlock HorizontalAlignment="Center" FontSize="20" FontWeight="Medium" Text="{Binding VersionText}"/>
+                </StackPanel>
+
+                <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
+                    <Button Style="{StaticResource DarkRoundButton}" Command="{Binding OpenFileCommand}" MinWidth="150" Margin="10"
+                            ui:Translator.Key="OPEN_FILE"/>
+                    <Button Style="{StaticResource DarkRoundButton}" Command="{Binding OpenNewFileCommand}" MinWidth="150" Margin="10"
+                            ui:Translator.Key="NEW_FILE"/>
+                </StackPanel>
+
+                <StackPanel Grid.Row="2" HorizontalAlignment="Center" Margin="0,30,0,0">
+                    <TextBlock FontSize="23" FontWeight="SemiBold" HorizontalAlignment="Center"
+                               ui:Translator.Key="RECENT_FILES"/>
+                    <TextBlock Margin="0,12.5,0,0" Foreground="LightGray" HorizontalAlignment="Center" 
+                               ui:Translator.Key="RECENT_EMPTY_TEXT">
+                        <TextBlock.Visibility>
+                            <Binding Path="RecentlyOpened.Count"
+                                     Converter="{converters:EqualityBoolToVisibilityConverter}">
+                                <Binding.ConverterParameter>
+                                    <sys:Int32/>
+                                </Binding.ConverterParameter>
+                            </Binding>
+                        </TextBlock.Visibility>
+                    </TextBlock>
+                    <ItemsControl ItemsSource="{Binding RecentlyOpened}">
+                        <ItemsControl.ItemTemplate>
+                            <DataTemplate DataType="{x:Type dataHolders:RecentlyOpenedDocument}">
+                                <Grid>
+                                    <StackPanel Margin="8,5,8,0">
+                                        <Button Margin="0,10,0,0" HorizontalAlignment="Center"
+                                                Width="100" Height="100"
+                                                Padding="0"
+                                                Command="{Binding DataContext.OpenRecentCommand, RelativeSource={RelativeSource AncestorType=uc:AlignableWrapPanel}}"
+                                                CommandParameter="{Binding FilePath}"
+                                                Style="{StaticResource DarkRoundButton}"
+                                                x:Name="fileButton">
+                                            <Grid Width="100" Height="100">
+                                                <Image Source="{Binding PreviewBitmap}" x:Name="image" Margin="20">
+                                                    <RenderOptions.BitmapScalingMode>
+                                                        <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
+                                                            <Binding Path="PreviewBitmap.PixelWidth"/>
+                                                            <Binding ElementName="image" Path="ActualWidth"/>
+                                                        </MultiBinding>
+                                                    </RenderOptions.BitmapScalingMode>
+                                                </Image>
+                                                <Border Grid.Row="1" Height="8" Width="8" x:Name="extensionBorder" Margin="5"
+                                                        Background="{Binding FileExtension, Converter={converters:FileExtensionToColorConverter}}" 
+                                                        VerticalAlignment="Bottom" HorizontalAlignment="Right">
+                                                    <Border.Style>
+                                                        <Style TargetType="Border">
+                                                            <Style.Triggers>
+                                                                <Trigger Property="IsMouseOver" Value="False">
+                                                                    <Setter Property="CornerRadius" Value="2"/>
+                                                                </Trigger>
+                                                                <DataTrigger Binding="{Binding IsMouseOver, ElementName=fileButton}" Value="True">
+                                                                    <DataTrigger.EnterActions>
+                                                                        <BeginStoryboard Name="open">
+                                                                            <Storyboard BeginTime="0:0:.1">
+                                                                                <DoubleAnimation Storyboard.TargetProperty="Height" By="8" To="70" BeginTime="0:0:.1" Duration="0:0:.3">
+                                                                                    <DoubleAnimation.EasingFunction>
+                                                                                        <ExponentialEase/>
+                                                                                    </DoubleAnimation.EasingFunction>
+                                                                                </DoubleAnimation>
+                                                                                <DoubleAnimation Storyboard.TargetProperty="Width" By="8" To="100" Duration="0:0:.1">
+                                                                                    <DoubleAnimation.EasingFunction>
+                                                                                        <ExponentialEase/>
+                                                                                    </DoubleAnimation.EasingFunction>
+                                                                                </DoubleAnimation>
+                                                                                <ThicknessAnimation Storyboard.TargetProperty="Margin" By="5" To="0" BeginTime="0:0:.1" Duration="0:0:.25">
+                                                                                    <ThicknessAnimation.EasingFunction>
+                                                                                        <ExponentialEase/>
+                                                                                    </ThicknessAnimation.EasingFunction>
+                                                                                </ThicknessAnimation>
+                                                                            </Storyboard>
+                                                                        </BeginStoryboard>
+                                                                    </DataTrigger.EnterActions>
+                                                                    <DataTrigger.ExitActions>
+                                                                        <BeginStoryboard Name="close">
+                                                                            <Storyboard>
+                                                                                <DoubleAnimation Storyboard.TargetProperty="Height" By="70" To="8"  Duration="0:0:.2">
+                                                                                    <DoubleAnimation.EasingFunction>
+                                                                                        <ExponentialEase/>
+                                                                                    </DoubleAnimation.EasingFunction>
+                                                                                </DoubleAnimation>
+                                                                                <DoubleAnimation Storyboard.TargetProperty="Width" By="100" To="8" BeginTime="0:0:.2" Duration="0:0:.1">
+                                                                                    <DoubleAnimation.EasingFunction>
+                                                                                        <ExponentialEase/>
+                                                                                    </DoubleAnimation.EasingFunction>
+                                                                                </DoubleAnimation>
+                                                                                <ThicknessAnimation Storyboard.TargetProperty="Margin" By="0" To="5" Duration="0:0:.1">
+                                                                                    <ThicknessAnimation.EasingFunction>
+                                                                                        <ExponentialEase/>
+                                                                                    </ThicknessAnimation.EasingFunction>
+                                                                                </ThicknessAnimation>
+                                                                            </Storyboard>
+                                                                        </BeginStoryboard>
+                                                                    </DataTrigger.ExitActions>
+                                                                    <Setter Property="CornerRadius" Value="0,0,4,4"/>
+                                                                </DataTrigger>
+                                                            </Style.Triggers>
+                                                        </Style>
+                                                    </Border.Style>
+                                                    <Grid HorizontalAlignment="Center" Margin="0,10,0,0" Opacity="0">
+                                                        <Grid.Style>
+                                                            <Style TargetType="Grid">
+                                                                <Style.Triggers>
+                                                                    <DataTrigger Binding="{Binding IsMouseOver, ElementName=fileButton}" Value="True">
+                                                                        <DataTrigger.EnterActions>
+                                                                            <BeginStoryboard Name="start">
+                                                                                <Storyboard BeginTime="0:0:.2">
+                                                                                    <DoubleAnimation Storyboard.TargetProperty="Opacity" By="0" To="1" Duration="0:0:.4">
+                                                                                        <DoubleAnimation.EasingFunction>
+                                                                                            <PowerEase/>
+                                                                                        </DoubleAnimation.EasingFunction>
+                                                                                    </DoubleAnimation>
+                                                                                </Storyboard>
+                                                                            </BeginStoryboard>
+                                                                        </DataTrigger.EnterActions>
+                                                                        <DataTrigger.ExitActions>
+                                                                            <RemoveStoryboard BeginStoryboardName="start"/>
+                                                                        </DataTrigger.ExitActions>
+                                                                    </DataTrigger>
+                                                                </Style.Triggers>
+                                                            </Style>
+                                                        </Grid.Style>
+                                                        <TextBlock x:Name="extension" VerticalAlignment="Top" Text="{Binding FileExtension}" FontSize="15" TextAlignment="Center"/>
+                                                        <StackPanel Orientation="Horizontal" VerticalAlignment="Bottom" HorizontalAlignment="Center">
+                                                            <StackPanel.Resources>
+                                                                <Style TargetType="Button" BasedOn="{StaticResource BaseDarkButton}">
+                                                                    <Setter Property="Margin" Value="0,0,0,5"/>
+                                                                    <Setter Property="Width" Value="25"/>
+                                                                    <Setter Property="Height" Value="25"/>
+                                                                    <Setter Property="MinWidth" Value="25"/>
+                                                                    <Setter Property="MinHeight" Value="25"/>
+                                                                    
+                                                                    <Style.Triggers>
+                                                                        <Trigger Property="IsMouseOver" Value="False">
+                                                                            <Setter Property="Background" Value="Transparent"/>
+                                                                        </Trigger>
+                                                                        <Trigger Property="IsMouseOver" Value="True">
+                                                                            <Setter Property="Background" Value="#70FFFFFF"/>
+                                                                        </Trigger>
+                                                                    </Style.Triggers>
+                                                                </Style>
+                                                            </StackPanel.Resources>
+                                                            <Button Command="{Binding DataContext.OpenInExplorerCommand, RelativeSource={RelativeSource AncestorType=uc:AlignableWrapPanel}}"
+                                                                    CommandParameter="{Binding FilePath}"
+                                                                    ToolTip="Open in File Explorer">
+                                                                <TextBlock Text="&#xEC50;" FontFamily="Segoe MDL2 Assets"
+                                                                           TextAlignment="Center" FontSize="18"/>
+                                                            </Button>
+                                                            <Button Command="{cmds:Command Name=PixiEditor.File.RemoveRecent, UseProvided=True}"
+                                                                    CommandParameter="{Binding FilePath}"
+                                                                    ToolTip="Remove from list">
+                                                                <TextBlock Text="" FontFamily="{StaticResource Feather}"
+                                                                           TextAlignment="Center" FontSize="20"/>
+                                                            </Button>
+                                                        </StackPanel>
+                                                    </Grid>
+                                                </Border>
+                                            </Grid>
+                                        </Button>
+
+                                        <TextBlock Text="{Binding FileName}" ToolTip="{Binding FilePath}"
+                                                   Width="110" TextAlignment="Center" TextTrimming="CharacterEllipsis"
+                                                   FontSize="18" Margin="10,10,10,2" HorizontalAlignment="Center" Foreground="White"/>
+                                    </StackPanel>
+                                </Grid>
+                            </DataTemplate>
+                        </ItemsControl.ItemTemplate>
+                        <ItemsControl.ItemsPanel>
+                            <ItemsPanelTemplate>
+                                <uc:AlignableWrapPanel HorizontalAlignment="Center" HorizontalContentAlignment="Center"/>
+                            </ItemsPanelTemplate>
+                        </ItemsControl.ItemsPanel>
+                    </ItemsControl>
+                </StackPanel>
+
+                <uc:AlignableWrapPanel Grid.Row="3" HorizontalContentAlignment="Center" HorizontalAlignment="Center" Margin="0,5,0,15">
+                    <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://pixieditor.net"
+                            ui:Translator.TooltipKey="WEBSITE"
+                            Style="{StaticResource SocialMediaButton}" Tag="#e3002d"
+                            Content="/Images/SocialMedia/WebsiteIcon.png"/>
+                    <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://discord.gg/tzkQFDkqQS"
+                            Style="{StaticResource SocialMediaButton}" Tag="#5865F2" ui:Translator.TooltipKey="DISCORD"
+                            Content="/Images/SocialMedia/DiscordIcon.png"/>
+                    <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://reddit.com/r/PixiEditor"
+                            Style="{StaticResource SocialMediaButton}" Tag="#FF4500" ui:Translator.TooltipKey="REDDIT"
+                            Content="/Images/SocialMedia/RedditIcon.png"/>
+                    <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://store.steampowered.com/app/2218560"
+                            Style="{StaticResource SocialMediaButton}" Tag="#00adee" ui:Translator.TooltipKey="STEAM"
+                            Content="/Images/SocialMedia/SteamIcon.png"/>
+                    <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/PixiEditor/PixiEditor"
+                            Style="{StaticResource SocialMediaButton}" Tag="Black" ui:Translator.TooltipKey="GITHUB"
+                            Content="/Images/SocialMedia/GithubIcon.png"/>
+                    <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://www.youtube.com/channel/UCT5XvyvX1q5PAIaXfWmpsMQ"
+                            Style="{StaticResource SocialMediaButton}" Tag="#FF0000" ui:Translator.TooltipKey="YOUTUBE"
+                            Content="/Images/SocialMedia/YouTubeIcon.png"/>
+                    <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}"
+                            Visibility="{Binding ShowDonateButton,
+                            Converter={BoolToVisibilityConverter}}"
+                            CommandParameter="https://opencollective.com/pixieditor"
+                            Style="{StaticResource SocialMediaButton}" Tag="#d4af37" ui:Translator.TooltipKey="DONATE"
+                            Content="/Images/SocialMedia/DonateIcon.png"/>
+                    <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}"
+                            Visibility="{Binding ShowDonateButton,
+                            Converter={InverseBoolToVisibilityConverter}}"
+                            CommandParameter="https://store.steampowered.com/app/2435860/PixiEditor__Supporter_Pack/"
+                            Style="{StaticResource SocialMediaButton}" Tag="#d4af37" ui:Translator.TooltipKey="BUY_SUPPORTER_PACK"
+                            Content="/Images/SocialMedia/DonateIcon.png"/>
+                </uc:AlignableWrapPanel>
+            </Grid>
+        </ScrollViewer>
+
+        <ScrollViewer Grid.Row="1" Grid.Column="1"
+                      Visibility="{Binding NewsPanelCollapsed, Converter={converters:InverseBoolToVisibilityConverter}}">
+            <Border Padding="5" BorderThickness="3 0 0 0" BorderBrush="{StaticResource MainColor}">
+                <Grid>
+                    <Image gif:ImageBehavior.AnimatedSource="/Images/Processing.gif" HorizontalAlignment="Center" VerticalAlignment="Center"
+                           Visibility="{Binding IsFetchingNews, Converter={converters:BoolToVisibilityConverter}}"
+                           Height="50" gif:ImageBehavior.AnimationSpeedRatio="1.5"/>
+                    <TextBlock ui:Translator.Key="FAILED_FETCH_NEWS" Foreground="White" FontSize="20"
+                               VerticalAlignment="Center" TextAlignment="Center"
+                               Visibility="{Binding Path=FailedFetchingNews, Converter={converters:BoolToVisibilityConverter}}"/>
+                    <StackPanel Orientation="Vertical" Visibility="{Binding IsFetchingNews, Converter={converters:InverseBoolToVisibilityConverter}}">
+                        <TextBlock HorizontalAlignment="Center" ui:Translator.Key="NEWS" FontSize="18"/>
+                        <ItemsControl ItemsSource="{Binding Path=News}">
+                            <ItemsControl.ItemTemplate>
+                                <DataTemplate>
+                                    <newsFeed:NewsItem Margin="5" News="{Binding Path=.}"/>
+                                </DataTemplate>
+                            </ItemsControl.ItemTemplate>
+                        </ItemsControl>
+                    </StackPanel>
+                </Grid>
+            </Border>
+        </ScrollViewer>
+    </Grid>
+</Window>

+ 237 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Windows/HelloTherePopup.axaml.cs

@@ -0,0 +1,237 @@
+using System.IO;
+using System.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using CommunityToolkit.Mvvm.Input;
+using PixiEditor.Avalonia.ViewModels;
+using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Helpers;
+using PixiEditor.Models.DataHolders;
+using PixiEditor.Models.Services.NewsFeed;
+using PixiEditor.OperatingSystem;
+using PixiEditor.ViewModels.SubViewModels.Main;
+
+namespace PixiEditor.Views.Dialogs;
+
+/// <summary>
+/// Interaction logic for HelloTherePopup.xaml.
+/// </summary>
+internal partial class HelloTherePopup : Window
+{
+    public RecentlyOpenedCollection RecentlyOpened { get => FileViewModel.RecentlyOpened; }
+
+    public static readonly StyledProperty<FileViewModel> FileViewModelProperty =
+        AvaloniaProperty.Register<HelloTherePopup, FileViewModel>(nameof(FileViewModel));
+
+    public static readonly StyledProperty<bool> RecentlyOpenedEmptyProperty =
+        AvaloniaProperty.Register<HelloTherePopup, bool>(nameof(RecentlyOpenedEmpty));
+
+    public static readonly StyledProperty<bool> IsFetchingNewsProperty =
+        AvaloniaProperty.Register<HelloTherePopup, bool>(nameof(IsFetchingNews), defaultValue: default(bool));
+
+    public static readonly StyledProperty<bool> NewsPanelCollapsedProperty =
+        AvaloniaProperty.Register<HelloTherePopup, bool>(nameof(NewsPanelCollapsed), defaultValue: false);
+
+    public static readonly StyledProperty<bool> FailedFetchingNewsProperty =
+        AvaloniaProperty.Register<HelloTherePopup, bool>(nameof(FailedFetchingNews), defaultValue: false);
+
+    static HelloTherePopup()
+    {
+        NewsPanelCollapsedProperty.Changed.Subscribe(NewsPanelCollapsedChangedCallback);
+    }
+
+    public bool NewsPanelCollapsed
+    {
+        get { return (bool)GetValue(NewsPanelCollapsedProperty); }
+        set { SetValue(NewsPanelCollapsedProperty, value); }
+    }
+    public bool IsFetchingNews
+    {
+        get { return (bool)GetValue(IsFetchingNewsProperty); }
+        set { SetValue(IsFetchingNewsProperty, value); }
+    }
+
+    public bool FailedFetchingNews
+    {
+        get { return (bool)GetValue(FailedFetchingNewsProperty); }
+        set { SetValue(FailedFetchingNewsProperty, value); }
+    }
+
+    public ObservableRangeCollection<News> News { get; set; } = new ObservableRangeCollection<News>();
+
+    public static string VersionText =>
+        $"v{VersionHelpers.GetCurrentAssemblyVersionString()}";
+
+    public FileViewModel FileViewModel { get => (FileViewModel)GetValue(FileViewModelProperty); set => SetValue(FileViewModelProperty, value); }
+
+    public bool RecentlyOpenedEmpty { get => (bool)GetValue(RecentlyOpenedEmptyProperty); set => SetValue(RecentlyOpenedEmptyProperty, value); }
+
+    public RelayCommand OpenFileCommand { get; set; }
+
+    public RelayCommand OpenNewFileCommand { get; set; }
+
+    public RelayCommand<string> OpenRecentCommand { get; set; }
+
+    public RelayCommand<string> OpenInExplorerCommand { get; set; }
+
+    public bool IsClosing { get; private set; }
+
+    private NewsProvider NewsProvider { get; set; }
+
+    public bool NewsDisabled => _newsDisabled;
+
+    public bool ShowDonateButton => // Steam doesn't allow external donations :(
+#if STEAM
+        false;
+#else
+        true;
+#endif
+
+    private bool _newsDisabled = false;
+
+    public HelloTherePopup(FileViewModel fileViewModel)
+    {
+        DataContext = this;
+        Owner = Application.Current.MainWindow;
+        FileViewModel = fileViewModel;
+
+        OpenFileCommand = new RelayCommand(OpenFile);
+        OpenNewFileCommand = new RelayCommand(OpenNewFile);
+        OpenRecentCommand = new RelayCommand<string>(OpenRecent);
+        OpenInExplorerCommand = new RelayCommand<string>(OpenInExplorer, CanOpenInExplorer);
+
+        RecentlyOpenedEmpty = RecentlyOpened.Count == 0;
+        RecentlyOpened.CollectionChanged += RecentlyOpened_CollectionChanged;
+
+        _newsDisabled = IPreferences.Current.GetPreference<bool>(PreferencesConstants.DisableNewsPanel);
+
+        NewsProvider = new NewsProvider();
+
+        Closing += (_, _) => { IsClosing = true; };
+
+        InitializeComponent();
+
+        int newsWidth = 300;
+
+        NewsPanelCollapsed = IPreferences.Current.GetPreference<bool>(PreferencesConstants.NewsPanelCollapsed);
+
+        if (_newsDisabled || NewsPanelCollapsed)
+        {
+            newsColumn.Width = new GridLength(0);
+            newsWidth = 0;
+        }
+
+        if (RecentlyOpenedEmpty)
+        {
+            Width = 500 + newsWidth;
+            Height = 500;
+        }
+        else if (RecentlyOpened.Count < 4)
+        {
+            Width = 545 + newsWidth;
+            Height = 500;
+        }
+        else if (RecentlyOpened.Count < 7)
+        {
+            Width = 575 + newsWidth;
+            Height = 670;
+        }
+    }
+
+    private static void NewsPanelCollapsedChangedCallback(AvaloniaPropertyChangedEventArgs<bool> e)
+    {
+        HelloTherePopup helloTherePopup = (HelloTherePopup)e.Sender;
+
+        if(helloTherePopup._newsDisabled)
+            return;
+
+        if (newValue)
+        {
+            helloTherePopup.Width -= 300;
+            helloTherePopup.newsColumn.Width = new GridLength(0);
+        }
+        else
+        {
+            helloTherePopup.Width += 300;
+            helloTherePopup.newsColumn.Width = new GridLength(300);
+        }
+
+        IPreferences.Current.UpdatePreference(PreferencesConstants.NewsPanelCollapsed, e.NewValue.Value);
+    }
+
+    private void RecentlyOpened_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+    {
+        RecentlyOpenedEmpty = FileViewModel.RecentlyOpened.Count == 0;
+    }
+
+    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
+    {
+        e.CanExecute = true;
+    }
+
+    private void CommandBinding_Executed_Close(object sender, ExecutedRoutedEventArgs e)
+    {
+        SystemCommands.CloseWindow(this);
+    }
+
+    private void OpenFile()
+    {
+        Application.Current.MainWindow.Activate();
+        Close();
+        FileViewModel.OpenFromOpenFileDialog();
+    }
+
+    private void OpenNewFile()
+    {
+        Application.Current.MainWindow.Activate();
+        Close();
+        FileViewModel.CreateFromNewFileDialog();
+    }
+
+    private void OpenRecent(string parameter)
+    {
+        Application.Current.MainWindow.Activate();
+        Close();
+        FileViewModel.OpenRecent(parameter);
+    }
+
+    private void OpenInExplorer(string parameter)
+    {
+        IOperatingSystem.Current.OpenFolder(parameter);
+    }
+
+    private bool CanOpenInExplorer(string parameter) => File.Exists(parameter);
+
+    private async void HelloTherePopup_OnLoaded(object sender, RoutedEventArgs e)
+    {
+        if(_newsDisabled) return;
+
+        try
+        {
+            IsFetchingNews = true;
+            var news = await NewsProvider.FetchNewsAsync();
+            if (news is not null)
+            {
+                IsFetchingNews = false;
+                News.Clear();
+                News.AddRange(news);
+                if (NewsPanelCollapsed && News.Any(x => x.IsNew))
+                {
+                    NewsPanelCollapsed = false;
+                }
+            }
+            else
+            {
+                IsFetchingNews = false;
+                FailedFetchingNews = true;
+            }
+        }
+        catch(Exception ex)
+        {
+            IsFetchingNews = false;
+            FailedFetchingNews = true;
+            await CrashHelper.SendExceptionInfoToWebhook(ex);
+        }
+    }
+}

+ 51 - 0
src/PixiEditor.Avalonia/PixiEditor.Avalonia/Views/Windows/ResizeablePopup.cs

@@ -0,0 +1,51 @@
+using System.Windows;
+using Avalonia;
+using Avalonia.Controls;
+using PixiEditor.Models.Enums;
+using PixiEditor.Views.UserControls;
+
+namespace PixiEditor.Views.Dialogs;
+
+internal class ResizeablePopup : Window
+{
+    public static readonly StyledProperty<int> NewPercentageSizeProperty =
+        AvaloniaProperty.Register<ResizeablePopup, int>
+            (nameof(NewPercentageSize), 0);
+
+    public static readonly StyledProperty<SizeUnit> NewSelectedUnitProperty =
+        AvaloniaProperty.Register<ResizeablePopup, SizeUnit>(
+            nameof(NewSelectedUnit),
+            SizeUnit.Pixel);
+
+    public static readonly StyledProperty<int> NewAbsoluteHeightProperty =
+        AvaloniaProperty<ResizeablePopup>.Register<ResizeablePopup, int>(
+            nameof(NewAbsoluteHeight));
+
+    public static readonly StyledProperty<int> NewAbsoluteWidthProperty =
+        AvaloniaProperty.Register<ResizeablePopup, int>(
+            nameof(NewAbsoluteWidth));
+
+    public int NewPercentageSize
+    {
+        get => (int)GetValue(NewPercentageSizeProperty);
+        set => SetValue(NewPercentageSizeProperty, value);
+    }
+
+    public SizeUnit NewSelectedUnit
+    {
+        get => (SizeUnit)GetValue(NewSelectedUnitProperty);
+        set => SetValue(NewSelectedUnitProperty, value);
+    }
+
+    public int NewAbsoluteHeight
+    {
+        get => (int)GetValue(NewAbsoluteHeightProperty);
+        set => SetValue(NewAbsoluteHeightProperty, value);
+    }
+
+    public int NewAbsoluteWidth
+    {
+        get => (int)GetValue(NewAbsoluteWidthProperty);
+        set => SetValue(NewAbsoluteWidthProperty, value);
+    }
+}