diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 951b117..6181b87 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -47,14 +47,30 @@ jobs:
$certificatePath = "GitHubActionsWorkflow.pfx"
[IO.File]::WriteAllBytes("$env:Project_Name/$certificatePath", $pfx_cert_byte)
+ - name: Adjust Package Version
+ run: |
+ $epoch = (Get-Date) - ([System.DateTime]::new(2024, 11, 01))
+ $file = (Resolve-Path "UniSky/Package.appxmanifest")
+ $xml = [System.Xml.XmlDocument]::new()
+ $xml.Load($file)
+
+ $node = $xml.GetElementsByTagName("Identity", "http://schemas.microsoft.com/appx/manifest/foundation/windows10")[0]
+ $version = [System.Version]::Parse($node.GetAttribute("Version"))
+ $version = [System.Version]::new($version.Major, $version.Minor, $version.Build, $epoch.TotalMinutes);
+ $node.SetAttribute("Version", $version.ToString())
+
+ $xml.Save($file)
+
# Create the app package by building and packaging the project
- name: Build App Packages
- run: msbuild $env:ProjectFile_Name /p:Configuration=$env:Configuration /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle /p:AppxBundlePlatforms="x86|x64|ARM|ARM64" /p:PackageCertificateKeyFile=GitHubActionsWorkflow.pfx /p:AppxPackageDir="$env:Appx_Package_Dir" /p:GenerateAppxPackageOnBuild=true
+ run: msbuild $env:ProjectFile_Name /p:Configuration=$env:Configuration /p:SourceRevisionBranch="$env:Head_Ref" /p:SourceRevisionCommit="$($env:Sha.Substring(0, 7))" /p:UapAppxPackageBuildMode=$env:Appx_Package_Build_Mode /p:AppxBundle=$env:Appx_Bundle /p:AppxBundlePlatforms="x86|x64|ARM|ARM64" /p:PackageCertificateKeyFile=GitHubActionsWorkflow.pfx /p:AppxPackageDir="$env:Appx_Package_Dir" /p:GenerateAppxPackageOnBuild=true
env:
Appx_Bundle: Always
Appx_Package_Build_Mode: SideloadOnly
Appx_Package_Dir: Packages\
Configuration: ${{ matrix.configuration }}
+ Head_Ref: ${{ github.head_ref }}
+ Sha: ${{ github.sha }}
# Remove the pfx
- name: Cleanup
@@ -67,7 +83,3 @@ jobs:
name: AppxPackage-${{ matrix.configuration }}
path: |
${{ env.Project_Name }}\\Packages
- ${{ env.Project_Name }}\\bin\\x86\\${{ matrix.configuration }}\\ilc\\${{ env.Project_Name }}.pdb
- ${{ env.Project_Name }}\\bin\\x64\\${{ matrix.configuration }}\\ilc\\${{ env.Project_Name }}.pdb
- ${{ env.Project_Name }}\\bin\\ARM\\${{ matrix.configuration }}\\ilc\\${{ env.Project_Name }}.pdb
- ${{ env.Project_Name }}\\bin\\ARM64\\${{ matrix.configuration }}\\ilc\\${{ env.Project_Name }}.pdb
diff --git a/UniSky.Models/UniSky.Models.csproj b/UniSky.Models/UniSky.Models.csproj
index 522f976..6668ea8 100644
--- a/UniSky.Models/UniSky.Models.csproj
+++ b/UniSky.Models/UniSky.Models.csproj
@@ -7,8 +7,22 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+ $(SourceRevisionCommit)
+ $(SourceRevisionCommit) ($(SourceRevisionBranch))
+
+
+
diff --git a/UniSky/App.xaml b/UniSky/App.xaml
index f3418e3..4d877b5 100644
--- a/UniSky/App.xaml
+++ b/UniSky/App.xaml
@@ -4,44 +4,42 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:UniSky"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
- xmlns:media="using:Microsoft.UI.Xaml.Media">
+ xmlns:media="using:Microsoft.UI.Xaml.Media"
+ xmlns:sheets="using:UniSky.Controls.Sheet"
+ xmlns:themes="using:UniSky.Themes">
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
- M13.873 3.77C21.21 9.243 29.103 20.342 32 26.3v15.732c0-.335-.13.043-.41.858-1.512 4.414-7.418 21.642-20.923 7.87-7.111-7.252-3.819-14.503 9.125-16.692-7.405 1.252-15.73-.817-18.014-8.93C1.12 22.804 0 8.431 0 6.488 0-3.237 8.579-.18 13.873 3.77ZM50.127 3.77C42.79 9.243 34.897 20.342 32 26.3v15.732c0-.335.13.043.41.858 1.512 4.414 7.418 21.642 20.923 7.87 7.111-7.252 3.819-14.503-9.125-16.692 7.405 1.252 15.73-.817 18.014-8.93C62.88 22.804 64 8.431 64 6.488 64-3.237 55.422-.18 50.127 3.77Z
- 64
- 57
-
-
+
+
+
+
- 0,1,0,0
- 0,0,0,0
-
14
- 14
+ 14
+
+
+
+
+
+
diff --git a/UniSky/App.xaml.cs b/UniSky/App.xaml.cs
index 3790068..7a2909f 100644
--- a/UniSky/App.xaml.cs
+++ b/UniSky/App.xaml.cs
@@ -1,26 +1,16 @@
using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Runtime.InteropServices.WindowsRuntime;
-using CommunityToolkit.Mvvm.DependencyInjection;
+using Humanizer.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using UniSky.Extensions;
+using UniSky.Helpers.Localisation;
using UniSky.Services;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
-using Windows.ApplicationModel.Core;
-using Windows.ApplicationModel.Resources.Core;
-using Windows.Foundation;
-using Windows.Foundation.Collections;
-using Windows.Storage;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
-using Windows.UI.Xaml.Controls.Primitives;
-using Windows.UI.Xaml.Data;
-using Windows.UI.Xaml.Input;
-using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
+using UnhandledExceptionEventArgs = Windows.UI.Xaml.UnhandledExceptionEventArgs;
namespace UniSky;
@@ -29,34 +19,64 @@ namespace UniSky;
///
sealed partial class App : Application
{
+ private ILogger _logger;
+
///
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
///
public App()
{
+ this.ConfigureServices();
+
this.InitializeComponent();
this.Suspending += OnSuspending;
+ this.UnhandledException += OnUnhandledException;
- this.ConfigureServices();
+ _logger = ServiceContainer.Default.GetRequiredService()
+ .CreateLogger();
// ResourceContext.SetGlobalQualifierValue("Custom", "Twitter", ResourceQualifierPersistence.LocalMachine);
}
+ private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ _logger.LogError(e.Exception, "Unhandled exception!!");
+
+ // hate this
+ e.Handled = true;
+ }
+
private void ConfigureServices()
{
var collection = new ServiceCollection();
collection.AddLogging(c => c.AddDebug()
.SetMinimumLevel(LogLevel.Trace));
+ collection.AddSingleton();
+ collection.AddSingleton();
+ collection.AddSingleton();
+ collection.AddSingleton();
+ collection.AddSingleton();
+ collection.AddScoped();
+ collection.AddScoped();
+
collection.AddTransient();
collection.AddTransient();
- collection.AddSingleton();
- collection.AddSingleton();
- collection.AddSingleton();
+ 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"));
+ }
- Ioc.Default.ConfigureServices(collection.BuildServiceProvider());
+ protected override void OnActivated(IActivatedEventArgs args)
+ {
+ if (args is ProtocolActivatedEventArgs e)
+ {
+ this.OnProtocolActivated(e);
+ }
}
///
@@ -66,6 +86,8 @@ private void ConfigureServices()
/// Details about the launch request and process.
protected override void OnLaunched(LaunchActivatedEventArgs e)
{
+ Hairline.Initialize();
+
// Do not repeat app initialization when the Window already has content,
// just ensure that the window is active
if (Window.Current.Content is not Frame rootFrame)
@@ -99,6 +121,21 @@ protected override void OnLaunched(LaunchActivatedEventArgs e)
}
}
+ private void OnProtocolActivated(ProtocolActivatedEventArgs e)
+ {
+ Hairline.Initialize();
+ if (Window.Current.Content is not Frame rootFrame)
+ {
+ rootFrame = new Frame();
+ rootFrame.NavigationFailed += OnNavigationFailed;
+ rootFrame.Navigate(typeof(RootPage));
+ Window.Current.Content = rootFrame;
+ }
+
+ // Ensure the current window is active
+ Window.Current.Activate();
+ }
+
///
/// Invoked when Navigation to a certain page fails
///
diff --git a/UniSky/Constants.cs b/UniSky/Constants.cs
new file mode 100644
index 0000000..93a6f63
--- /dev/null
+++ b/UniSky/Constants.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using UniSky.Models;
+using Windows.ApplicationModel;
+using Windows.UI.Xaml;
+
+namespace UniSky;
+
+public static class Constants
+{
+ public static string Version
+ {
+ get
+ {
+ var gitSha = "";
+ var versionedAssembly = typeof(LoginModel).Assembly;
+ var attribute = versionedAssembly.GetCustomAttribute();
+
+ int idx;
+ if (attribute != null && (idx = attribute.InformationalVersion.IndexOf('+')) != -1)
+ {
+ gitSha = "-" + attribute.InformationalVersion.Substring(idx + 1);
+ }
+
+ var v = Package.Current.Id.Version;
+ return $"{v.Major}.{v.Minor}.{v.Build}.{v.Revision}{gitSha}";
+ }
+ }
+
+ public static string UserAgent
+ => $"UniSky/{Version} (https://github.com/UnicordDev/UniSky)";
+
+ public static class Settings
+ {
+ public const string REQUESTED_COLOUR_SCHEME = "RequestedColourScheme_v1";
+ public const int REQUESTED_COLOUR_SCHEME_DEFAULT = (int)ElementTheme.Default;
+
+ public const string USE_MULTIPLE_WINDOWS = "UseMultipleWindows_v1";
+ // default: calculated
+
+ public const string AUTO_FEED_REFRESH = "AutoRefreshFeeds_v1";
+ public const bool AUTO_FEED_REFRESH_DEFAULT = true;
+ }
+}
diff --git a/UniSky/Controls/Compose/ComposeAddAltTextDialog.xaml b/UniSky/Controls/Compose/ComposeAddAltTextDialog.xaml
new file mode 100644
index 0000000..e9aa0bb
--- /dev/null
+++ b/UniSky/Controls/Compose/ComposeAddAltTextDialog.xaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UniSky/Controls/Compose/ComposeAddAltTextDialog.xaml.cs b/UniSky/Controls/Compose/ComposeAddAltTextDialog.xaml.cs
new file mode 100644
index 0000000..4d1f4d0
--- /dev/null
+++ b/UniSky/Controls/Compose/ComposeAddAltTextDialog.xaml.cs
@@ -0,0 +1,13 @@
+using UniSky.ViewModels.Compose;
+using Windows.UI.Xaml.Controls;
+
+namespace UniSky.Controls.Compose;
+
+public sealed partial class ComposeAddAltTextDialog : ContentDialog
+{
+ public ComposeAddAltTextDialog(ComposeViewAttachmentViewModel viewModel)
+ {
+ this.InitializeComponent();
+ this.DataContext = viewModel;
+ }
+}
diff --git a/UniSky/Controls/Compose/ComposeDialog.xaml b/UniSky/Controls/Compose/ComposeDialog.xaml
deleted file mode 100644
index 8a8d533..0000000
--- a/UniSky/Controls/Compose/ComposeDialog.xaml
+++ /dev/null
@@ -1,42 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/UniSky/Controls/Compose/ComposeDiscardDraftDialog.xaml b/UniSky/Controls/Compose/ComposeDiscardDraftDialog.xaml
new file mode 100644
index 0000000..6a90f3f
--- /dev/null
+++ b/UniSky/Controls/Compose/ComposeDiscardDraftDialog.xaml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/UniSky/Controls/Compose/ComposeDiscardDraftDialog.xaml.cs b/UniSky/Controls/Compose/ComposeDiscardDraftDialog.xaml.cs
new file mode 100644
index 0000000..6a77069
--- /dev/null
+++ b/UniSky/Controls/Compose/ComposeDiscardDraftDialog.xaml.cs
@@ -0,0 +1,12 @@
+using Windows.UI.Xaml.Controls;
+
+
+namespace UniSky.Controls.Compose;
+
+public sealed partial class ComposeDiscardDraftDialog : ContentDialog
+{
+ public ComposeDiscardDraftDialog()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/UniSky/Controls/Compose/ComposeSheet.xaml b/UniSky/Controls/Compose/ComposeSheet.xaml
new file mode 100644
index 0000000..bf62f5a
--- /dev/null
+++ b/UniSky/Controls/Compose/ComposeSheet.xaml
@@ -0,0 +1,260 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UniSky/Controls/Compose/ComposeSheet.xaml.cs b/UniSky/Controls/Compose/ComposeSheet.xaml.cs
new file mode 100644
index 0000000..5c5bd1a
--- /dev/null
+++ b/UniSky/Controls/Compose/ComposeSheet.xaml.cs
@@ -0,0 +1,163 @@
+using System;
+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.Metadata;
+using Windows.UI.ViewManagement;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+
+// The User Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234236
+
+namespace UniSky.Controls.Compose
+{
+ public sealed partial class ComposeSheet : SheetControl
+ {
+ private readonly ResourceLoader strings;
+
+ public ComposeViewModel ViewModel
+ {
+ get => (ComposeViewModel)GetValue(ViewModelProperty);
+ set => SetValue(ViewModelProperty, value);
+ }
+
+ public static readonly DependencyProperty ViewModelProperty =
+ DependencyProperty.Register("ViewModel", typeof(ComposeViewModel), typeof(ComposeSheet), new PropertyMetadata(null));
+
+ public ComposeSheet()
+ : base()
+ {
+ this.InitializeComponent();
+ this.Showing += OnShowing;
+ this.Shown += OnShown;
+ this.Hiding += OnHiding;
+ this.Hidden += OnHidden;
+ 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;
+
+ private void OnShowing(SheetControl sender, SheetShowingEventArgs e)
+ {
+ var inputPane = InputPane.GetForCurrentView();
+ inputPane.Showing += OnInputPaneShowing;
+ inputPane.Hiding += OnInputPaneHiding;
+
+ if (Window.Current.Content is FrameworkElement element)
+ {
+ element.AllowDrop = true;
+ element.DragEnter += HandleDrag;
+ element.DragOver += HandleDrag;
+ element.DragLeave += HandleDrag;
+ element.Drop += HandleDrop;
+ }
+
+ if (e.Parameter is PostViewModel replyTo)
+ {
+ this.ViewModel = ActivatorUtilities.CreateInstance(ServiceContainer.Scoped, replyTo, Controller);
+ }
+ else
+ {
+ this.ViewModel = ActivatorUtilities.CreateInstance(ServiceContainer.Scoped, Controller);
+ }
+ }
+
+ private void OnHidden(SheetControl sender, RoutedEventArgs args)
+ {
+ var inputPane = InputPane.GetForCurrentView();
+ inputPane.Showing -= OnInputPaneShowing;
+ inputPane.Hiding -= OnInputPaneHiding;
+
+ if (Window.Current.Content is FrameworkElement element)
+ {
+ element.AllowDrop = false;
+ element.DragEnter -= HandleDrag;
+ element.DragOver -= HandleDrag;
+ element.DragLeave -= HandleDrag;
+ element.Drop -= HandleDrop;
+ }
+ }
+
+ private void OnShown(SheetControl sender, RoutedEventArgs args)
+ {
+ PrimaryTextBox.Focus(FocusState.Programmatic);
+ }
+
+ private async void OnHiding(SheetControl sender, SheetHidingEventArgs e)
+ {
+ var deferral = e.GetDeferral();
+ try
+ {
+ if (ViewModel.IsDirty)
+ {
+ 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
+ {
+ deferral.Complete();
+ }
+ }
+
+ private void OnInputPaneShowing(InputPane sender, InputPaneVisibilityEventArgs args)
+ {
+ if (ActualWidth > 620) return;
+
+ ContentGrid.Padding = new Thickness(0, 0, 0, args.OccludedRect.Height);
+ args.EnsuredFocusedElementInView = true;
+ }
+
+ private void OnInputPaneHiding(InputPane sender, InputPaneVisibilityEventArgs args)
+ {
+ if (ActualWidth > 620) return;
+
+ ContentGrid.Padding = new Thickness(0, 0, 0, args.OccludedRect.Height);
+ args.EnsuredFocusedElementInView = true;
+ }
+
+ private void HandleDrag(object sender, DragEventArgs e)
+ {
+ if (e.DataView.Contains(StandardDataFormats.Bitmap) ||
+ e.DataView.Contains(StandardDataFormats.StorageItems))
+ {
+ e.AcceptedOperation = DataPackageOperation.Copy;
+ e.DragUIOverride.Caption = strings.GetString("UploadToBluesky");
+ e.DragUIOverride.IsCaptionVisible = true;
+ }
+ else if (e.DataView.Contains(StandardDataFormats.Text) ||
+ e.DataView.Contains(StandardDataFormats.WebLink))
+ {
+ e.AcceptedOperation = DataPackageOperation.Link;
+ }
+ }
+
+ private void HandleDrop(object sender, DragEventArgs e)
+ {
+ e.Handled = ViewModel.HandleDrop(e.DataView);
+ }
+
+ private void PrimaryTextBox_Paste(object sender, TextControlPasteEventArgs e)
+ {
+ e.Handled = ViewModel.HandlePaste();
+ }
+ }
+}
diff --git a/UniSky/Controls/ConstrainedBox/AspectRatioConstraint.cs b/UniSky/Controls/ConstrainedBox/AspectRatioConstraint.cs
index df5412c..af2e208 100644
--- a/UniSky/Controls/ConstrainedBox/AspectRatioConstraint.cs
+++ b/UniSky/Controls/ConstrainedBox/AspectRatioConstraint.cs
@@ -2,7 +2,6 @@
// 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.Globalization;
namespace Microsoft.Toolkit.Uwp.UI.Controls
diff --git a/UniSky/Controls/ConstrainedBox/ConstrainedBox.Properties.cs b/UniSky/Controls/ConstrainedBox/ConstrainedBox.Properties.cs
index 2f6d122..2082a26 100644
--- a/UniSky/Controls/ConstrainedBox/ConstrainedBox.Properties.cs
+++ b/UniSky/Controls/ConstrainedBox/ConstrainedBox.Properties.cs
@@ -16,8 +16,8 @@ public partial class ConstrainedBox
///
public double ScaleX
{
- get { return (double)GetValue(ScaleXProperty); }
- set { SetValue(ScaleXProperty, value); }
+ get => (double)GetValue(ScaleXProperty);
+ set => SetValue(ScaleXProperty, value);
}
///
@@ -31,8 +31,8 @@ public double ScaleX
///
public double ScaleY
{
- get { return (double)GetValue(ScaleYProperty); }
- set { SetValue(ScaleYProperty, value); }
+ get => (double)GetValue(ScaleYProperty);
+ set => SetValue(ScaleYProperty, value);
}
///
@@ -46,8 +46,8 @@ public double ScaleY
///
public int MultipleX
{
- get { return (int)GetValue(MultipleXProperty); }
- set { SetValue(MultipleXProperty, value); }
+ get => (int)GetValue(MultipleXProperty);
+ set => SetValue(MultipleXProperty, value);
}
///
@@ -61,8 +61,8 @@ public int MultipleX
///
public int MultipleY
{
- get { return (int)GetValue(MultipleYProperty); }
- set { SetValue(MultipleYProperty, value); }
+ get => (int)GetValue(MultipleYProperty);
+ set => SetValue(MultipleYProperty, value);
}
///
@@ -76,15 +76,15 @@ public int MultipleY
///
public AspectRatioConstraint AspectRatio
{
- get { return (AspectRatioConstraint)GetValue(AspectRatioProperty); }
- set { SetValue(AspectRatioProperty, value); }
+ get => (AspectRatioConstraint)GetValue(AspectRatioProperty);
+ set => SetValue(AspectRatioProperty, value);
}
///
/// Identifies the property.
///
public static readonly DependencyProperty AspectRatioProperty =
- DependencyProperty.Register(nameof(AspectRatio), typeof(AspectRatioConstraint), typeof(ConstrainedBox), new PropertyMetadata(null, ConstraintPropertyChanged));
+ DependencyProperty.Register(nameof(AspectRatio), typeof(AspectRatioConstraint), typeof(ConstrainedBox), new PropertyMetadata(default(AspectRatioConstraint), ConstraintPropertyChanged));
private bool _propertyUpdating;
diff --git a/UniSky/Controls/ConstrainedBox/ConstrainedBox.cs b/UniSky/Controls/ConstrainedBox/ConstrainedBox.cs
index 4038f4f..10456d6 100644
--- a/UniSky/Controls/ConstrainedBox/ConstrainedBox.cs
+++ b/UniSky/Controls/ConstrainedBox/ConstrainedBox.cs
@@ -23,7 +23,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
///
public partial class ConstrainedBox : ContentPresenter // TODO: Should be FrameworkElement directly, see https://github.com/microsoft/microsoft-ui-xaml/issues/5530
{
- //// Value used to determine when we re-calculate in the arrange step or re-use a previous calculation. Within roughly a pixel seems like a good value?
+ // Value used to determine when we re-calculate in the arrange step or re-use a previous calculation.
+ // Within roughly a pixel seems like a good value?
private const double CalculationTolerance = 1.5;
private Size _originalSize;
diff --git a/UniSky/Controls/PreviewPaneAuroraControl.xaml b/UniSky/Controls/PreviewPaneAuroraControl.xaml
new file mode 100644
index 0000000..d89eb4d
--- /dev/null
+++ b/UniSky/Controls/PreviewPaneAuroraControl.xaml
@@ -0,0 +1,1282 @@
+
+
+
+
+
+
diff --git a/UniSky/Controls/PreviewPaneAuroraControl.xaml.cs b/UniSky/Controls/PreviewPaneAuroraControl.xaml.cs
new file mode 100644
index 0000000..55fa3f3
--- /dev/null
+++ b/UniSky/Controls/PreviewPaneAuroraControl.xaml.cs
@@ -0,0 +1,171 @@
+using Microsoft.Toolkit.Uwp.Helpers;
+using Windows.UI;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Media;
+using Windows.UI.Xaml.Media.Animation;
+using Windows.UI.Xaml.Shapes;
+using ColorHelper = Microsoft.Toolkit.Uwp.Helpers.ColorHelper;
+
+namespace System.Windows.Shell.Aurora
+{
+ ///
+ /// Interaction logic for PreviewPaneAuroraControl.xaml
+ ///
+ public partial class PreviewPaneAuroraControl : UserControl
+ {
+ public Color Color
+ {
+ get { return (Color)GetValue(ColorProperty); }
+ set { SetValue(ColorProperty, value); }
+ }
+
+ public static readonly DependencyProperty ColorProperty =
+ DependencyProperty.Register("Color", typeof(Color), typeof(PreviewPaneAuroraControl), new PropertyMetadata(Color.FromArgb(255, 0x85, 0x99, 0xB4), OnColorChanged));
+
+ public TimeSpan AnimationDuration
+ {
+ get { return (TimeSpan)GetValue(AnimationDurationProperty); }
+ set { SetValue(AnimationDurationProperty, value); }
+ }
+
+ // Using a DependencyProperty as the backing store for AnimationDuration. This enables animation, styling, binding, etc...
+ public static readonly DependencyProperty AnimationDurationProperty =
+ DependencyProperty.Register("AnimationDuration", typeof(TimeSpan), typeof(PreviewPaneAuroraControl), new PropertyMetadata(TimeSpan.FromSeconds(0.5)));
+
+ private static void OnColorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var oldValue = (Color)e.OldValue;
+ var newValue = (Color)e.NewValue;
+ var control = (PreviewPaneAuroraControl)d;
+
+ var hslColor = newValue.ToHsl();
+ 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);
+
+ control._AnimateAurora(oldValue, newValue, colorBackground);
+ }
+ else
+ {
+ var hslForeground = hslColor with { L = Math.Clamp(hslColor.L * 1.25, 0, 1) };
+ var colorForeground = ColorHelper.FromHsl(hslForeground.H, hslForeground.S, hslForeground.L);
+
+ control._AnimateAurora(oldValue, colorForeground, newValue);
+ }
+ }
+
+ public PreviewPaneAuroraControl()
+ {
+ InitializeComponent();
+ }
+
+ private void _AnimateAurora(Color colorOld, Color colorAurora, Color colorBackground)
+ {
+ var storyboard = new Storyboard();
+ if (BackgroundLayer.Background is not SolidColorBrush backgroundBrush)
+ backgroundBrush = new SolidColorBrush();
+
+ // RegisterName("BackgroundLayerBrush", backgroundBrush);
+
+ var backgroundAnim = new ColorAnimation();
+ backgroundAnim.From = backgroundBrush.Color;
+ backgroundAnim.To = colorBackground;
+ backgroundAnim.Duration = new Duration(AnimationDuration);
+
+ Storyboard.SetTarget(backgroundAnim, backgroundBrush);
+ Storyboard.SetTargetProperty(backgroundAnim, "Color");
+ storyboard.Children.Add(backgroundAnim);
+
+ this._AdjustAurora(storyboard, colorOld, colorAurora, BackgroundLayer);
+
+ storyboard.Begin();
+ }
+
+ private void _AdjustAurora(Storyboard sb, Color colorOld, Color colorNew, UIElement pe)
+ {
+ if (pe is Shape shape)
+ {
+ if (shape.Fill is GradientBrush fill)
+ {
+ Color colorNew1 = colorNew;
+ Color colorOld1 = colorOld;
+ this._AdjustedLinearGradient(sb, fill, colorOld1, colorNew1);
+ }
+ if (shape.Stroke is GradientBrush stroke)
+ {
+ Color colorNew2 = colorNew;
+ Color colorOld2 = colorOld;
+ this._AdjustedLinearGradient(sb, stroke, colorOld2, colorNew2);
+ }
+ }
+
+
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(pe); i++)
+ {
+ var child = VisualTreeHelper.GetChild(pe, i);
+ if (child is not UIElement element) continue;
+
+ Color colorNew3 = colorNew;
+ this._AdjustAurora(sb, colorOld, colorNew3, element);
+ }
+ }
+
+ private void _AdjustedLinearGradient(
+ Storyboard sb,
+ GradientBrush pLinearGradient,
+ Color colorOld,
+ Color colorNew)
+ {
+ GradientStopCollection gradientStops = pLinearGradient.GradientStops;
+ for (int i = 0; i < gradientStops.Count; i++)
+ {
+ GradientStop gradientStop = gradientStops[i];
+ Color color1 = gradientStop.Color;
+ Color color2 = gradientStop.Color;
+ Color color1_1 = Color.FromArgb(255, gradientStop.Color.R, color2.G, color1.B);
+ Color color2_1 = colorOld;
+ Color color4 = !AreClose(color1_1, color2_1) ?
+ gradientStop.Color :
+ Color.FromArgb(gradientStop.Color.A, colorNew.R, colorNew.G, colorNew.B);
+
+ var colorAnimation = new ColorAnimation();
+ colorAnimation.From = color1;
+ colorAnimation.To = color4;
+ colorAnimation.Duration = new Duration(AnimationDuration);
+ colorAnimation.EnableDependentAnimation = true;
+
+ Storyboard.SetTarget(colorAnimation, gradientStop);
+ Storyboard.SetTargetProperty(colorAnimation, "Color");
+
+ sb.Children.Add(colorAnimation);
+ }
+ }
+
+ private bool AreClose(Color color1, Color color2)
+ {
+ return AreClose(color1.R, color2.R) && AreClose(color1.G, color2.G) && AreClose(color1.B, color2.B) && AreClose(color1.A, color2.A);
+ }
+
+ internal static float FLT_EPSILON = 1.1920929E-07f;
+
+ public static bool AreClose(float a, float b)
+ {
+ if (a == b)
+ {
+ return true;
+ }
+
+ float num = (Math.Abs(a) + Math.Abs(b) + 10f) * FLT_EPSILON;
+ float num2 = a - b;
+ if (0f - num < num2)
+ {
+ return num > num2;
+ }
+
+ return false;
+ }
+
+ }
+}
diff --git a/UniSky/Controls/RadialProgressBar/RadialProgressBar.cs b/UniSky/Controls/RadialProgressBar/RadialProgressBar.cs
new file mode 100644
index 0000000..027695f
--- /dev/null
+++ b/UniSky/Controls/RadialProgressBar/RadialProgressBar.cs
@@ -0,0 +1,202 @@
+// 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 Windows.Foundation;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Media;
+
+namespace Microsoft.Toolkit.Uwp.UI.Controls
+{
+ ///
+ /// An alternative implementation of a progress bar.
+ /// Progression is represented by a loop filling up in a clockwise fashion.
+ /// Like the traditional progress bar, it inherits from RangeBase, so Minimum, Maximum and Value properties work the same way.
+ ///
+ [TemplatePart(Name = OutlineFigurePartName, Type = typeof(PathFigure))]
+ [TemplatePart(Name = OutlineArcPartName, Type = typeof(ArcSegment))]
+ [TemplatePart(Name = BarFigurePartName, Type = typeof(PathFigure))]
+ [TemplatePart(Name = BarArcPartName, Type = typeof(ArcSegment))]
+ [TemplatePart(Name = TextPartName, Type = typeof(TextBlock))]
+ public partial class RadialProgressBar : ProgressBar
+ {
+ private const string OutlineFigurePartName = "OutlineFigurePart";
+ private const string OutlineArcPartName = "OutlineArcPart";
+ private const string BarFigurePartName = "BarFigurePart";
+ private const string BarArcPartName = "BarArcPart";
+ private const string TextPartName = "ProgressTextPart";
+
+ private PathFigure outlineFigure;
+ private PathFigure barFigure;
+ private ArcSegment outlineArc;
+ private ArcSegment barArc;
+ private TextBlock textBlock;
+
+ private bool allTemplatePartsDefined = false;
+
+ ///
+ /// Called when the Minimum property changes.
+ ///
+ /// Old value of the Minimum property.
+ /// New value of the Minimum property.
+ protected override void OnMinimumChanged(double oldMinimum, double newMinimum)
+ {
+ base.OnMinimumChanged(oldMinimum, newMinimum);
+ RenderSegment();
+ }
+
+ ///
+ /// Called when the Maximum property changes.
+ ///
+ /// Old value of the Maximum property.
+ /// New value of the Maximum property.
+ protected override void OnMaximumChanged(double oldMaximum, double newMaximum)
+ {
+ base.OnMaximumChanged(oldMaximum, newMaximum);
+ RenderSegment();
+ }
+
+ ///
+ /// Called when the Value property changes.
+ ///
+ /// Old value of the Value property.
+ /// New value of the Value property.
+ protected override void OnValueChanged(double oldValue, double newValue)
+ {
+ base.OnValueChanged(oldValue, newValue);
+ RenderSegment();
+ }
+
+ ///
+ /// Update the visual state of the control when its template is changed.
+ ///
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ outlineFigure = GetTemplateChild(OutlineFigurePartName) as PathFigure;
+ outlineArc = GetTemplateChild(OutlineArcPartName) as ArcSegment;
+ barFigure = GetTemplateChild(BarFigurePartName) as PathFigure;
+ barArc = GetTemplateChild(BarArcPartName) as ArcSegment;
+ textBlock = GetTemplateChild(TextPartName) as TextBlock;
+
+ allTemplatePartsDefined = outlineFigure != null && outlineArc != null && barFigure != null && barArc != null && textBlock != null;
+
+ RenderAll();
+ }
+
+ ///
+ /// Gets or sets the thickness of the circular outline and segment
+ ///
+ public double Thickness
+ {
+ get => (double)GetValue(ThicknessProperty);
+ set => SetValue(ThicknessProperty, value);
+ }
+
+ ///
+ /// Identifies the Thickness dependency property
+ ///
+ public static readonly DependencyProperty ThicknessProperty = DependencyProperty.Register(nameof(Thickness), typeof(double), typeof(RadialProgressBar), new PropertyMetadata(0.0, ThicknessChangedHandler));
+
+ ///
+ /// Gets or sets the color of the circular outline on which the segment is drawn
+ ///
+ public Brush Outline
+ {
+ get => (Brush)GetValue(OutlineProperty);
+ set => SetValue(OutlineProperty, value);
+ }
+
+ ///
+ /// Identifies the Outline dependency property
+ ///
+ public static readonly DependencyProperty OutlineProperty = DependencyProperty.Register(nameof(Outline), typeof(Brush), typeof(RadialProgressBar), new PropertyMetadata(new SolidColorBrush(Windows.UI.Colors.Transparent)));
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Create a default circular progress bar
+ ///
+ public RadialProgressBar()
+ {
+ DefaultStyleKey = typeof(RadialProgressBar);
+ SizeChanged += SizeChangedHandler;
+ }
+
+ // Render outline and progress segment when thickness is changed
+ private static void ThicknessChangedHandler(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var sender = d as RadialProgressBar;
+ sender.RenderAll();
+ }
+
+ // Render outline and progress segment when control is resized.
+ private void SizeChangedHandler(object sender, SizeChangedEventArgs e)
+ {
+ var self = sender as RadialProgressBar;
+ self.RenderAll();
+ }
+
+ private double ComputeNormalizedRange()
+ {
+ var range = Maximum - Minimum;
+ var delta = Value - Minimum;
+ var output = range == 0.0 ? 0.0 : delta / range;
+ output = Math.Min(Math.Max(0.0, output), 0.9999);
+ return output;
+ }
+
+ // Compute size of ellipse so that the outer edge touches the bounding rectangle
+ private Size ComputeEllipseSize()
+ {
+ var safeThickness = Math.Max(Thickness, 0.0);
+ var width = Math.Max((ActualWidth - safeThickness) / 2.0, 0.0);
+ var height = Math.Max((ActualHeight - safeThickness) / 2.0, 0.0);
+ return new Size(width, height);
+ }
+
+ // Render the segment representing progress ratio.
+ private void RenderSegment()
+ {
+ if (!allTemplatePartsDefined)
+ {
+ return;
+ }
+
+ textBlock.Text = ((int)(Maximum - Value)).ToString();
+
+ var normalizedRange = ComputeNormalizedRange();
+
+ var angle = 2 * Math.PI * normalizedRange;
+ var size = ComputeEllipseSize();
+ var translationFactor = Math.Max(Thickness / 2.0, 0.0);
+
+ double x = (Math.Sin(angle) * size.Width) + size.Width + translationFactor;
+ double y = (((Math.Cos(angle) * size.Height) - size.Height) * -1) + translationFactor;
+
+ barArc.IsLargeArc = angle >= Math.PI;
+ barArc.Point = new Point(x, y);
+ }
+
+ // Render the progress segment and the loop outline. Needs to run when control is resized or retemplated
+ private void RenderAll()
+ {
+ if (!allTemplatePartsDefined)
+ {
+ return;
+ }
+
+ var size = ComputeEllipseSize();
+ var segmentWidth = size.Width;
+ var translationFactor = Math.Max(Thickness / 2.0, 0.0);
+
+ outlineFigure.StartPoint = barFigure.StartPoint = new Point(segmentWidth + translationFactor, translationFactor);
+ outlineArc.Size = barArc.Size = new Size(segmentWidth, size.Height);
+ outlineArc.Point = new Point(segmentWidth + translationFactor - 0.05, translationFactor);
+
+ RenderSegment();
+ }
+ }
+}
\ No newline at end of file
diff --git a/UniSky/Controls/RadialProgressBar/RadialProgressBar.xaml b/UniSky/Controls/RadialProgressBar/RadialProgressBar.xaml
new file mode 100644
index 0000000..8b2f8c5
--- /dev/null
+++ b/UniSky/Controls/RadialProgressBar/RadialProgressBar.xaml
@@ -0,0 +1,63 @@
+
+
+
+
\ No newline at end of file
diff --git a/UniSky/Controls/RichTextBlock/RichTextBlock.cs b/UniSky/Controls/RichTextBlock/RichTextBlock.cs
new file mode 100644
index 0000000..310e85b
--- /dev/null
+++ b/UniSky/Controls/RichTextBlock/RichTextBlock.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Toolkit.Uwp.UI.Extensions;
+using UniSky.Pages;
+using UniSky.Services;
+using UniSky.ViewModels.Text;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Documents;
+
+// The Templated Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234235
+
+namespace UniSky.Controls
+{
+ [TemplatePart(Name = "PART_TextBlock", Type = typeof(TextBlock))]
+ public sealed class RichTextBlock : Control
+ {
+ private static readonly DependencyProperty HyperlinkUrlProperty =
+ DependencyProperty.RegisterAttached("HyperlinkUrl", typeof(Uri), typeof(RichTextBlock), new PropertyMetadata(null));
+
+ public TextWrapping TextWrapping
+ {
+ get { return (TextWrapping)GetValue(TextWrappingProperty); }
+ set { SetValue(TextWrappingProperty, value); }
+ }
+
+ public static readonly DependencyProperty TextWrappingProperty =
+ DependencyProperty.Register("TextWrapping", typeof(TextWrapping), typeof(RichTextBlock), new PropertyMetadata(TextWrapping.Wrap));
+
+ public bool IsTextSelectionEnabled
+ {
+ get { return (bool)GetValue(IsTextSelectionEnabledProperty); }
+ set { SetValue(IsTextSelectionEnabledProperty, value); }
+ }
+
+ public static readonly DependencyProperty IsTextSelectionEnabledProperty =
+ DependencyProperty.Register("IsTextSelectionEnabled", typeof(bool), typeof(RichTextBlock), new PropertyMetadata(true));
+
+ public IList Inlines
+ {
+ get { return (IList)GetValue(InlinesProperty); }
+ set { SetValue(InlinesProperty, value); }
+ }
+
+ public static readonly DependencyProperty InlinesProperty =
+ DependencyProperty.Register("Inlines", typeof(IList), typeof(RichTextBlock), new PropertyMetadata(null, OnInlinesChanged));
+
+ private static void OnInlinesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is not RichTextBlock text)
+ return;
+
+ text.UpdateInlines();
+ }
+
+ public RichTextBlock()
+ {
+ this.DefaultStyleKey = typeof(RichTextBlock);
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ UpdateInlines();
+ }
+
+ private void UpdateInlines()
+ {
+ this.ApplyTemplate();
+
+ if (this.FindDescendantByName("PART_TextBlock") is not TextBlock text)
+ return;
+
+ text.Inlines.Clear();
+
+ // TODO: this could be cleaner
+ foreach (var inline in Inlines)
+ {
+ var mention = inline.Properties.OfType()
+ .FirstOrDefault();
+ if (mention != null)
+ {
+ var hyperlink = new Hyperlink()
+ {
+ Inlines = { new Run() { Text = inline.Text } }
+ };
+
+ hyperlink.Click += Hyperlink_Click;
+ hyperlink.SetValue(HyperlinkUrlProperty, new Uri("unisky:///profile/" + mention.Did.ToString()));
+
+ text.Inlines.Add(hyperlink);
+ continue;
+ }
+
+ var tag = inline.Properties.OfType()
+ .FirstOrDefault();
+ if (tag != null)
+ {
+ var hyperlink = new Hyperlink()
+ {
+ Inlines = { new Run() { Text = inline.Text } }
+ };
+
+ hyperlink.Click += Hyperlink_Click;
+ hyperlink.SetValue(HyperlinkUrlProperty, new Uri("unisky:///tag/" + tag.Tag));
+
+ text.Inlines.Add(hyperlink);
+ continue;
+ }
+
+ var link = inline.Properties.OfType()
+ .FirstOrDefault();
+ if (link != null && Uri.TryCreate(link.Url, UriKind.Absolute, out var uri))
+ {
+ var hyperlink = new Hyperlink()
+ {
+ NavigateUri = uri,
+ Inlines = { new Run() { Text = inline.Text } }
+ };
+
+ text.Inlines.Add(hyperlink);
+ continue;
+ }
+
+ text.Inlines.Add(new Run() { Text = inline.Text });
+ }
+ }
+
+ // TODO: move this somewhere better
+ private void Hyperlink_Click(Hyperlink sender, HyperlinkClickEventArgs args)
+ {
+ if (sender.GetValue(HyperlinkUrlProperty) is not Uri { Scheme: "unisky" } uri)
+ return;
+
+ var service = ServiceContainer.Scoped.GetRequiredService()
+ .GetNavigationService("Home");
+
+ var path = uri.PathAndQuery.Split('/', StringSplitOptions.RemoveEmptyEntries);
+ switch (path.FirstOrDefault()?.ToLowerInvariant())
+ {
+ case "profile":
+ service.Navigate(uri);
+ break;
+ case "tag":
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/UniSky/Controls/Settings/SettingsNeedsRelaunchDialog.xaml b/UniSky/Controls/Settings/SettingsNeedsRelaunchDialog.xaml
new file mode 100644
index 0000000..74eaadf
--- /dev/null
+++ b/UniSky/Controls/Settings/SettingsNeedsRelaunchDialog.xaml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/UniSky/Controls/Compose/ComposeDialog.xaml.cs b/UniSky/Controls/Settings/SettingsNeedsRelaunchDialog.xaml.cs
similarity index 51%
rename from UniSky/Controls/Compose/ComposeDialog.xaml.cs
rename to UniSky/Controls/Settings/SettingsNeedsRelaunchDialog.xaml.cs
index cfbbbe9..f598a32 100644
--- a/UniSky/Controls/Compose/ComposeDialog.xaml.cs
+++ b/UniSky/Controls/Settings/SettingsNeedsRelaunchDialog.xaml.cs
@@ -3,9 +3,6 @@
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
-using CommunityToolkit.Mvvm.DependencyInjection;
-using Microsoft.Extensions.DependencyInjection;
-using UniSky.ViewModels.Compose;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
@@ -18,28 +15,15 @@
// The Content Dialog item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238
-namespace UniSky.Controls.Compose
+namespace UniSky.Controls.Settings
{
- public sealed partial class ComposeDialog : ContentDialog
+ public sealed partial class SettingsNeedsRelaunchDialog : ContentDialog
{
- public ComposeViewModel ViewModel
- {
- get { return (ComposeViewModel)GetValue(ViewModelProperty); }
- set { SetValue(ViewModelProperty, value); }
- }
-
- public static readonly DependencyProperty ViewModelProperty =
- DependencyProperty.Register("ViewModel", typeof(ComposeViewModel), typeof(ComposeDialog), new PropertyMetadata(null));
-
- public ComposeDialog()
+ public SettingsNeedsRelaunchDialog()
{
this.InitializeComponent();
- this.ViewModel = ActivatorUtilities.CreateInstance(Ioc.Default);
}
- public bool Not(bool b, bool a)
- => !a && !b;
-
private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
}
diff --git a/UniSky/Controls/Settings/SettingsSheet.xaml b/UniSky/Controls/Settings/SettingsSheet.xaml
new file mode 100644
index 0000000..d9ff25e
--- /dev/null
+++ b/UniSky/Controls/Settings/SettingsSheet.xaml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/UniSky/Controls/Settings/SettingsSheet.xaml.cs b/UniSky/Controls/Settings/SettingsSheet.xaml.cs
new file mode 100644
index 0000000..2369f34
--- /dev/null
+++ b/UniSky/Controls/Settings/SettingsSheet.xaml.cs
@@ -0,0 +1,52 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using UniSky.Controls.Sheet;
+using UniSky.Services;
+using UniSky.ViewModels.Settings;
+using Windows.ApplicationModel.Core;
+using Windows.Foundation;
+using Windows.Foundation.Metadata;
+using Windows.UI.Xaml.Controls;
+
+namespace UniSky.Controls.Settings;
+
+public sealed partial class SettingsSheet : SheetControl
+{
+ public SettingsSheet()
+ {
+ this.InitializeComponent();
+ this.Showing += OnShowing;
+ this.Hiding += OnHiding;
+ }
+
+ private void OnShowing(SheetControl sender, SheetShowingEventArgs args)
+ {
+ this.DataContext = ActivatorUtilities.CreateInstance(ServiceContainer.Scoped);
+ }
+
+ private async void OnHiding(SheetControl sender, SheetHidingEventArgs args)
+ {
+ if (!ApiInformation.IsMethodPresent("Windows.ApplicationModel.Core.CoreApplication", "RequestRestartAsync"))
+ return;
+
+ if (this.DataContext is not SettingsViewModel { IsDirty: true })
+ return;
+
+ var deferral = args.GetDeferral();
+ try
+ {
+ var needsRelaunchDialog = new SettingsNeedsRelaunchDialog();
+ if (Controller != null && ApiInformation.IsApiContractPresent(typeof(UniversalApiContract).FullName, 8))
+ needsRelaunchDialog.XamlRoot = Controller.Root.XamlRoot;
+
+ if (await needsRelaunchDialog.ShowAsync() == ContentDialogResult.Primary)
+ {
+ await CoreApplication.RequestRestartAsync("");
+ }
+ }
+ finally
+ {
+ deferral.Complete();
+ }
+ }
+}
diff --git a/UniSky/Controls/Sheet/SheetControl.cs b/UniSky/Controls/Sheet/SheetControl.cs
new file mode 100644
index 0000000..daa7658
--- /dev/null
+++ b/UniSky/Controls/Sheet/SheetControl.cs
@@ -0,0 +1,309 @@
+using System.Threading.Tasks;
+using System.Windows.Input;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Toolkit.Uwp.UI.Extensions;
+using UniSky.Services;
+using Windows.Foundation;
+using Windows.UI.ViewManagement;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Markup;
+
+// The Templated Control item template is documented at https://go.microsoft.com/fwlink/?LinkId=234235
+
+namespace UniSky.Controls.Sheet
+{
+ public class SheetShowingEventArgs : RoutedEventArgs
+ {
+ public object Parameter { get; }
+
+ public SheetShowingEventArgs(object parameter)
+ {
+ Parameter = parameter;
+ }
+ }
+
+ public class SheetHidingEventArgs : RoutedEventArgs
+ {
+ private Deferral _deferral;
+ private TaskCompletionSource