diff --git a/UniSky/App.xaml.cs b/UniSky/App.xaml.cs index 2c52097..375cbd5 100644 --- a/UniSky/App.xaml.cs +++ b/UniSky/App.xaml.cs @@ -53,18 +53,21 @@ private void ConfigureServices() collection.AddLogging(c => c.AddDebug() .SetMinimumLevel(LogLevel.Trace)); + collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); - collection.AddSingleton(); + collection.AddScoped(); + collection.AddScoped(); collection.AddTransient(); collection.AddTransient(); - Ioc.Default.ConfigureServices(collection.BuildServiceProvider()); + ServiceContainer.Default.ConfigureServices(collection.BuildServiceProvider()); + Configurator.Formatters.Register("en", (locale) => new ShortTimespanFormatter("en")); + Configurator.Formatters.Register("en-GB", (locale) => new ShortTimespanFormatter("en")); + Configurator.Formatters.Register("en-US", (locale) => new ShortTimespanFormatter("en")); } protected override void OnActivated(IActivatedEventArgs args) diff --git a/UniSky/Controls/Compose/ComposeSheet.xaml b/UniSky/Controls/Compose/ComposeSheet.xaml index 6edd777..d9fc1cf 100644 --- a/UniSky/Controls/Compose/ComposeSheet.xaml +++ b/UniSky/Controls/Compose/ComposeSheet.xaml @@ -11,7 +11,7 @@ xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:compose="using:UniSky.ViewModels.Compose" - mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="320" + mc:Ignorable="d" d:DesignHeight="350" d:DesignWidth="480" Style="{StaticResource DefaultSheetControlStyle}" DataContext="{x:Bind ViewModel, Mode=OneWay}" PrimaryButtonCommand="{x:Bind ViewModel.PostCommand}" @@ -96,7 +96,7 @@ Height="48" Margin="0,0,8,0" VerticalAlignment="Top" - ProfilePicture="{x:Bind ViewModel.ReplyTo.Author.AvatarUrl, Mode=OneWay}"/> + ProfilePicture="{Binding ReplyTo.Author.AvatarUrl, Mode=OneWay}"/> - - - - - - - - - + diff --git a/UniSky/Controls/Compose/ComposeSheet.xaml.cs b/UniSky/Controls/Compose/ComposeSheet.xaml.cs index b83c5eb..71694a4 100644 --- a/UniSky/Controls/Compose/ComposeSheet.xaml.cs +++ b/UniSky/Controls/Compose/ComposeSheet.xaml.cs @@ -4,14 +4,17 @@ using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; using CommunityToolkit.Mvvm.DependencyInjection; +using Ipfs; using Microsoft.Extensions.DependencyInjection; using UniSky.Controls.Sheet; +using UniSky.Services; using UniSky.ViewModels.Compose; using UniSky.ViewModels.Posts; using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.Resources; using Windows.Foundation; using Windows.Foundation.Collections; +using Windows.Foundation.Metadata; using Windows.UI.Popups; using Windows.UI.ViewManagement; using Windows.UI.Xaml; @@ -50,6 +53,11 @@ public ComposeSheet() this.strings = ResourceLoader.GetForCurrentView(); } + protected override void OnBottomInsetsChanged(double leftInset, double rightInset) + { + FooterContainer.Padding = new Thickness(leftInset, 0, rightInset, 2); + } + public bool Not(bool b, bool a) => !a && !b; @@ -70,11 +78,11 @@ private void OnShowing(SheetControl sender, SheetShowingEventArgs e) if (e.Parameter is PostViewModel replyTo) { - this.ViewModel = ActivatorUtilities.CreateInstance(Ioc.Default, replyTo); + this.ViewModel = ActivatorUtilities.CreateInstance(ServiceContainer.Scoped, replyTo, Controller); } else { - this.ViewModel = ActivatorUtilities.CreateInstance(Ioc.Default); + this.ViewModel = ActivatorUtilities.CreateInstance(ServiceContainer.Scoped, Controller); } } @@ -104,10 +112,17 @@ private async void OnHiding(SheetControl sender, SheetHidingEventArgs e) var deferral = e.GetDeferral(); try { - if (ViewModel.IsDirty && await new ComposeDiscardDraftDialog().ShowAsync() != ContentDialogResult.Primary) + if (ViewModel.IsDirty) { - e.Cancel = true; - return; + var discardDraftDialog = new ComposeDiscardDraftDialog(); + if (Controller != null && ApiInformation.IsApiContractPresent(typeof(UniversalApiContract).FullName, 8)) + discardDraftDialog.XamlRoot = Controller.Root.XamlRoot; + + if (await discardDraftDialog.ShowAsync() != ContentDialogResult.Primary) + { + e.Cancel = true; + return; + } } } finally diff --git a/UniSky/Controls/PreviewPaneAuroraControl.xaml.cs b/UniSky/Controls/PreviewPaneAuroraControl.xaml.cs index d364164..f79d65d 100644 --- a/UniSky/Controls/PreviewPaneAuroraControl.xaml.cs +++ b/UniSky/Controls/PreviewPaneAuroraControl.xaml.cs @@ -1,4 +1,5 @@ using Microsoft.Toolkit.Uwp.Helpers; +using Microsoft.Toolkit.Uwp.UI.Helpers; using Microsoft.Toolkit.Uwp.UI.Media.Geometry; using Windows.UI; using Windows.UI.Xaml; @@ -16,23 +17,6 @@ namespace System.Windows.Shell.Aurora /// public partial class PreviewPaneAuroraControl : UserControl { - //private record ColorSet(Color Aurora, Color Background); - - //private ColorSet[] _colorSets = new[] - //{ - // new ColorSet(Color.FromArgb(255, 0x85, 0x99, 0xB4), Color.FromArgb(255, 0x5A, 0x6B, 0x7D)), // Default - // new ColorSet(Color.FromArgb(255, 0x51, 0x90, 0xDA), Color.FromArgb(255, 0x24, 0x43, 0x8E)), // Documents - // new ColorSet(Color.FromArgb(255, 0xF2, 0xA4, 0x7B), Color.FromArgb(255, 0xD2, 0x64, 0x2A)), // Contacts - // new ColorSet(Color.FromArgb(255, 0xDA, 0x51, 0x51), Color.FromArgb(255, 0x74, 0x14, 0x14)), // Music - // new ColorSet(Color.FromArgb(255, 0x9E, 0xCA, 0x4E), Color.FromArgb(255, 0x6E, 0x97, 0x24)), // Games - // new ColorSet(Color.FromArgb(255, 0x6F, 0x49, 0x70), Color.FromArgb(255, 0x26, 0x08, 0x27)), // Photos - //}; - - //private static Random _random = new Random(); - //private int _i = _random.Next(6); - //private Color _color = Color.FromArgb(255, 0x85, 0x99, 0xB4); - //private TimeSpan _duration = TimeSpan.FromSeconds(0.5); - public Color Color { get { return (Color)GetValue(ColorProperty); } @@ -59,7 +43,7 @@ private static void OnColorChanged(DependencyObject d, DependencyPropertyChanged var control = (PreviewPaneAuroraControl)d; var hslColor = newValue.ToHsl(); - if (control.ActualTheme == ElementTheme.Dark) + if (Application.Current.RequestedTheme == ApplicationTheme.Dark) { var hslBackground = hslColor with { L = hslColor.L * 0.7 }; var colorBackground = ColorHelper.FromHsl(hslBackground.H, hslBackground.S, hslBackground.L); diff --git a/UniSky/Controls/RichTextBlock/RichTextBlock.cs b/UniSky/Controls/RichTextBlock/RichTextBlock.cs index d51c79a..064eae0 100644 --- a/UniSky/Controls/RichTextBlock/RichTextBlock.cs +++ b/UniSky/Controls/RichTextBlock/RichTextBlock.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; using CommunityToolkit.Mvvm.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Toolkit.Uwp.UI.Extensions; using UniSky.Pages; using UniSky.Services; @@ -139,7 +140,7 @@ private void Hyperlink_Click(Hyperlink sender, HyperlinkClickEventArgs args) if (sender.GetValue(HyperlinkUrlProperty) is not Uri { Scheme: "unisky" } uri) return; - var service = Ioc.Default.GetRequiredService() + var service = ServiceContainer.Scoped.GetRequiredService() .GetNavigationService("Home"); var path = uri.PathAndQuery.Split('/', StringSplitOptions.RemoveEmptyEntries); diff --git a/UniSky/Controls/Sheet/SheetControl.cs b/UniSky/Controls/Sheet/SheetControl.cs index 10b5ef4..5d25026 100644 --- a/UniSky/Controls/Sheet/SheetControl.cs +++ b/UniSky/Controls/Sheet/SheetControl.cs @@ -1,15 +1,20 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection.PortableExecutable; using System.Runtime.InteropServices.WindowsRuntime; using System.Threading.Tasks; using System.Windows.Input; using CommunityToolkit.Mvvm.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Toolkit.Uwp.Deferred; using Microsoft.Toolkit.Uwp.UI.Extensions; using UniSky.Services; using Windows.ApplicationModel; using Windows.Foundation; +using Windows.UI.ViewManagement; +using Windows.UI.WindowManagement; +using Windows.UI.WindowManagement.Preview; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; @@ -188,16 +193,63 @@ public bool IsSecondaryButtonEnabled public static readonly DependencyProperty IsSecondaryButtonEnabledProperty = DependencyProperty.Register("IsSecondaryButtonEnabled", typeof(bool), typeof(SheetControl), new PropertyMetadata(true)); + public Size PreferredWindowSize + { + get { return (Size)GetValue(PreferredWindowSizeProperty); } + set { SetValue(PreferredWindowSizeProperty, value); } + } + + // Using a DependencyProperty as the backing store for PreferredWindowSize. This enables animation, styling, binding, etc... + public static readonly DependencyProperty PreferredWindowSizeProperty = + DependencyProperty.Register("PreferredWindowSize", typeof(Size), typeof(SheetControl), new PropertyMetadata(new Size(320, 400))); + + public event TypedEventHandler Showing; public event TypedEventHandler Shown; public event TypedEventHandler Hiding; public event TypedEventHandler Hidden; + public ISheetController Controller { get; private set; } + public SheetControl() { this.DefaultStyleKey = typeof(SheetControl); } + internal void SetSheetController(ISheetController controller) + { + Controller = controller; + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + if (Controller != null && Controller.IsFullWindow) + { + VisualStateManager.GoToState(this, "FullWindow", false); + var titleBarDragArea = this.FindDescendantByName("TitleBarDragArea"); + Controller.SafeAreaService.SafeAreaUpdated += OnSafeAreaUpdated; + Controller.SafeAreaService.SetTitleBar(titleBarDragArea); + + var inputPane = InputPane.GetForCurrentView(); + inputPane.Showing += OnInputPaneShowing; + inputPane.Hiding += OnInputPaneHiding; + + this.SizeChanged += OnSizeChanged; + } + else + { + VisualStateManager.GoToState(this, "Standard", false); + } + } + + private void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + var rightButton = this.FindDescendantByName("PrimaryTitleBarButton"); + this.OnBottomInsetsChanged(0, rightButton.ActualWidth + 16); + } + internal void InvokeShowing(object parameter) { Showing?.Invoke(this, new SheetShowingEventArgs(parameter)); @@ -220,7 +272,46 @@ internal void InvokeShown() internal void InvokeHidden() { + if (Controller.IsFullWindow) + { + Controller.SafeAreaService.SafeAreaUpdated += OnSafeAreaUpdated; + } + Hidden?.Invoke(this, new RoutedEventArgs()); } + + private void OnInputPaneShowing(InputPane sender, InputPaneVisibilityEventArgs args) + { + var ButtonsGrid = (Grid)this.FindDescendantByName("ButtonsGrid"); + ButtonsGrid.Margin = new Thickness(0, 0, 0, args.OccludedRect.Height); + args.EnsuredFocusedElementInView = true; + } + + private void OnInputPaneHiding(InputPane sender, InputPaneVisibilityEventArgs args) + { + var ButtonsGrid = (Grid)this.FindDescendantByName("ButtonsGrid"); + ButtonsGrid.Margin = new Thickness(0, 0, 0, args.OccludedRect.Height); + args.EnsuredFocusedElementInView = true; + } + + private void OnSafeAreaUpdated(object sender, SafeAreaUpdatedEventArgs e) + { + var titleBarGrid = (Grid)this.FindDescendantByName("TitleBarGrid"); + + if (e.SafeArea.HasTitleBar) + { + titleBarGrid.Height = e.SafeArea.Bounds.Top; + titleBarGrid.Padding = new Thickness(); + } + else + { + titleBarGrid.Height = 42; + titleBarGrid.Padding = new Thickness(0, e.SafeArea.Bounds.Top, 0, 4); + } + + Margin = new Thickness(e.SafeArea.Bounds.Left, 0, e.SafeArea.Bounds.Right, e.SafeArea.Bounds.Bottom); + } + + protected virtual void OnBottomInsetsChanged(double leftInset, double rightInset) { } } } diff --git a/UniSky/Controls/Sheet/SheetControl.xaml b/UniSky/Controls/Sheet/SheetControl.xaml index 5ee46e2..fe8a544 100644 --- a/UniSky/Controls/Sheet/SheetControl.xaml +++ b/UniSky/Controls/Sheet/SheetControl.xaml @@ -10,37 +10,68 @@ - + + Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> - + + - - - - - - - + + + Grid.ColumnSpan="3" + VerticalAlignment="Center" + HorizontalAlignment="Center" + Content="{TemplateBinding TitleContent}" + ContentTemplate="{TemplateBinding TitleContentTemplate}" + Margin="16,0"/> + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UniSky/Controls/Sheet/SheetRootControl.xaml.cs b/UniSky/Controls/Sheet/SheetRootControl.xaml.cs index 716ef95..f0f000f 100644 --- a/UniSky/Controls/Sheet/SheetRootControl.xaml.cs +++ b/UniSky/Controls/Sheet/SheetRootControl.xaml.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Toolkit.Uwp.UI; using Microsoft.Toolkit.Uwp.UI.Extensions; using UniSky.Pages; @@ -86,7 +87,7 @@ internal void ShowSheet(SheetControl control, object parameter) Window.Current.SetTitleBar(TitleBar); - var safeAreaService = Ioc.Default.GetRequiredService(); + var safeAreaService = ServiceContainer.Scoped.GetRequiredService(); safeAreaService.SafeAreaUpdated += OnSafeAreaUpdated; var systemNavigationManager = SystemNavigationManager.GetForCurrentView(); @@ -106,7 +107,6 @@ internal async Task HideSheetAsync() try { - // TODO: allow deferrals if (SheetRoot.Child is SheetControl control) { if (!await control.InvokeHidingAsync()) @@ -115,7 +115,7 @@ internal async Task HideSheetAsync() VisualStateManager.GoToState(this, "Closed", true); - var safeAreaService = Ioc.Default.GetRequiredService(); + var safeAreaService = ServiceContainer.Scoped.GetRequiredService(); safeAreaService.SafeAreaUpdated -= OnSafeAreaUpdated; var systemNavigationManager = SystemNavigationManager.GetForCurrentView(); diff --git a/UniSky/DataTemplates/FeedTemplates.xaml b/UniSky/DataTemplates/FeedTemplates.xaml index 83b3f02..5cbb166 100644 --- a/UniSky/DataTemplates/FeedTemplates.xaml +++ b/UniSky/DataTemplates/FeedTemplates.xaml @@ -217,7 +217,7 @@ diff --git a/UniSky/Extensions/Hairline.cs b/UniSky/Extensions/Hairline.cs index 7ccd099..804ec8f 100644 --- a/UniSky/Extensions/Hairline.cs +++ b/UniSky/Extensions/Hairline.cs @@ -6,6 +6,7 @@ using Windows.UI.Xaml.Controls; using Windows.UI.Xaml; using Windows.Graphics.Display; +using System.Threading; namespace UniSky.Extensions { @@ -37,15 +38,23 @@ public static void SetMargin(DependencyObject obj, Thickness value) public static readonly DependencyProperty MarginProperty = DependencyProperty.RegisterAttached("Margin", typeof(Thickness), typeof(Hairline), new PropertyMetadata(DependencyProperty.UnsetValue, OnMarginPropertyChanged)); - private static List> Elements { get; set; } = []; - private static DisplayInformation DisplayInfo { get; set; } + private static ThreadLocal>> Elements { get; } + = new ThreadLocal>>(() => new List>(), true); + + private static ThreadLocal DisplayInfo { get; } + = new ThreadLocal(() => + { + var info = DisplayInformation.GetForCurrentView(); + info.DpiChanged += OnDpiChanged; + return info; + }, true); private static void OnThicknessPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not (Grid or StackPanel or Control or Border)) return; - Elements.Add(new WeakReference(d)); + Elements.Value.Add(new WeakReference(d)); if (DisplayInfo == null) { @@ -53,7 +62,7 @@ private static void OnThicknessPropertyChanged(DependencyObject d, DependencyPro } var newValue = (Thickness)(e.NewValue); - ApplyBorderThickness(d, newValue, DisplayInfo); + ApplyBorderThickness(d, newValue, DisplayInfo.Value); } private static void OnMarginPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) @@ -61,7 +70,7 @@ private static void OnMarginPropertyChanged(DependencyObject d, DependencyProper if (d is not FrameworkElement) return; - Elements.Add(new WeakReference(d)); + Elements.Value.Add(new WeakReference(d)); if (DisplayInfo == null) { @@ -69,7 +78,7 @@ private static void OnMarginPropertyChanged(DependencyObject d, DependencyProper } var newValue = (Thickness)(e.NewValue); - ApplyMargin(d, newValue, DisplayInfo); + ApplyMargin(d, newValue, DisplayInfo.Value); } private static void ApplyBorderThickness(DependencyObject d, Thickness newValue, DisplayInformation info) @@ -117,14 +126,12 @@ private static void ApplyMargin(DependencyObject d, Thickness newValue, DisplayI public static void Initialize() { - DisplayInfo = DisplayInformation.GetForCurrentView(); - DisplayInfo.DpiChanged += OnDpiChanged; - ApplyResources(DisplayInfo); + ApplyResources(DisplayInfo.Value); } private static void OnDpiChanged(DisplayInformation sender, object args) { - foreach (var item in Elements) + foreach (var item in Elements.Value) { if (item.TryGetTarget(out var element)) { diff --git a/UniSky/Helpers/Composition/BirdAnimation.cs b/UniSky/Helpers/Composition/BirdAnimation.cs index 8b5b071..cfbcb5e 100644 --- a/UniSky/Helpers/Composition/BirdAnimation.cs +++ b/UniSky/Helpers/Composition/BirdAnimation.cs @@ -59,12 +59,14 @@ public static void RunBirdAnimation(FrameworkElement frame) var scaleAnimation = compositor.CreateVector2KeyFrameAnimation(); scaleAnimation.InsertKeyFrame(1.0f, new Vector2(scale, scale), ease); + scaleAnimation.DelayTime = TimeSpan.FromSeconds(0.15); scaleAnimation.Duration = TimeSpan.FromSeconds(0.15); scaleAnimation.Target = "Scale"; group.Add(scaleAnimation); var offsetAnimation = compositor.CreateVector2KeyFrameAnimation(); offsetAnimation.InsertKeyFrame(1.0f, new Vector2(offsetX, offsetY - (6 * scale)), ease); + offsetAnimation.DelayTime = TimeSpan.FromSeconds(0.15); offsetAnimation.Duration = TimeSpan.FromSeconds(0.15); offsetAnimation.Target = "Offset"; group.Add(offsetAnimation); @@ -72,6 +74,7 @@ public static void RunBirdAnimation(FrameworkElement frame) var scaleAnimation2 = compositor.CreateVector3KeyFrameAnimation(); scaleAnimation2.InsertKeyFrame(0.5f, new Vector3(1.1f), ease); scaleAnimation2.InsertKeyFrame(1.0f, new Vector3(1.0f), ease2); + scaleAnimation2.DelayTime = TimeSpan.FromSeconds(0.15); scaleAnimation2.Duration = TimeSpan.FromSeconds(0.3); scaleAnimation2.Target = "Scale"; diff --git a/UniSky/Package.appxmanifest b/UniSky/Package.appxmanifest index 8b8f8e6..f1b340a 100644 --- a/UniSky/Package.appxmanifest +++ b/UniSky/Package.appxmanifest @@ -4,13 +4,14 @@ xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" - xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" - IgnorableNamespaces="uap mp"> + xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" + xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" + IgnorableNamespaces="uap mp uap5 rescap"> + Version="1.0.151.0" /> @@ -60,5 +61,6 @@ + \ No newline at end of file diff --git a/UniSky/Pages/FeedsPage.xaml.cs b/UniSky/Pages/FeedsPage.xaml.cs index 6fd112f..45112e9 100644 --- a/UniSky/Pages/FeedsPage.xaml.cs +++ b/UniSky/Pages/FeedsPage.xaml.cs @@ -39,9 +39,9 @@ protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); - this.ViewModel = ActivatorUtilities.CreateInstance(Ioc.Default); + this.ViewModel = ActivatorUtilities.CreateInstance(ServiceContainer.Scoped); - var safeAreaService = Ioc.Default.GetRequiredService(); + var safeAreaService = ServiceContainer.Scoped.GetRequiredService(); safeAreaService.SafeAreaUpdated += OnSafeAreaUpdated; } diff --git a/UniSky/Pages/HomePage.xaml.cs b/UniSky/Pages/HomePage.xaml.cs index b27dbc4..de80b63 100644 --- a/UniSky/Pages/HomePage.xaml.cs +++ b/UniSky/Pages/HomePage.xaml.cs @@ -39,16 +39,16 @@ protected override void OnNavigatedTo(NavigationEventArgs e) Window.Current.SetTitleBar(TitleBarDrag); - var safeAreaService = Ioc.Default.GetRequiredService(); + var safeAreaService = ServiceContainer.Scoped.GetRequiredService(); safeAreaService.SafeAreaUpdated += OnSafeAreaUpdated; - var serviceLocator = Ioc.Default.GetRequiredService(); + var serviceLocator = ServiceContainer.Scoped.GetRequiredService(); var service = serviceLocator.GetNavigationService("Home"); service.Frame = NavigationFrame; if (e.Parameter is string session || e.Parameter is ATDid did && (session = did.Handler) != null) { - ViewModel = ActivatorUtilities.CreateInstance(Ioc.Default, session); + ViewModel = ActivatorUtilities.CreateInstance(ServiceContainer.Scoped, session); } } diff --git a/UniSky/Pages/LoginPage.xaml.cs b/UniSky/Pages/LoginPage.xaml.cs index 7858183..4b37c5e 100644 --- a/UniSky/Pages/LoginPage.xaml.cs +++ b/UniSky/Pages/LoginPage.xaml.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices.WindowsRuntime; using CommunityToolkit.Mvvm.DependencyInjection; using Microsoft.Extensions.DependencyInjection; +using UniSky.Services; using UniSky.ViewModels; using Windows.Foundation; using Windows.Foundation.Collections; @@ -32,7 +33,7 @@ public LoginViewModel ViewModel public LoginPage() { this.InitializeComponent(); - this.ViewModel = ActivatorUtilities.CreateInstance(Ioc.Default); + this.ViewModel = ActivatorUtilities.CreateInstance(ServiceContainer.Scoped); } protected override void OnNavigatedTo(NavigationEventArgs e) diff --git a/UniSky/Pages/ProfilePage.xaml.cs b/UniSky/Pages/ProfilePage.xaml.cs index f70fee9..694bb0a 100644 --- a/UniSky/Pages/ProfilePage.xaml.cs +++ b/UniSky/Pages/ProfilePage.xaml.cs @@ -73,15 +73,15 @@ protected override void OnNavigatedTo(NavigationEventArgs e) if (e.Parameter is Uri { Scheme: "unisky" } uri) HandleUniskyProtocol(uri); else if (e.Parameter is ATObject basic) - this.DataContext = ViewModel = ActivatorUtilities.CreateInstance(Ioc.Default, basic); + this.DataContext = ViewModel = ActivatorUtilities.CreateInstance(ServiceContainer.Default, basic); - var safeAreaService = Ioc.Default.GetRequiredService(); + var safeAreaService = ServiceContainer.Scoped.GetRequiredService(); safeAreaService.SafeAreaUpdated += OnSafeAreaUpdated; } protected override void OnNavigatedFrom(NavigationEventArgs e) { - var safeAreaService = Ioc.Default.GetRequiredService(); + var safeAreaService = ServiceContainer.Scoped.GetRequiredService(); safeAreaService.SafeAreaUpdated -= OnSafeAreaUpdated; safeAreaService.SetTitlebarTheme(ElementTheme.Default); } @@ -95,7 +95,7 @@ private void HandleUniskyProtocol(Uri uri) } if (ATDid.TryCreate(path[1], out var did)) - this.DataContext = ViewModel = ActivatorUtilities.CreateInstance(Ioc.Default, did); + this.DataContext = ViewModel = ActivatorUtilities.CreateInstance(ServiceContainer.Default, did); } private void Page_Loaded(object sender, RoutedEventArgs e) @@ -236,7 +236,7 @@ private void UpdateSizeDependentProperties() if (ProfileContainer.ActualHeight == 0) return; - var safeAreaService = Ioc.Default.GetRequiredService(); + var safeAreaService = ServiceContainer.Scoped.GetRequiredService(); var titleBarHeight = (float)safeAreaService.State.Bounds.Top + 4; var stickyHeight = (float)StickyFooter.ActualHeight; diff --git a/UniSky/RootPage.xaml.cs b/UniSky/RootPage.xaml.cs index 660a65b..e8791e6 100644 --- a/UniSky/RootPage.xaml.cs +++ b/UniSky/RootPage.xaml.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices.WindowsRuntime; using System.Xml; using CommunityToolkit.Mvvm.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Graphics.Canvas; using Microsoft.Graphics.Canvas.Geometry; using UniSky.Helpers.Composition; @@ -51,11 +52,11 @@ protected override void OnNavigatedTo(NavigationEventArgs e) { base.OnNavigatedTo(e); - var serviceLocator = Ioc.Default.GetRequiredService(); + var serviceLocator = ServiceContainer.Scoped.GetRequiredService(); var service = serviceLocator.GetNavigationService("Root"); service.Frame = RootFrame; - var sessionService = Ioc.Default.GetRequiredService(); + var sessionService = ServiceContainer.Scoped.GetRequiredService(); if (ApplicationData.Current.LocalSettings.Values.TryGetValue("LastUsedUser", out var userObj) && userObj is string user) { diff --git a/UniSky/Services/ISafeAreaService.cs b/UniSky/Services/ISafeAreaService.cs deleted file mode 100644 index 329a9f9..0000000 --- a/UniSky/Services/ISafeAreaService.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using Windows.UI.Xaml; - -namespace UniSky.Services -{ - internal interface ISafeAreaService - { - SafeAreaInfo State { get; } - event EventHandler SafeAreaUpdated; - - void SetTitlebarTheme(ElementTheme theme); - } -} \ No newline at end of file diff --git a/UniSky/Services/ISheetService.cs b/UniSky/Services/ISheetService.cs index 792fde2..629d5ab 100644 --- a/UniSky/Services/ISheetService.cs +++ b/UniSky/Services/ISheetService.cs @@ -5,7 +5,6 @@ namespace UniSky.Services { public interface ISheetService { - Task ShowAsync(object parameter = null) where T : SheetControl, new(); - Task TryCloseAsync(); + Task ShowAsync(object parameter = null) where T : SheetControl, new(); } } \ No newline at end of file diff --git a/UniSky/Services/NavigationServiceLocator.cs b/UniSky/Services/NavigationServiceLocator.cs index b3cc916..d087c06 100644 --- a/UniSky/Services/NavigationServiceLocator.cs +++ b/UniSky/Services/NavigationServiceLocator.cs @@ -14,7 +14,7 @@ public INavigationService GetNavigationService(string name) if (_services.TryGetValue(name, out var service)) return service; - service = ActivatorUtilities.CreateInstance(Ioc.Default); + service = ActivatorUtilities.CreateInstance(ServiceContainer.Scoped); _services.Add(name, service); return service; diff --git a/UniSky/Services/ProtocolService.cs b/UniSky/Services/ProtocolService.cs index 6594cfa..7dee517 100644 --- a/UniSky/Services/ProtocolService.cs +++ b/UniSky/Services/ProtocolService.cs @@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.DependencyInjection; using FishyFlip; using FishyFlip.Events; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using UniSky.Models; @@ -34,7 +35,7 @@ private void OnSessionUpdated(object sender, SessionUpdatedEventArgs e) logger.LogInformation("Session updated, saving new tokens!"); var session = new SessionModel(true, e.InstanceUri.Host.ToLowerInvariant(), e.Session.Session, e.Session); - var sessionService = Ioc.Default.GetRequiredService(); + var sessionService = ServiceContainer.Scoped.GetRequiredService(); sessionService.SaveSession(session); } } diff --git a/UniSky/Services/SafeArea/AppWindowSafeAreaService.cs b/UniSky/Services/SafeArea/AppWindowSafeAreaService.cs new file mode 100644 index 0000000..8505d6e --- /dev/null +++ b/UniSky/Services/SafeArea/AppWindowSafeAreaService.cs @@ -0,0 +1,120 @@ +using System; +using Microsoft.Toolkit.Uwp.UI.Helpers; +using Windows.UI; +using Windows.UI.Xaml; +using Windows.UI.WindowManagement; +using Windows.Foundation; +using Windows.UI.WindowManagement.Preview; +using System.Linq; + +namespace UniSky.Services; + +internal class AppWindowSafeAreaService : ISafeAreaService +{ + private readonly AppWindow _appWindow; + private readonly ThemeListener _themeListener; + + private SafeAreaInfo _state; + private event EventHandler _event; + + public SafeAreaInfo State + => _state; + + public event EventHandler SafeAreaUpdated + { + add + { + _event += value; + Update(); + } + + remove => _event -= value; + } + + public AppWindowSafeAreaService(AppWindow appWindow) + { + this._appWindow = appWindow; + this._themeListener = new ThemeListener(); + + WindowManagementPreview.SetPreferredMinSize(appWindow, new Size(320, 320)); + + var titleBar = appWindow.TitleBar; + titleBar.ExtendsContentIntoTitleBar = true; + + appWindow.Changed += OnChanged; + + _state = new SafeAreaInfo(true, true, new Thickness(), ElementTheme.Default); + SetTitlebarTheme(ElementTheme.Default); + Update(); + } + + private void OnChanged(AppWindow sender, AppWindowChangedEventArgs args) + { + Update(); + } + + private void Update() + { + if (_appWindow.TitleBar.IsVisible) + { + _state = _state with { HasTitleBar = true }; + } + else + { + _state = _state with { HasTitleBar = false }; + } + + UpdateBounds(); + } + + private void UpdateBounds() + { + var top = 0.0f; + var left = 0.0f; + var right = 0.0f; + var bottom = 0.0f; + + var titleBar = _appWindow.TitleBar; + if (titleBar.IsVisible) + { + top += (float)titleBar.GetTitleBarOcclusions().Max(t => t.OccludingRect.Height); + } + + _state = _state with { Bounds = new Thickness(left, top, right, bottom) }; + _event?.Invoke(this, new SafeAreaUpdatedEventArgs() { SafeArea = _state }); + } + + public void SetTitlebarTheme(ElementTheme theme) + { + var actualTheme = theme switch + { + ElementTheme.Default => _themeListener.CurrentTheme == ApplicationTheme.Dark + ? ElementTheme.Dark + : ElementTheme.Light, + _ => theme + }; + + var appTitleBar = _appWindow.TitleBar; + appTitleBar.ButtonBackgroundColor = Colors.Transparent; + appTitleBar.ButtonInactiveBackgroundColor = Colors.Transparent; + + if (actualTheme == ElementTheme.Dark) + { + appTitleBar.ButtonForegroundColor = Colors.White; + appTitleBar.ButtonInactiveForegroundColor = Colors.LightGray; + } + else + { + appTitleBar.ButtonForegroundColor = Colors.Black; + appTitleBar.ButtonInactiveForegroundColor = Colors.DarkGray; + } + + _state = _state with { Theme = theme }; + _event?.Invoke(this, new SafeAreaUpdatedEventArgs() { SafeArea = _state }); + } + + public void SetTitleBar(UIElement uiElement) + { + // N/A afaict? + } +} \ No newline at end of file diff --git a/UniSky/Services/SafeAreaService.cs b/UniSky/Services/SafeArea/CoreWindowSafeAreaService.cs similarity index 93% rename from UniSky/Services/SafeAreaService.cs rename to UniSky/Services/SafeArea/CoreWindowSafeAreaService.cs index e98bad4..f751dde 100644 --- a/UniSky/Services/SafeAreaService.cs +++ b/UniSky/Services/SafeArea/CoreWindowSafeAreaService.cs @@ -1,21 +1,13 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Humanizer; -using Microsoft.Toolkit.Uwp.UI.Extensions; using Microsoft.Toolkit.Uwp.UI.Helpers; using Windows.ApplicationModel.Core; using Windows.Foundation; using Windows.Foundation.Metadata; -using Windows.Graphics.Display; using Windows.Phone; using Windows.UI; using Windows.UI.Core; using Windows.UI.ViewManagement; using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; namespace UniSky.Services; @@ -26,7 +18,7 @@ public class SafeAreaUpdatedEventArgs : EventArgs public SafeAreaInfo SafeArea { get; init; } } -internal class SafeAreaService : ISafeAreaService +internal class CoreWindowSafeAreaService : ISafeAreaService { private readonly CoreWindow _window; private readonly ApplicationView _applicationView; @@ -40,7 +32,7 @@ internal class SafeAreaService : ISafeAreaService public SafeAreaInfo State => _state; - public SafeAreaService() + public CoreWindowSafeAreaService() { _window = CoreWindow.GetForCurrentThread(); _window.SizeChanged += CoreWindow_SizeChanged; @@ -49,7 +41,11 @@ public SafeAreaService() _applicationView = ApplicationView.GetForCurrentView(); _coreApplicationView = CoreApplication.GetCurrentView(); - _applicationView.SetPreferredMinSize(new Size(320, 640)); + if (_coreApplicationView.IsMain) + _applicationView.SetPreferredMinSize(new Size(320, 640)); + else + _applicationView.SetPreferredMinSize(new Size(320, 320)); + _applicationView.SetDesiredBoundsMode(ApplicationViewBoundsMode.UseCoreWindow); _applicationView.VisibleBoundsChanged += ApplicationView_VisibleBoundsChanged; @@ -205,4 +201,9 @@ private void OnThemeChanged(ThemeListener sender) { SetTitlebarTheme(_state.Theme); } + + public void SetTitleBar(UIElement uiElement) + { + Window.Current.SetTitleBar(uiElement); + } } \ No newline at end of file diff --git a/UniSky/Services/SafeArea/ISafeAreaService.cs b/UniSky/Services/SafeArea/ISafeAreaService.cs new file mode 100644 index 0000000..6e87e6e --- /dev/null +++ b/UniSky/Services/SafeArea/ISafeAreaService.cs @@ -0,0 +1,14 @@ +using System; +using Windows.UI.Xaml; + +namespace UniSky.Services; + +public interface ISafeAreaService +{ + SafeAreaInfo State { get; } + + event EventHandler SafeAreaUpdated; + + void SetTitleBar(UIElement uiElement); + void SetTitlebarTheme(ElementTheme theme); +} \ No newline at end of file diff --git a/UniSky/Services/ServiceContainer.cs b/UniSky/Services/ServiceContainer.cs new file mode 100644 index 0000000..d67157d --- /dev/null +++ b/UniSky/Services/ServiceContainer.cs @@ -0,0 +1,171 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.Extensions.DependencyInjection; + +namespace UniSky.Services; + +#nullable enable + +/// +/// A type that facilitates the use of the type. +/// The provides the ability to configure services in a singleton, thread-safe +/// service provider instance, which can then be used to resolve service instances. +/// The first step to use this feature is to declare some services, for instance: +/// +/// public interface ILogger +/// { +/// void Log(string text); +/// } +/// +/// +/// public class ConsoleLogger : ILogger +/// { +/// void Log(string text) => Console.WriteLine(text); +/// } +/// +/// Then the services configuration should then be done at startup, by calling the +/// method and passing an instance with the services to use. That instance can +/// be from any library offering dependency injection functionality, such as Microsoft.Extensions.DependencyInjection. +/// For instance, using that library, can be used as follows in this example: +/// +/// ServiceContainer.Scoped.ConfigureServices( +/// new ServiceCollection() +/// .AddSingleton<ILogger, Logger>() +/// .BuildServiceProvider()); +/// +/// Finally, you can use the instance (which implements ) +/// to retrieve the service instances from anywhere in your application, by doing as follows: +/// +/// ServiceContainer.Scoped.GetService<ILogger>().Log("Hello world!"); +/// +/// +public sealed partial class ServiceContainer : IServiceProvider +{ + /// + /// Gets the default instance. + /// + public static ServiceContainer Default { get; } = new(); + private static ThreadLocal _scoped = new ThreadLocal(() => Default.CreateScope(), true); + public static IServiceProvider Scoped => _scoped.Value.ServiceProvider; + + /// + /// The instance to use, if initialized. + /// + private volatile IServiceProvider? serviceProvider; + + /// + public object? GetService(Type serviceType) + { + // As per section I.12.6.6 of the official CLI ECMA-335 spec: + // "[...] read and write access to properly aligned memory locations no larger than the native + // word size is atomic when all the write accesses to a location are the same size. Atomic writes + // shall alter no bits other than those written. Unless explicit layout control is used [...], + // data elements no larger than the natural word size [...] shall be properly aligned. + // Object references shall be treated as though they are stored in the native word size." + // The field being accessed here is of native int size (reference type), and is only ever accessed + // directly and atomically by a compare exchange instruction (see below), or here. We can therefore + // assume this read is thread safe with respect to accesses to this property or to invocations to one + // of the available configuration methods. So we can just read the field directly and make the necessary + // check with our local copy, without the need of paying the locking overhead from this get accessor. + IServiceProvider? provider = this.serviceProvider; + + if (provider is null) + { + ThrowInvalidOperationExceptionForMissingInitialization(); + } + + return provider!.GetService(serviceType); + } + + /// + /// Tries to resolve an instance of a specified service type. + /// + /// The type of service to resolve. + /// An instance of the specified service, or . + /// Thrown if the current instance has not been initialized. + public T? GetService() + where T : class + { + IServiceProvider? provider = this.serviceProvider; + + if (provider is null) + { + ThrowInvalidOperationExceptionForMissingInitialization(); + } + + return (T?)provider!.GetService(typeof(T)); + } + + /// + /// Resolves an instance of a specified service type. + /// + /// The type of service to resolve. + /// An instance of the specified service, or . + /// + /// Thrown if the current instance has not been initialized, or if the + /// requested service type was not registered in the service provider currently in use. + /// + public T GetRequiredService() + where T : class + { + IServiceProvider? provider = this.serviceProvider; + + if (provider is null) + { + ThrowInvalidOperationExceptionForMissingInitialization(); + } + + T? service = (T?)provider!.GetService(typeof(T)); + + if (service is null) + { + ThrowInvalidOperationExceptionForUnregisteredType(); + } + + return service!; + } + + /// + /// Initializes the shared instance. + /// + /// The input instance to use. + /// Thrown if is . + public void ConfigureServices(IServiceProvider serviceProvider) + { + IServiceProvider? oldServices = Interlocked.CompareExchange(ref this.serviceProvider, serviceProvider, null); + + if (oldServices is not null) + { + ThrowInvalidOperationExceptionForRepeatedConfiguration(); + } + } + + /// + /// Throws an when the property is used before initialization. + /// + private static void ThrowInvalidOperationExceptionForMissingInitialization() + { + throw new InvalidOperationException("The service provider has not been configured yet."); + } + + /// + /// Throws an when the property is missing a type registration. + /// + private static void ThrowInvalidOperationExceptionForUnregisteredType() + { + throw new InvalidOperationException("The requested service type was not registered."); + } + + /// + /// Throws an when a configuration is attempted more than once. + /// + private static void ThrowInvalidOperationExceptionForRepeatedConfiguration() + { + throw new InvalidOperationException("The default service provider has already been configured."); + } +} diff --git a/UniSky/Services/SettingsService.cs b/UniSky/Services/SettingsService.cs index dc15920..f3102dc 100644 --- a/UniSky/Services/SettingsService.cs +++ b/UniSky/Services/SettingsService.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Numerics; using System.Text.Json; using System.Text.Json.Serialization; +using Windows.Foundation; using Windows.Storage; namespace UniSky.Services; @@ -18,6 +20,12 @@ namespace UniSky.Services; [JsonSerializable(typeof(ulong))] [JsonSerializable(typeof(bool))] [JsonSerializable(typeof(string))] +[JsonSerializable(typeof(Size))] +[JsonSerializable(typeof(Rect))] +[JsonSerializable(typeof(Point))] +[JsonSerializable(typeof(Vector2))] +[JsonSerializable(typeof(Vector3))] +[JsonSerializable(typeof(Vector4))] [JsonSourceGenerationOptions(WriteIndented = false, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] public partial class SettingsJsonContext : JsonSerializerContext { } diff --git a/UniSky/Services/Sheet/AppWindowSheetController.cs b/UniSky/Services/Sheet/AppWindowSheetController.cs new file mode 100644 index 0000000..66e5111 --- /dev/null +++ b/UniSky/Services/Sheet/AppWindowSheetController.cs @@ -0,0 +1,120 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using UniSky.Controls.Sheet; +using Windows.Foundation; +using Windows.UI.ViewManagement; +using Windows.UI.WindowManagement; +using Windows.UI.Xaml; + +namespace UniSky.Services; + +public class AppWindowSheetController : ISheetController +{ + private readonly AppWindow appWindow; + private readonly SheetControl control; + private readonly ISettingsService settingsService; + + private readonly string settingsKey; + + public AppWindowSheetController(AppWindow window, SheetControl control) + { + this.appWindow = window; + this.control = control; + this.settingsService = ServiceContainer.Scoped.GetRequiredService(); + this.SafeAreaService = new AppWindowSafeAreaService(appWindow); + + this.settingsKey = "CoreWindow_LastSize_" + control.GetType().FullName.Replace(".", "_"); + var initialSize = settingsService.Read(settingsKey, control.PreferredWindowSize); + + appWindow.PersistedStateId = settingsKey; + appWindow.CloseRequested += OnCloseRequested; + appWindow.Closed += OnClosed; + appWindow.Changed += OnChanged; + appWindow.RequestSize(initialSize); + + PlaceAppWindow(initialSize); + } + + public UIElement Root => control; + public bool IsFullWindow => true; + public ISafeAreaService SafeAreaService { get; } + + public async Task TryHideSheetAsync() + { + if (await control.InvokeHidingAsync()) + { + await appWindow.CloseAsync(); + return true; + } + + return false; + } + + private async void OnCloseRequested(AppWindow sender, AppWindowCloseRequestedEventArgs args) + { + var deferral = args.GetDeferral(); + if (!await control.InvokeHidingAsync()) + args.Cancel = true; + + deferral.Complete(); + } + + private void OnClosed(AppWindow sender, AppWindowClosedEventArgs args) + { + control.InvokeHidden(); + } + + private void OnChanged(AppWindow sender, AppWindowChangedEventArgs args) + { + if (args.DidSizeChange) + { + var settingsKey = "AppWindow_LastSize_" + control.GetType().FullName.Replace(".", "_"); + settingsService.Save(settingsKey, new Size(control.ActualSize.X, control.ActualSize.Y)); + } + } + + private void PlaceAppWindow(Size initialSize) + { + var applicationView = ApplicationView.GetForCurrentView(); + var currentViewRect = applicationView.VisibleBounds; + var environment = applicationView.WindowingEnvironment; + if (environment.Kind == WindowingEnvironmentKind.Overlapped) + { + var regions = environment.GetDisplayRegions(); + var currentRegion = regions[0]; + foreach (var region in regions) + { + var regionRect = new Rect(region.WorkAreaOffset, region.WorkAreaSize); + if (regionRect.Contains(new Point(applicationView.VisibleBounds.X, applicationView.VisibleBounds.Y))) + currentRegion = region; + } + + var currentDisplayOffset = currentRegion.WorkAreaOffset; + var currentDisplaySize = currentRegion.WorkAreaSize; + currentViewRect = new Rect( + currentViewRect.X - currentDisplayOffset.X, + currentViewRect.Y - currentDisplayOffset.Y, + currentViewRect.Width, + currentViewRect.Height); + + var currentDisplayCenter = currentDisplaySize.Width / 2; + var offset = (currentViewRect.Left + Math.Max(currentViewRect.Width / 2, initialSize.Width / 2)) - currentDisplayCenter; + + if (applicationView.AdjacentToLeftDisplayEdge && applicationView.AdjacentToRightDisplayEdge) + { + appWindow.RequestMoveRelativeToDisplayRegion(currentRegion, new Point((currentDisplayCenter - (initialSize.Width / 2)) + 20, 150)); + } + else if (offset < 0) + { + // right + appWindow.RequestMoveRelativeToCurrentViewContent(new Point(applicationView.VisibleBounds.Width + 8, 0)); + } + else + { + // left + appWindow.RequestMoveRelativeToCurrentViewContent(new Point(-initialSize.Width - 8, 0)); + } + } + } +} diff --git a/UniSky/Services/Sheet/ApplicationViewSheetController.cs b/UniSky/Services/Sheet/ApplicationViewSheetController.cs new file mode 100644 index 0000000..cf15b2a --- /dev/null +++ b/UniSky/Services/Sheet/ApplicationViewSheetController.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using UniSky.Controls.Sheet; +using Windows.Foundation; +using Windows.Foundation.Metadata; +using Windows.UI.Core; +using Windows.UI.Core.Preview; +using Windows.UI.ViewManagement; +using Windows.UI.Xaml; + +namespace UniSky.Services; + +internal class ApplicationViewSheetController : ISheetController +{ + private readonly SheetControl control; + private readonly int hostViewId; + private readonly int viewId; + private readonly ISettingsService settingsService; + + private readonly string settingsKey; + private bool hasActivated = false; + + public ApplicationViewSheetController(SheetControl control, + int hostViewId, + int viewId) + { + this.control = control; + this.hostViewId = hostViewId; + this.viewId = viewId; + this.settingsService = ServiceContainer.Scoped.GetRequiredService(); + this.settingsKey = "CoreWindow_LastSize_" + control.GetType().FullName.Replace(".", "_"); + + // a surprise tool that'll help us later + // (instanciating this now so it handles min. window sizes, etc.) + this.SafeAreaService = ServiceContainer.Scoped.GetRequiredService(); + + var systemNavigationManager = SystemNavigationManager.GetForCurrentView(); + systemNavigationManager.BackRequested += OnBackRequested; + + if (ApiInformation.IsTypePresent("Windows.UI.Core.Preview.SystemNavigationManagerPreview")) + { + var systemNavigationManagerPreview = SystemNavigationManagerPreview.GetForCurrentView(); + systemNavigationManagerPreview.CloseRequested += OnCloseRequested; + } + + var coreWindow = CoreWindow.GetForCurrentThread(); + coreWindow.SizeChanged += OnWindowSizeChanged; + coreWindow.Activated += OnActivated; + coreWindow.Closed += OnWindowClosed; + } + + private void OnActivated(CoreWindow sender, WindowActivatedEventArgs args) + { + if (!hasActivated) + { + var initialSize = settingsService.Read(settingsKey, control.PreferredWindowSize); + var applicationView = ApplicationView.GetForCurrentView(); + applicationView.TryResizeView(initialSize); + + hasActivated = true; + } + } + + public UIElement Root => control; + public bool IsFullWindow => true; + public ISafeAreaService SafeAreaService { get; } + + public async Task TryHideSheetAsync() + { + if (await control.InvokeHidingAsync()) + { + await ApplicationViewSwitcher.SwitchAsync(hostViewId, viewId, ApplicationViewSwitchingOptions.ConsolidateViews); + return true; + } + + return false; + } + + private async void OnBackRequested(object sender, BackRequestedEventArgs e) + { + e.Handled = true; + await TryHideSheetAsync(); + } + + private async void OnCloseRequested(object sender, SystemNavigationCloseRequestedPreviewEventArgs e) + { + var deferral = e.GetDeferral(); + if (!await control.InvokeHidingAsync()) + e.Handled = true; + + deferral.Complete(); + } + + private void OnWindowClosed(object sender, CoreWindowEventArgs e) + { + control.InvokeHidden(); + } + + private void OnWindowSizeChanged(object sender, WindowSizeChangedEventArgs e) + { + settingsService.Save(settingsKey, e.Size); + } +} diff --git a/UniSky/Services/Sheet/ISheetController.cs b/UniSky/Services/Sheet/ISheetController.cs new file mode 100644 index 0000000..371088d --- /dev/null +++ b/UniSky/Services/Sheet/ISheetController.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using UniSky.Controls.Sheet; +using Windows.UI.Xaml; + +namespace UniSky.Services; + +public interface ISheetController +{ + UIElement Root { get; } + bool IsFullWindow { get; } + ISafeAreaService SafeAreaService { get; } + Task TryHideSheetAsync(); +} diff --git a/UniSky/Services/Sheet/SheetRootController.cs b/UniSky/Services/Sheet/SheetRootController.cs new file mode 100644 index 0000000..847a19a --- /dev/null +++ b/UniSky/Services/Sheet/SheetRootController.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using UniSky.Controls.Sheet; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml; + +namespace UniSky.Services; + +public class SheetRootController(SheetRootControl rootControl, + ISafeAreaService safeAreaService) : ISheetController +{ + public UIElement Root => rootControl; + public bool IsFullWindow => false; + public ISafeAreaService SafeAreaService => safeAreaService; + + public async Task TryHideSheetAsync() + { + return await rootControl.HideSheetAsync(); + } +} diff --git a/UniSky/Services/Sheet/SheetService.cs b/UniSky/Services/Sheet/SheetService.cs new file mode 100644 index 0000000..ed6f9f3 --- /dev/null +++ b/UniSky/Services/Sheet/SheetService.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Toolkit.Uwp.UI.Extensions; +using UniSky.Controls.Sheet; +using Windows.ApplicationModel.Core; +using Windows.Foundation; +using Windows.Foundation.Metadata; +using Windows.Graphics.Display; +using Windows.System.Profile; +using Windows.UI.Core; +using Windows.UI.Core.Preview; +using Windows.UI.ViewManagement; +using Windows.UI.WindowManagement; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Hosting; + +namespace UniSky.Services; + +internal class SheetService : ISheetService +{ + private readonly SheetRootControl sheetRoot; + private readonly ISettingsService settingsService; + + public SheetService(ISettingsService settingsService) + { + this.settingsService = settingsService; + this.sheetRoot = Window.Current.Content.FindDescendant(); + } + + public async Task ShowAsync(object parameter = null) where T : SheetControl, new() + { + if (sheetRoot != null && !settingsService.Read("WindowedSheets", AnalyticsInfo.VersionInfo.DeviceFamily == "Windows.Desktop")) + { + var safeArea = ServiceContainer.Scoped.GetRequiredService(); + + var control = new T(); + var controller = new SheetRootController(sheetRoot, safeArea); + + control.SetSheetController(controller); + + sheetRoot.ShowSheet(control, parameter); + return controller; + } + else + { + if (ApiInformation.IsApiContractPresent(typeof(UniversalApiContract).FullName, 8, 0)) + { + return await ShowSheetForAppWindow(parameter); + } + else + { + return await ShowSheetForCoreWindow(parameter); + } + } + } + + private async Task ShowSheetForAppWindow(object parameter) where T : SheetControl, new() + { + var control = new T(); + var appWindow = await AppWindow.TryCreateAsync(); + + var controller = new AppWindowSheetController(appWindow, control); + control.SetSheetController(controller); + control.InvokeShowing(parameter); + + ElementCompositionPreview.SetAppWindowContent(appWindow, control); + + await appWindow.TryShowAsync(); + + control.InvokeShown(); + + return controller; + } + + private static async Task ShowSheetForCoreWindow(object parameter) where T : SheetControl, new() + { + ISheetController controller = null; + + var currentViewId = ApplicationView.GetForCurrentView().Id; + var view = CoreApplication.CreateNewView(); + var newViewId = 0; + await view.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => + { + newViewId = ApplicationView.GetForCurrentView().Id; + + var control = new T(); + controller = new ApplicationViewSheetController(control, currentViewId, newViewId); + control.SetSheetController(controller); + + Window.Current.Content = control; + Window.Current.Activate(); + + control.InvokeShowing(parameter); + control.InvokeShown(); + + }); + + await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId, ViewSizePreference.UseMinimum); + + return controller; + } +} diff --git a/UniSky/Services/SheetService.cs b/UniSky/Services/SheetService.cs deleted file mode 100644 index 09264c3..0000000 --- a/UniSky/Services/SheetService.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Toolkit.Uwp.UI.Extensions; -using UniSky.Controls.Sheet; -using Windows.UI.Xaml; - -namespace UniSky.Services -{ - internal class SheetService : ISheetService - { - private readonly SheetRootControl sheetRoot; - - public SheetService() - { - this.sheetRoot = Window.Current.Content.FindDescendant(); - } - - public async Task ShowAsync(object parameter = null) where T : SheetControl, new() - { - var control = new T(); - sheetRoot.ShowSheet(control, parameter); - } - - /// - /// Tries to close the currently active sheet, returns a boolean to indicate if the sheet was hidden or not - /// - /// - public async Task TryCloseAsync() - { - return await sheetRoot.HideSheetAsync(); - } - } -} diff --git a/UniSky/Templates/custom-twitter/Icons.xaml b/UniSky/Templates/custom-twitter/Icons.xaml index 1288865..c95a87c 100644 --- a/UniSky/Templates/custom-twitter/Icons.xaml +++ b/UniSky/Templates/custom-twitter/Icons.xaml @@ -4,6 +4,6 @@ m64.67 6.525c-1.74 2.61-4.06 4.93-6.525 6.815 0 .58 0 1.16 0 1.74 0 17.4-13.195 37.555-37.555 37.555-7.395 0-14.355-2.175-20.155-5.945 1.015.145 2.03.145 3.19.145 6.235 0 11.89-2.175 16.385-5.655-5.8-.145-10.585-3.915-12.325-9.135.87.145 1.595.29 2.465.29 1.16 0 2.32-.145 3.48-.435C7.54 30.595 2.9 25.23 2.9 18.85c0 0 0-.145 0-.145 1.74 1.015 3.77 1.595 5.945 1.595C5.365 17.98 3.045 13.92 3.045 9.425c0-2.465.58-4.64 1.74-6.67 6.525 7.975 16.24 13.195 27.115 13.775-.29-1.015-.29-2.03-.29-3.045 0-7.25 5.945-13.195 13.195-13.195 3.77 0 7.25 1.595 9.57 4.205 3.045-.58 5.8-1.74 8.41-3.19-1.015 3.045-3.045 5.655-5.8 7.25 2.61-.29 5.22-1.015 7.54-2.03z 64 - 38 + 48 diff --git a/UniSky/Themes/ThemeResources.cs b/UniSky/Themes/ThemeResources.cs index 7c6271c..f7c200e 100644 --- a/UniSky/Themes/ThemeResources.cs +++ b/UniSky/Themes/ThemeResources.cs @@ -1,6 +1,9 @@ using System; using CommunityToolkit.Mvvm.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Toolkit.Uwp.UI; using UniSky.Services; +using Windows.ApplicationModel; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls.Primitives; @@ -10,8 +13,16 @@ internal class ThemeResources : ResourceDictionary { public ThemeResources() { - var theme = Ioc.Default.GetRequiredService() - .GetTheme(); + AppTheme theme; + if (!DesignMode.DesignModeEnabled) + { + theme = ServiceContainer.Scoped.GetRequiredService() + .GetTheme(); + } + else + { + theme = AppTheme.Fluent; + } Uri uri = theme switch { diff --git a/UniSky/UniSky.csproj b/UniSky/UniSky.csproj index d7eda2c..842b4dd 100644 --- a/UniSky/UniSky.csproj +++ b/UniSky/UniSky.csproj @@ -34,7 +34,7 @@ False True Always - x64 + arm True 0 @@ -224,7 +224,6 @@ - @@ -232,9 +231,16 @@ FeedTemplates.xaml - + + + + - + + + + + @@ -489,7 +495,7 @@ 8.3.2 - 3.1.0-alpha.8 + 3.1.0-alpha.9 2.14.1 @@ -557,4 +563,48 @@ --> + + + $(TargetPlatformMinVersion) + 10.0.15063.0 + + + + + $(OriginalTargetPlatformMinVersion) + + + + + $(TargetPlatformMinVersion) + 10.0.15063.0 + + + + + $(OriginalTargetPlatformMinVersion) + + + + + $(TargetPlatformMinVersion) + 10.0.15063.0 + + + + + $(OriginalTargetPlatformMinVersion) + + + + + $(TargetPlatformMinVersion) + 10.0.15063.0 + + + + + $(OriginalTargetPlatformMinVersion) + + \ No newline at end of file diff --git a/UniSky/ViewModels/Compose/ComposeViewAttachmentViewModel.cs b/UniSky/ViewModels/Compose/ComposeViewAttachmentViewModel.cs index 7ee069c..a4b0d68 100644 --- a/UniSky/ViewModels/Compose/ComposeViewAttachmentViewModel.cs +++ b/UniSky/ViewModels/Compose/ComposeViewAttachmentViewModel.cs @@ -7,6 +7,7 @@ using UniSky.Controls.Compose; using UniSky.Extensions; using UniSky.Helpers; +using Windows.Foundation; using Windows.Foundation.Metadata; using Windows.Graphics.Imaging; using Windows.Storage; @@ -108,6 +109,9 @@ private async Task AddAltTextAsync() { var previousAltText = AltText; var altTextDialog = new ComposeAddAltTextDialog(this); + if (parent.SheetController != null && ApiInformation.IsApiContractPresent(typeof(UniversalApiContract).FullName, 8)) + altTextDialog.XamlRoot = parent.SheetController.Root.XamlRoot; + if (await altTextDialog.ShowAsync() != ContentDialogResult.Primary) AltText = previousAltText; } @@ -151,6 +155,8 @@ private async Task CompressImageAsync(IStorageFile input, int size = 2048) encoder.SetSoftwareBitmap(await decoder.GetSoftwareBitmapAsync()); encoder.BitmapTransform.ScaledWidth = (uint)Math.Ceiling(width); encoder.BitmapTransform.ScaledHeight = (uint)Math.Ceiling(height); + encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant; + await encoder.FlushAsync(); } diff --git a/UniSky/ViewModels/Compose/ComposeViewModel.cs b/UniSky/ViewModels/Compose/ComposeViewModel.cs index 5992da9..f30488e 100644 --- a/UniSky/ViewModels/Compose/ComposeViewModel.cs +++ b/UniSky/ViewModels/Compose/ComposeViewModel.cs @@ -67,15 +67,20 @@ public bool HasReply => ReplyTo != null; public ObservableCollection AttachedFiles { get; } + public bool HasAttachments => AttachedFiles.Count > 0; + public ISheetController SheetController { get; } + public ComposeViewModel(IProtocolService protocolService, + ISheetController sheetController, ILogger logger, PostViewModel replyTo = null) { this.protocolService = protocolService; this.logger = logger; + this.SheetController = sheetController; this.resources = ResourceLoader.GetForCurrentView(); this.Text = ""; @@ -179,8 +184,7 @@ private async Task GetImageEmbed() [RelayCommand] private async Task Hide() { - var sheetService = Ioc.Default.GetRequiredService(); - await sheetService.TryCloseAsync(); + await this.SheetController.TryHideSheetAsync(); } [RelayCommand] @@ -354,7 +358,7 @@ private void AddFile(IStorageFile storageFile, bool isTemporary) throw new InvalidOperationException(resources.GetString("E_VideosUnsupported")); } - AttachedFiles.Add(ActivatorUtilities.CreateInstance(Ioc.Default, this, storageFile, type, isTemporary)); + AttachedFiles.Add(ActivatorUtilities.CreateInstance(ServiceContainer.Scoped, this, storageFile, type, isTemporary)); } internal void UpdateLoading(ComposeViewAttachmentViewModel attachmentViewModel, bool value) diff --git a/UniSky/ViewModels/FeedsViewModel.cs b/UniSky/ViewModels/FeedsViewModel.cs index 7c049fa..d932248 100644 --- a/UniSky/ViewModels/FeedsViewModel.cs +++ b/UniSky/ViewModels/FeedsViewModel.cs @@ -8,6 +8,7 @@ using FishyFlip.Lexicon.App.Bsky.Feed; using FishyFlip.Models; using FishyFlip.Tools; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Toolkit.Uwp.UI.Extensions; using UniSky.Controls.Compose; @@ -42,7 +43,7 @@ public FeedsViewModel( [RelayCommand] public async Task Post() { - var sheetsService = Ioc.Default.GetRequiredService(); + var sheetsService = ServiceContainer.Scoped.GetRequiredService(); await sheetsService.ShowAsync(); } diff --git a/UniSky/ViewModels/HomeViewModel.cs b/UniSky/ViewModels/HomeViewModel.cs index e7df101..0195910 100644 --- a/UniSky/ViewModels/HomeViewModel.cs +++ b/UniSky/ViewModels/HomeViewModel.cs @@ -10,6 +10,7 @@ using FishyFlip.Lexicon.Com.Atproto.Server; using FishyFlip.Models; using FishyFlip.Tools; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using UniSky.Extensions; using UniSky.Helpers; @@ -190,7 +191,7 @@ private async Task RefreshSessionAsync(ATProtocol protocol) throw new InvalidOperationException("Authentication failed!"); var sessionModel2 = new SessionModel(true, sessionModel.Service, authSession2.Session, authSession2); - var sessionService = Ioc.Default.GetRequiredService(); + var sessionService = ServiceContainer.Scoped.GetRequiredService(); sessionService.SaveSession(sessionModel2); protocolService.SetProtocol(protocol); diff --git a/UniSky/ViewModels/Post/PostViewModel.cs b/UniSky/ViewModels/Post/PostViewModel.cs index ea68da8..aecb80c 100644 --- a/UniSky/ViewModels/Post/PostViewModel.cs +++ b/UniSky/ViewModels/Post/PostViewModel.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using System.Web; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.DependencyInjection; using CommunityToolkit.Mvvm.Input; using FishyFlip.Lexicon; using FishyFlip.Lexicon.App.Bsky.Actor; @@ -15,6 +14,7 @@ using FishyFlip.Models; using FishyFlip.Tools; using Humanizer; +using Microsoft.Extensions.DependencyInjection; using UniSky.Controls.Compose; using UniSky.Helpers; using UniSky.Pages; @@ -161,7 +161,7 @@ public PostViewModel(PostView view, bool hasChild = false) [RelayCommand] private async Task LikeAsync() { - var protocol = Ioc.Default.GetRequiredService() + var protocol = ServiceContainer.Scoped.GetRequiredService() .Protocol; if (IsLiked) @@ -189,7 +189,7 @@ private async Task LikeAsync() [RelayCommand] private void OpenProfile(UIElement element) { - var service = Ioc.Default.GetRequiredService() + var service = ServiceContainer.Scoped.GetRequiredService() .GetNavigationService("Home"); service.Navigate(this.view.Author, new ContinuumNavigationTransitionInfo() { ExitElement = element }); @@ -199,7 +199,7 @@ private void OpenProfile(UIElement element) [RelayCommand] private async Task ReplyAsync() { - var service = Ioc.Default.GetRequiredService(); + var service = ServiceContainer.Scoped.GetRequiredService(); await service.ShowAsync(this); } diff --git a/UniSky/ViewModels/Profile/ProfilePageViewModel.cs b/UniSky/ViewModels/Profile/ProfilePageViewModel.cs index 32575f7..2279dd0 100644 --- a/UniSky/ViewModels/Profile/ProfilePageViewModel.cs +++ b/UniSky/ViewModels/Profile/ProfilePageViewModel.cs @@ -9,6 +9,7 @@ using FishyFlip.Models; using FishyFlip.Tools; using Humanizer; +using Microsoft.Extensions.DependencyInjection; using UniSky.Extensions; using UniSky.Helpers.Interop; using UniSky.Services; @@ -79,7 +80,7 @@ public ProfilePageViewModel(ATDid did) public ProfilePageViewModel(ATObject profile) : base(profile) { - var protocol = Ioc.Default.GetRequiredService(); + var protocol = ServiceContainer.Scoped.GetRequiredService(); if (profile is ProfileViewDetailed detailed) { Populate(detailed); @@ -105,7 +106,7 @@ private async Task LoadAsync() try { - var protocol = Ioc.Default.GetRequiredService(); + var protocol = ServiceContainer.Scoped.GetRequiredService(); var profile = (await protocol.Protocol.GetProfileAsync(this.id).ConfigureAwait(false)) .HandleResult(); @@ -175,7 +176,6 @@ internal void Select(ProfileFeedViewModel profileFeedViewModel) private async Task CalculateLightnessAsync(ProfileViewDetailed profile) { - var protocol = Ioc.Default.GetRequiredService(); var lightness = 0.0f; if (string.IsNullOrWhiteSpace(profile.Banner)) return; @@ -192,7 +192,7 @@ private async Task CalculateLightnessAsync(ProfileViewDetailed profile) syncContext.Post(() => { - var safeAreaService = Ioc.Default.GetRequiredService(); + var safeAreaService = ServiceContainer.Scoped.GetRequiredService(); if (IsLight == true) { safeAreaService.SetTitlebarTheme(ElementTheme.Dark);