feat: create cloud-free Wino Mail fork

This commit is contained in:
root
2026-04-29 01:32:51 +02:00
parent ef12e0ff3a
commit e2cc79a471
37 changed files with 137 additions and 3905 deletions

View File

@@ -1,51 +0,0 @@
<ContentDialog
x:Class="Wino.Dialogs.WinoAccountEmailConfirmationRequiredDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{x:Bind domain:Translator.WinoAccount_EmailConfirmationPendingDialog_Title}"
PrimaryButtonClick="ResendClicked"
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
PrimaryButtonText="{x:Bind domain:Translator.WinoAccount_EmailConfirmationPendingDialog_ResendButton}"
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Close}"
Style="{StaticResource WinoDialogStyle}"
mc:Ignorable="d">
<ContentDialog.Resources>
<x:Double x:Key="ContentDialogMinWidth">520</x:Double>
<x:Double x:Key="ContentDialogMaxWidth">520</x:Double>
</ContentDialog.Resources>
<StackPanel Spacing="16">
<Border
Padding="14"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="12">
<StackPanel Spacing="8">
<TextBlock
x:Name="MessageTextBlock"
TextWrapping="WrapWholeWords" />
<TextBlock
x:Name="CountdownTextBlock"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Border>
<ProgressRing
x:Name="BusyRing"
Width="20"
Height="20"
HorizontalAlignment="Left"
IsActive="False"
Visibility="Collapsed" />
<TextBlock
x:Name="ErrorTextBlock"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
TextWrapping="WrapWholeWords"
Visibility="Collapsed" />
</StackPanel>
</ContentDialog>

View File

@@ -1,124 +0,0 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain;
using Wino.Core.Domain.Interfaces;
using Wino.Mail.Api.Contracts.Auth;
using Wino.Mail.WinUI.Services;
namespace Wino.Dialogs;
public sealed partial class WinoAccountEmailConfirmationRequiredDialog : ContentDialog
{
private readonly IWinoAccountProfileService _profileService;
private readonly DispatcherTimer _countdownTimer;
private readonly string _email;
private readonly string _endpoint;
private readonly string _ticket;
private DateTimeOffset _resendAvailableAtUtc;
public WinoAccountEmailConfirmationRequiredDialog(IWinoAccountProfileService profileService, string email, EmailConfirmationRequiredDetailsDto details)
{
_profileService = profileService;
_email = email;
_endpoint = details.ResendConfirmationEndpoint;
_ticket = details.ResendConfirmationTicket;
_resendAvailableAtUtc = details.ResendAvailableAtUtc;
InitializeComponent();
MessageTextBlock.Text = string.Format(Translator.WinoAccount_EmailConfirmationPendingDialog_Message, email);
_countdownTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_countdownTimer.Tick += CountdownTimer_Tick;
Closing += DialogClosing;
UpdateCountdown();
_countdownTimer.Start();
}
public bool ResendSucceeded { get; private set; }
private async void ResendClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
args.Cancel = true;
if (DateTimeOffset.UtcNow < _resendAvailableAtUtc)
{
UpdateCountdown();
return;
}
var deferral = args.GetDeferral();
try
{
SetBusyState(true);
HideError();
var response = await _profileService.ResendEmailConfirmationAsync(_endpoint, _ticket);
if (!response.IsSuccess)
{
ShowError(WinoAccountAuthErrorTranslator.Translate(response.ErrorCode));
return;
}
ResendSucceeded = true;
Hide();
}
finally
{
SetBusyState(false);
deferral.Complete();
}
}
private void CountdownTimer_Tick(object? sender, object e) => UpdateCountdown();
private void UpdateCountdown()
{
var remaining = _resendAvailableAtUtc - DateTimeOffset.UtcNow;
if (remaining <= TimeSpan.Zero)
{
IsPrimaryButtonEnabled = true;
CountdownTextBlock.Text = Translator.WinoAccount_EmailConfirmationPendingDialog_ReadyToResend;
return;
}
IsPrimaryButtonEnabled = false;
CountdownTextBlock.Text = string.Format(
Translator.WinoAccount_EmailConfirmationPendingDialog_Countdown,
$"{Math.Max(0, (int)remaining.TotalMinutes):00}:{Math.Max(0, remaining.Seconds):00}");
}
private void SetBusyState(bool isBusy)
{
IsPrimaryButtonEnabled = !isBusy && DateTimeOffset.UtcNow >= _resendAvailableAtUtc;
IsSecondaryButtonEnabled = !isBusy;
BusyRing.IsActive = isBusy;
BusyRing.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
}
private void ShowError(string message)
{
ErrorTextBlock.Text = message;
ErrorTextBlock.Visibility = Visibility.Visible;
}
private void HideError()
{
ErrorTextBlock.Text = string.Empty;
ErrorTextBlock.Visibility = Visibility.Collapsed;
}
private void DialogClosing(ContentDialog sender, ContentDialogClosingEventArgs args)
{
_countdownTimer.Stop();
_countdownTimer.Tick -= CountdownTimer_Tick;
Closing -= DialogClosing;
}
}

