Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bba3523ec6 | |||
| feff929333 | |||
| aa16609f89 | |||
| 4bea53a667 | |||
| b2ad4a1664 | |||
| dad3a51885 | |||
| 59ff0a1d7d | |||
| df19ab3196 | |||
| c622858d2d | |||
| 2e36772a4c |
@@ -178,6 +178,18 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel,
|
||||
await InitializeAccountCalendarsAsync();
|
||||
ValidateConfiguredNewEventCalendar();
|
||||
|
||||
if (activationContext?.Parameter is CalendarItemTarget calendarItemTarget)
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.EventDetailsPage, calendarItemTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
if (activationContext?.Parameter is CalendarPageNavigationArgs calendarPageNavigationArgs)
|
||||
{
|
||||
NavigationService.Navigate(WinoPage.CalendarPage, calendarPageNavigationArgs);
|
||||
return;
|
||||
}
|
||||
|
||||
TodayClicked();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ public static class Constants
|
||||
public const string ToastCalendarJoinOnlineAction = nameof(ToastCalendarJoinOnlineAction);
|
||||
public const string ToastCalendarSnoozeAction = nameof(ToastCalendarSnoozeAction);
|
||||
public const string ToastCalendarSnoozeDurationInputId = nameof(ToastCalendarSnoozeDurationInputId);
|
||||
public const string ToastCalendarSnoozeDurationMinutesKey = nameof(ToastCalendarSnoozeDurationMinutesKey);
|
||||
public const string ToastModeKey = nameof(ToastModeKey);
|
||||
public const string ToastModeMail = nameof(ToastModeMail);
|
||||
public const string ToastModeCalendar = nameof(ToastModeCalendar);
|
||||
|
||||
@@ -112,6 +112,16 @@ public class MailAccount
|
||||
/// </summary>
|
||||
public DateTime? LastFolderStructureSyncDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets when the account was created in Wino.
|
||||
/// </summary>
|
||||
public DateTime? CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timespan used for the account's initial mail synchronization.
|
||||
/// </summary>
|
||||
public InitialSynchronizationRange InitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether the account can perform ProfileInformation sync type.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Wino.Core.Domain.Enums;
|
||||
|
||||
public enum InitialSynchronizationRange
|
||||
{
|
||||
SixMonths = 0,
|
||||
ThreeMonths = 1,
|
||||
NineMonths = 2,
|
||||
OneYear = 3,
|
||||
Everything = 4
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Extensions;
|
||||
|
||||
public static class InitialSynchronizationRangeExtensions
|
||||
{
|
||||
public static DateTime? ToCutoffDateUtc(this InitialSynchronizationRange range, DateTime utcNow)
|
||||
{
|
||||
var normalizedUtcNow = utcNow.Kind == DateTimeKind.Utc
|
||||
? utcNow
|
||||
: utcNow.ToUniversalTime();
|
||||
|
||||
return range switch
|
||||
{
|
||||
InitialSynchronizationRange.ThreeMonths => normalizedUtcNow.AddMonths(-3),
|
||||
InitialSynchronizationRange.SixMonths => normalizedUtcNow.AddMonths(-6),
|
||||
InitialSynchronizationRange.NineMonths => normalizedUtcNow.AddMonths(-9),
|
||||
InitialSynchronizationRange.OneYear => normalizedUtcNow.AddYears(-1),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public interface INewThemeService : IInitializeAsync
|
||||
Task<List<AppThemeBase>> GetAvailableThemesAsync();
|
||||
Task<CustomThemeMetadata> CreateNewCustomThemeAsync(string themeName, string accentColor, byte[] wallpaperData);
|
||||
Task<List<CustomThemeMetadata>> GetCurrentCustomThemesAsync();
|
||||
Task<bool> DeleteCustomThemeAsync(Guid themeId);
|
||||
List<string> GetAvailableAccountColors();
|
||||
Task ApplyCustomThemeAsync(bool isInitializing);
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Wino.Core.Domain.Models.Launch;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces;
|
||||
|
||||
public interface IShareActivationService
|
||||
{
|
||||
MailShareRequest? PendingShareRequest { get; set; }
|
||||
MailShareRequest? ConsumePendingShareRequest();
|
||||
void ClearPendingShareRequest();
|
||||
void StagePendingComposeShareRequest(Guid draftUniqueId, MailShareRequest shareRequest);
|
||||
MailShareRequest? ConsumePendingComposeShareRequest(Guid draftUniqueId);
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Accounts;
|
||||
|
||||
public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, SpecialImapProviderDetails SpecialImapProviderDetails, string AccountColorHex);
|
||||
public record AccountCreationDialogResult(
|
||||
MailProviderType ProviderType,
|
||||
string AccountName,
|
||||
SpecialImapProviderDetails SpecialImapProviderDetails,
|
||||
string AccountColorHex,
|
||||
InitialSynchronizationRange InitialSynchronizationRange);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Accounts;
|
||||
|
||||
public sealed class InitialSynchronizationRangeOption
|
||||
{
|
||||
public InitialSynchronizationRange Range { get; }
|
||||
public string DisplayText { get; }
|
||||
|
||||
public bool IsEverything => Range == InitialSynchronizationRange.Everything;
|
||||
|
||||
public InitialSynchronizationRangeOption(InitialSynchronizationRange range, string displayText)
|
||||
{
|
||||
Range = range;
|
||||
DisplayText = displayText;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Wino.Core.Domain.Models.Common;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Launch;
|
||||
|
||||
public sealed class MailShareRequest
|
||||
{
|
||||
public MailShareRequest(IReadOnlyList<SharedFile> files)
|
||||
{
|
||||
Files = files ?? throw new ArgumentNullException(nameof(files));
|
||||
}
|
||||
|
||||
public IReadOnlyList<SharedFile> Files { get; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Launch;
|
||||
|
||||
public sealed class PendingComposeMailShareRequest
|
||||
{
|
||||
public PendingComposeMailShareRequest(Guid draftUniqueId, MailShareRequest shareRequest)
|
||||
{
|
||||
DraftUniqueId = draftUniqueId;
|
||||
ShareRequest = shareRequest ?? throw new ArgumentNullException(nameof(shareRequest));
|
||||
}
|
||||
|
||||
public Guid DraftUniqueId { get; }
|
||||
public MailShareRequest ShareRequest { get; }
|
||||
}
|
||||
@@ -23,6 +23,14 @@
|
||||
"AccountCreationDialog_Initializing": "initializing",
|
||||
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
|
||||
"AccountCreationDialog_SigninIn": "Account information is being saved.",
|
||||
"AccountCreation_InitialSynchronization_Title": "Mail synchronization range",
|
||||
"AccountCreation_InitialSynchronization_Description": "Choose how far back Wino should download your mail during the first synchronization.",
|
||||
"AccountCreation_InitialSynchronization_3Months": "3 Months",
|
||||
"AccountCreation_InitialSynchronization_6Months": "6 Months",
|
||||
"AccountCreation_InitialSynchronization_9Months": "9 Months",
|
||||
"AccountCreation_InitialSynchronization_Year": "Year",
|
||||
"AccountCreation_InitialSynchronization_Everything": "Everything",
|
||||
"AccountCreation_InitialSynchronization_EverythingWarning": "This will synchronize all your mails to your computer. Extensive use of disk storage is needed. This is not recommended. For optimal performance use smaller synchronization timespan and use online search to access your mails.",
|
||||
"Purchased": "Purchased",
|
||||
"AccountEditDialog_Message": "Account Name",
|
||||
"AccountEditDialog_Title": "Edit Account",
|
||||
@@ -37,6 +45,8 @@
|
||||
"AccountDetailsPage_TabMail": "Mail",
|
||||
"AccountDetailsPage_TabCalendar": "Calendar",
|
||||
"AccountDetailsPage_CalendarListDescription": "Select a calendar to configure its settings",
|
||||
"AccountDetailsPage_InitialSynchronization_Title": "Initial synchronization",
|
||||
"AccountDetailsPage_InitialSynchronization_Description": "Wino synchronized your mails until {0} going back.",
|
||||
"AddHyperlink": "Add",
|
||||
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
|
||||
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
|
||||
@@ -797,6 +807,10 @@
|
||||
"SettingsConfigureSpecialFolders_Description": "Set folders with special functions. Folders such as Archive, Inbox, and Drafts are essential for Wino to function properly.",
|
||||
"SettingsConfigureSpecialFolders_Title": "Configure System Folders",
|
||||
"SettingsCustomTheme_Description": "Create your own custom theme with custom wallpaper and accent color.",
|
||||
"SettingsCustomTheme_DeleteConfirm_Message": "Delete custom theme \"{0}\"? Its saved wallpaper will also be removed from disk.",
|
||||
"SettingsCustomTheme_DeleteConfirm_Title": "Delete Theme",
|
||||
"SettingsCustomTheme_DeleteMissing": "This custom theme no longer exists.",
|
||||
"SettingsCustomTheme_DeleteSuccess": "Custom theme \"{0}\" was deleted.",
|
||||
"SettingsCustomTheme_Title": "Custom Theme",
|
||||
"SettingsDeleteAccount_Description": "Delete all e-mails and credentials associated with this account.",
|
||||
"SettingsDeleteAccount_Title": "Delete this account",
|
||||
|
||||
@@ -174,8 +174,46 @@ public partial class PersonalizationPageViewModel : CoreBaseViewModel
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteCustomThemeAsync(AppThemeBase theme)
|
||||
{
|
||||
if (theme == null || theme.AppThemeType != AppThemeType.Custom)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var shouldDelete = await _dialogService.ShowConfirmationDialogAsync(
|
||||
string.Format(Translator.SettingsCustomTheme_DeleteConfirm_Message, theme.ThemeName),
|
||||
Translator.SettingsCustomTheme_DeleteConfirm_Title,
|
||||
Translator.Buttons_Delete);
|
||||
|
||||
if (!shouldDelete)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isDeleted = await _newThemeService.DeleteCustomThemeAsync(theme.Id);
|
||||
|
||||
if (!isDeleted)
|
||||
{
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.GeneralTitle_Warning,
|
||||
Translator.SettingsCustomTheme_DeleteMissing,
|
||||
InfoBarMessageType.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
await InitializeSettingsAsync();
|
||||
|
||||
_dialogService.InfoBarMessage(
|
||||
Translator.GeneralTitle_Info,
|
||||
string.Format(Translator.SettingsCustomTheme_DeleteSuccess, theme.ThemeName),
|
||||
InfoBarMessageType.Success);
|
||||
}
|
||||
|
||||
private void InitializeColors()
|
||||
{
|
||||
Colors.Clear();
|
||||
Colors.Add(new AppColorViewModel("#0078d7"));
|
||||
Colors.Add(new AppColorViewModel("#00838c"));
|
||||
Colors.Add(new AppColorViewModel("#e3008c"));
|
||||
|
||||
@@ -81,9 +81,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
public override uint BatchModificationSize => 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum messages to fetch per folder during initial sync (1500).
|
||||
/// All messages are downloaded with METADATA ONLY - no raw MIME content.
|
||||
/// Uses Gmail API's Metadata format which includes headers, labels, and snippet but NOT full message body.
|
||||
/// Legacy page size hint kept for compatibility with shared synchronizer contracts.
|
||||
/// Gmail initial sync now downloads all messages inside the selected cutoff window.
|
||||
/// </summary>
|
||||
public override uint InitialMessageDownloadCountPerFolder => 1500;
|
||||
|
||||
@@ -304,13 +303,18 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
|
||||
/// <summary>
|
||||
/// Performs initial synchronization by downloading messages per-folder.
|
||||
/// Each folder gets up to 1500 messages, but we track already downloaded message IDs globally
|
||||
/// to avoid downloading the same message multiple times (Gmail messages can have multiple labels).
|
||||
/// Messages are filtered by the account's configured initial synchronization cutoff date when present,
|
||||
/// and duplicates are avoided globally because Gmail messages can have multiple labels.
|
||||
/// </summary>
|
||||
private async Task<List<string>> PerformInitialSyncAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Track all downloaded message IDs globally to avoid duplicate downloads
|
||||
var downloadedMessageIds = new HashSet<string>();
|
||||
var referenceDateUtc = Account.CreatedAt ?? DateTime.UtcNow;
|
||||
var initialSynchronizationCutoffDateUtc = Account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
|
||||
var queryText = initialSynchronizationCutoffDateUtc.HasValue
|
||||
? $"after:{initialSynchronizationCutoffDateUtc.Value.ToUniversalTime():yyyy/MM/dd}"
|
||||
: null;
|
||||
|
||||
_logger.Information("Performing initial sync for {Name} - downloading messages per folder", Account.Name);
|
||||
|
||||
@@ -337,7 +341,6 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
|
||||
var folderDownloaded = 0;
|
||||
string pageToken = null;
|
||||
var remainingToDownload = (int)InitialMessageDownloadCountPerFolder;
|
||||
|
||||
do
|
||||
{
|
||||
@@ -345,8 +348,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
|
||||
var request = _gmailService.Users.Messages.List("me");
|
||||
request.LabelIds = new Google.Apis.Util.Repeatable<string>(new[] { folder.RemoteFolderId });
|
||||
request.MaxResults = Math.Min(remainingToDownload, 500); // API max is 500
|
||||
request.MaxResults = 500; // API max is 500
|
||||
request.PageToken = pageToken;
|
||||
request.Q = queryText;
|
||||
|
||||
var response = await request.ExecuteAsync(cancellationToken);
|
||||
|
||||
@@ -373,19 +377,12 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
totalMessagesDownloaded += newMessageIds.Count;
|
||||
}
|
||||
|
||||
// Count all messages (including duplicates) toward the folder limit
|
||||
remainingToDownload -= response.Messages.Count;
|
||||
|
||||
_logger.Debug("Folder {FolderName}: Downloaded {New} new messages ({Total} total in folder)",
|
||||
folder.FolderName, newMessageIds.Count, folderDownloaded);
|
||||
}
|
||||
|
||||
pageToken = response.NextPageToken;
|
||||
|
||||
// Stop if we've processed enough messages for this folder or no more pages
|
||||
if (remainingToDownload <= 0 || string.IsNullOrEmpty(pageToken))
|
||||
break;
|
||||
|
||||
} while (!string.IsNullOrEmpty(pageToken));
|
||||
|
||||
_logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded);
|
||||
|
||||
@@ -9,6 +9,7 @@ using MailKit.Search;
|
||||
using MoreLinq;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
@@ -252,9 +253,20 @@ public class UnifiedImapSynchronizer
|
||||
.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, knownUidStructs, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var changedUids = await remoteFolder
|
||||
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
IList<UniqueId> changedUids;
|
||||
|
||||
if (folder.HighestModeSeq == 0)
|
||||
{
|
||||
changedUids = await remoteFolder
|
||||
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
changedUids = await remoteFolder
|
||||
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -308,25 +320,26 @@ public class UnifiedImapSynchronizer
|
||||
{
|
||||
IList<UniqueId> changedUids;
|
||||
|
||||
if (client.Capabilities.HasFlag(ImapCapabilities.Sort))
|
||||
if (isInitialSync)
|
||||
{
|
||||
changedUids = await remoteFolder
|
||||
.SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken)
|
||||
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
changedUids = await remoteFolder
|
||||
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (isInitialSync)
|
||||
{
|
||||
changedUids = changedUids
|
||||
.OrderByDescending(a => a.Id)
|
||||
.Take((int)synchronizer.InitialMessageDownloadCountPerFolder)
|
||||
.ToList();
|
||||
if (client.Capabilities.HasFlag(ImapCapabilities.Sort))
|
||||
{
|
||||
changedUids = await remoteFolder
|
||||
.SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
changedUids = await remoteFolder
|
||||
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||
@@ -367,15 +380,12 @@ public class UnifiedImapSynchronizer
|
||||
|
||||
if (folder.HighestKnownUid == 0)
|
||||
{
|
||||
var remoteUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var initialUids = remoteUids
|
||||
.OrderByDescending(a => a.Id)
|
||||
.Take((int)synchronizer.InitialMessageDownloadCountPerFolder)
|
||||
.ToList();
|
||||
var initialUids = await remoteFolder
|
||||
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, initialUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||
UpdateHighestKnownUid(folder, remoteFolder, remoteUids.Select(a => a.Id));
|
||||
UpdateHighestKnownUid(folder, remoteFolder, initialUids.Select(a => a.Id));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -410,6 +420,22 @@ public class UnifiedImapSynchronizer
|
||||
|
||||
#region Shared Helpers
|
||||
|
||||
private static SearchQuery BuildInitialSyncQuery(IImapSynchronizer synchronizer)
|
||||
{
|
||||
if (synchronizer is IBaseSynchronizer { Account: { } account })
|
||||
{
|
||||
var referenceDateUtc = account.CreatedAt ?? DateTime.UtcNow;
|
||||
var cutoffDateUtc = account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
|
||||
|
||||
if (cutoffDateUtc.HasValue)
|
||||
{
|
||||
return SearchQuery.DeliveredAfter(cutoffDateUtc.Value.ToUniversalTime().Date);
|
||||
}
|
||||
}
|
||||
|
||||
return SearchQuery.All;
|
||||
}
|
||||
|
||||
private async Task EnsureUidValidityStateAsync(MailItemFolder folder, IMailFolder remoteFolder)
|
||||
{
|
||||
if (folder.UidValidity != 0 && remoteFolder.UidValidity != folder.UidValidity)
|
||||
|
||||
@@ -55,14 +55,14 @@ public partial class OutlookSynchronizerJsonContext : JsonSerializerContext;
|
||||
///
|
||||
/// SYNCHRONIZATION STRATEGY:
|
||||
/// - Uses delta API for both initial and incremental sync
|
||||
/// - Initial sync: Downloads last 30 days of emails with metadata only
|
||||
/// - Initial sync: Downloads messages using the account's configured cutoff date with metadata only
|
||||
/// - Incremental sync: Uses delta token to get only changes since last sync
|
||||
/// - Messages are downloaded with metadata only (no MIME content during sync)
|
||||
/// - MIME files are downloaded on-demand when user explicitly reads a message
|
||||
///
|
||||
/// Key implementation details:
|
||||
/// - SynchronizeFolderAsync: Main entry point for per-folder synchronization
|
||||
/// - DownloadMailsForInitialSyncAsync: Downloads last 30 days using delta API with filter
|
||||
/// - DownloadMailsForInitialSyncAsync: Downloads messages using delta API with an optional cutoff filter
|
||||
/// - ProcessDeltaChangesAsync: Processes incremental changes using delta token
|
||||
/// - DownloadMessageMetadataBatchAsync: Downloads metadata in batches using Graph batch API
|
||||
/// - CreateMailCopyFromMessageAsync: Creates MailCopy from Message metadata
|
||||
@@ -343,9 +343,9 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
// Check if we have a delta token
|
||||
if (string.IsNullOrEmpty(folder.DeltaToken))
|
||||
{
|
||||
_logger.Debug("No delta token for folder {FolderName}. Starting initial sync (last 30 days).", folder.FolderName);
|
||||
_logger.Debug("No delta token for folder {FolderName}. Starting initial sync.", folder.FolderName);
|
||||
|
||||
// Download mails for initial sync (last 30 days)
|
||||
// Download mails for initial sync using the account's configured cutoff date.
|
||||
await DownloadMailsForInitialSyncAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
@@ -367,27 +367,37 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads mails for initial synchronization using Delta API with 30-day filter.
|
||||
/// Downloads metadata only (no MIME content) for messages received in the last 30 days.
|
||||
/// Downloads mails for initial synchronization using Delta API with the account's configured cutoff date.
|
||||
/// Downloads metadata only (no MIME content) for messages received after that date.
|
||||
/// </summary>
|
||||
private async Task DownloadMailsForInitialSyncAsync(MailItemFolder folder, List<string> downloadedMessageIds, CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.Debug("Starting initial mail download for folder {FolderName} (last 6 months)", folder.FolderName);
|
||||
_logger.Debug("Starting initial mail download for folder {FolderName}", folder.FolderName);
|
||||
|
||||
try
|
||||
{
|
||||
// Calculate date 6 months ago
|
||||
var sixMonthsAgo = DateTime.UtcNow.AddMonths(-6);
|
||||
var filterDate = sixMonthsAgo.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
var referenceDateUtc = Account.CreatedAt ?? DateTime.UtcNow;
|
||||
var initialSynchronizationCutoffDateUtc = Account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
|
||||
var filterDate = initialSynchronizationCutoffDateUtc?.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||
|
||||
_logger.Information("Downloading messages received after {FilterDate} for folder {FolderName}", filterDate, folder.FolderName);
|
||||
if (filterDate != null)
|
||||
{
|
||||
_logger.Information("Downloading messages received after {FilterDate} for folder {FolderName}", filterDate, folder.FolderName);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Information("Downloading all available messages for folder {FolderName}", folder.FolderName);
|
||||
}
|
||||
|
||||
// Use Delta API with receivedDateTime filter for last 6 months
|
||||
var messageCollectionPage = await _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.GetAsDeltaGetResponseAsync((config) =>
|
||||
{
|
||||
config.QueryParameters.Select = outlookMessageSelectParameters;
|
||||
config.QueryParameters.Orderby = ["receivedDateTime desc"];
|
||||
config.QueryParameters.Filter = $"receivedDateTime ge {filterDate}";
|
||||
|
||||
if (filterDate != null)
|
||||
{
|
||||
config.QueryParameters.Filter = $"receivedDateTime ge {filterDate}";
|
||||
}
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var totalProcessed = 0;
|
||||
|
||||
@@ -2,24 +2,25 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Misc;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Misc;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Core.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
|
||||
namespace Wino.Mail.ViewModels;
|
||||
@@ -101,6 +102,14 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png"
|
||||
: $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png";
|
||||
public string Address => Account?.Address ?? string.Empty;
|
||||
public bool IsInitialSynchronizationSummaryVisible => Account?.CreatedAt.HasValue == true && Account.InitialSynchronizationRange != InitialSynchronizationRange.Everything;
|
||||
public string InitialSynchronizationSummary => Account?.CreatedAt is not DateTime createdAtUtc
|
||||
? string.Empty
|
||||
: Account.InitialSynchronizationRange.ToCutoffDateUtc(createdAtUtc) is not DateTime cutoffDateUtc
|
||||
? string.Empty
|
||||
: string.Format(
|
||||
Translator.AccountDetailsPage_InitialSynchronization_Description,
|
||||
cutoffDateUtc.ToLocalTime().ToString("D", CultureInfo.CurrentUICulture));
|
||||
|
||||
public List<ImapAuthenticationMethodModel> AvailableAuthenticationMethods { get; } =
|
||||
[
|
||||
@@ -363,13 +372,15 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
||||
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
|
||||
OnPropertyChanged(nameof(ProviderIconPath));
|
||||
OnPropertyChanged(nameof(Address));
|
||||
OnPropertyChanged(nameof(IsInitialSynchronizationSummaryVisible));
|
||||
OnPropertyChanged(nameof(InitialSynchronizationSummary));
|
||||
}
|
||||
|
||||
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||
{
|
||||
base.OnPropertyChanged(e);
|
||||
|
||||
if (!IsActive || !isLoaded) return;
|
||||
if (!isLoaded) return;
|
||||
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
|
||||
@@ -170,6 +170,7 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
|
||||
try
|
||||
{
|
||||
CustomServerInformation customServerInformation = null;
|
||||
var accountCreatedAt = DateTime.UtcNow;
|
||||
|
||||
// Build account in memory
|
||||
_createdAccount = new MailAccount
|
||||
@@ -179,6 +180,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
|
||||
Name = WizardContext.AccountName,
|
||||
SpecialImapProvider = WizardContext.SelectedProvider.SpecialImapProvider,
|
||||
AccountColorHex = WizardContext.AccountColorHex,
|
||||
CreatedAt = accountCreatedAt,
|
||||
InitialSynchronizationRange = WizardContext.SelectedInitialSynchronizationRange,
|
||||
IsCalendarAccessGranted = true
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ using Wino.Core.Domain.Extensions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Launch;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Extensions;
|
||||
using Wino.Core.Services;
|
||||
@@ -159,6 +160,7 @@ public partial class ComposePageViewModel : MailBaseViewModel,
|
||||
public readonly IPreferencesService PreferencesService;
|
||||
public readonly IContactService ContactService;
|
||||
public readonly ISmimeCertificateService _smimeCertificateService;
|
||||
private readonly IShareActivationService _shareActivationService;
|
||||
|
||||
public ComposePageViewModel(IMailDialogService dialogService,
|
||||
IMailService mailService,
|
||||
@@ -172,7 +174,8 @@ public partial class ComposePageViewModel : MailBaseViewModel,
|
||||
IContactService contactService,
|
||||
IFontService fontService,
|
||||
IPreferencesService preferencesService,
|
||||
ISmimeCertificateService smimeCertificateService)
|
||||
ISmimeCertificateService smimeCertificateService,
|
||||
IShareActivationService shareActivationService)
|
||||
{
|
||||
NativeAppService = nativeAppService;
|
||||
ContactService = contactService;
|
||||
@@ -188,6 +191,7 @@ public partial class ComposePageViewModel : MailBaseViewModel,
|
||||
_emailTemplateService = emailTemplateService;
|
||||
_worker = worker;
|
||||
_smimeCertificateService = smimeCertificateService;
|
||||
_shareActivationService = shareActivationService;
|
||||
|
||||
foreach (var cert in _smimeCertificateService.GetCertificates(emailAddress: SelectedAlias?.AliasAddress))
|
||||
{
|
||||
@@ -752,6 +756,7 @@ public partial class ComposePageViewModel : MailBaseViewModel,
|
||||
await LoadAddressInfoAsync(replyingMime.Bcc, BCCItems);
|
||||
|
||||
LoadAttachments();
|
||||
ApplyPendingSharedAttachments();
|
||||
|
||||
if (replyingMime.Cc.Any() || replyingMime.Bcc.Any())
|
||||
IsCCBCCVisible = true;
|
||||
@@ -783,6 +788,24 @@ public partial class ComposePageViewModel : MailBaseViewModel,
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyPendingSharedAttachments()
|
||||
{
|
||||
var draftUniqueId = CurrentMailDraftItem?.MailCopy?.UniqueId ?? Guid.Empty;
|
||||
|
||||
if (draftUniqueId == Guid.Empty)
|
||||
return;
|
||||
|
||||
var shareRequest = _shareActivationService.ConsumePendingComposeShareRequest(draftUniqueId);
|
||||
|
||||
if (shareRequest?.Files == null || shareRequest.Files.Count == 0)
|
||||
return;
|
||||
|
||||
foreach (var sharedFile in shareRequest.Files)
|
||||
{
|
||||
IncludedAttachments.Add(new MailAttachmentViewModel(sharedFile));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAddressInfoAsync(InternetAddressList list, ObservableCollection<AccountContact> collection)
|
||||
{
|
||||
foreach (var item in list)
|
||||
|
||||
@@ -18,6 +18,9 @@ public partial class WelcomeWizardContext : ObservableObject
|
||||
[ObservableProperty]
|
||||
public partial string AccountColorHex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial InitialSynchronizationRange SelectedInitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
|
||||
|
||||
// Special IMAP fields (iCloud/Yahoo)
|
||||
[ObservableProperty]
|
||||
public partial string DisplayName { get; set; }
|
||||
@@ -62,7 +65,8 @@ public partial class WelcomeWizardContext : ObservableObject
|
||||
SelectedProvider.Type,
|
||||
AccountName,
|
||||
BuildSpecialImapProviderDetails(),
|
||||
AccountColorHex);
|
||||
AccountColorHex,
|
||||
SelectedInitialSynchronizationRange);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
@@ -70,6 +74,7 @@ public partial class WelcomeWizardContext : ObservableObject
|
||||
SelectedProvider = null;
|
||||
AccountName = null;
|
||||
AccountColorHex = null;
|
||||
SelectedInitialSynchronizationRange = InitialSynchronizationRange.SixMonths;
|
||||
DisplayName = null;
|
||||
EmailAddress = null;
|
||||
AppSpecificPassword = null;
|
||||
|
||||
@@ -16,7 +16,6 @@ using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Services;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
using Wino.Messaging.Client.Navigation;
|
||||
using Wino.Messaging.Server;
|
||||
|
||||
@@ -319,7 +318,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
||||
try
|
||||
{
|
||||
var minimalSettings = BuildMinimalSettingsOrThrow();
|
||||
await AutoDiscoverAndApplySettingsAsync(minimalSettings).ConfigureAwait(false);
|
||||
await AutoDiscoverAndApplySettingsAsync(minimalSettings);
|
||||
|
||||
_mailDialogService.InfoBarMessage(
|
||||
Translator.IMAPSetupDialog_ValidationSuccess_Title,
|
||||
@@ -399,7 +398,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
||||
{
|
||||
try
|
||||
{
|
||||
await EnsureImapSettingsPreparedAsync().ConfigureAwait(false);
|
||||
await EnsureImapSettingsPreparedAsync();
|
||||
|
||||
var serverInformation = BuildServerInformation();
|
||||
|
||||
@@ -407,12 +406,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
||||
ValidateImapSettings(serverInformation);
|
||||
ValidateCalendarModeSpecificSettings(serverInformation);
|
||||
|
||||
await ValidateImapConnectivityAsync(serverInformation).ConfigureAwait(false);
|
||||
await ValidateImapConnectivityAsync(serverInformation);
|
||||
IsImapValidationSucceeded = true;
|
||||
|
||||
if (serverInformation.CalendarSupportMode == ImapCalendarSupportMode.CalDav)
|
||||
{
|
||||
await ValidateCalDavConnectivityAsync(serverInformation).ConfigureAwait(false);
|
||||
await ValidateCalDavConnectivityAsync(serverInformation);
|
||||
IsCalDavValidationSucceeded = true;
|
||||
}
|
||||
else
|
||||
@@ -432,7 +431,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
||||
return;
|
||||
}
|
||||
|
||||
await SaveEditFlowAsync(serverInformation).ConfigureAwait(false);
|
||||
await SaveEditFlowAsync(serverInformation);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -654,7 +653,7 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
||||
return;
|
||||
|
||||
var minimalSettings = BuildMinimalSettingsOrThrow();
|
||||
await AutoDiscoverAndApplySettingsAsync(minimalSettings).ConfigureAwait(false);
|
||||
await AutoDiscoverAndApplySettingsAsync(minimalSettings);
|
||||
|
||||
if (!HasCompleteImapSettings())
|
||||
throw new InvalidOperationException(Translator.Exception_ImapAutoDiscoveryFailed);
|
||||
@@ -676,22 +675,25 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
||||
if (serverInformation == null)
|
||||
throw new InvalidOperationException(Translator.Exception_ImapAutoDiscoveryFailed);
|
||||
|
||||
ApplyServerInformation(serverInformation);
|
||||
|
||||
if (IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav)
|
||||
await ExecuteUIThread(async () =>
|
||||
{
|
||||
var discoveredCalDavUri = await _autoDiscoveryService.DiscoverCalDavServiceUriAsync(minimalSettings.Email).ConfigureAwait(false);
|
||||
if (discoveredCalDavUri != null)
|
||||
ApplyServerInformation(serverInformation);
|
||||
|
||||
if (IsCalendarSupportEnabled && SelectedCalendarSupportMode == ImapCalendarSupportMode.CalDav)
|
||||
{
|
||||
CalDavServiceUrl = discoveredCalDavUri.ToString();
|
||||
var discoveredCalDavUri = await _autoDiscoveryService.DiscoverCalDavServiceUriAsync(minimalSettings.Email);
|
||||
if (discoveredCalDavUri != null)
|
||||
{
|
||||
CalDavServiceUrl = discoveredCalDavUri.ToString();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CalDavUsername))
|
||||
CalDavUsername = minimalSettings.Email;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CalDavPassword))
|
||||
CalDavPassword = minimalSettings.Password;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CalDavUsername))
|
||||
CalDavUsername = minimalSettings.Email;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CalDavPassword))
|
||||
CalDavPassword = minimalSettings.Password;
|
||||
}
|
||||
});
|
||||
}
|
||||
private async Task ValidateImapConnectivityAsync(CustomServerInformation serverInformation)
|
||||
{
|
||||
@@ -995,7 +997,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
||||
SpecialImapProvider = _editingSpecialImapProvider,
|
||||
IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled
|
||||
},
|
||||
new AccountCreationDialogResult(MailProviderType.IMAP4, DisplayName.Trim(), providerDetails, string.Empty));
|
||||
new AccountCreationDialogResult(
|
||||
MailProviderType.IMAP4,
|
||||
DisplayName.Trim(),
|
||||
providerDetails,
|
||||
string.Empty,
|
||||
_wizardContext.SelectedInitialSynchronizationRange));
|
||||
|
||||
if (serverInformation == null)
|
||||
return false;
|
||||
|
||||
@@ -15,8 +15,9 @@ using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.MenuItems;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
using Wino.Core.Domain.Models.Launch;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
@@ -84,6 +85,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
private readonly IMimeFileService _mimeFileService;
|
||||
private readonly IWebView2RuntimeValidatorService _webView2RuntimeValidatorService;
|
||||
private readonly IStoreUpdateService _storeUpdateService;
|
||||
private readonly IShareActivationService _shareActivationService;
|
||||
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
private readonly IMailService _mailService;
|
||||
@@ -109,7 +111,8 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
IConfigurationService configurationService,
|
||||
IStartupBehaviorService startupBehaviorService,
|
||||
IWebView2RuntimeValidatorService webView2RuntimeValidatorService,
|
||||
IStoreUpdateService storeUpdateService)
|
||||
IStoreUpdateService storeUpdateService,
|
||||
IShareActivationService shareActivationService)
|
||||
{
|
||||
StatePersistenceService = statePersistanceService;
|
||||
|
||||
@@ -131,6 +134,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
_winoRequestDelegator = winoRequestDelegator;
|
||||
_webView2RuntimeValidatorService = webView2RuntimeValidatorService;
|
||||
_storeUpdateService = storeUpdateService;
|
||||
_shareActivationService = shareActivationService;
|
||||
}
|
||||
|
||||
protected override void OnDispatcherAssigned()
|
||||
@@ -274,6 +278,7 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
}
|
||||
|
||||
await ProcessLaunchOptionsAsync();
|
||||
await HandlePendingShareRequestAsync();
|
||||
await ValidateWebView2RuntimeAsync();
|
||||
|
||||
if (shouldRunStartupFlows && !Debugger.IsAttached)
|
||||
@@ -943,6 +948,9 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
}
|
||||
|
||||
public async Task CreateNewMailForAsync(MailAccount account)
|
||||
=> await CreateNewMailForAsync(account, null);
|
||||
|
||||
public async Task CreateNewMailForAsync(MailAccount account, MailShareRequest shareRequest)
|
||||
{
|
||||
if (account == null) return;
|
||||
|
||||
@@ -974,6 +982,11 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
|
||||
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account.Id, draftOptions).ConfigureAwait(false);
|
||||
|
||||
if (shareRequest?.Files?.Count > 0)
|
||||
{
|
||||
_shareActivationService.StagePendingComposeShareRequest(draftMailCopy.UniqueId, shareRequest);
|
||||
}
|
||||
|
||||
var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason);
|
||||
await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
|
||||
}
|
||||
@@ -1034,6 +1047,35 @@ public partial class MailAppShellViewModel : MailBaseViewModel,
|
||||
await CreateNewMailForAsync(targetAccount);
|
||||
}
|
||||
|
||||
public async Task HandlePendingShareRequestAsync()
|
||||
{
|
||||
var shareRequest = _shareActivationService.ConsumePendingShareRequest();
|
||||
|
||||
if (shareRequest?.Files == null || shareRequest.Files.Count == 0)
|
||||
return;
|
||||
|
||||
var accounts = await _accountService.GetAccountsAsync();
|
||||
|
||||
if (!accounts.Any())
|
||||
return;
|
||||
|
||||
MailAccount targetAccount = null;
|
||||
|
||||
if (accounts.Count == 1)
|
||||
{
|
||||
targetAccount = accounts[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
targetAccount = await _dialogService.ShowAccountPickerDialogAsync(accounts);
|
||||
}
|
||||
|
||||
if (targetAccount == null)
|
||||
return;
|
||||
|
||||
await CreateNewMailForAsync(targetAccount, shareRequest);
|
||||
}
|
||||
|
||||
private async Task RecreateMenuItemsAsync()
|
||||
{
|
||||
await _menuRefreshSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.ViewModels.Data;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
@@ -22,13 +23,26 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
||||
|
||||
public List<IProviderDetail> Providers { get; private set; } = [];
|
||||
public List<AppColorViewModel> AvailableColors { get; private set; } = [];
|
||||
public List<InitialSynchronizationRangeOption> InitialSynchronizationRanges { get; } =
|
||||
[
|
||||
new(InitialSynchronizationRange.ThreeMonths, Translator.AccountCreation_InitialSynchronization_3Months),
|
||||
new(InitialSynchronizationRange.SixMonths, Translator.AccountCreation_InitialSynchronization_6Months),
|
||||
new(InitialSynchronizationRange.NineMonths, Translator.AccountCreation_InitialSynchronization_9Months),
|
||||
new(InitialSynchronizationRange.OneYear, Translator.AccountCreation_InitialSynchronization_Year),
|
||||
new(InitialSynchronizationRange.Everything, Translator.AccountCreation_InitialSynchronization_Everything)
|
||||
];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial IProviderDetail SelectedProvider { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsColorSelected))]
|
||||
public partial AppColorViewModel SelectedColor { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsInitialSynchronizationWarningVisible))]
|
||||
public partial InitialSynchronizationRangeOption SelectedInitialSynchronizationRange { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string AccountName { get; set; }
|
||||
|
||||
@@ -36,6 +50,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
||||
public partial bool CanProceed { get; set; }
|
||||
|
||||
public bool IsColorSelected => SelectedColor != null;
|
||||
public bool IsInitialSynchronizationWarningVisible => SelectedInitialSynchronizationRange?.IsEverything == true;
|
||||
|
||||
public ProviderSelectionPageViewModel(
|
||||
IProviderService providerService,
|
||||
@@ -45,6 +60,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
||||
_providerService = providerService;
|
||||
_themeService = themeService;
|
||||
WizardContext = wizardContext;
|
||||
SelectedInitialSynchronizationRange = InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||
@@ -56,6 +72,10 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
||||
.Select(hex => new AppColorViewModel(hex))
|
||||
.ToList();
|
||||
|
||||
SelectedInitialSynchronizationRange = InitialSynchronizationRanges
|
||||
.FirstOrDefault(option => option.Range == WizardContext.SelectedInitialSynchronizationRange)
|
||||
?? InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
|
||||
|
||||
// Restore from wizard context if navigating back
|
||||
if (WizardContext.SelectedProvider != null)
|
||||
{
|
||||
@@ -71,9 +91,12 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
||||
Validate();
|
||||
}
|
||||
|
||||
partial void OnSelectedProviderChanged(IProviderDetail value) => Validate();
|
||||
partial void OnSelectedProviderChanged(IProviderDetail value)
|
||||
{
|
||||
Validate();
|
||||
}
|
||||
|
||||
partial void OnAccountNameChanged(string value) => Validate();
|
||||
partial void OnSelectedColorChanged(AppColorViewModel value) => OnPropertyChanged(nameof(IsColorSelected));
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearColor() => SelectedColor = null;
|
||||
@@ -92,6 +115,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
||||
WizardContext.SelectedProvider = SelectedProvider;
|
||||
WizardContext.AccountName = AccountName?.Trim();
|
||||
WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
|
||||
WizardContext.SelectedInitialSynchronizationRange = SelectedInitialSynchronizationRange?.Range ?? InitialSynchronizationRange.SixMonths;
|
||||
|
||||
if (WizardContext.IsGenericImap)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using System;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
@@ -49,8 +49,31 @@ internal static class ToastActivationResolver
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryResolveMode(NotificationArguments toastArguments, out WinoApplicationMode mode)
|
||||
{
|
||||
mode = WinoApplicationMode.Mail;
|
||||
|
||||
if (!toastArguments.TryGetValue(Constants.ToastModeKey, out string toastMode))
|
||||
return false;
|
||||
|
||||
if (string.Equals(toastMode, Constants.ToastModeCalendar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mode = WinoApplicationMode.Calendar;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (string.Equals(toastMode, Constants.ToastModeMail, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mode = WinoApplicationMode.Mail;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool ContainsKnownToastKey(NotificationArguments toastArguments)
|
||||
=> toastArguments.TryGetValue(Constants.ToastStoreUpdateActionKey, out string _) ||
|
||||
toastArguments.TryGetValue(Constants.ToastModeKey, out string _) ||
|
||||
toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string _) ||
|
||||
toastArguments.TryGetValue(Constants.ToastActionKey, out string _);
|
||||
}
|
||||
|
||||
+225
-42
@@ -4,8 +4,8 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.UI.Dispatching;
|
||||
@@ -15,6 +15,8 @@ using Microsoft.Windows.AppLifecycle;
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using MimeKit.Cryptography;
|
||||
using Windows.ApplicationModel.Activation;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage;
|
||||
using Wino.Calendar.ViewModels;
|
||||
using Wino.Calendar.ViewModels.Interfaces;
|
||||
using Wino.Core;
|
||||
@@ -22,6 +24,8 @@ using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Common;
|
||||
using Wino.Core.Domain.Models.Launch;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Navigation;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
@@ -30,6 +34,7 @@ using Wino.Mail.Services;
|
||||
using Wino.Mail.ViewModels;
|
||||
using Wino.Mail.ViewModels.Data;
|
||||
using Wino.Mail.WinUI.Activation;
|
||||
using Wino.Mail.WinUI.Extensions;
|
||||
using Wino.Mail.WinUI.Interfaces;
|
||||
using Wino.Mail.WinUI.Models;
|
||||
using Wino.Mail.WinUI.Services;
|
||||
@@ -61,6 +66,7 @@ public partial class App : WinoApplication,
|
||||
private bool _isExiting;
|
||||
private bool _activationInfrastructureInitialized;
|
||||
private int _initialNotificationActivationHandled;
|
||||
private int _initialShareActivationHandled;
|
||||
private CancellationTokenSource? _autoSynchronizationLoopCts;
|
||||
private readonly SemaphoreSlim _autoSynchronizationSemaphore = new(1, 1);
|
||||
private readonly SemaphoreSlim _activationInfrastructureSemaphore = new(1, 1);
|
||||
@@ -446,12 +452,26 @@ public partial class App : WinoApplication,
|
||||
private bool TryMarkInitialNotificationActivationHandled()
|
||||
=> Interlocked.Exchange(ref _initialNotificationActivationHandled, 1) == 0;
|
||||
|
||||
private bool TryMarkInitialShareActivationHandled()
|
||||
=> Interlocked.Exchange(ref _initialShareActivationHandled, 1) == 0;
|
||||
|
||||
protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
|
||||
{
|
||||
base.OnLaunched(args);
|
||||
|
||||
await EnsureActivationInfrastructureAsync();
|
||||
|
||||
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
|
||||
if (activationArgs.Kind == ExtendedActivationKind.ShareTarget &&
|
||||
TryMarkInitialShareActivationHandled())
|
||||
{
|
||||
LogActivation("Processing share target activation from OnLaunched.");
|
||||
|
||||
if (await HandleShareTargetActivationAsync(activationArgs, activateWindow: true))
|
||||
return;
|
||||
}
|
||||
|
||||
var hasAnyAccount = _hasConfiguredAccounts;
|
||||
if (!IsStartupTaskLaunch() && !hasAnyAccount)
|
||||
{
|
||||
@@ -522,23 +542,20 @@ public partial class App : WinoApplication,
|
||||
{
|
||||
var activationArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
|
||||
if (activationArgs.Kind != ExtendedActivationKind.AppNotification ||
|
||||
activationArgs.Data is not AppNotificationActivatedEventArgs toastArgs ||
|
||||
!TryMarkInitialNotificationActivationHandled())
|
||||
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
||||
activationArgs.Data is ILaunchActivatedEventArgs launchArgs &&
|
||||
ToastActivationResolver.TryParse(launchArgs.Arguments, out var launchToastArguments) &&
|
||||
TryMarkInitialNotificationActivationHandled())
|
||||
{
|
||||
return;
|
||||
LogActivation($"Processing initial toast launch activation from application startup. Arguments: {launchArgs.Arguments}");
|
||||
|
||||
await EnsureActivationInfrastructureAsync();
|
||||
await HandleToastActivationAsync(launchToastArguments);
|
||||
}
|
||||
|
||||
LogActivation($"Processing initial notification activation from application startup. Arguments: {toastArgs.Argument}");
|
||||
|
||||
await EnsureActivationInfrastructureAsync();
|
||||
await HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput);
|
||||
}
|
||||
|
||||
private void AppNotificationInvoked(AppNotificationManager sender, AppNotificationActivatedEventArgs args)
|
||||
{
|
||||
// AppNotification callbacks are not guaranteed to run on the UI thread.
|
||||
// Marshal toast handling to the window dispatcher before touching window APIs.
|
||||
if (MainWindow?.DispatcherQueue?.TryEnqueue(() => _ = HandleToastActivationAsync(args.Argument, args.UserInput)) == true)
|
||||
return;
|
||||
|
||||
@@ -548,19 +565,7 @@ public partial class App : WinoApplication,
|
||||
|
||||
private void TryRegisterAppNotifications()
|
||||
{
|
||||
var notificationManager = AppNotificationManager.Default;
|
||||
|
||||
notificationManager.NotificationInvoked -= AppNotificationInvoked;
|
||||
notificationManager.NotificationInvoked += AppNotificationInvoked;
|
||||
|
||||
try
|
||||
{
|
||||
notificationManager.Register();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogActivation($"App notification registration failed: {ex.GetType().Name} - {ex.Message}");
|
||||
}
|
||||
// Classic targeted toasts use normal launch activation instead of COM toast activators.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -590,7 +595,7 @@ public partial class App : WinoApplication,
|
||||
|
||||
if (calendarAction == Constants.ToastCalendarSnoozeAction)
|
||||
{
|
||||
await HandleCalendarToastSnoozeAsync(userInput, calendarItemId);
|
||||
await HandleCalendarToastSnoozeAsync(toastArguments, userInput, calendarItemId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -635,6 +640,109 @@ public partial class App : WinoApplication,
|
||||
return HandleToastActivationAsync(toastArguments, userInput);
|
||||
}
|
||||
|
||||
private static int? GetToastSnoozeDurationMinutes(NotificationArguments toastArguments, IDictionary<string, string>? userInput)
|
||||
{
|
||||
if (toastArguments.TryGetValue(Constants.ToastCalendarSnoozeDurationMinutesKey, out var snoozeDurationValue) &&
|
||||
int.TryParse(snoozeDurationValue, out var snoozeDurationMinutes) &&
|
||||
snoozeDurationMinutes > 0)
|
||||
{
|
||||
return snoozeDurationMinutes;
|
||||
}
|
||||
|
||||
if (userInput != null &&
|
||||
userInput.TryGetValue(Constants.ToastCalendarSnoozeDurationInputId, out var selectedValue) &&
|
||||
int.TryParse(selectedValue, out snoozeDurationMinutes) &&
|
||||
snoozeDurationMinutes > 0)
|
||||
{
|
||||
return snoozeDurationMinutes;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<bool> HandleShareTargetActivationAsync(AppActivationArguments activationArgs, bool activateWindow)
|
||||
{
|
||||
if (activationArgs.Kind != ExtendedActivationKind.ShareTarget ||
|
||||
activationArgs.Data is not ShareTargetActivatedEventArgs shareTargetArgs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var shareRequest = await ExtractMailShareRequestAsync(shareTargetArgs);
|
||||
|
||||
if (shareRequest?.Files == null || shareRequest.Files.Count == 0)
|
||||
{
|
||||
Services.GetRequiredService<IShareActivationService>().ClearPendingShareRequest();
|
||||
return false;
|
||||
}
|
||||
|
||||
var shareActivationService = Services.GetRequiredService<IShareActivationService>();
|
||||
shareActivationService.PendingShareRequest = shareRequest;
|
||||
|
||||
if (!_hasConfiguredAccounts)
|
||||
{
|
||||
shareActivationService.ClearPendingShareRequest();
|
||||
return false;
|
||||
}
|
||||
|
||||
var shellWindowAlreadyExists = HasShellWindow();
|
||||
|
||||
await EnsureShellWindowAsync(WinoApplicationMode.Mail, activateWindow, suppressStartupFlows: true);
|
||||
|
||||
if (shellWindowAlreadyExists)
|
||||
{
|
||||
await Services.GetRequiredService<MailAppShellViewModel>().HandlePendingShareRequestAsync();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<MailShareRequest?> ExtractMailShareRequestAsync(ShareTargetActivatedEventArgs shareTargetArgs)
|
||||
{
|
||||
var shareOperation = shareTargetArgs.ShareOperation;
|
||||
|
||||
try
|
||||
{
|
||||
shareOperation.ReportStarted();
|
||||
|
||||
if (!shareOperation.Data.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
shareOperation.ReportCompleted();
|
||||
return null;
|
||||
}
|
||||
|
||||
var storageItems = await shareOperation.Data.GetStorageItemsAsync();
|
||||
List<SharedFile> sharedFiles = [];
|
||||
|
||||
foreach (var storageFile in storageItems.OfType<StorageFile>())
|
||||
{
|
||||
sharedFiles.Add(await storageFile.ToSharedFileAsync());
|
||||
}
|
||||
|
||||
shareOperation.ReportDataRetrieved();
|
||||
shareOperation.ReportCompleted();
|
||||
|
||||
return sharedFiles.Count == 0
|
||||
? null
|
||||
: new MailShareRequest(sharedFiles);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogActivation($"Failed to extract share target payload: {ex.GetType().Name} - {ex.Message}");
|
||||
|
||||
try
|
||||
{
|
||||
shareOperation.ReportError(ex.Message);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore share reporting failures and fall back to normal launch flow.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IWinoShellWindow?> EnsureShellWindowAsync(WinoApplicationMode mode, bool activateWindow, bool suppressStartupFlows = true)
|
||||
{
|
||||
var windowManager = Services.GetRequiredService<IWinoWindowManager>();
|
||||
@@ -706,9 +814,59 @@ public partial class App : WinoApplication,
|
||||
navigationService.Navigate(WinoPage.EventDetailsPage, target);
|
||||
}
|
||||
|
||||
private async Task HandleCalendarToastSnoozeAsync(IDictionary<string, string>? userInput, Guid calendarItemId)
|
||||
private async Task<object?> TryCreateToastNavigationParameterAsync(NotificationArguments toastArguments)
|
||||
{
|
||||
if (!TryGetSnoozeDurationMinutes(userInput, out var snoozeDurationMinutes))
|
||||
if (toastArguments.TryGetValue(Constants.ToastCalendarActionKey, out string calendarAction) &&
|
||||
calendarAction == Constants.ToastCalendarNavigateAction &&
|
||||
toastArguments.TryGetValue(Constants.ToastCalendarItemIdKey, out string calendarItemIdString) &&
|
||||
Guid.TryParse(calendarItemIdString, out Guid calendarItemId))
|
||||
{
|
||||
var calendarService = Services.GetRequiredService<ICalendarService>();
|
||||
var calendarItem = await calendarService.GetCalendarItemAsync(calendarItemId);
|
||||
|
||||
if (calendarItem != null)
|
||||
{
|
||||
return new CalendarItemTarget(calendarItem, CalendarEventTargetType.Single);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<(WinoApplicationMode Mode, object? Parameter)?> TryResolveToastActivationTargetAsync(AppActivationArguments activationArgs)
|
||||
{
|
||||
NotificationArguments? toastArguments = null;
|
||||
|
||||
if (activationArgs.Kind == ExtendedActivationKind.Launch &&
|
||||
activationArgs.Data is ILaunchActivatedEventArgs launchArgs &&
|
||||
ToastActivationResolver.TryParse(launchArgs.Arguments, out var launchToastArguments))
|
||||
{
|
||||
toastArguments = launchToastArguments;
|
||||
}
|
||||
else if (activationArgs.Kind == ExtendedActivationKind.AppNotification &&
|
||||
activationArgs.Data is AppNotificationActivatedEventArgs appNotificationArgs &&
|
||||
ToastActivationResolver.TryParse(appNotificationArgs.Argument, out var appNotificationToastArguments))
|
||||
{
|
||||
toastArguments = appNotificationToastArguments;
|
||||
}
|
||||
else if (activationArgs.Data is ToastNotificationActivatedEventArgs classicToastArgs &&
|
||||
ToastActivationResolver.TryParse(classicToastArgs.Argument, out var classicToastArguments))
|
||||
{
|
||||
toastArguments = classicToastArguments;
|
||||
}
|
||||
|
||||
if (toastArguments == null ||
|
||||
!ToastActivationResolver.TryResolveMode(toastArguments, out var mode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (mode, await TryCreateToastNavigationParameterAsync(toastArguments));
|
||||
}
|
||||
|
||||
private async Task HandleCalendarToastSnoozeAsync(NotificationArguments toastArguments, IDictionary<string, string>? userInput, Guid calendarItemId)
|
||||
{
|
||||
if (!TryGetSnoozeDurationMinutes(toastArguments, userInput, out var snoozeDurationMinutes))
|
||||
return;
|
||||
|
||||
var calendarService = Services.GetRequiredService<ICalendarService>();
|
||||
@@ -732,20 +890,13 @@ public partial class App : WinoApplication,
|
||||
await nativeAppService.LaunchUriAsync(joinUri);
|
||||
}
|
||||
|
||||
private bool TryGetSnoozeDurationMinutes(IDictionary<string, string>? userInput, out int snoozeDurationMinutes)
|
||||
private bool TryGetSnoozeDurationMinutes(NotificationArguments toastArguments, IDictionary<string, string>? userInput, out int snoozeDurationMinutes)
|
||||
{
|
||||
snoozeDurationMinutes = _preferencesService?.DefaultSnoozeDurationInMinutes ?? 0;
|
||||
snoozeDurationMinutes = GetToastSnoozeDurationMinutes(toastArguments, userInput)
|
||||
?? _preferencesService?.DefaultSnoozeDurationInMinutes
|
||||
?? 0;
|
||||
|
||||
if (userInput == null ||
|
||||
!userInput.TryGetValue(Constants.ToastCalendarSnoozeDurationInputId, out var selectedValue) ||
|
||||
selectedValue == null)
|
||||
{
|
||||
return snoozeDurationMinutes > 0;
|
||||
}
|
||||
|
||||
var selectedText = selectedValue.ToString();
|
||||
|
||||
return int.TryParse(selectedText, out snoozeDurationMinutes) && snoozeDurationMinutes > 0;
|
||||
return snoozeDurationMinutes > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1446,6 +1597,11 @@ public partial class App : WinoApplication,
|
||||
LogActivation($"Processing redirected notification activation. Arguments: {toastArgs.Argument}");
|
||||
_ = HandleToastActivationAsync(toastArgs.Argument, toastArgs.UserInput);
|
||||
}
|
||||
else if (args.Kind == ExtendedActivationKind.ShareTarget)
|
||||
{
|
||||
LogActivation("Processing redirected share target activation.");
|
||||
await HandleShareTargetActivationAsync(args, activateWindow: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var shouldActivateWindow = true;
|
||||
@@ -1475,7 +1631,22 @@ public partial class App : WinoApplication,
|
||||
}
|
||||
else if (TryResolveActivationMode(args, _preferencesService?.DefaultApplicationMode ?? WinoApplicationMode.Mail, out var redirectedMode))
|
||||
{
|
||||
shellWindow.HandleAppActivation(AppEntryConstants.GetModeLaunchArgument(redirectedMode));
|
||||
var navigationService = Services.GetRequiredService<INavigationService>();
|
||||
var toastActivationTarget = await TryResolveToastActivationTargetAsync(args);
|
||||
|
||||
if (toastActivationTarget is { Parameter: CalendarItemTarget calendarTarget })
|
||||
{
|
||||
navigationService.ChangeApplicationMode(toastActivationTarget.Value.Mode, new ShellModeActivationContext
|
||||
{
|
||||
SuppressStartupFlows = true,
|
||||
Parameter = calendarTarget
|
||||
});
|
||||
navigationService.Navigate(WinoPage.EventDetailsPage, calendarTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
shellWindow.HandleAppActivation(AppEntryConstants.GetModeLaunchArgument(redirectedMode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1488,7 +1659,6 @@ public partial class App : WinoApplication,
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch to UI thread since this is called from Program.OnActivated.
|
||||
if (MainWindow?.DispatcherQueue.TryEnqueue(() => _ = HandleRedirectedActivationAsync()) == true)
|
||||
return;
|
||||
|
||||
@@ -1527,6 +1697,19 @@ public partial class App : WinoApplication,
|
||||
|
||||
}
|
||||
|
||||
if (activationArgs.Kind == ExtendedActivationKind.ShareTarget)
|
||||
{
|
||||
mode = WinoApplicationMode.Mail;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (activationArgs.Data is ToastNotificationActivatedEventArgs classicToastArgs &&
|
||||
ToastActivationResolver.TryParse(classicToastArgs.Argument, out var classicToastArguments) &&
|
||||
ToastActivationResolver.TryResolveMode(classicToastArguments, out mode))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (activationArgs.Kind == ExtendedActivationKind.File &&
|
||||
activationArgs.Data is IFileActivatedEventArgs fileArgs)
|
||||
{
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
<Grid MinWidth="400" RowSpacing="12">
|
||||
<Grid Visibility="{x:Bind IsProviderSelectionVisible, Mode=OneWay}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
@@ -96,6 +97,48 @@
|
||||
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
x:Name="InitialSynchronizationPanel"
|
||||
Grid.Row="1"
|
||||
Margin="0,12,0,0"
|
||||
Padding="12"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Title}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Description}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
|
||||
<ComboBox
|
||||
x:Name="InitialSynchronizationComboBox"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{x:Bind InitialSynchronizationRanges, Mode=OneWay}"
|
||||
SelectionChanged="InitialSynchronizationSelectionChanged">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="accounts:InitialSynchronizationRangeOption">
|
||||
<TextBlock Text="{x:Bind DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<muxc:InfoBar
|
||||
x:Name="InitialSynchronizationWarningBar"
|
||||
IsOpen="True"
|
||||
Message="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_EverythingWarning}"
|
||||
Severity="Warning"
|
||||
Title="{x:Bind domain:Translator.GeneralTitle_Warning}"
|
||||
Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
<ListView
|
||||
Grid.Row="2"
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Wino.Mail.WinUI.Dialogs;
|
||||
|
||||
public sealed partial class NewAccountDialog : ContentDialog
|
||||
{
|
||||
private readonly Dictionary<SpecialImapProvider, string> helpingLinks = new Dictionary<SpecialImapProvider, string>()
|
||||
private readonly Dictionary<SpecialImapProvider, string> helpingLinks = new()
|
||||
{
|
||||
{ SpecialImapProvider.iCloud, "https://support.apple.com/en-us/102654" },
|
||||
{ SpecialImapProvider.Yahoo, "http://help.yahoo.com/kb/SLN15241.html" },
|
||||
@@ -27,7 +27,6 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(AppColorViewModel), typeof(NewAccountDialog), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedColorChanged)));
|
||||
public static readonly DependencyProperty SelectedCalendarModeIndexProperty = DependencyProperty.Register(nameof(SelectedCalendarModeIndex), typeof(int), typeof(NewAccountDialog), new PropertyMetadata(0));
|
||||
|
||||
|
||||
public AppColorViewModel? SelectedColor
|
||||
{
|
||||
get { return (AppColorViewModel?)GetValue(SelectedColorProperty); }
|
||||
@@ -49,7 +48,6 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
set { SetValue(SelectedMailProviderProperty, value); }
|
||||
}
|
||||
|
||||
|
||||
public bool IsProviderSelectionVisible
|
||||
{
|
||||
get { return (bool)GetValue(IsProviderSelectionVisibleProperty); }
|
||||
@@ -63,10 +61,16 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
}
|
||||
|
||||
// List of available mail providers for now.
|
||||
|
||||
public List<IProviderDetail> Providers { get; set; } = [];
|
||||
|
||||
public List<AppColorViewModel> AvailableColors { get; set; } = [];
|
||||
public List<InitialSynchronizationRangeOption> InitialSynchronizationRanges { get; } =
|
||||
[
|
||||
new(InitialSynchronizationRange.ThreeMonths, Translator.AccountCreation_InitialSynchronization_3Months),
|
||||
new(InitialSynchronizationRange.SixMonths, Translator.AccountCreation_InitialSynchronization_6Months),
|
||||
new(InitialSynchronizationRange.NineMonths, Translator.AccountCreation_InitialSynchronization_9Months),
|
||||
new(InitialSynchronizationRange.OneYear, Translator.AccountCreation_InitialSynchronization_Year),
|
||||
new(InitialSynchronizationRange.Everything, Translator.AccountCreation_InitialSynchronization_Everything)
|
||||
];
|
||||
public List<string> CalendarModeOptions { get; } =
|
||||
[
|
||||
Translator.ImapCalDavSettingsPage_CalendarModeCalDav,
|
||||
@@ -74,7 +78,6 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
Translator.ImapCalDavSettingsPage_CalendarModeDisabled
|
||||
];
|
||||
|
||||
|
||||
public AccountCreationDialogResult? Result = null;
|
||||
|
||||
public NewAccountDialog()
|
||||
@@ -85,6 +88,8 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
AvailableColors = themeService.Select(a => new AppColorViewModel(a)).ToList();
|
||||
|
||||
UpdateSelectedColor();
|
||||
InitialSynchronizationComboBox.SelectedItem = InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
|
||||
UpdateInitialSynchronizationState();
|
||||
}
|
||||
|
||||
private static void OnSelectedProviderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||
@@ -105,6 +110,19 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
SelectedColorEllipse.Fill = SelectedColor == null ? null : XamlHelpers.GetSolidColorBrushFromHex(SelectedColor.Hex);
|
||||
}
|
||||
|
||||
private void UpdateInitialSynchronizationState()
|
||||
{
|
||||
InitialSynchronizationPanel.Visibility = SelectedMailProvider == null ? Visibility.Collapsed : Visibility.Visible;
|
||||
var selectedOption = InitialSynchronizationComboBox.SelectedItem as InitialSynchronizationRangeOption;
|
||||
InitialSynchronizationWarningBar.Visibility = selectedOption?.IsEverything == true ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private InitialSynchronizationRange GetInitialSynchronizationRange()
|
||||
{
|
||||
var selectedRange = (InitialSynchronizationComboBox.SelectedItem as InitialSynchronizationRangeOption)?.Range
|
||||
?? InitialSynchronizationRange.SixMonths;
|
||||
return selectedRange;
|
||||
}
|
||||
|
||||
private void CancelClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||
{
|
||||
@@ -116,9 +134,11 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
if (SelectedMailProvider == null)
|
||||
return;
|
||||
|
||||
var initialSynchronizationRange = GetInitialSynchronizationRange();
|
||||
|
||||
if (IsSpecialImapServerPartVisible)
|
||||
{
|
||||
// Special imap detail input.
|
||||
// Special IMAP detail input.
|
||||
var calendarSupportMode = SelectedCalendarModeIndex switch
|
||||
{
|
||||
1 => ImapCalendarSupportMode.LocalOnly,
|
||||
@@ -132,7 +152,12 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
DisplayNameTextBox.Text.Trim(),
|
||||
SelectedMailProvider.SpecialImapProvider,
|
||||
calendarSupportMode);
|
||||
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), details, SelectedColor?.Hex ?? string.Empty);
|
||||
Result = new AccountCreationDialogResult(
|
||||
SelectedMailProvider.Type,
|
||||
AccountNameTextbox.Text.Trim(),
|
||||
details,
|
||||
SelectedColor?.Hex ?? string.Empty,
|
||||
initialSynchronizationRange);
|
||||
Hide();
|
||||
|
||||
return;
|
||||
@@ -140,11 +165,11 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
|
||||
Validate();
|
||||
|
||||
if (IsSecondaryButtonEnabled)
|
||||
if (IsPrimaryButtonEnabled)
|
||||
{
|
||||
if (SelectedMailProvider.SpecialImapProvider != SpecialImapProvider.None)
|
||||
{
|
||||
// This step requires app-sepcific password login for some providers.
|
||||
// This step requires app-specific password login for some providers.
|
||||
args.Cancel = true;
|
||||
|
||||
IsProviderSelectionVisible = false;
|
||||
@@ -154,7 +179,12 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
}
|
||||
else
|
||||
{
|
||||
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), null, SelectedColor?.Hex ?? string.Empty);
|
||||
Result = new AccountCreationDialogResult(
|
||||
SelectedMailProvider.Type,
|
||||
AccountNameTextbox.Text.Trim(),
|
||||
null,
|
||||
SelectedColor?.Hex ?? string.Empty,
|
||||
initialSynchronizationRange);
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
@@ -167,6 +197,7 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
{
|
||||
ValidateCreateButton();
|
||||
ValidateNames();
|
||||
UpdateInitialSynchronizationState();
|
||||
}
|
||||
|
||||
// Returns whether we can create account or not.
|
||||
@@ -199,6 +230,9 @@ public sealed partial class NewAccountDialog : ContentDialog
|
||||
|
||||
private void ImapPasswordChanged(object sender, RoutedEventArgs e) => Validate();
|
||||
|
||||
private void InitialSynchronizationSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
=> UpdateInitialSynchronizationState();
|
||||
|
||||
private async void AppSpecificHelpButtonClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (SelectedMailProvider == null ||
|
||||
|
||||
@@ -6,10 +6,8 @@
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
|
||||
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
|
||||
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
IgnorableNamespaces="uap uap10 rescap com desktop">
|
||||
IgnorableNamespaces="uap uap10 rescap">
|
||||
|
||||
<!-- Publisher Cache Folders -->
|
||||
<Extensions>
|
||||
@@ -23,7 +21,7 @@
|
||||
<Identity
|
||||
Name="58272BurakKSE.WinoMailPreview"
|
||||
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
||||
Version="2.0.0.0" />
|
||||
Version="2.0.1.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="3879fcfb-a561-4599-9103-e0c9b35a271f" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
@@ -47,16 +45,15 @@
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$"
|
||||
uap10:Parameters="--wino-mail">
|
||||
<uap:VisualElements
|
||||
<uap:VisualElements
|
||||
DisplayName="Wino Mail"
|
||||
Description="Wino.Mail.WinUI"
|
||||
Description="Wino Mail"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\AppEntries\MailAssets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\AppEntries\MailAssets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\AppEntries\MailAssets\Wide310x150Logo.png" Square71x71Logo="Assets\AppEntries\MailAssets\SmallTile.png" Square310x310Logo="Assets\AppEntries\MailAssets\LargeTile.png"/>
|
||||
<uap:SplashScreen Image="Assets\AppEntries\MailAssets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
|
||||
<Extensions>
|
||||
<uap5:Extension Category="windows.startupTask">
|
||||
<uap5:StartupTask
|
||||
@@ -65,19 +62,6 @@
|
||||
DisplayName="Wino Startup Service" />
|
||||
</uap5:Extension>
|
||||
|
||||
<!-- App notification activation -->
|
||||
<desktop:Extension Category="windows.toastNotificationActivation">
|
||||
<desktop:ToastNotificationActivation ToastActivatorCLSID="72c6d2d0-2538-44fe-a1b1-499f47bb1181" />
|
||||
</desktop:Extension>
|
||||
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:ExeServer Executable="Wino.Mail.WinUI.exe" Arguments="----AppNotificationActivated:" DisplayName="Toast activator">
|
||||
<com:Class Id="72c6d2d0-2538-44fe-a1b1-499f47bb1181" DisplayName="Toast activator"/>
|
||||
</com:ExeServer>
|
||||
</com:ComServer>
|
||||
</com:Extension>
|
||||
|
||||
<!-- Protocol activation: mailto -->
|
||||
<uap:Extension Category="windows.protocol">
|
||||
<uap:Protocol Name="mailto" />
|
||||
@@ -97,6 +81,15 @@
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
|
||||
<!-- Share target activation -->
|
||||
<uap:Extension Category="windows.shareTarget">
|
||||
<uap:ShareTarget>
|
||||
<uap:SupportedFileTypes>
|
||||
<uap:SupportsAnyFileType />
|
||||
</uap:SupportedFileTypes>
|
||||
</uap:ShareTarget>
|
||||
</uap:Extension>
|
||||
|
||||
<!-- File Assosication: EML -->
|
||||
<uap:Extension Category="windows.fileTypeAssociation">
|
||||
<uap:FileTypeAssociation Name="eml">
|
||||
@@ -109,55 +102,43 @@
|
||||
</Extensions>
|
||||
</Application>
|
||||
|
||||
<Application Id="CalendarApp"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$"
|
||||
uap10:Parameters="--wino-calendar">
|
||||
<uap:VisualElements
|
||||
DisplayName="Wino Calendar"
|
||||
Description="Wino.Mail.WinUI.Calendar"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\AppEntries\CalendarAssets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\AppEntries\CalendarAssets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\AppEntries\CalendarAssets\Wide310x150Logo.png" Square71x71Logo="Assets\AppEntries\CalendarAssets\SmallTile.png" Square310x310Logo="Assets\AppEntries\CalendarAssets\LargeTile.png"/>
|
||||
<uap:SplashScreen Image="Assets\AppEntries\CalendarAssets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
<Application Id="CalendarApp"
|
||||
Executable="$targetnametoken$.exe"
|
||||
EntryPoint="$targetentrypoint$"
|
||||
uap10:Parameters="--wino-calendar">
|
||||
<uap:VisualElements
|
||||
DisplayName="Wino Calendar"
|
||||
Description="Wino Calendar"
|
||||
BackgroundColor="transparent"
|
||||
Square150x150Logo="Assets\AppEntries\CalendarAssets\Square150x150Logo.png"
|
||||
Square44x44Logo="Assets\AppEntries\CalendarAssets\Square44x44Logo.png">
|
||||
<uap:DefaultTile Wide310x150Logo="Assets\AppEntries\CalendarAssets\Wide310x150Logo.png" Square71x71Logo="Assets\AppEntries\CalendarAssets\SmallTile.png" Square310x310Logo="Assets\AppEntries\CalendarAssets\LargeTile.png"/>
|
||||
<uap:SplashScreen Image="Assets\AppEntries\CalendarAssets\SplashScreen.png" />
|
||||
</uap:VisualElements>
|
||||
|
||||
<Extensions>
|
||||
<desktop:Extension Category="windows.toastNotificationActivation">
|
||||
<desktop:ToastNotificationActivation ToastActivatorCLSID="44c05d2b-aa1d-4e59-9d7d-8b4c8607cb8d" />
|
||||
</desktop:Extension>
|
||||
<Extensions>
|
||||
<uap:Extension Category="windows.protocol">
|
||||
<uap:Protocol Name="webcal">
|
||||
<uap:DisplayName>Calendar Protocol</uap:DisplayName>
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
|
||||
<com:Extension Category="windows.comServer">
|
||||
<com:ComServer>
|
||||
<com:ExeServer Executable="Wino.Mail.WinUI.exe" Arguments="----AppNotificationActivated:" DisplayName="Calendar toast activator">
|
||||
<com:Class Id="44c05d2b-aa1d-4e59-9d7d-8b4c8607cb8d" DisplayName="Calendar toast activator"/>
|
||||
</com:ExeServer>
|
||||
</com:ComServer>
|
||||
</com:Extension>
|
||||
<uap:Extension Category="windows.protocol">
|
||||
<uap:Protocol Name="webcals">
|
||||
<uap:DisplayName>Calendar Protocol (Secure)</uap:DisplayName>
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
|
||||
<uap:Extension Category="windows.protocol">
|
||||
<uap:Protocol Name="webcal">
|
||||
<uap:DisplayName>Calendar Protocol</uap:DisplayName>
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
|
||||
<uap:Extension Category="windows.protocol">
|
||||
<uap:Protocol Name="webcals">
|
||||
<uap:DisplayName>Calendar Protocol (Secure)</uap:DisplayName>
|
||||
</uap:Protocol>
|
||||
</uap:Extension>
|
||||
|
||||
<uap:Extension Category="windows.fileTypeAssociation">
|
||||
<uap:FileTypeAssociation Name="ics">
|
||||
<uap:Logo>Assets\AppEntries\CalendarAssets\Square44x44Logo.png</uap:Logo>
|
||||
<uap:SupportedFileTypes>
|
||||
<uap:FileType>.ics</uap:FileType>
|
||||
</uap:SupportedFileTypes>
|
||||
</uap:FileTypeAssociation>
|
||||
</uap:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
<uap:Extension Category="windows.fileTypeAssociation">
|
||||
<uap:FileTypeAssociation Name="ics">
|
||||
<uap:Logo>Assets\AppEntries\CalendarAssets\Square44x44Logo.png</uap:Logo>
|
||||
<uap:SupportedFileTypes>
|
||||
<uap:FileType>.ics</uap:FileType>
|
||||
</uap:SupportedFileTypes>
|
||||
</uap:FileTypeAssociation>
|
||||
</uap:Extension>
|
||||
</Extensions>
|
||||
</Application>
|
||||
</Applications>
|
||||
|
||||
<Capabilities>
|
||||
|
||||
@@ -207,6 +207,13 @@ public class Program
|
||||
: true;
|
||||
}
|
||||
|
||||
if (args.Data is Windows.ApplicationModel.Activation.ToastNotificationActivatedEventArgs classicToastArgs)
|
||||
{
|
||||
return ToastActivationResolver.TryParse(classicToastArgs.Argument, out var toastArguments)
|
||||
? ToastActivationResolver.ShouldBringToForeground(toastArguments)
|
||||
: true;
|
||||
}
|
||||
|
||||
if (args.Kind == ExtendedActivationKind.Launch &&
|
||||
args.Data is Windows.ApplicationModel.Activation.ILaunchActivatedEventArgs launchArgs &&
|
||||
ToastActivationResolver.TryParse(launchArgs.Arguments, out var launchToastArguments))
|
||||
|
||||
@@ -587,6 +587,31 @@ public class NewThemeService : INewThemeService
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteCustomThemeAsync(Guid themeId)
|
||||
{
|
||||
var themeFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(CustomThemeFolderName, CreationCollisionOption.OpenIfExists);
|
||||
var metadataFileName = $"{themeId}.json";
|
||||
var themeItem = await themeFolder.TryGetItemAsync(metadataFileName);
|
||||
|
||||
if (themeItem == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (currentApplicationThemeId == themeId)
|
||||
{
|
||||
currentApplicationThemeId = preDefinedThemes[0].Id;
|
||||
_configurationService.Set(CurrentApplicationThemeKey, currentApplicationThemeId);
|
||||
await ApplyCustomThemeAsync(false);
|
||||
}
|
||||
|
||||
await DeleteThemeAssetIfExistsAsync(themeFolder, metadataFileName);
|
||||
await DeleteThemeAssetIfExistsAsync(themeFolder, $"{themeId}.jpg");
|
||||
await DeleteThemeAssetIfExistsAsync(themeFolder, $"{themeId}_preview.jpg");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task<CustomThemeMetadata?> GetCustomMetadataAsync(IStorageFile file)
|
||||
{
|
||||
var fileContent = await FileIO.ReadTextAsync(file);
|
||||
@@ -594,6 +619,16 @@ public class NewThemeService : INewThemeService
|
||||
return JsonSerializer.Deserialize(fileContent, DomainModelsJsonContext.Default.CustomThemeMetadata);
|
||||
}
|
||||
|
||||
private static async Task DeleteThemeAssetIfExistsAsync(StorageFolder themeFolder, string fileName)
|
||||
{
|
||||
var item = await themeFolder.TryGetItemAsync(fileName);
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
await item.DeleteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public string GetSystemAccentColorHex()
|
||||
=> uiSettings.GetColorValue(UIColorType.Accent).ToHex();
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using Microsoft.Windows.AppNotifications.Builder;
|
||||
using CommunityToolkit.WinUI.Notifications;
|
||||
using Serilog;
|
||||
using Windows.Data.Xml.Dom;
|
||||
using Windows.UI.Notifications;
|
||||
@@ -76,9 +75,9 @@ public class NotificationBuilder : INotificationBuilder
|
||||
builder.AddText(Translator.Notifications_MultipleNotificationsTitle);
|
||||
builder.AddText(string.Format(Translator.Notifications_MultipleNotificationsMessage, mailCount));
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail"));
|
||||
builder.AddAudio(new Uri("ms-winsoundevent:Notification.Mail"));
|
||||
|
||||
ShowNotification(builder);
|
||||
ShowNotification(builder, WinoApplicationMode.Mail);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -146,9 +145,9 @@ public class NotificationBuilder : INotificationBuilder
|
||||
{
|
||||
try
|
||||
{
|
||||
AppNotificationManager.Default.RemoveByTagAsync(mailUniqueId.ToString()).AsTask().GetAwaiter().GetResult();
|
||||
ToastNotificationManager.History.Remove(mailUniqueId.ToString());
|
||||
}
|
||||
catch (ArgumentException)
|
||||
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -164,11 +163,12 @@ public class NotificationBuilder : INotificationBuilder
|
||||
builder.AddText(string.Format(Translator.Exception_AccountNeedsAttention_Message, account.Name));
|
||||
builder.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString());
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
builder.AddButton(new AppNotificationButton(Translator.Buttons_FixAccount)
|
||||
builder.AddButton(new ToastButton()
|
||||
.SetContent(Translator.Buttons_FixAccount)
|
||||
.AddArgument(Constants.ToastMailAccountIdKey, account.Id.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail));
|
||||
|
||||
ShowNotification(builder);
|
||||
ShowNotification(builder, WinoApplicationMode.Mail);
|
||||
}
|
||||
|
||||
public void CreateWebView2RuntimeMissingNotification()
|
||||
@@ -178,7 +178,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
builder.AddText(Translator.Exception_WebView2RuntimeMissing_Message);
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
ShowNotification(builder);
|
||||
ShowNotification(builder, WinoApplicationMode.Mail);
|
||||
}
|
||||
|
||||
public void CreateStoreUpdateNotification()
|
||||
@@ -189,7 +189,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
builder.AddArgument(Constants.ToastStoreUpdateActionKey, Constants.ToastStoreUpdateActionInstall);
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
ShowNotification(builder, "store-update-available");
|
||||
ShowNotification(builder, WinoApplicationMode.Mail, "store-update-available");
|
||||
}
|
||||
|
||||
public Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds)
|
||||
@@ -197,7 +197,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
if (calendarItem == null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var builder = CreateBuilder(AppNotificationScenario.Reminder);
|
||||
var builder = CreateBuilder(ToastScenario.Reminder);
|
||||
var localStart = calendarItem.GetLocalStartDate();
|
||||
var reminderContext = GetCalendarReminderContext(localStart, DateTime.Now);
|
||||
|
||||
@@ -210,7 +210,7 @@ public class NotificationBuilder : INotificationBuilder
|
||||
builder.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction);
|
||||
builder.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString());
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar);
|
||||
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Reminder"));
|
||||
builder.AddAudio(new Uri("ms-winsoundevent:Notification.Reminder"));
|
||||
|
||||
var allowedSnoozeMinutes = CalendarReminderSnoozeOptions.GetAllowedSnoozeMinutes(
|
||||
reminderDurationInSeconds,
|
||||
@@ -223,40 +223,33 @@ public class NotificationBuilder : INotificationBuilder
|
||||
? preferredSnoozeMinutes
|
||||
: allowedSnoozeMinutes[0];
|
||||
|
||||
var selectionBox = new AppNotificationComboBox(Constants.ToastCalendarSnoozeDurationInputId)
|
||||
.SetSelectedItem(defaultSnoozeMinutes.ToString());
|
||||
|
||||
foreach (var snoozeMinutes in allowedSnoozeMinutes)
|
||||
{
|
||||
selectionBox.AddItem(
|
||||
snoozeMinutes.ToString(),
|
||||
string.Format(Translator.CalendarReminder_SnoozeMinutesOption, snoozeMinutes));
|
||||
}
|
||||
|
||||
builder.AddComboBox(selectionBox);
|
||||
builder.AddButton(new AppNotificationButton(Translator.CalendarReminder_SnoozeAction)
|
||||
.SetIcon(GetNotificationIconUri("calendar-snooze"))
|
||||
builder.AddButton(new ToastButton()
|
||||
.SetContent(Translator.CalendarReminder_SnoozeAction)
|
||||
.SetImageUri(GetNotificationIconUri("calendar-snooze"))
|
||||
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarSnoozeAction)
|
||||
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
||||
.AddArgument(Constants.ToastCalendarSnoozeDurationMinutesKey, defaultSnoozeMinutes.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
||||
}
|
||||
|
||||
builder.AddButton(new AppNotificationButton(Translator.Buttons_Open)
|
||||
builder.AddButton(new ToastButton()
|
||||
.SetContent(Translator.Buttons_Open)
|
||||
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarNavigateAction)
|
||||
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
||||
|
||||
if (Uri.TryCreate(calendarItem.HtmlLink, UriKind.Absolute, out _))
|
||||
{
|
||||
builder.AddButton(new AppNotificationButton(Translator.CalendarEventDetails_JoinOnline)
|
||||
.SetIcon(GetNotificationIconUri("calendar-join"))
|
||||
builder.AddButton(new ToastButton()
|
||||
.SetContent(Translator.CalendarEventDetails_JoinOnline)
|
||||
.SetImageUri(GetNotificationIconUri("calendar-join"))
|
||||
.AddArgument(Constants.ToastCalendarActionKey, Constants.ToastCalendarJoinOnlineAction)
|
||||
.AddArgument(Constants.ToastCalendarItemIdKey, calendarItem.Id.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeCalendar));
|
||||
}
|
||||
|
||||
var tag = $"calendar-reminder-{calendarItem.Id:N}-{reminderDurationInSeconds}";
|
||||
ShowNotification(builder, tag);
|
||||
ShowNotification(builder, WinoApplicationMode.Calendar, tag);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -278,10 +271,10 @@ public class NotificationBuilder : INotificationBuilder
|
||||
await stream.WriteAsync(bytes);
|
||||
}
|
||||
|
||||
builder.SetAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), AppNotificationImageCrop.Default);
|
||||
builder.AddAppLogoOverride(new Uri($"ms-appdata:///temp/{tempFile.Name}"), ToastGenericAppLogoCrop.Default);
|
||||
}
|
||||
|
||||
builder.SetTimeStamp(mailItem.CreationDate.ToLocalTime());
|
||||
builder.AddCustomTimeStamp(mailItem.CreationDate.ToLocalTime());
|
||||
builder.AddText(mailItem.FromName);
|
||||
builder.AddText(mailItem.Subject);
|
||||
builder.AddText(mailItem.PreviewText);
|
||||
@@ -291,9 +284,9 @@ public class NotificationBuilder : INotificationBuilder
|
||||
builder.AddButton(GetMarkAsReadButton(mailItem.UniqueId));
|
||||
builder.AddButton(GetDeleteButton(mailItem.UniqueId));
|
||||
builder.AddButton(GetArchiveButton(mailItem.UniqueId));
|
||||
builder.SetAudioUri(new Uri("ms-winsoundevent:Notification.Mail"));
|
||||
builder.AddAudio(new Uri("ms-winsoundevent:Notification.Mail"));
|
||||
|
||||
ShowNotification(builder, mailItem.UniqueId.ToString());
|
||||
ShowNotification(builder, WinoApplicationMode.Mail, mailItem.UniqueId.ToString());
|
||||
}
|
||||
|
||||
private void UpdateBadge(string applicationId, int? badgeCount)
|
||||
@@ -347,40 +340,43 @@ public class NotificationBuilder : INotificationBuilder
|
||||
return string.Format(Translator.CalendarReminder_StartedMinutesAgo, minutesAgo);
|
||||
}
|
||||
|
||||
private AppNotificationButton GetArchiveButton(Guid mailUniqueId)
|
||||
=> new AppNotificationButton(Translator.MailOperation_Archive)
|
||||
.SetIcon(GetNotificationIconUri("mail-archive"))
|
||||
private ToastButton GetArchiveButton(Guid mailUniqueId)
|
||||
=> new ToastButton()
|
||||
.SetContent(Translator.MailOperation_Archive)
|
||||
.SetImageUri(GetNotificationIconUri("mail-archive"))
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.Archive.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
private AppNotificationButton GetDeleteButton(Guid mailUniqueId)
|
||||
=> new AppNotificationButton(Translator.MailOperation_Delete)
|
||||
.SetIcon(GetNotificationIconUri("mail-delete"))
|
||||
private ToastButton GetDeleteButton(Guid mailUniqueId)
|
||||
=> new ToastButton()
|
||||
.SetContent(Translator.MailOperation_Delete)
|
||||
.SetImageUri(GetNotificationIconUri("mail-delete"))
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
private AppNotificationButton GetMarkAsReadButton(Guid mailUniqueId)
|
||||
=> new AppNotificationButton(Translator.MailOperation_MarkAsRead)
|
||||
.SetIcon(GetNotificationIconUri("mail-markread"))
|
||||
private ToastButton GetMarkAsReadButton(Guid mailUniqueId)
|
||||
=> new ToastButton()
|
||||
.SetContent(Translator.MailOperation_MarkAsRead)
|
||||
.SetImageUri(GetNotificationIconUri("mail-markread"))
|
||||
.AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString())
|
||||
.AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead.ToString())
|
||||
.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
|
||||
private static AppNotificationBuilder CreateBuilder(AppNotificationScenario scenario = AppNotificationScenario.Default)
|
||||
=> new AppNotificationBuilder().SetScenario(scenario);
|
||||
private static ToastContentBuilder CreateBuilder(ToastScenario scenario = ToastScenario.Default)
|
||||
=> new ToastContentBuilder().SetToastScenario(scenario);
|
||||
|
||||
private static void ShowNotification(AppNotificationBuilder builder, string? tag = null)
|
||||
private static void ShowNotification(ToastContentBuilder builder, WinoApplicationMode mode, string? tag = null)
|
||||
{
|
||||
var notification = builder.BuildNotification();
|
||||
var notification = new ToastNotification(builder.GetXml());
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tag))
|
||||
{
|
||||
notification.Tag = tag;
|
||||
}
|
||||
|
||||
AppNotificationManager.Default.Show(notification);
|
||||
ToastNotificationManager.CreateToastNotifier(AppEntryConstants.GetAppUserModelId(mode)).Show(notification);
|
||||
}
|
||||
|
||||
private static Uri GetNotificationIconUri(string iconName)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
@@ -37,7 +36,6 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
|
||||
_configurationService.Set(propertyName, value ?? string.Empty);
|
||||
|
||||
OnPropertyChanged(propertyName);
|
||||
Debug.WriteLine($"PreferencesService -> {propertyName}:{value?.ToString()}");
|
||||
}
|
||||
|
||||
public MailRenderingOptions GetRenderingOptions()
|
||||
|
||||
@@ -210,6 +210,13 @@
|
||||
</TransitionCollection>
|
||||
</StackPanel.ChildrenTransitions>
|
||||
|
||||
<muxc:InfoBar
|
||||
IsOpen="True"
|
||||
Margin="0,0,0,8"
|
||||
Message="{x:Bind ViewModel.InitialSynchronizationSummary, Mode=OneWay}"
|
||||
Severity="Informational"
|
||||
Title="{x:Bind domain:Translator.AccountDetailsPage_InitialSynchronization_Title}"
|
||||
Visibility="{x:Bind ViewModel.IsInitialSynchronizationSummaryVisible, Mode=OneWay}" />
|
||||
|
||||
<controls:SettingsCard
|
||||
Command="{x:Bind ViewModel.EditAliasesCommand}"
|
||||
|
||||
@@ -4,267 +4,267 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Views.Abstract"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel
|
||||
<Grid
|
||||
MaxWidth="980"
|
||||
Padding="36,28,36,36"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="24">
|
||||
|
||||
<!-- Page Header -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock
|
||||
FontSize="28"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.PageTitle, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SubtitleText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.ProviderHint, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords"
|
||||
Visibility="{x:Bind ViewModel.HasProviderHint, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Setup Mode Selector -->
|
||||
<SelectorBar x:Name="SetupModeSelector" SelectionChanged="OnSetupModeSelectionChanged">
|
||||
<SelectorBarItem Icon="Library" Text="{x:Bind ViewModel.BasicTabText, Mode=OneWay}" />
|
||||
<SelectorBarItem Icon="Setting" Text="{x:Bind ViewModel.AdvancedTabText, Mode=OneWay}" />
|
||||
</SelectorBar>
|
||||
|
||||
<!-- Basic Setup Card -->
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsAdvancedSetupSelected), Mode=OneWay}">
|
||||
<StackPanel Spacing="16">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.BasicSectionTitleText, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.BasicSectionDescriptionText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Header="{x:Bind ViewModel.DisplayNameHeaderText, Mode=OneWay}"
|
||||
PlaceholderText="{x:Bind ViewModel.DisplayNamePlaceholderText, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.DisplayName, Mode=TwoWay}" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Header="{x:Bind ViewModel.EmailAddressHeaderText, Mode=OneWay}"
|
||||
PlaceholderText="{x:Bind ViewModel.EmailAddressPlaceholderText, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.EmailAddress, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
|
||||
<PasswordBox Header="{x:Bind ViewModel.PasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.Password, Mode=TwoWay}" />
|
||||
|
||||
<CheckBox Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}" IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" />
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Left"
|
||||
Command="{x:Bind ViewModel.AutoDiscoverSettingsCommand}"
|
||||
Content="{x:Bind ViewModel.AutoDiscoverButtonText, Mode=OneWay}"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
HorizontalAlignment="Center">
|
||||
<StackPanel Spacing="20">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock
|
||||
FontSize="28"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.PageTitle, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SubtitleText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorTertiaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.ProviderHint, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords"
|
||||
Visibility="{x:Bind ViewModel.HasProviderHint, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Advanced Setup Card -->
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsBasicSetupSelected), Mode=OneWay}">
|
||||
<StackPanel Spacing="20">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.AdvancedSectionTitleText, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.AdvancedSectionDescriptionText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnSpacing="24">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Incoming (IMAP) Settings -->
|
||||
<Border
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
CornerRadius="6">
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.IncomingSectionTitleText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<TextBox Header="{x:Bind ViewModel.IncomingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServer, Mode=TwoWay}" />
|
||||
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerPort, Mode=TwoWay}" />
|
||||
<TextBox Header="{x:Bind ViewModel.IncomingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerUsername, Mode=TwoWay}" />
|
||||
<PasswordBox Header="{x:Bind ViewModel.IncomingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.IncomingServerPassword, Mode=TwoWay}" />
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerConnectionSecurityIndex, Mode=TwoWay}" />
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerAuthenticationMethodIndex, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Outgoing (SMTP) Settings -->
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
CornerRadius="6">
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.OutgoingSectionTitleText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<TextBox Header="{x:Bind ViewModel.OutgoingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServer, Mode=TwoWay}" />
|
||||
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerPort, Mode=TwoWay}" />
|
||||
<TextBox Header="{x:Bind ViewModel.OutgoingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerUsername, Mode=TwoWay}" />
|
||||
<PasswordBox Header="{x:Bind ViewModel.OutgoingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.OutgoingServerPassword, Mode=TwoWay}" />
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerConnectionSecurityIndex, Mode=TwoWay}" />
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerAuthenticationMethodIndex, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Calendar Settings Card -->
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="16">
|
||||
<StackPanel Spacing="2">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="16">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.CalendarSectionTitleText, Mode=OneWay}" />
|
||||
Text="{x:Bind ViewModel.BasicSectionTitleText, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.BasicSectionDescriptionText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnSpacing="12" RowSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Header="{x:Bind ViewModel.DisplayNameHeaderText, Mode=OneWay}"
|
||||
PlaceholderText="{x:Bind ViewModel.DisplayNamePlaceholderText, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.DisplayName, Mode=TwoWay}" />
|
||||
<TextBox
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Header="{x:Bind ViewModel.EmailAddressHeaderText, Mode=OneWay}"
|
||||
PlaceholderText="{x:Bind ViewModel.EmailAddressPlaceholderText, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.EmailAddress, Mode=TwoWay}" />
|
||||
<PasswordBox
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Header="{x:Bind ViewModel.PasswordHeaderText, Mode=OneWay}"
|
||||
Password="{x:Bind ViewModel.Password, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
|
||||
<CheckBox
|
||||
Content="{x:Bind ViewModel.EnableCalendarSupportText, Mode=OneWay}"
|
||||
IsChecked="{x:Bind ViewModel.IsCalendarSupportEnabled, Mode=TwoWay}" />
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.AdvancedSectionDescriptionText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
<Button
|
||||
HorizontalAlignment="Left"
|
||||
Command="{x:Bind ViewModel.AutoDiscoverSettingsCommand}"
|
||||
Content="{x:Bind ViewModel.AutoDiscoverButtonText, Mode=OneWay}"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="16">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.AdvancedSectionTitleText, Mode=OneWay}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.AdvancedSectionDescriptionText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnSpacing="16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
CornerRadius="6">
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.IncomingSectionTitleText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<TextBox Header="{x:Bind ViewModel.IncomingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServer, Mode=TwoWay}" />
|
||||
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerPort, Mode=TwoWay}" />
|
||||
<TextBox Header="{x:Bind ViewModel.IncomingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.IncomingServerUsername, Mode=TwoWay}" />
|
||||
<PasswordBox Header="{x:Bind ViewModel.IncomingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.IncomingServerPassword, Mode=TwoWay}" />
|
||||
<ComboBox
|
||||
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerConnectionSecurityIndex, Mode=TwoWay}" />
|
||||
<ComboBox
|
||||
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedIncomingServerAuthenticationMethodIndex, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Padding="16"
|
||||
Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
|
||||
CornerRadius="6">
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="14" Glyph="" />
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind ViewModel.OutgoingSectionTitleText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<TextBox Header="{x:Bind ViewModel.OutgoingServerHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServer, Mode=TwoWay}" />
|
||||
<TextBox Header="{x:Bind ViewModel.PortHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerPort, Mode=TwoWay}" />
|
||||
<TextBox Header="{x:Bind ViewModel.OutgoingUsernameHeaderText, Mode=OneWay}" Text="{x:Bind ViewModel.OutgoingServerUsername, Mode=TwoWay}" />
|
||||
<PasswordBox Header="{x:Bind ViewModel.OutgoingPasswordHeaderText, Mode=OneWay}" Password="{x:Bind ViewModel.OutgoingServerPassword, Mode=TwoWay}" />
|
||||
<ComboBox
|
||||
Header="{x:Bind ViewModel.ConnectionSecurityHeaderText, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableConnectionSecurityDisplayNames}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerConnectionSecurityIndex, Mode=TwoWay}" />
|
||||
<ComboBox
|
||||
Header="{x:Bind ViewModel.AuthenticationMethodHeaderText, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableAuthenticationMethodDisplayNames}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedOutgoingServerAuthenticationMethodIndex, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Bind ViewModel.TestImapConnectionCommand}"
|
||||
Content="{x:Bind ViewModel.TestImapButtonText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="16">
|
||||
<StackPanel Spacing="2">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind ViewModel.CalendarSectionTitleText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.CalendarSectionDescriptionText, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
|
||||
<ComboBox
|
||||
Header="{x:Bind ViewModel.CalendarModeHeaderText, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableCalendarSupportModeTitles}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedCalendarSupportModeIndex, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.CalendarSectionDescriptionText, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.SelectedCalendarSupportDescription, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
|
||||
<HyperlinkButton
|
||||
Command="{x:Bind ViewModel.ShowLocalCalendarExplanationCommand}"
|
||||
Content="{x:Bind ViewModel.LocalCalendarLearnMoreText, Mode=OneWay}"
|
||||
HorizontalAlignment="Left"
|
||||
IsEnabled="{x:Bind ViewModel.IsLocalCalendarModeSelected, Mode=OneWay}" />
|
||||
|
||||
<Grid ColumnSpacing="12" Visibility="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Header="{x:Bind ViewModel.CalDavServiceUrlHeaderText, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.CalDavServiceUrl, Mode=TwoWay}" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Header="{x:Bind ViewModel.CalDavUsernameHeaderText, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.CalDavUsername, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
|
||||
<PasswordBox
|
||||
Header="{x:Bind ViewModel.CalDavPasswordHeaderText, Mode=OneWay}"
|
||||
Password="{x:Bind ViewModel.CalDavPassword, Mode=TwoWay}"
|
||||
Visibility="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}" />
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Right"
|
||||
Command="{x:Bind ViewModel.TestCalDavConnectionCommand}"
|
||||
Content="{x:Bind ViewModel.TestCalDavButtonText, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<ComboBox
|
||||
HorizontalAlignment="Stretch"
|
||||
Header="{x:Bind ViewModel.CalendarModeHeaderText, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.IsCalendarModeSelectionVisible, Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.AvailableCalendarSupportModeTitles}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedCalendarSupportModeIndex, Mode=TwoWay}" />
|
||||
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind ViewModel.SelectedCalendarSupportDescription, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
|
||||
<HyperlinkButton
|
||||
Command="{x:Bind ViewModel.ShowLocalCalendarExplanationCommand}"
|
||||
Content="{x:Bind ViewModel.LocalCalendarLearnMoreText, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.IsLocalCalendarModeSelected, Mode=OneWay}" />
|
||||
|
||||
<Grid ColumnSpacing="12" Visibility="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="2*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Header="{x:Bind ViewModel.CalDavServiceUrlHeaderText, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.CalDavServiceUrl, Mode=TwoWay}" />
|
||||
<TextBox
|
||||
Grid.Column="1"
|
||||
Header="{x:Bind ViewModel.CalDavUsernameHeaderText, Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.CalDavUsername, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
|
||||
<PasswordBox
|
||||
Header="{x:Bind ViewModel.CalDavPasswordHeaderText, Mode=OneWay}"
|
||||
Password="{x:Bind ViewModel.CalDavPassword, Mode=TwoWay}"
|
||||
Visibility="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Action Bar -->
|
||||
<Grid Margin="0,4,0,0" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Grid.Column="0"
|
||||
Command="{x:Bind ViewModel.TestImapConnectionCommand}"
|
||||
Content="{x:Bind ViewModel.TestImapButtonText, Mode=OneWay}" />
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Command="{x:Bind ViewModel.TestCalDavConnectionCommand}"
|
||||
Content="{x:Bind ViewModel.TestCalDavButtonText, Mode=OneWay}"
|
||||
IsEnabled="{x:Bind ViewModel.IsCalDavSettingsVisible, Mode=OneWay}" />
|
||||
<Button
|
||||
Grid.Column="3"
|
||||
Command="{x:Bind ViewModel.CancelCommand}"
|
||||
Content="{x:Bind ViewModel.CancelButtonText, Mode=OneWay}" />
|
||||
<Button
|
||||
Grid.Column="4"
|
||||
Command="{x:Bind ViewModel.SaveCommand}"
|
||||
Content="{x:Bind ViewModel.SaveButtonText, Mode=OneWay}"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
<Grid Margin="0,4,0,0" ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
Command="{x:Bind ViewModel.CancelCommand}"
|
||||
Content="{x:Bind ViewModel.CancelButtonText, Mode=OneWay}" />
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
Command="{x:Bind ViewModel.SaveCommand}"
|
||||
Content="{x:Bind ViewModel.SaveButtonText, Mode=OneWay}"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</abstract:ImapCalDavSettingsPageAbstract>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Wino.Views.Abstract;
|
||||
|
||||
namespace Wino.Views;
|
||||
@@ -10,22 +8,4 @@ public sealed partial class ImapCalDavSettingsPage : ImapCalDavSettingsPageAbstr
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnSetupModeSelectionChanged(SelectorBar sender, SelectorBarSelectionChangedEventArgs e)
|
||||
{
|
||||
ViewModel.SelectedSetupTabIndex = SetupModeSelector.SelectedItem == null ? 0 : SetupModeSelector.Items.IndexOf(SetupModeSelector.SelectedItem);
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
base.OnNavigatedTo(e);
|
||||
|
||||
var tabIndex = ViewModel.SelectedSetupTabIndex;
|
||||
if (tabIndex < 0 || tabIndex >= SetupModeSelector.Items.Count)
|
||||
{
|
||||
tabIndex = 0;
|
||||
}
|
||||
|
||||
SetupModeSelector.SelectedItem = SetupModeSelector.Items[tabIndex];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,12 +210,12 @@
|
||||
Background="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
|
||||
<!-- Test Notification -->
|
||||
<!--<Button Command="{x:Bind ViewModel.CreateTestNotificationCommand}" Style="{StaticResource TransparentActionButtonStyle}">
|
||||
<Button Command="{x:Bind ViewModel.CreateTestNotificationCommand}" Style="{StaticResource TransparentActionButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<coreControls:WinoFontIcon FontSize="16" Icon="Reminder" />
|
||||
<TextBlock VerticalAlignment="Center" Text="Test notification" />
|
||||
</StackPanel>
|
||||
</Button>-->
|
||||
</Button>
|
||||
|
||||
<!-- Edit Series -->
|
||||
<Border
|
||||
|
||||
@@ -10,15 +10,12 @@
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
xmlns:interfaces="using:Wino.Core.Domain.Interfaces"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<ScrollViewer
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ScrollViewer HorizontalAlignment="Center" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel
|
||||
MaxWidth="480"
|
||||
Margin="0,24,0,24"
|
||||
Margin="0,12"
|
||||
HorizontalAlignment="Stretch"
|
||||
Spacing="20">
|
||||
|
||||
@@ -76,6 +73,50 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
MaxWidth="600"
|
||||
Padding="12"
|
||||
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8">
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Title}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Description}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
|
||||
<ListView
|
||||
HorizontalAlignment="Center"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ItemsSource="{x:Bind ViewModel.InitialSynchronizationRanges, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedInitialSynchronizationRange, Mode=TwoWay}">
|
||||
<ListView.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<ItemsStackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListView.ItemsPanel>
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="accounts:InitialSynchronizationRangeOption">
|
||||
<TextBlock Text="{x:Bind DisplayText}" />
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
|
||||
<muxc:InfoBar
|
||||
Title="{x:Bind domain:Translator.GeneralTitle_Warning}"
|
||||
Margin="0,2,0,0"
|
||||
IsOpen="True"
|
||||
Message="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_EverythingWarning}"
|
||||
Severity="Warning"
|
||||
Visibility="{x:Bind ViewModel.IsInitialSynchronizationWarningVisible, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Provider List -->
|
||||
<ItemsView
|
||||
HorizontalContentAlignment="Stretch"
|
||||
|
||||
@@ -49,6 +49,68 @@
|
||||
CompactTemplate="{StaticResource CompactDisplayModePreviewTemplate}"
|
||||
MediumTemplate="{StaticResource MediumDisplayModePreviewTemplate}"
|
||||
SpaciousTemplate="{StaticResource SpaciousDisplayModePreviewTemplate}" />
|
||||
|
||||
<DataTemplate x:Key="PersonalizationCustomAppThemeTemplate" x:DataType="personalization:CustomAppTheme">
|
||||
<Grid
|
||||
Width="175"
|
||||
CornerRadius="6"
|
||||
RowSpacing="0">
|
||||
<Grid.ContextFlyout>
|
||||
<MenuFlyout Placement="BottomEdgeAlignedRight">
|
||||
<MenuFlyoutItem
|
||||
Command="{Binding ElementName=root, Path=ViewModel.DeleteCustomThemeCommand}"
|
||||
CommandParameter="{x:Bind}"
|
||||
Text="{x:Bind domain:Translator.Buttons_Delete}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<SymbolIcon Symbol="Delete" />
|
||||
</MenuFlyoutItem.Icon>
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
</Grid.ContextFlyout>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="125" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid CornerRadius="6,6,0,0">
|
||||
<Grid.Background>
|
||||
<ImageBrush ImageSource="{x:Bind BackgroundPreviewImage}" />
|
||||
</Grid.Background>
|
||||
<Grid x:Name="AccentAssignedGrid" x:Load="{x:Bind IsAccentColorAssigned}">
|
||||
<Grid
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="0,12,12,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(AccentColor)}" />
|
||||
</Grid>
|
||||
|
||||
<Grid x:Name="AccentNotAssignedGrid" x:Load="{x:Bind IsAccentColorAssigned, Converter={StaticResource ReverseBooleanConverter}}">
|
||||
<Grid
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="0,12,12,0"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Background="{ThemeResource SystemAccentColor}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
<TextBlock
|
||||
Padding="0,8"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{x:Bind ThemeName}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<coreSelectors:AppThemePreviewTemplateSelector
|
||||
x:Key="PersonalizationAppThemePreviewTemplateSelector"
|
||||
CustomAppTemplate="{StaticResource PersonalizationCustomAppThemeTemplate}"
|
||||
PreDefinedThemeTemplate="{StaticResource PreDefinedAppThemeTemplate}"
|
||||
SystemThemeTemplate="{StaticResource SystemAppThemeTemplate}" />
|
||||
</Page.Resources>
|
||||
|
||||
<ScrollViewer>
|
||||
@@ -147,7 +209,7 @@
|
||||
<!-- TODO: Group by custom/wino -->
|
||||
<GridView
|
||||
toolkitExt:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
|
||||
ItemTemplateSelector="{StaticResource AppThemePreviewTemplateSelector}"
|
||||
ItemTemplateSelector="{StaticResource PersonalizationAppThemePreviewTemplateSelector}"
|
||||
ItemsSource="{x:Bind ViewModel.AppThemes, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedAppTheme, Mode=TwoWay}" />
|
||||
</controls:SettingsCard>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -11,6 +11,9 @@
|
||||
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<EnableMsixTooling>true</EnableMsixTooling>
|
||||
<!-- Bundle the Windows App SDK and .NET runtime into per-architecture MSIX outputs. -->
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<SelfContained Condition="'$(RuntimeIdentifier)' != ''">true</SelfContained>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<!-- Single instancing -->
|
||||
@@ -182,6 +185,7 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.TokenizingTextBox" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Lottie" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Notifications" />
|
||||
|
||||
<PackageReference Include="Microsoft.Graphics.Win2D" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
|
||||
@@ -475,7 +475,7 @@ public class AccountService : BaseDatabaseService, IAccountService
|
||||
|
||||
public async Task UpdateAccountCustomServerInformationAsync(CustomServerInformation customServerInformation)
|
||||
{
|
||||
await Connection.UpdateAsync(customServerInformation, typeof(CustomServerInformation)).ConfigureAwait(false);
|
||||
await Connection.InsertOrReplaceAsync(customServerInformation, typeof(CustomServerInformation)).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task UpdateAccountAliasesAsync(Guid accountId, List<MailAccountAlias> aliases)
|
||||
@@ -590,6 +590,11 @@ public class AccountService : BaseDatabaseService, IAccountService
|
||||
{
|
||||
Guard.IsNotNull(account);
|
||||
|
||||
if (!account.CreatedAt.HasValue)
|
||||
{
|
||||
account.CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
var accountCount = await Connection.Table<MailAccount>().CountAsync();
|
||||
|
||||
// If there are no accounts before this one, set it as startup account.
|
||||
|
||||
@@ -79,6 +79,22 @@ public class DatabaseService : IDatabaseService
|
||||
{
|
||||
await EnsureKeyboardShortcutSchemaAsync().ConfigureAwait(false);
|
||||
|
||||
var accountColumns = await Connection.GetTableInfoAsync(nameof(MailAccount)).ConfigureAwait(false);
|
||||
|
||||
if (!accountColumns.Any(c => c.Name == nameof(MailAccount.CreatedAt)))
|
||||
{
|
||||
await Connection
|
||||
.ExecuteAsync($"ALTER TABLE {nameof(MailAccount)} ADD COLUMN {nameof(MailAccount.CreatedAt)} TEXT NULL")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!accountColumns.Any(c => c.Name == nameof(MailAccount.InitialSynchronizationRange)))
|
||||
{
|
||||
await Connection
|
||||
.ExecuteAsync($"ALTER TABLE {nameof(MailAccount)} ADD COLUMN {nameof(MailAccount.InitialSynchronizationRange)} INTEGER NOT NULL DEFAULT {(int)InitialSynchronizationRange.SixMonths}")
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false);
|
||||
|
||||
if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid)))
|
||||
|
||||
@@ -13,6 +13,7 @@ public static class ServicesContainerSetup
|
||||
services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>();
|
||||
services.AddSingleton<IWinoLogger, WinoLogger>();
|
||||
services.AddSingleton<ILaunchProtocolService, LaunchProtocolService>();
|
||||
services.AddSingleton<IShareActivationService, ShareActivationService>();
|
||||
services.AddSingleton<IMimeFileService, MimeFileService>();
|
||||
services.AddSingleton<ICalendarIcsFileService, CalendarIcsFileService>();
|
||||
services.AddTransient<IMimeStorageService, MimeStorageService>();
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Launch;
|
||||
|
||||
namespace Wino.Services;
|
||||
|
||||
public class ShareActivationService : IShareActivationService
|
||||
{
|
||||
private readonly object _syncRoot = new();
|
||||
private MailShareRequest? _pendingShareRequest;
|
||||
private PendingComposeMailShareRequest? _pendingComposeShareRequest;
|
||||
|
||||
public MailShareRequest? PendingShareRequest
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _pendingShareRequest;
|
||||
}
|
||||
}
|
||||
set
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_pendingShareRequest = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MailShareRequest? ConsumePendingShareRequest()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
var pendingRequest = _pendingShareRequest;
|
||||
_pendingShareRequest = null;
|
||||
return pendingRequest;
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearPendingShareRequest()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_pendingShareRequest = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void StagePendingComposeShareRequest(Guid draftUniqueId, MailShareRequest shareRequest)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_pendingComposeShareRequest = new PendingComposeMailShareRequest(draftUniqueId, shareRequest);
|
||||
}
|
||||
}
|
||||
|
||||
public MailShareRequest? ConsumePendingComposeShareRequest(Guid draftUniqueId)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_pendingComposeShareRequest?.DraftUniqueId != draftUniqueId)
|
||||
return null;
|
||||
|
||||
var pendingRequest = _pendingComposeShareRequest.ShareRequest;
|
||||
_pendingComposeShareRequest = null;
|
||||
return pendingRequest;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,6 +211,8 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
||||
SpecialImapProvider = (SpecialImapProvider)mailbox.SpecialImapProvider,
|
||||
AccountColorHex = mailbox.AccountColorHex?.Trim(),
|
||||
Base64ProfilePictureData = string.Empty,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
InitialSynchronizationRange = InitialSynchronizationRange.SixMonths,
|
||||
IsCalendarAccessGranted = mailbox.IsCalendarAccessGranted,
|
||||
SynchronizationDeltaIdentifier = string.Empty,
|
||||
CalendarSynchronizationDeltaIdentifier = string.Empty,
|
||||
|
||||
Reference in New Issue
Block a user