//----------------------------------------------------------------------- // // Copyright (c) aliasvault. All rights reserved. // Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. // //----------------------------------------------------------------------- namespace AliasVault.Client.Services; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AliasClientDb; using AliasVault.Client.Main.Models; using AliasVault.Client.Main.Utilities; using Microsoft.EntityFrameworkCore; /// /// Service class for Folder operations. /// All mutations use background sync to avoid blocking the UI. /// public sealed class FolderService(DbService dbService) { /// /// Get all folders with item counts. /// /// List of FolderWithCount objects. public async Task> GetAllWithCountsAsync() { var context = await dbService.GetDbContextAsync(); var folders = await context.Folders .Where(f => !f.IsDeleted) .Include(f => f.Items.Where(i => !i.IsDeleted && i.DeletedAt == null)) .ToListAsync(); // Build a map of direct item counts per folder var directCounts = new Dictionary(); foreach (var folder in folders) { directCounts[folder.Id] = folder.Items?.Count ?? 0; } // Calculate recursive counts (includes subfolders) var folderWithCounts = folders.Select(f => new FolderWithCount { Id = f.Id, Name = f.Name ?? string.Empty, ParentFolderId = f.ParentFolderId, Weight = f.Weight, ItemCount = GetRecursiveItemCount(f.Id, folders, directCounts), }).ToList(); // Sort by weight, then by name (case-insensitive) folderWithCounts = folderWithCounts .OrderBy(f => f.Weight) .ThenBy(f => f.Name, StringComparer.OrdinalIgnoreCase) .ToList(); return folderWithCounts; } /// /// Get folders filtered by parent folder ID with recursive item counts. /// /// Parent folder ID (null for root folders). /// List of FolderWithCount objects. public async Task> GetByParentWithCountsAsync(Guid? parentFolderId) { var allFolders = await GetAllWithCountsAsync(); return allFolders.Where(f => f.ParentFolderId == parentFolderId).ToList(); } /// /// Get a folder by ID. /// /// The folder ID. /// The Folder entity or null. public async Task GetByIdAsync(Guid folderId) { var context = await dbService.GetDbContextAsync(); return await context.Folders .Where(f => f.Id == folderId && !f.IsDeleted) .FirstOrDefaultAsync(); } /// /// Create a new folder. Syncs to server in background without blocking UI. /// /// The folder name. /// Optional parent folder ID. /// The created folder ID. public async Task CreateAsync(string name, Guid? parentFolderId = null) { var context = await dbService.GetDbContextAsync(); var currentDateTime = DateTime.UtcNow; var folder = new Folder { Id = Guid.NewGuid(), Name = name, ParentFolderId = parentFolderId, Weight = 0, CreatedAt = currentDateTime, UpdatedAt = currentDateTime, }; context.Folders.Add(folder); await context.SaveChangesAsync(); dbService.SaveDatabaseInBackground(); return folder.Id; } /// /// Update a folder's name. Syncs to server in background without blocking UI. /// /// The folder ID. /// The new folder name. /// True if folder was found and updated. public async Task UpdateAsync(Guid folderId, string name) { var context = await dbService.GetDbContextAsync(); var folder = await context.Folders .Where(f => f.Id == folderId && !f.IsDeleted) .FirstOrDefaultAsync(); if (folder == null) { return false; } folder.Name = name; folder.UpdatedAt = DateTime.UtcNow; await context.SaveChangesAsync(); dbService.SaveDatabaseInBackground(); return true; } /// /// Delete a folder, moving its items and subfolders to parent (or root if no parent). /// Syncs to server in background without blocking UI. /// /// The folder ID. /// True if folder was found and deleted. public async Task DeleteAsync(Guid folderId) { var context = await dbService.GetDbContextAsync(); var folder = await context.Folders .Where(f => f.Id == folderId && !f.IsDeleted) .FirstOrDefaultAsync(); if (folder == null) { return false; } var currentDateTime = DateTime.UtcNow; // Move all items in this folder to parent (or root if no parent) var itemsInFolder = await context.Items .Where(i => i.FolderId == folderId && !i.IsDeleted) .ToListAsync(); foreach (var item in itemsInFolder) { item.FolderId = folder.ParentFolderId; item.UpdatedAt = currentDateTime; } // Move all subfolders to parent (or root if no parent) var subfolders = await context.Folders .Where(f => f.ParentFolderId == folderId && !f.IsDeleted) .ToListAsync(); foreach (var subfolder in subfolders) { subfolder.ParentFolderId = folder.ParentFolderId; subfolder.UpdatedAt = currentDateTime; } // Soft delete the folder folder.IsDeleted = true; folder.UpdatedAt = currentDateTime; await context.SaveChangesAsync(); dbService.SaveDatabaseInBackground(); return true; } /// /// Delete a folder and all its contents recursively (move items to trash, delete subfolders). /// Syncs to server in background without blocking UI. /// /// The folder ID. /// True if folder was found and deleted. public async Task DeleteWithContentsAsync(Guid folderId) { var context = await dbService.GetDbContextAsync(); var folder = await context.Folders .Where(f => f.Id == folderId && !f.IsDeleted) .FirstOrDefaultAsync(); if (folder == null) { return false; } var currentDateTime = DateTime.UtcNow; // Get all descendant folders var allFolders = await context.Folders .Where(f => !f.IsDeleted) .ToListAsync(); var descendantIds = FolderTreeUtilities.GetDescendantFolderIds(folderId, allFolders); var allFolderIdsToDelete = new List { folderId }; allFolderIdsToDelete.AddRange(descendantIds); // Move all items in this folder and all subfolders to trash var itemsInFolders = await context.Items .Where(i => allFolderIdsToDelete.Contains(i.FolderId!.Value) && !i.IsDeleted && i.DeletedAt == null) .ToListAsync(); foreach (var item in itemsInFolders) { item.DeletedAt = currentDateTime; item.UpdatedAt = currentDateTime; } // Soft delete the folder and all subfolders var foldersToDelete = await context.Folders .Where(f => allFolderIdsToDelete.Contains(f.Id) && !f.IsDeleted) .ToListAsync(); foreach (var folderToDelete in foldersToDelete) { folderToDelete.IsDeleted = true; folderToDelete.UpdatedAt = currentDateTime; } await context.SaveChangesAsync(); dbService.SaveDatabaseInBackground(); return true; } /// /// Calculate recursive item count for a folder. /// private static int GetRecursiveItemCount(Guid folderId, List allFolders, Dictionary directCounts) { // Start with direct items in this folder int count = directCounts.TryGetValue(folderId, out var directCount) ? directCount : 0; // Add items from all descendant folders var descendantIds = FolderTreeUtilities.GetDescendantFolderIds(folderId, allFolders); foreach (var descendantId in descendantIds) { count += directCounts.TryGetValue(descendantId, out var descendantCount) ? descendantCount : 0; } return count; } }