View File

@@ -1,273 +0,0 @@
<ContentDialog
x:Class="Wino.Dialogs.WinoAccountLoginDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{x:Bind domain:Translator.WinoAccount_LoginDialog_Title}"
PrimaryButtonClick="LoginClicked"
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
PrimaryButtonText="{x:Bind domain:Translator.Buttons_SignIn}"
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Cancel}"
Style="{StaticResource WinoDialogStyle}"
mc:Ignorable="d">
<ContentDialog.Resources>
<x:Double x:Key="ContentDialogMinWidth">520</x:Double>
<x:Double x:Key="ContentDialogMaxWidth">520</x:Double>
</ContentDialog.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="20">
<!-- Hero illustration area -->
<Border
Height="140"
CornerRadius="12">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="#1A6EE7B7" />
<GradientStop Offset="0.5" Color="#2038BDF8" />
<GradientStop Offset="1" Color="#1A818CF8" />
</LinearGradientBrush>
</Border.Background>
<Canvas>
<!-- Background decorative circles -->
<Ellipse
Canvas.Left="340"
Canvas.Top="-20"
Width="100"
Height="100"
Opacity="0.15">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Offset="0" Color="#38BDF8" />
<GradientStop Offset="1" Color="Transparent" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse
Canvas.Left="20"
Canvas.Top="80"
Width="80"
Height="80"
Opacity="0.1">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Offset="0" Color="#6EE7B7" />
<GradientStop Offset="1" Color="Transparent" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!-- Shield with lock illustration -->
<Canvas Canvas.Left="190" Canvas.Top="16">
<!-- Shield shape -->
<Path
Data="M46 4 L82 18 L82 48 C82 66 65 80 46 84 C27 80 10 66 10 48 L10 18 Z"
Opacity="0.9">
<Path.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="#6EE7B7" />
<GradientStop Offset="1" Color="#059669" />
</LinearGradientBrush>
</Path.Fill>
</Path>
<!-- Inner shield -->
<Path
Data="M46 12 L76 24 L76 48 C76 64 62 76 46 80 C30 76 16 64 16 48 L16 24 Z"
Opacity="0.5">
<Path.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#A7F3D0" />
<GradientStop Offset="1" Color="#34D399" />
</LinearGradientBrush>
</Path.Fill>
</Path>
<!-- Lock body -->
<Rectangle
Canvas.Left="32"
Canvas.Top="44"
Width="28"
Height="20"
Fill="White"
RadiusX="5"
RadiusY="5" />
<!-- Lock shackle -->
<Path
Data="M38 44 L38 38 A8 8 0 0 1 54 38 L54 44"
Stroke="White"
StrokeEndLineCap="Round"
StrokeStartLineCap="Round"
StrokeThickness="4"
Fill="Transparent" />
<!-- Keyhole -->
<Ellipse
Canvas.Left="42"
Canvas.Top="49"
Width="8"
Height="8"
Fill="#059669" />
<Rectangle
Canvas.Left="44"
Canvas.Top="55"
Width="4"
Height="6"
Fill="#059669"
RadiusX="2"
RadiusY="2" />
</Canvas>
<!-- Decorative sparkles -->
<Ellipse
Canvas.Left="150"
Canvas.Top="30"
Width="6"
Height="6"
Fill="{ThemeResource AccentTextFillColorPrimaryBrush}"
Opacity="0.5" />
<Ellipse
Canvas.Left="320"
Canvas.Top="50"
Width="5"
Height="5"
Fill="#6EE7B7"
Opacity="0.6" />
<Ellipse
Canvas.Left="130"
Canvas.Top="100"
Width="4"
Height="4"
Fill="#38BDF8"
Opacity="0.4" />
<Ellipse
Canvas.Left="360"
Canvas.Top="100"
Width="4"
Height="4"
Fill="#A78BFA"
Opacity="0.5" />
</Canvas>
</Border>
<!-- Benefits cards -->
<StackPanel
x:Name="BenefitsPanel"
Spacing="8">
<Border
Padding="14"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="12">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Width="36"
Height="36"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="8"
Opacity="0.15" />
<FontIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE8D7;" />
<StackPanel Grid.Column="1" Spacing="2">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.WinoAccount_LoginDialog_BenefitsTitle}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.WinoAccount_LoginDialog_BenefitsDescription}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Grid>
</Border>
<Border
Padding="14"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="12">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Width="36"
Height="36"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="8"
Opacity="0.15" />
<FontIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE946;" />
<StackPanel Grid.Column="1" Spacing="2">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.WinoAccount_LoginDialog_DifferenceTitle}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.WinoAccount_LoginDialog_DifferenceDescription}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
<!-- Input fields -->
<StackPanel Spacing="12">
<TextBox
x:Name="EmailTextBox"
Header="{x:Bind domain:Translator.WinoAccount_EmailLabel}"
KeyDown="EmailTextBox_KeyDown"
PlaceholderText="{x:Bind domain:Translator.WinoAccount_EmailPlaceholder}"
TextChanging="InputChanged" />
<StackPanel x:Name="PasswordPanel">
<PasswordBox
x:Name="PasswordBox"
Header="{x:Bind domain:Translator.WinoAccount_PasswordLabel}"
KeyDown="PasswordBox_KeyDown"
PasswordChanged="InputChanged" />
</StackPanel>
<Border
x:Name="ForgotPasswordInfoPanel"
Padding="14"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="12"
Visibility="Collapsed">
<TextBlock
Text="{x:Bind domain:Translator.WinoAccount_ForgotPasswordDialog_Description}"
TextWrapping="WrapWholeWords" />
</Border>
</StackPanel>
<HyperlinkButton
x:Name="ModeToggleButton"
HorizontalAlignment="Left"
Click="ModeToggleButton_Click"
Content="{x:Bind domain:Translator.WinoAccount_LoginDialog_ForgotPasswordLink}" />
<ProgressRing
x:Name="BusyRing"
Width="20"
Height="20"
HorizontalAlignment="Left"
IsActive="False"
Visibility="Collapsed" />
<TextBlock
x:Name="ErrorTextBlock"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
TextWrapping="WrapWholeWords"
Visibility="Collapsed" />
</StackPanel>
</ScrollViewer>
</ContentDialog>

