Add capability-first account and calendar setup flow

This commit is contained in:
Burak Kaan Köse
2026-04-20 19:38:30 +02:00
parent 54148716bb
commit d85812ed7b
41 changed files with 1369 additions and 333 deletions
@@ -158,7 +158,9 @@ public sealed partial class NewAccountDialog : ContentDialog
AccountNameTextbox.Text.Trim(),
details,
SelectedColor?.Hex ?? string.Empty,
initialSynchronizationRange);
initialSynchronizationRange,
true,
calendarSupportMode != ImapCalendarSupportMode.Disabled);
Hide();
return;
@@ -185,7 +187,9 @@ public sealed partial class NewAccountDialog : ContentDialog
AccountNameTextbox.Text.Trim(),
null,
SelectedColor?.Hex ?? string.Empty,
initialSynchronizationRange);
initialSynchronizationRange,
true,
true);
Hide();
}
}
+20
View File
@@ -72,6 +72,26 @@ public static class XamlHelpers
return null;
}
}
public static Microsoft.UI.Xaml.Media.Imaging.BitmapImage? StringToBitmapImage(string? imagePath)
{
if (string.IsNullOrWhiteSpace(imagePath))
return null;
try
{
var uri = imagePath.StartsWith("/")
? new Uri($"ms-appx://{imagePath}")
: new Uri(imagePath, UriKind.Absolute);
return new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(uri);
}
catch
{
return null;
}
}
public static InfoBarSeverity InfoBarSeverityConverter(InfoBarMessageType messageType)
{
return messageType switch
@@ -105,6 +105,9 @@ public partial class AccountCalendarStateService : ObservableRecipient,
{
lock (_calendarStateLock)
{
if (!GroupedAccountCalendarViewModel.SupportsCalendar(groupedAccountCalendar.Account))
return;
groupedAccountCalendar.CalendarSelectionStateChanged += SingleCalendarSelectionStateChanged;
groupedAccountCalendar.CollectiveSelectionStateChanged += SingleGroupCalendarCollectiveStateChanged;
try
@@ -180,6 +183,9 @@ public partial class AccountCalendarStateService : ObservableRecipient,
{
lock (_calendarStateLock)
{
if (!GroupedAccountCalendarViewModel.SupportsCalendar(accountCalendar.Account))
return;
// Find the group that this calendar belongs to.
var group = _internalGroupedAccountCalendars.FirstOrDefault(g => g.Account.Id == accountCalendar.Account.Id);
@@ -396,6 +402,16 @@ public partial class AccountCalendarStateService : ObservableRecipient,
lock (_calendarStateLock)
{
groupedAccount = _internalGroupedAccountCalendars.FirstOrDefault(a => a.Account.Id == updatedAccount.Id);
if (!GroupedAccountCalendarViewModel.SupportsCalendar(updatedAccount))
{
if (groupedAccount != null)
{
RemoveGroupedAccountCalendar(groupedAccount);
}
return;
}
}
groupedAccount?.UpdateAccount(updatedAccount);
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Wino.Core.Domain.Interfaces;
namespace Wino.Services;
@@ -6,35 +7,73 @@ public class MailAuthenticatorConfiguration : IAuthenticatorConfig
{
public string OutlookAuthenticatorClientId => "b19c2035-d740-49ff-b297-de6ec561b208";
public string[] OutlookScope =>
[
"email",
"mail.readwrite",
"offline_access",
"mail.send",
"Mail.Send.Shared",
"Mail.ReadWrite.Shared",
"User.Read",
"Calendars.ReadBasic",
"Calendars.ReadWrite",
"Calendars.ReadWrite.Shared",
"Calendars.Read",
"Calendars.Read.Shared",
];
public string GmailAuthenticatorClientId => "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com";
public string[] GmailScope =>
[
"https://mail.google.com/",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/gmail.labels",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar.settings.readonly",
"https://www.googleapis.com/auth/drive.file",
];
public string GmailTokenStoreIdentifier => "WinoMailGmailTokenStore";
public string[] GetOutlookScope(bool isMailAccessGranted, bool isCalendarAccessGranted)
{
var scopes = new List<string>
{
"email",
"offline_access",
"User.Read"
};
if (isMailAccessGranted)
{
scopes.AddRange(
[
"mail.readwrite",
"mail.send",
"Mail.Send.Shared",
"Mail.ReadWrite.Shared"
]);
}
if (isCalendarAccessGranted)
{
scopes.AddRange(
[
"Calendars.ReadBasic",
"Calendars.ReadWrite",
"Calendars.ReadWrite.Shared",
"Calendars.Read",
"Calendars.Read.Shared"
]);
}
return [.. scopes];
}
public string[] GetGmailScope(bool isMailAccessGranted, bool isCalendarAccessGranted)
{
var scopes = new List<string>
{
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email"
};
if (isMailAccessGranted)
{
scopes.AddRange(
[
"https://mail.google.com/",
"https://www.googleapis.com/auth/gmail.labels"
]);
}
if (isCalendarAccessGranted)
{
scopes.AddRange(
[
"https://www.googleapis.com/auth/calendar",
"https://www.googleapis.com/auth/calendar.events",
"https://www.googleapis.com/auth/calendar.settings.readonly",
"https://www.googleapis.com/auth/drive.file"
]);
}
return [.. scopes];
}
}
@@ -1,5 +1,4 @@
using Wino.Mail.ViewModels;
using Wino.Mail.WinUI;
namespace Wino.Mail.WinUI.Views.Abstract;
@@ -90,12 +90,12 @@
<SymbolIcon Symbol="ContactInfo" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabMail, Mode=OneTime}">
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabMail, Mode=OneTime}" Visibility="{x:Bind ViewModel.HasMailAccess, Mode=OneWay}">
<controls:SegmentedItem.Icon>
<SymbolIcon Symbol="Mail" />
</controls:SegmentedItem.Icon>
</controls:SegmentedItem>
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabCalendar, Mode=OneTime}">
<controls:SegmentedItem Content="{x:Bind domain:Translator.AccountDetailsPage_TabCalendar, Mode=OneTime}" Visibility="{x:Bind ViewModel.HasCalendarAccess, Mode=OneWay}">
<controls:SegmentedItem.Icon>
<FontIcon Glyph="&#xE163;" />
</controls:SegmentedItem.Icon>
@@ -117,7 +117,10 @@
Text="{x:Bind ViewModel.AccountName, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard Description="{x:Bind domain:Translator.SettingsEditAccountDetails_Description}" Header="{x:Bind domain:Translator.AccountSettingsDialog_AccountName}">
<controls:SettingsCard
Description="{x:Bind domain:Translator.SettingsEditAccountDetails_Description}"
Header="{x:Bind domain:Translator.AccountSettingsDialog_AccountName}"
Visibility="{x:Bind ViewModel.HasMailAccess, Mode=OneWay}">
<controls:SettingsCard.HeaderIcon>
<SymbolIcon Symbol="Mail" />
</controls:SettingsCard.HeaderIcon>
@@ -127,7 +130,9 @@
Text="{x:Bind ViewModel.SenderName, Mode=TwoWay}" />
</controls:SettingsCard>
<controls:SettingsCard Header="{x:Bind domain:Translator.IMAPSetupDialog_MailAddress}">
<controls:SettingsCard
Header="{x:Bind domain:Translator.IMAPSetupDialog_MailAddress}"
Visibility="{x:Bind ViewModel.HasMailAccess, Mode=OneWay}">
<controls:SettingsCard.HeaderIcon>
<SymbolIcon Symbol="Mail" />
</controls:SettingsCard.HeaderIcon>
@@ -137,6 +142,25 @@
Text="{x:Bind ViewModel.Address, Mode=OneWay}" />
</controls:SettingsCard>
<controls:SettingsCard
Description="{x:Bind domain:Translator.AccountDetailsPage_CapabilityDescription, Mode=OneTime}"
Header="{x:Bind domain:Translator.AccountDetailsPage_CapabilityTitle, Mode=OneTime}"
Visibility="{x:Bind ViewModel.IsOAuthCapabilityEditable, Mode=OneWay}">
<controls:SettingsCard.HeaderIcon>
<FontIcon Glyph="&#xE787;" />
</controls:SettingsCard.HeaderIcon>
<ComboBox
MinWidth="220"
ItemsSource="{x:Bind ViewModel.CapabilityOptions, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedCapabilityOption, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="mailViewModels:AccountCapabilityOption">
<TextBlock Text="{x:Bind DisplayText}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</controls:SettingsCard>
<controls:SettingsCard
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
@@ -212,6 +236,7 @@
<Grid
x:Name="MailSettingsPanel"
Grid.Row="2"
x:Load="{x:Bind ViewModel.HasMailAccess, Mode=OneWay}"
Visibility="Collapsed">
<StackPanel Spacing="4">
<StackPanel.ChildrenTransitions>
@@ -403,6 +428,7 @@
<Grid
x:Name="CalendarSettingsPanel"
Grid.Row="2"
x:Load="{x:Bind ViewModel.HasCalendarAccess, Mode=OneWay}"
Visibility="Collapsed">
<StackPanel MaxWidth="900" Spacing="12">
@@ -25,7 +25,7 @@
Margin="0,2,0,0"
Click="RootAccountTemplate_Click"
CommandParameter="{x:Bind}"
Description="{x:Bind Account.Address}"
Description="{x:Bind DescriptionText}"
Header="{x:Bind Account.Name}"
IsClickEnabled="True">
<winuiControls:SettingsCard.HeaderIcon>
@@ -199,6 +199,7 @@
x:Name="AccountsListView"
Grid.Row="1"
x:Load="{x:Bind ViewModel.HasAccountsDefined, Mode=OneWay}"
CanReorderItems="True"
ItemContainerStyle="{StaticResource StretchedItemContainerStyle}"
ItemTemplateSelector="{StaticResource AccountProviderViewModelTemplateSelector}"
ItemsSource="{x:Bind ViewModel.Accounts, Mode=OneWay}"
@@ -208,7 +209,7 @@
<winuiControls:SettingsCard Description="{x:Bind domain:Translator.SettingsStartupItem_Description}" Header="{x:Bind domain:Translator.SettingsStartupItem_Title}">
<ComboBox
MinWidth="150"
ItemsSource="{x:Bind ViewModel.Accounts, Mode=OneTime}"
ItemsSource="{x:Bind ViewModel.StartupAccounts, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.StartupAccount, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="interfaces:IAccountProviderDetailViewModel">
@@ -220,16 +221,10 @@
<SymbolIcon Symbol="Account" />
</winuiControls:SettingsCard.HeaderIcon>
</winuiControls:SettingsCard>
<winuiControls:SettingsCard
Description="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionDescription}"
Header="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionTitle}">
<winuiControls:SettingsCard Description="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionDescription}" Header="{x:Bind domain:Translator.WinoAccount_Management_LocalDataSectionTitle}">
<StackPanel Orientation="Horizontal" Spacing="12">
<Button
Command="{x:Bind ViewModel.ImportLocalDataCommand}"
Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataImportAction}" />
<Button
Command="{x:Bind ViewModel.ExportLocalDataCommand}"
Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataExportAction}" />
<Button Command="{x:Bind ViewModel.ImportLocalDataCommand}" Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataImportAction}" />
<Button Command="{x:Bind ViewModel.ExportLocalDataCommand}" Content="{x:Bind domain:Translator.WinoAccount_Management_LocalDataExportAction}" />
</StackPanel>
<winuiControls:SettingsCard.HeaderIcon>
<SymbolIcon Symbol="Sync" />
@@ -76,12 +76,18 @@
Grid.Row="1"
Grid.ColumnSpan="2"
Header="{x:Bind ViewModel.PasswordHeaderText, Mode=OneWay}"
Password="{x:Bind ViewModel.Password, Mode=TwoWay}" />
Password="{x:Bind ViewModel.Password, Mode=TwoWay}"
Visibility="{x:Bind ViewModel.IsMailPasswordInputVisible, Mode=OneWay}" />
</Grid>
<CheckBox
Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}"
IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" />
<StackPanel Spacing="10">
<CheckBox
Content="{x:Bind ViewModel.EnableMailSupportText, Mode=OneWay}"
IsChecked="{x:Bind ViewModel.IsMailSupportEnabled, Mode=TwoWay}" />
<CheckBox
Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}"
IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" />
</StackPanel>
<StackPanel Spacing="8">
<TextBlock
@@ -93,7 +99,8 @@
HorizontalAlignment="Left"
Command="{x:Bind ViewModel.AutoDiscoverSettingsCommand}"
Content="{x:Bind ViewModel.AutoDiscoverButtonText, Mode=OneWay}"
Style="{ThemeResource AccentButtonStyle}" />
Style="{ThemeResource AccentButtonStyle}"
Visibility="{x:Bind ViewModel.IsMailActionsVisible, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</Border>
@@ -103,7 +110,8 @@
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8">
CornerRadius="8"
Visibility="{x:Bind ViewModel.IsMailSettingsVisible, Mode=OneWay}">
<StackPanel Spacing="16">
<StackPanel Spacing="2">
<TextBlock
@@ -176,7 +184,8 @@
<Button
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.TestImapConnectionCommand}"
Content="{x:Bind ViewModel.TestImapButtonText, Mode=OneWay}" />
Content="{x:Bind ViewModel.TestImapButtonText, Mode=OneWay}"
Visibility="{x:Bind ViewModel.IsMailActionsVisible, Mode=OneWay}" />
</StackPanel>
</Border>
+34 -2
View File
@@ -9,7 +9,39 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<!-- Empty page for disposing composer or renderer page. -->
<Grid />
<Grid>
<StackPanel
MaxWidth="440"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12"
Visibility="{x:Bind ViewModel.IsMailEmptyStateVisible, Mode=OneWay}">
<TextBlock
HorizontalAlignment="Center"
Style="{StaticResource TitleTextBlockStyle}"
Text="{x:Bind ViewModel.MailEmptyStateTitle, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="WrapWholeWords" />
<TextBlock
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{x:Bind ViewModel.MailEmptyStateMessage, Mode=OneWay}"
TextAlignment="Center"
TextWrapping="WrapWholeWords" />
<StackPanel
HorizontalAlignment="Center"
Orientation="Horizontal"
Spacing="12">
<Button
Command="{x:Bind ViewModel.AddAccountCommand}"
Content="{x:Bind ViewModel.AddAccountText, Mode=OneWay}"
Style="{ThemeResource AccentButtonStyle}" />
<Button
Command="{x:Bind ViewModel.ManageAccountsCommand}"
Content="{x:Bind ViewModel.ManageAccountsText, Mode=OneWay}" />
</StackPanel>
</StackPanel>
</Grid>
</abstract:IdlePageAbstract>
File diff suppressed because one or more lines are too long
@@ -12,6 +12,16 @@ public sealed partial class ProviderSelectionPage : ProviderSelectionPageAbstrac
private void ProviderSelectionChanged(ItemsView sender, ItemsViewSelectionChangedEventArgs args)
{
if (sender.SelectedItem == null) return;
ViewModel.SelectedProvider = sender.SelectedItem as Wino.Core.Domain.Interfaces.IProviderDetail;
}
private void AccountColorGridView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
{
AccountColorFlyout.Hide();
}
}
}
@@ -50,24 +50,33 @@
<PasswordBox
x:Name="AppPasswordBox"
Header="{x:Bind domain:Translator.ProviderSelection_AppPasswordHeader}"
PasswordChanged="AppPasswordChanged" />
PasswordChanged="AppPasswordChanged"
Visibility="{x:Bind ViewModel.RequiresAppSpecificPassword, Mode=OneWay}" />
<HyperlinkButton
HorizontalAlignment="Right"
Command="{x:Bind ViewModel.OpenAppPasswordHelpCommand}"
Content="{x:Bind domain:Translator.ProviderSelection_AppPasswordHelp}" />
Content="{x:Bind domain:Translator.ProviderSelection_AppPasswordHelp}"
Visibility="{x:Bind ViewModel.RequiresAppSpecificPassword, Mode=OneWay}" />
<!-- Divider -->
<Rectangle Height="1" Fill="{ThemeResource CardStrokeColorDefaultBrush}" />
<Rectangle
Height="1"
Fill="{ThemeResource CardStrokeColorDefaultBrush}"
Visibility="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}" />
<!-- Calendar Mode -->
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind domain:Translator.ProviderSelection_CalendarModeHeader}" />
<TextBlock
Style="{StaticResource BodyStrongTextBlockStyle}"
Text="{x:Bind domain:Translator.ProviderSelection_CalendarModeHeader}"
Visibility="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}" />
<ListView
x:Name="CalendarModeListView"
IsItemClickEnabled="False"
SelectionChanged="CalendarModeSelectionChanged"
SelectionMode="Single">
SelectionMode="Single"
Visibility="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}">
<!-- Disabled -->
<ListViewItem>
<Grid Padding="12" ColumnSpacing="10">
+6 -1
View File
@@ -371,8 +371,13 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
{
_ = DispatcherQueue.EnqueueAsync(async () =>
{
ViewModel.NavigationService.ChangeApplicationMode(WinoApplicationMode.Mail);
await ViewModel.MailClient.HandleAccountCreatedAsync(message.Account);
var targetMode = !message.Account.IsMailAccessGranted && message.Account.IsCalendarAccessGranted
? WinoApplicationMode.Calendar
: WinoApplicationMode.Mail;
ViewModel.NavigationService.ChangeApplicationMode(targetMode);
});
}