diff --git a/.gitignore b/.gitignore index cdbd927..61b4475 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ TelegramDownloader/userData.json TelegramDownloader/.claude/ +.claude/ diff --git a/TelegramDownloader/Data/FileService.cs b/TelegramDownloader/Data/FileService.cs index c9a8ba1..f33befd 100644 --- a/TelegramDownloader/Data/FileService.cs +++ b/TelegramDownloader/Data/FileService.cs @@ -1408,7 +1408,8 @@ public async Task UploadFileFromServer(string dbName, string currentPath, List> getAllChats(); Task> getAllSavedChats(); Task getChatsWithFolders(); + Task PreloadChannelsAsync(); Task> getAllMessages(long id, Boolean onlyFiles = false); Task> getPaginatedMessages(long id, int page, int size, Boolean onlyFiles = false); Task> getAllMediaMessages(long id, Boolean onlyFiles = false); diff --git a/TelegramDownloader/Data/TelegramService.cs b/TelegramDownloader/Data/TelegramService.cs index 5a2dc68..186bc62 100644 --- a/TelegramDownloader/Data/TelegramService.cs +++ b/TelegramDownloader/Data/TelegramService.cs @@ -50,7 +50,24 @@ public TelegramService(TransactionInfoService tis, IDbService db, ILogger getChatsWithFolders() return result; } + /// + /// Preloads channels and configuration at startup if user is logged in. + /// This ensures channels are available for API calls without requiring UI access first. + /// + public async Task PreloadChannelsAsync() + { + if (!checkUserLogin()) + { + _logger.LogInformation("PreloadChannelsAsync: User not logged in, skipping preload"); + return; + } + + try + { + _logger.LogInformation("PreloadChannelsAsync: Starting channel preload..."); + + // Load all channels into cache + var channels = await getAllChats(); + _logger.LogInformation("PreloadChannelsAsync: Loaded {Count} channels", channels.Count); + + // Also preload folders + var folders = await getChatsWithFolders(); + _logger.LogInformation("PreloadChannelsAsync: Loaded {FolderCount} folders with {UngroupedCount} ungrouped chats", + folders.Folders.Count, folders.UngroupedChats.Count); + + // Preload favourites + var favourites = await GetFouriteChannels(true); + _logger.LogInformation("PreloadChannelsAsync: Loaded {Count} favourite channels", favourites.Count); + + _logger.LogInformation("PreloadChannelsAsync: Channel preload completed successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "PreloadChannelsAsync: Error preloading channels"); + } + } + public async Task uploadFile(string chatId, Stream file, string fileName, string mimeType = null, UploadModel um = null, string caption = null) { _logger.LogInformation("Starting file upload - FileName: {FileName}, Size: {SizeMB:F2}MB, ChatId: {ChatId}", diff --git a/TelegramDownloader/Models/GeneralConfig.cs b/TelegramDownloader/Models/GeneralConfig.cs index 49c7f10..4d1a715 100644 --- a/TelegramDownloader/Models/GeneralConfig.cs +++ b/TelegramDownloader/Models/GeneralConfig.cs @@ -25,9 +25,11 @@ public static async Task SaveChanges(IDbService db, GeneralConfig gc) gc.MemorySplitSizeGB = gc.SplitSize; } - // Ensure MemorySplitSizeGB is within valid range (1-4 GB) + // Ensure MemorySplitSizeGB is within valid range based on Telegram limits + // Premium: max 4, Non-premium: max 2 (same as Telegram file size limits) + int maxAllowedSize = TelegramService.isPremium ? 4 : 2; if (gc.MemorySplitSizeGB < 1) gc.MemorySplitSizeGB = 1; - if (gc.MemorySplitSizeGB > 4) gc.MemorySplitSizeGB = 4; + if (gc.MemorySplitSizeGB > maxAllowedSize) gc.MemorySplitSizeGB = maxAllowedSize; await db.SaveConfig(gc); config = gc; @@ -131,8 +133,10 @@ public class GeneralConfig public bool EnableMemorySplitUpload { get; set; } = false; /// - /// Size in GB for each memory chunk when uploading large files (1-4 GB). + /// Size in GB for each memory chunk when uploading large files. + /// Premium accounts: 1-4 GB, Non-premium: 1-2 GB. /// Only used when EnableMemorySplitUpload is true. + /// Note: Uses Telegram's actual size limits (1GB = 1024*1024*1000 bytes, not 1024^3). /// public int MemorySplitSizeGB { get; set; } = 2; diff --git a/TelegramDownloader/Pages/Config.razor b/TelegramDownloader/Pages/Config.razor index 0e05380..2d0fcad 100644 --- a/TelegramDownloader/Pages/Config.razor +++ b/TelegramDownloader/Pages/Config.razor @@ -345,7 +345,15 @@ Memory Chunk Size
- Size of each chunk when splitting in memory (1-4 GB). Must be ≤ Split Size. + Size of each chunk when splitting in memory. Must be ≤ Split Size. + @if (TelegramService.isPremium) + { + Premium: 1-4 GB + } + else + { + 1-2 GB + }
@@ -1156,8 +1164,10 @@ private int GetMaxMemorySplitSize() { - // MemorySplitSizeGB must be <= SplitSize and <= 4 - return Math.Min(Model?.SplitSize ?? 4, 4); + // MemorySplitSizeGB must be <= SplitSize and <= Telegram's max file size + // Premium: max 4, Non-premium: max 2 + int telegramMaxSize = TelegramService.isPremium ? 4 : 2; + return Math.Min(Model?.SplitSize ?? telegramMaxSize, telegramMaxSize); } public void Dispose() diff --git a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor index 036be6a..681fd55 100644 --- a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor +++ b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor @@ -102,7 +102,10 @@ OnPathChanged="OnMobilePathChanged" OnFilterChanged="OnMobileFilterChanged" InitialSearch="@_initialSearch" - InitialFilters="@_initialFilters" /> + InitialFilters="@_initialFilters" + InitialSortBy="@_initialSortBy" + InitialSortAscending="@_initialSortAscending" + InitialPage="@_initialPage" />
} @@ -172,11 +175,17 @@ private bool _firstRenderComplete = false; private bool _needsRefresh = false; - // URL state for filters and search + // URL state for filters, search, sorting, and pagination private string _initialSearch = string.Empty; private HashSet _initialFilters = new(); + private string _initialSortBy = "Name"; + private bool _initialSortAscending = true; + private int _initialPage = 1; private string _currentSearch = string.Empty; private HashSet _currentFilters = new(); + private string _currentSortBy = "Name"; + private bool _currentSortAscending = true; + private int _currentPage = 1; protected override async Task OnParametersSetAsync() { @@ -184,6 +193,12 @@ bool idChanged = _previousId != id; bool bsiChanged = isShared && _previousBsiId != currentBsiId && currentBsiId != null; + // Parse URL params early so they're available before first render + if (!_firstRenderComplete) + { + ParseUrlParameters(); + } + if (idChanged || bsiChanged) { _previousId = id; @@ -198,6 +213,59 @@ } } + private void ParseUrlParameters() + { + try + { + var uri = new Uri(MyNavigationManager.Uri); + var query = System.Web.HttpUtility.ParseQueryString(uri.Query); + var searchFromUrl = query["search"]; + var filtersFromUrl = query["filters"]; + var sortByFromUrl = query["sortBy"]; + var sortAscFromUrl = query["sortAsc"]; + + // Parse initial search + if (!string.IsNullOrEmpty(searchFromUrl)) + { + _initialSearch = Uri.UnescapeDataString(searchFromUrl); + _currentSearch = _initialSearch; + } + + // Parse initial filters (comma-separated) + if (!string.IsNullOrEmpty(filtersFromUrl)) + { + var filterList = Uri.UnescapeDataString(filtersFromUrl).Split(',', StringSplitOptions.RemoveEmptyEntries); + _initialFilters = new HashSet(filterList); + _currentFilters = new HashSet(filterList); + } + + // Parse initial sort + if (!string.IsNullOrEmpty(sortByFromUrl)) + { + _initialSortBy = Uri.UnescapeDataString(sortByFromUrl); + _currentSortBy = _initialSortBy; + } + + if (!string.IsNullOrEmpty(sortAscFromUrl)) + { + _initialSortAscending = sortAscFromUrl.ToLower() != "false"; + _currentSortAscending = _initialSortAscending; + } + + // Parse initial page + var pageFromUrl = query["page"]; + if (!string.IsNullOrEmpty(pageFromUrl) && int.TryParse(pageFromUrl, out int page) && page > 0) + { + _initialPage = page; + _currentPage = page; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error parsing URL parameters"); + } + } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -261,23 +329,6 @@ var uri = new Uri(MyNavigationManager.Uri); var query = System.Web.HttpUtility.ParseQueryString(uri.Query); var pathFromUrl = query["path"]; - var searchFromUrl = query["search"]; - var filtersFromUrl = query["filters"]; - - // Parse initial search - if (!string.IsNullOrEmpty(searchFromUrl)) - { - _initialSearch = Uri.UnescapeDataString(searchFromUrl); - _currentSearch = _initialSearch; - } - - // Parse initial filters (comma-separated) - if (!string.IsNullOrEmpty(filtersFromUrl)) - { - var filterList = Uri.UnescapeDataString(filtersFromUrl).Split(',', StringSplitOptions.RemoveEmptyEntries); - _initialFilters = new HashSet(filterList); - _currentFilters = new HashSet(filterList); - } // Decode the path and ensure it ends with / string decodedPath = "/"; @@ -310,7 +361,9 @@ { try { - UpdateUrlWithState(path, _currentSearch, _currentFilters); + // Reset page to 1 when navigating to a different folder + _currentPage = 1; + UpdateUrlWithState(path, _currentSearch, _currentFilters, _currentSortBy, _currentSortAscending, _currentPage); } catch (Exception ex) { @@ -324,10 +377,13 @@ { _currentSearch = args.SearchText; _currentFilters = args.TypeFilters; + _currentSortBy = args.SortBy; + _currentSortAscending = args.SortAscending; + _currentPage = args.CurrentPage; // Get current path from mobile file manager var currentPath = mobileFileManager?.Path ?? "/"; - UpdateUrlWithState(currentPath, _currentSearch, _currentFilters); + UpdateUrlWithState(currentPath, _currentSearch, _currentFilters, _currentSortBy, _currentSortAscending, _currentPage); } catch (Exception ex) { @@ -335,7 +391,7 @@ } } - private void UpdateUrlWithState(string path, string search, HashSet filters) + private void UpdateUrlWithState(string path, string search, HashSet filters, string sortBy = "Name", bool sortAscending = true, int page = 1) { var baseUri = MyNavigationManager.Uri.Split('?')[0]; var queryParams = new List(); @@ -358,6 +414,24 @@ queryParams.Add($"filters={Uri.EscapeDataString(string.Join(",", filters))}"); } + // Add sort if not default + if (sortBy != "Name") + { + queryParams.Add($"sortBy={Uri.EscapeDataString(sortBy)}"); + } + + // Add sort direction if descending + if (!sortAscending) + { + queryParams.Add("sortAsc=false"); + } + + // Add page if not first page + if (page > 1) + { + queryParams.Add($"page={page}"); + } + var newUrl = queryParams.Count > 0 ? $"{baseUri}?{string.Join("&", queryParams)}" : baseUri; MyNavigationManager.NavigateTo(newUrl, forceLoad: false, replace: true); } diff --git a/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor b/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor index 4a9e963..749ea0f 100644 --- a/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor +++ b/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor @@ -54,6 +54,11 @@ CanUpload="true" CanUploadToTelegram="@isMyChannel" CanShowUrlMedia="true" + InitialSearch="@_initialSearch" + InitialFilters="@_initialFilters" + InitialSortBy="@_initialSortBy" + InitialSortAscending="@_initialSortAscending" + InitialPage="@_initialPage" OnRead="OnMobileReadAsync" OnItemsDeleting="OnMobileItemsDeletingAsync" OnItemsMoving="OnMobileItemsMovingAsync" @@ -67,7 +72,8 @@ OnUrlMedia="OnMobileUrlMediaAsync" CanAddToPlaylist="true" OnAddToPlaylist="OnMobileAddToPlaylistAsync" - OnPathChanged="OnMobilePathChanged" /> + OnPathChanged="OnMobilePathChanged" + OnFilterChanged="OnMobileFilterChanged" /> } @@ -138,8 +144,26 @@ private bool _firstRenderComplete = false; private bool _needsRefresh = false; + // URL state for filters, search, sorting, and pagination + private string _initialSearch = string.Empty; + private HashSet _initialFilters = new(); + private string _initialSortBy = "Name"; + private bool _initialSortAscending = true; + private int _initialPage = 1; + private string _currentSearch = string.Empty; + private HashSet _currentFilters = new(); + private string _currentSortBy = "Name"; + private bool _currentSortAscending = true; + private int _currentPage = 1; + protected override async Task OnParametersSetAsync() { + // Parse URL params early so they're available before first render + if (!_firstRenderComplete) + { + ParseUrlParameters(); + } + if (_previousId != id) { _previousId = id; @@ -153,6 +177,59 @@ } } + private void ParseUrlParameters() + { + try + { + var uri = new Uri(MyNavigationManager.Uri); + var query = HttpUtility.ParseQueryString(uri.Query); + var searchFromUrl = query["search"]; + var filtersFromUrl = query["filters"]; + var sortByFromUrl = query["sortBy"]; + var sortAscFromUrl = query["sortAsc"]; + var pageFromUrl = query["page"]; + + // Parse initial search + if (!string.IsNullOrEmpty(searchFromUrl)) + { + _initialSearch = Uri.UnescapeDataString(searchFromUrl); + _currentSearch = _initialSearch; + } + + // Parse initial filters (comma-separated) + if (!string.IsNullOrEmpty(filtersFromUrl)) + { + var filterList = Uri.UnescapeDataString(filtersFromUrl).Split(',', StringSplitOptions.RemoveEmptyEntries); + _initialFilters = new HashSet(filterList); + _currentFilters = new HashSet(filterList); + } + + // Parse initial sort + if (!string.IsNullOrEmpty(sortByFromUrl)) + { + _initialSortBy = Uri.UnescapeDataString(sortByFromUrl); + _currentSortBy = _initialSortBy; + } + + if (!string.IsNullOrEmpty(sortAscFromUrl)) + { + _initialSortAscending = sortAscFromUrl.ToLower() != "false"; + _currentSortAscending = _initialSortAscending; + } + + // Parse initial page + if (!string.IsNullOrEmpty(pageFromUrl) && int.TryParse(pageFromUrl, out int page) && page > 0) + { + _initialPage = page; + _currentPage = page; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error parsing URL parameters"); + } + } + protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) @@ -261,17 +338,76 @@ } private void OnMobilePathChanged(string path) + { + // Reset page when navigating to a different folder + _currentPage = 1; + UpdateUrlWithState(path, _currentSearch, _currentFilters, _currentSortBy, _currentSortAscending, _currentPage); + } + + private void OnMobileFilterChanged(MfmFilterChangedEventArgs args) + { + _currentSearch = args.SearchText; + _currentFilters = args.TypeFilters; + _currentSortBy = args.SortBy; + _currentSortAscending = args.SortAscending; + _currentPage = args.CurrentPage; + + var path = mobileFileManager?.Path ?? "/"; + UpdateUrlWithState(path, _currentSearch, _currentFilters, _currentSortBy, _currentSortAscending, _currentPage); + } + + private void UpdateUrlWithState(string path, string search, HashSet filters, string sortBy = "Name", bool sortAscending = true, int page = 1) { try { - // Update URL without triggering navigation var baseUri = MyNavigationManager.Uri.Split('?')[0]; - var newUrl = path == "/" ? baseUri : $"{baseUri}?path={Uri.EscapeDataString(path)}"; + var queryParams = new List(); + + // Add path if not root + if (path != "/") + { + queryParams.Add($"path={Uri.EscapeDataString(path)}"); + } + + // Add search if not empty + if (!string.IsNullOrEmpty(search)) + { + queryParams.Add($"search={Uri.EscapeDataString(search)}"); + } + + // Add filters if any selected + if (filters.Count > 0) + { + queryParams.Add($"filters={Uri.EscapeDataString(string.Join(",", filters))}"); + } + + // Add sortBy if not default + if (sortBy != "Name") + { + queryParams.Add($"sortBy={Uri.EscapeDataString(sortBy)}"); + } + + // Add sortAsc if not default (ascending) + if (!sortAscending) + { + queryParams.Add("sortAsc=false"); + } + + // Add page if not first page + if (page > 1) + { + queryParams.Add($"page={page}"); + } + + var newUrl = queryParams.Count > 0 + ? $"{baseUri}?{string.Join("&", queryParams)}" + : baseUri; + MyNavigationManager.NavigateTo(newUrl, forceLoad: false, replace: true); } catch (Exception ex) { - Logger.LogError(ex, "Error updating URL with path"); + Logger.LogError(ex, "Error updating URL with state"); } } diff --git a/TelegramDownloader/Pages/Setup.razor b/TelegramDownloader/Pages/Setup.razor index 23ec3a9..126a031 100644 --- a/TelegramDownloader/Pages/Setup.razor +++ b/TelegramDownloader/Pages/Setup.razor @@ -475,6 +475,7 @@ @code { private int currentStep = 1; private bool isLoading = false; + private bool _shouldRedirectHome = false; private string mongoConnectionString = ""; private string mongoError = ""; @@ -483,6 +484,14 @@ private string apiHash = ""; private string telegramError = ""; + protected override void OnAfterRender(bool firstRender) + { + if (firstRender && _shouldRedirectHome) + { + NavigationManager.NavigateTo("/", forceLoad: true); + } + } + protected override void OnInitialized() { // Check current setup status using cached/sync method @@ -515,8 +524,8 @@ currentStep = 2; break; case SetupStep.Complete: - // Redirect to home if setup is already complete - NavigationManager.NavigateTo("/", forceLoad: true); + // Redirect to home if setup is already complete (done in OnAfterRender) + _shouldRedirectHome = true; break; } } diff --git a/TelegramDownloader/Services/TaskResumeService.cs b/TelegramDownloader/Services/TaskResumeService.cs index d91aa63..206e479 100644 --- a/TelegramDownloader/Services/TaskResumeService.cs +++ b/TelegramDownloader/Services/TaskResumeService.cs @@ -110,9 +110,21 @@ private async Task TryResumeTasksAsync() return; } - _logger.LogInformation("Telegram session is ready - proceeding to resume tasks"); + _logger.LogInformation("Telegram session is ready - proceeding with startup tasks"); _hasResumedTasks = true; + // Preload channels first - this ensures they're available for API calls + _logger.LogInformation("========== Preloading channels =========="); + try + { + await telegramService.PreloadChannelsAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error preloading channels: {Message}", ex.Message); + } + + // Then resume pending tasks await ResumeTasksAsync(_cancellationToken); } diff --git a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs index fa663a1..cf0347a 100644 --- a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs +++ b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs @@ -138,6 +138,15 @@ public partial class MobileFileManager : ComponentBase [Parameter] public HashSet InitialFilters { get; set; } = new(); + [Parameter] + public string InitialSortBy { get; set; } = "Name"; + + [Parameter] + public bool InitialSortAscending { get; set; } = true; + + [Parameter] + public int InitialPage { get; set; } = 1; + #endregion #region State @@ -235,6 +244,19 @@ protected override async Task OnInitializedAsync() SelectedTypeFilters = new HashSet(InitialFilters); } + // Initialize sort from URL parameters + if (!string.IsNullOrEmpty(InitialSortBy)) + { + SortBy = InitialSortBy; + } + SortAscending = InitialSortAscending; + + // Initialize page from URL parameters + if (InitialPage > 0) + { + CurrentPage = InitialPage; + } + await LoadFiles(); } @@ -1276,7 +1298,7 @@ private async Task RefreshFiles() await LoadFiles(); } - private void SortFiles() + private async Task SortFiles() { SortBy = SortBy switch { @@ -1288,14 +1310,16 @@ private void SortFiles() }; InvalidateDisplayFilesCache(); ResetPagination(); + await NotifyFilterChanged(); StateHasChanged(); } - private void ToggleSortDirection() + private async Task ToggleSortDirection() { SortAscending = !SortAscending; InvalidateDisplayFilesCache(); ResetPagination(); + await NotifyFilterChanged(); StateHasChanged(); } @@ -1377,7 +1401,10 @@ private async Task NotifyFilterChanged() var args = new MfmFilterChangedEventArgs { SearchText = SearchText, - TypeFilters = new HashSet(SelectedTypeFilters) + TypeFilters = new HashSet(SelectedTypeFilters), + SortBy = SortBy, + SortAscending = SortAscending, + CurrentPage = CurrentPage }; await OnFilterChanged.InvokeAsync(args); } @@ -1405,41 +1432,46 @@ private string FormatSize(long bytes) #region Pagination - private void GoToFirstPage() + private async Task GoToFirstPage() { CurrentPage = 1; + await NotifyFilterChanged(); StateHasChanged(); } - private void GoToPreviousPage() + private async Task GoToPreviousPage() { if (CurrentPage > 1) { CurrentPage--; + await NotifyFilterChanged(); StateHasChanged(); } } - private void GoToNextPage() + private async Task GoToNextPage() { if (CurrentPage < TotalPages) { CurrentPage++; + await NotifyFilterChanged(); StateHasChanged(); } } - private void GoToLastPage() + private async Task GoToLastPage() { CurrentPage = TotalPages; + await NotifyFilterChanged(); StateHasChanged(); } - private void GoToPage(int page) + private async Task GoToPage(int page) { if (page >= 1 && page <= TotalPages) { CurrentPage = page; + await NotifyFilterChanged(); StateHasChanged(); } } diff --git a/TelegramDownloader/Shared/MobileFileManager/MobileFileManagerEventArgs.cs b/TelegramDownloader/Shared/MobileFileManager/MobileFileManagerEventArgs.cs index d408a41..92dec24 100644 --- a/TelegramDownloader/Shared/MobileFileManager/MobileFileManagerEventArgs.cs +++ b/TelegramDownloader/Shared/MobileFileManager/MobileFileManagerEventArgs.cs @@ -129,5 +129,8 @@ public class MfmFilterChangedEventArgs { public string SearchText { get; set; } = string.Empty; public HashSet TypeFilters { get; set; } = new(); + public string SortBy { get; set; } = "Name"; + public bool SortAscending { get; set; } = true; + public int CurrentPage { get; set; } = 1; } } diff --git a/TelegramDownloader/TelegramDownloader.csproj b/TelegramDownloader/TelegramDownloader.csproj index 59e0d40..e97ed74 100644 --- a/TelegramDownloader/TelegramDownloader.csproj +++ b/TelegramDownloader/TelegramDownloader.csproj @@ -24,17 +24,17 @@ - - - - + + + + - + - +