View File

@@ -1,201 +0,0 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Windows.System;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
using Wino.Mail.Api.Contracts.Auth;
using Wino.Mail.WinUI.Services;
namespace Wino.Dialogs;
public sealed partial class WinoAccountLoginDialog : ContentDialog
{
private readonly IWinoAccountProfileService _profileService;
private bool _isForgotPasswordMode;
public WinoAccountLoginDialog(IWinoAccountProfileService profileService)
{
_profileService = profileService;
InitializeComponent();
UpdateMode();
}
public WinoAccount? Result { get; private set; }
public string? PendingConfirmationEmailAddress { get; private set; }
public EmailConfirmationRequiredDetailsDto? EmailConfirmationRequiredDetails { get; private set; }
public string? PasswordResetEmailAddress { get; private set; }
private async void LoginClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
args.Cancel = true;
var validationError = ValidateInput();
if (!string.IsNullOrWhiteSpace(validationError))
{
ShowError(validationError);
return;
}
var deferral = args.GetDeferral();
try
{
await PerformPrimaryActionAsync();
}
finally
{
deferral.Complete();
}
}
private async System.Threading.Tasks.Task PerformPrimaryActionAsync()
{
var validationError = ValidateInput();
if (!string.IsNullOrWhiteSpace(validationError))
{
ShowError(validationError);
return;
}
try
{
SetBusyState(true);
HideError();
if (_isForgotPasswordMode)
{
var forgotPasswordResponse = await _profileService.ForgotPasswordAsync(EmailTextBox.Text.Trim());
if (!forgotPasswordResponse.IsSuccess)
{
ShowError(WinoAccountAuthErrorTranslator.Translate(forgotPasswordResponse.ErrorCode));
return;
}
PasswordResetEmailAddress = EmailTextBox.Text.Trim();
Hide();
return;
}
var result = await _profileService.LoginAsync(EmailTextBox.Text.Trim(), PasswordBox.Password);
if (!result.IsSuccess || result.Account == null)
{
var confirmationDetails = WinoAccountEmailConfirmationHelper.Parse(result.ErrorDetails);
if (WinoAccountEmailConfirmationHelper.IsEmailConfirmationRequiredError(result.ErrorCode) && confirmationDetails != null)
{
PendingConfirmationEmailAddress = EmailTextBox.Text.Trim();
EmailConfirmationRequiredDetails = confirmationDetails;
Hide();
return;
}
ShowError(WinoAccountAuthErrorTranslator.Format(result.ErrorCode, result.ErrorMessage));
return;
}
Result = result.Account;
Hide();
}
finally
{
SetBusyState(false);
}
}
private string ValidateInput()
{
if (string.IsNullOrWhiteSpace(EmailTextBox.Text))
{
return Translator.WinoAccount_Validation_EmailRequired;
}
if (_isForgotPasswordMode)
{
return string.Empty;
}
if (string.IsNullOrWhiteSpace(PasswordBox.Password))
{
return Translator.WinoAccount_Validation_PasswordRequired;
}
return string.Empty;
}
private async void EmailTextBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
{
if (_isForgotPasswordMode)
{
e.Handled = true;
await PerformPrimaryActionAsync();
return;
}
PasswordBox.Focus(FocusState.Programmatic);
e.Handled = true;
}
}
private async void PasswordBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
{
e.Handled = true;
await PerformPrimaryActionAsync();
}
}
private void ModeToggleButton_Click(object sender, RoutedEventArgs e)
{
_isForgotPasswordMode = !_isForgotPasswordMode;
PasswordBox.Password = string.Empty;
HideError();
UpdateMode();
}
private void InputChanged(TextBox sender, TextBoxTextChangingEventArgs args) => HideError();
private void InputChanged(object sender, RoutedEventArgs e) => HideError();
private void SetBusyState(bool isBusy)
{
IsPrimaryButtonEnabled = !isBusy;
IsSecondaryButtonEnabled = !isBusy;
BusyRing.IsActive = isBusy;
BusyRing.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
}
private void ShowError(string message)
{
ErrorTextBlock.Text = message;
ErrorTextBlock.Visibility = Visibility.Visible;
}
private void HideError()
{
ErrorTextBlock.Text = string.Empty;
ErrorTextBlock.Visibility = Visibility.Collapsed;
}
private void UpdateMode()
{
Title = _isForgotPasswordMode
? Translator.WinoAccount_ForgotPasswordDialog_Title
: Translator.WinoAccount_LoginDialog_Title;
PrimaryButtonText = _isForgotPasswordMode
? Translator.WinoAccount_ForgotPasswordDialog_PrimaryButton
: Translator.Buttons_SignIn;
BenefitsPanel.Visibility = _isForgotPasswordMode ? Visibility.Collapsed : Visibility.Visible;
PasswordPanel.Visibility = _isForgotPasswordMode ? Visibility.Collapsed : Visibility.Visible;
ForgotPasswordInfoPanel.Visibility = _isForgotPasswordMode ? Visibility.Visible : Visibility.Collapsed;
ModeToggleButton.Content = _isForgotPasswordMode
? Translator.WinoAccount_ForgotPasswordDialog_BackToSignIn
: Translator.WinoAccount_LoginDialog_ForgotPasswordLink;
}
}

