Add per-account folder customization page (#855)

Introduce a dedicated settings page that lets users reorder, hide,
and pin/unpin folders per account. Folders are organized into Pinned,
Categories (Gmail only), and More sections with drag-to-reorder via
ListView. New Order column on MailItemFolder persists the custom
layout; the default sort falls back to alphabetic when no custom
order is set. A reset action wipes all customization in a single
transaction and restores system-folder stickiness.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Burak Kaan Köse
2026-04-16 14:07:17 +02:00
committed by GitHub
parent 0b136b3d66
commit 98eed39fe6
17 changed files with 599 additions and 10 deletions
+7
View File
@@ -113,6 +113,13 @@ public class DatabaseService : IDatabaseService
.ConfigureAwait(false);
}
if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.Order)))
{
await Connection
.ExecuteAsync($"ALTER TABLE {nameof(MailItemFolder)} ADD COLUMN \"{nameof(MailItemFolder.Order)}\" INTEGER NOT NULL DEFAULT 0")
.ConfigureAwait(false);
}
var customServerColumns = await Connection.GetTableInfoAsync(nameof(CustomServerInformation)).ConfigureAwait(false);
if (!customServerColumns.Any(c => c.Name == nameof(CustomServerInformation.CalDavServiceUrl)))
+74 -10
View File
@@ -45,6 +45,60 @@ public class FolderService : BaseDatabaseService, IFolderService
public async Task ChangeStickyStatusAsync(Guid folderId, bool isSticky)
=> await Connection.ExecuteAsync("UPDATE MailItemFolder SET IsSticky = ? WHERE Id = ?", isSticky, folderId);
public async Task ChangeFolderHiddenStatusAsync(Guid folderId, bool isHidden)
{
await Connection.ExecuteAsync("UPDATE MailItemFolder SET IsHidden = ? WHERE Id = ?", isHidden, folderId);
var folder = await GetFolderAsync(folderId).ConfigureAwait(false);
if (folder != null)
{
Messenger.Send(new AccountFolderConfigurationUpdated(folder.MailAccountId));
}
}
public async Task UpdateFolderOrdersAsync(Guid accountId, IReadOnlyList<Guid> orderedFolderIds)
{
if (orderedFolderIds == null || orderedFolderIds.Count == 0) return;
await Connection.RunInTransactionAsync(conn =>
{
for (int i = 0; i < orderedFolderIds.Count; i++)
{
conn.Execute("UPDATE MailItemFolder SET \"Order\" = ? WHERE Id = ? AND MailAccountId = ?",
i + 1, orderedFolderIds[i], accountId);
}
}).ConfigureAwait(false);
Messenger.Send(new AccountFolderConfigurationUpdated(accountId));
}
public async Task ResetFolderCustomizationAsync(Guid accountId)
{
await Connection.RunInTransactionAsync(conn =>
{
conn.Execute("UPDATE MailItemFolder SET \"Order\" = 0, IsHidden = 0 WHERE MailAccountId = ?", accountId);
// Restore system folder stickiness. Category-type folders are virtual stickies too.
conn.Execute(
"UPDATE MailItemFolder SET IsSticky = 1 WHERE MailAccountId = ? AND (IsSystemFolder = 1 OR SpecialFolderType = ?)",
accountId, (int)SpecialFolderType.Category);
}).ConfigureAwait(false);
Messenger.Send(new AccountFolderConfigurationUpdated(accountId));
}
/// <summary>
/// Orders folders by user-set Order first (customized entries ahead of uncustomized ones),
/// then falls back to alphabetic folder name (culture-aware), then to SpecialFolderType
/// as a final canonical tiebreak.
/// </summary>
private static IOrderedEnumerable<MailItemFolder> ApplyFolderSort(IEnumerable<MailItemFolder> folders)
=> folders
.OrderBy(a => a.Order == 0 ? 1 : 0)
.ThenBy(a => a.Order)
.ThenBy(a => a.FolderName, StringComparer.CurrentCultureIgnoreCase)
.ThenBy(a => a.SpecialFolderType);
public async Task<int> GetFolderNotificationBadgeAsync(Guid folderId)
{
var folder = await GetFolderAsync(folderId);
@@ -104,8 +158,10 @@ public class FolderService : BaseDatabaseService, IFolderService
if (!includeHiddenFolders)
folderQuery = folderQuery.Where(a => !a.IsHidden);
// Load child folders for each folder.
var allFolders = await folderQuery.OrderBy(a => a.SpecialFolderType).ToListAsync();
// Load child folders for each folder, applying user-defined ordering with
// alphabetic fallback for folders the user hasn't explicitly re-ordered.
var rawFolders = await folderQuery.ToListAsync();
var allFolders = ApplyFolderSort(rawFolders).ToList();
if (allFolders.Any())
{
@@ -235,7 +291,7 @@ public class FolderService : BaseDatabaseService, IFolderService
var mailAccount = accountMenuItem.HoldingAccounts.First();
var listingFolders = folders.OrderBy(a => a.SpecialFolderType);
var listingFolders = ApplyFolderSort(folders);
var moreFolder = MailItemFolder.CreateMoreFolder();
var categoryFolder = MailItemFolder.CreateCategoriesFolder();
@@ -394,10 +450,12 @@ public class FolderService : BaseDatabaseService, IFolderService
if (folder == null)
return null;
var childFolders = await Connection.Table<MailItemFolder>()
var childFoldersRaw = await Connection.Table<MailItemFolder>()
.Where(a => a.ParentRemoteFolderId == folder.RemoteFolderId && a.MailAccountId == folder.MailAccountId)
.ToListAsync();
var childFolders = ApplyFolderSort(childFoldersRaw).ToList();
foreach (var childFolder in childFolders)
{
var subChild = await GetChildFolderItemsRecursiveAsync(childFolder.Id, accountId);
@@ -416,16 +474,20 @@ public class FolderService : BaseDatabaseService, IFolderService
public Task<int> GetCurrentItemCountForFolder(Guid folderId)
=> Connection.Table<MailCopy>().Where(a => a.FolderId == folderId).CountAsync();
public Task<List<MailItemFolder>> GetFoldersAsync(Guid accountId)
public async Task<List<MailItemFolder>> GetFoldersAsync(Guid accountId)
{
const string query = "SELECT * FROM MailItemFolder WHERE MailAccountId = ? ORDER BY SpecialFolderType";
return Connection.QueryAsync<MailItemFolder>(query, accountId);
// Ordering is applied in managed code so that StringComparer.CurrentCultureIgnoreCase
// is honored. SQLite's default ORDER BY is not culture-aware.
const string query = "SELECT * FROM MailItemFolder WHERE MailAccountId = ?";
var rows = await Connection.QueryAsync<MailItemFolder>(query, accountId).ConfigureAwait(false);
return ApplyFolderSort(rows).ToList();
}
public Task<List<MailItemFolder>> GetVisibleFoldersAsync(Guid accountId)
public async Task<List<MailItemFolder>> GetVisibleFoldersAsync(Guid accountId)
{
const string query = "SELECT * FROM MailItemFolder WHERE MailAccountId = ? AND IsHidden = ? ORDER BY SpecialFolderType";
return Connection.QueryAsync<MailItemFolder>(query, accountId, 0);
const string query = "SELECT * FROM MailItemFolder WHERE MailAccountId = ? AND IsHidden = ?";
var rows = await Connection.QueryAsync<MailItemFolder>(query, accountId, 0).ConfigureAwait(false);
return ApplyFolderSort(rows).ToList();
}
public async Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId)
@@ -528,6 +590,8 @@ public class FolderService : BaseDatabaseService, IFolderService
folder.ShowUnreadCount = existingFolder.ShowUnreadCount;
folder.TextColorHex = existingFolder.TextColorHex;
folder.BackgroundColorHex = existingFolder.BackgroundColorHex;
folder.Order = existingFolder.Order;
folder.IsHidden = existingFolder.IsHidden;
_logger.Debug("Folder {Id} - {FolderName} already exists. Updating.", folder.Id, folder.FolderName);