View File

@@ -1,300 +0,0 @@
<ContentDialog
x:Class="Wino.Dialogs.WinoAccountRegistrationDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{x:Bind domain:Translator.WinoAccount_RegisterDialog_Title}"
FullSizeDesired="True"
PrimaryButtonClick="RegisterClicked"
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
PrimaryButtonText="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrimaryButton}"
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Cancel}"
Style="{StaticResource WinoDialogStyle}"
mc:Ignorable="d">
<ContentDialog.Resources>
<x:Double x:Key="ContentDialogMinWidth">560</x:Double>
<x:Double x:Key="ContentDialogMaxWidth">560</x:Double>
<x:Double x:Key="ContentDialogMaxHeight">900</x:Double>
</ContentDialog.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="20">
<!-- Hero illustration area -->
<Border Height="140" CornerRadius="12">
<Border.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="#1A818CF8" />
<GradientStop Offset="0.5" Color="#20A78BFA" />
<GradientStop Offset="1" Color="#1AE879F9" />
</LinearGradientBrush>
</Border.Background>
<Canvas>
<!-- Background decorative circles -->
<Ellipse
Canvas.Left="370"
Canvas.Top="-10"
Width="90"
Height="90"
Opacity="0.12">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Offset="0" Color="#A78BFA" />
<GradientStop Offset="1" Color="Transparent" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse
Canvas.Left="30"
Canvas.Top="90"
Width="70"
Height="70"
Opacity="0.1">
<Ellipse.Fill>
<RadialGradientBrush>
<GradientStop Offset="0" Color="#38BDF8" />
<GradientStop Offset="1" Color="Transparent" />
</RadialGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!-- Person/account illustration -->
<Canvas Canvas.Left="200" Canvas.Top="12">
<!-- Circular badge background -->
<Ellipse
Canvas.Left="8"
Canvas.Top="8"
Width="80"
Height="80"
Opacity="0.9">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="#818CF8" />
<GradientStop Offset="1" Color="#6366F1" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!-- Inner circle -->
<Ellipse
Canvas.Left="14"
Canvas.Top="14"
Width="68"
Height="68"
Opacity="0.35">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="#C7D2FE" />
<GradientStop Offset="1" Color="#A5B4FC" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!-- Person head -->
<Ellipse
Canvas.Left="34"
Canvas.Top="24"
Width="28"
Height="28"
Fill="White" />
<!-- Person body -->
<Path Data="M28 68 A20 16 0 0 1 68 68" Fill="White" />
<!-- Plus badge -->
<Ellipse
Canvas.Left="62"
Canvas.Top="62"
Width="28"
Height="28">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="#4ADE80" />
<GradientStop Offset="1" Color="#16A34A" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<!-- Plus sign -->
<Rectangle
Canvas.Left="73"
Canvas.Top="70"
Width="4"
Height="16"
Fill="White"
RadiusX="2"
RadiusY="2" />
<Rectangle
Canvas.Left="69"
Canvas.Top="74"
Width="12"
Height="4"
Fill="White"
RadiusX="2"
RadiusY="2" />
</Canvas>
<!-- Decorative sparkles -->
<Ellipse
Canvas.Left="140"
Canvas.Top="35"
Width="6"
Height="6"
Fill="#A78BFA"
Opacity="0.5" />
<Ellipse
Canvas.Left="350"
Canvas.Top="55"
Width="5"
Height="5"
Fill="#38BDF8"
Opacity="0.6" />
<Ellipse
Canvas.Left="120"
Canvas.Top="95"
Width="4"
Height="4"
Fill="#6EE7B7"
Opacity="0.4" />
<Ellipse
Canvas.Left="380"
Canvas.Top="105"
Width="4"
Height="4"
Fill="#F472B6"
Opacity="0.5" />
</Canvas>
</Border>
<!-- Benefits section -->
<StackPanel Spacing="8">
<TextBlock
Margin="0,0,0,4"
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.WinoAccount_RegisterDialog_BenefitsTitle}" />
<Border
Padding="14"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="12">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Width="36"
Height="36"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="8"
Opacity="0.15" />
<FontIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xE8D7;" />
<StackPanel Grid.Column="1" Spacing="2">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.WinoAccount_RegisterDialog_BenefitSyncTitle}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.WinoAccount_RegisterDialog_BenefitSyncDescription}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Grid>
</Border>
<Border
Padding="14"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="12">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border
Width="36"
Height="36"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="8"
Opacity="0.15" />
<FontIcon
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="16"
Foreground="{ThemeResource AccentTextFillColorPrimaryBrush}"
Glyph="&#xEA18;" />
<StackPanel Grid.Column="1" Spacing="2">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.WinoAccount_RegisterDialog_BenefitAiTitle}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.WinoAccount_RegisterDialog_BenefitAiDescription}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Grid>
</Border>
</StackPanel>
<TextBlock
x:Name="ErrorTextBlock"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"
TextWrapping="WrapWholeWords"
Visibility="Collapsed" />
<!-- Input fields -->
<StackPanel Spacing="12">
<TextBox
x:Name="EmailTextBox"
Header="{x:Bind domain:Translator.WinoAccount_EmailLabel}"
KeyDown="EmailTextBox_KeyDown"
PlaceholderText="{x:Bind domain:Translator.WinoAccount_EmailPlaceholder}"
TextChanging="InputChanged" />
<PasswordBox
x:Name="PasswordBox"
Header="{x:Bind domain:Translator.WinoAccount_PasswordLabel}"
KeyDown="PasswordBox_KeyDown"
PasswordChanged="InputChanged" />
<PasswordBox
x:Name="ConfirmPasswordBox"
Header="{x:Bind domain:Translator.WinoAccount_ConfirmPasswordLabel}"
KeyDown="ConfirmPasswordBox_KeyDown"
PasswordChanged="InputChanged" />
</StackPanel>
<Border
Padding="14"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="12">
<StackPanel Spacing="10">
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrivacyTitle}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrivacyDescription}"
TextWrapping="WrapWholeWords" />
<HyperlinkButton
HorizontalAlignment="Left"
Click="PrivacyPolicyLink_Click"
Content="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrivacyLinkText}" />
<CheckBox
x:Name="PrivacyPolicyCheckBox"
Checked="InputChanged"
Content="{x:Bind domain:Translator.WinoAccount_RegisterDialog_PrivacyCheckbox}"
Unchecked="InputChanged" />
</StackPanel>
</Border>
<ProgressRing
x:Name="BusyRing"
Width="20"
Height="20"
HorizontalAlignment="Left"
IsActive="False"
Visibility="Collapsed" />
</StackPanel>
</ScrollViewer>
</ContentDialog>

View File

@@ -1,161 +0,0 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Windows.System;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Interfaces;
using Wino.Mail.WinUI.Services;
namespace Wino.Dialogs;
public sealed partial class WinoAccountRegistrationDialog : ContentDialog
{
private const string PrivacyPolicyUrl = "https://www.winomail.app/accounts_policy.html";
private readonly IWinoAccountProfileService _profileService;
public WinoAccountRegistrationDialog(IWinoAccountProfileService profileService)
{
_profileService = profileService;
InitializeComponent();
}
public WinoAccount? Result { get; private set; }
public string? ConfirmationEmailAddress { get; private set; }
private async void RegisterClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
args.Cancel = true;
var validationError = ValidateInput();
if (!string.IsNullOrWhiteSpace(validationError))
{
ShowError(validationError);
return;
}
var deferral = args.GetDeferral();
try
{
await PerformRegistrationAsync();
}
finally
{
deferral.Complete();
}
}
private async System.Threading.Tasks.Task PerformRegistrationAsync()
{
var validationError = ValidateInput();
if (!string.IsNullOrWhiteSpace(validationError))
{
ShowError(validationError);
return;
}
try
{
SetBusyState(true);
HideError();
var result = await _profileService.RegisterAsync(EmailTextBox.Text.Trim(), PasswordBox.Password);
if (!result.IsSuccess || result.Account == null)
{
ShowError(WinoAccountAuthErrorTranslator.Format(result.ErrorCode, result.ErrorMessage));
return;
}
ConfirmationEmailAddress = result.Account.Email;
Hide();
}
finally
{
SetBusyState(false);
}
}
private string ValidateInput()
{
if (string.IsNullOrWhiteSpace(EmailTextBox.Text))
{
return Translator.WinoAccount_Validation_EmailRequired;
}
if (string.IsNullOrWhiteSpace(PasswordBox.Password))
{
return Translator.WinoAccount_Validation_PasswordRequired;
}
if (!string.Equals(PasswordBox.Password, ConfirmPasswordBox.Password, StringComparison.Ordinal))
{
return Translator.WinoAccount_Validation_PasswordMismatch;
}
if (PrivacyPolicyCheckBox.IsChecked != true)
{
return Translator.WinoAccount_Validation_PrivacyConsentRequired;
}
return string.Empty;
}
private void EmailTextBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
{
PasswordBox.Focus(FocusState.Programmatic);
e.Handled = true;
}
}
private void PasswordBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
{
ConfirmPasswordBox.Focus(FocusState.Programmatic);
e.Handled = true;
}
}
private async void ConfirmPasswordBox_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Enter)
{
e.Handled = true;
await PerformRegistrationAsync();
}
}
private async void PrivacyPolicyLink_Click(object sender, RoutedEventArgs e)
{
await Launcher.LaunchUriAsync(new Uri(PrivacyPolicyUrl));
}
private void InputChanged(TextBox sender, TextBoxTextChangingEventArgs args) => HideError();
private void InputChanged(object sender, RoutedEventArgs e) => HideError();
private void SetBusyState(bool isBusy)
{
IsPrimaryButtonEnabled = !isBusy;
IsSecondaryButtonEnabled = !isBusy;
BusyRing.IsActive = isBusy;
BusyRing.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
}
private void ShowError(string message)
{
ErrorTextBlock.Text = message;
ErrorTextBlock.Visibility = Visibility.Visible;
}
private void HideError()
{
ErrorTextBlock.Text = string.Empty;
ErrorTextBlock.Visibility = Visibility.Collapsed;
}
}

View File

@@ -1,76 +0,0 @@
<ContentDialog
x:Class="Wino.Dialogs.WinoAccountSyncExportDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DefaultButton="Primary"
PrimaryButtonClick="ExportClicked"
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
PrimaryButtonText="{x:Bind domain:Translator.Buttons_Export, Mode=OneTime}"
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Close, Mode=OneTime}"
Style="{StaticResource WinoDialogStyle}"
Title="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_Title, Mode=OneTime}"
mc:Ignorable="d">
<ContentDialog.Resources>
<x:Double x:Key="ContentDialogMinWidth">520</x:Double>
<x:Double x:Key="ContentDialogMaxWidth">520</x:Double>
</ContentDialog.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="16">
<TextBlock
Text="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_Description, Mode=OneTime}"
TextWrapping="WrapWholeWords" />
<StackPanel Spacing="12">
<CheckBox
x:Name="PreferencesCheckBox"
Checked="SelectionChanged"
Content="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_IncludePreferences, Mode=OneTime}"
IsChecked="True"
Unchecked="SelectionChanged" />
<StackPanel Spacing="8">
<CheckBox
x:Name="AccountsCheckBox"
Checked="SelectionChanged"
Content="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_IncludeAccounts, Mode=OneTime}"
IsChecked="True"
Unchecked="SelectionChanged" />
<Border
Padding="12"
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CornerRadius="10">
<StackPanel Spacing="6">
<TextBlock
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_AccountsDisclaimer, Mode=OneTime}"
TextWrapping="WrapWholeWords" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_AccountsRelogin, Mode=OneTime}"
TextWrapping="WrapWholeWords" />
</StackPanel>
</Border>
</StackPanel>
</StackPanel>
<StackPanel
x:Name="ProgressPanel"
Spacing="8"
Visibility="Collapsed">
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind domain:Translator.WinoAccount_Management_ExportDialog_InProgress, Mode=OneTime}"
TextWrapping="WrapWholeWords" />
<ProgressBar IsIndeterminate="True" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</ContentDialog>

View File

@@ -1,73 +0,0 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
namespace Wino.Dialogs;
public sealed partial class WinoAccountSyncExportDialog : ContentDialog
{
private readonly IWinoAccountDataSyncService _syncService;
private bool _isBusy;
public WinoAccountSyncExportDialog(IWinoAccountDataSyncService syncService)
{
_syncService = syncService;
InitializeComponent();
UpdateButtonState();
}
public WinoAccountSyncExportResult? Result { get; private set; }
public Exception? FailureException { get; private set; }
private async void ExportClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
args.Cancel = true;
if (!HasSelection())
{
return;
}
var deferral = args.GetDeferral();
try
{
SetBusyState(true);
FailureException = null;
Result = await _syncService.ExportAsync(new WinoAccountSyncSelection(
PreferencesCheckBox.IsChecked == true,
AccountsCheckBox.IsChecked == true));
Hide();
}
catch (Exception ex)
{
FailureException = ex;
Hide();
}
finally
{
SetBusyState(false);
deferral.Complete();
}
}
private void SelectionChanged(object sender, RoutedEventArgs e)
=> UpdateButtonState();
private void SetBusyState(bool isBusy)
{
_isBusy = isBusy;
ProgressPanel.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
IsSecondaryButtonEnabled = !isBusy;
UpdateButtonState();
}
private void UpdateButtonState()
=> IsPrimaryButtonEnabled = !_isBusy && HasSelection();
private bool HasSelection()
=> PreferencesCheckBox.IsChecked == true || AccountsCheckBox.IsChecked == true;
}