From 41e81af04104f297ddf9263449c71bbaa77c2e33 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Thu, 18 Dec 2025 12:34:13 +0100 Subject: [PATCH 1/7] feat: add url and port in terminal --- TelegramDownloader/Program.cs | 100 ++++++++++++++++++- TelegramDownloader/TelegramDownloader.csproj | 2 +- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/TelegramDownloader/Program.cs b/TelegramDownloader/Program.cs index b0808b7..84670aa 100644 --- a/TelegramDownloader/Program.cs +++ b/TelegramDownloader/Program.cs @@ -10,8 +10,10 @@ using Serilog.Debugging; using Serilog.Events; using Syncfusion.Blazor; +using System.Diagnostics; using System.Net; using System.Net.Http; +using System.Runtime.InteropServices; using TelegramDownloader.Data; using TelegramDownloader.Data.db; using TelegramDownloader.Helpers; @@ -206,8 +208,70 @@ try { - Log.Information("TelegramFileManager application started"); - app.Run(); + // Check if running in Docker + var isDocker = Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true" + || File.Exists("/.dockerenv"); + + // Start the application + await app.StartAsync(); + + // Get the URLs the application is listening on + var urls = app.Urls; + var serverAddresses = app.Services.GetRequiredService() + .Features.Get()?.Addresses; + + var listeningUrls = serverAddresses?.ToList() ?? urls.ToList(); + + // Display startup banner with URLs + Console.WriteLine(); + Console.WriteLine("╔══════════════════════════════════════════════════════════════╗"); + Console.WriteLine("║ TelegramFileManager - Application Started ║"); + Console.WriteLine("╠══════════════════════════════════════════════════════════════╣"); + foreach (var url in listeningUrls) + { + var paddedUrl = url.PadRight(46); + Console.WriteLine($"║ 🌐 Listening on: {paddedUrl}║"); + } + Console.WriteLine("╠══════════════════════════════════════════════════════════════╣"); + if (isDocker) + { + Console.WriteLine("║ 🐳 Running in Docker container ║"); + } + else + { + Console.WriteLine("║ 💻 Running on local machine ║"); + } + Console.WriteLine("╚══════════════════════════════════════════════════════════════╝"); + Console.WriteLine(); + + Log.Information("TelegramFileManager application started. Listening on: {Urls}", string.Join(", ", listeningUrls)); + + // Open browser automatically if not in Docker + if (!isDocker && listeningUrls.Any()) + { + var urlToOpen = listeningUrls.FirstOrDefault(u => u.StartsWith("http://")) ?? listeningUrls.First(); + + // Replace 0.0.0.0 or * with localhost for browser + urlToOpen = urlToOpen.Replace("://0.0.0.0", "://localhost") + .Replace("://[::]", "://localhost") + .Replace("://*", "://localhost"); + + Log.Information("Opening browser at: {Url}", urlToOpen); + Console.WriteLine($"🚀 Opening browser at: {urlToOpen}"); + + try + { + OpenBrowser(urlToOpen); + } + catch (Exception ex) + { + Log.Warning(ex, "Could not open browser automatically. Please navigate to {Url} manually.", urlToOpen); + Console.WriteLine($"⚠️ Could not open browser automatically. Please navigate to {urlToOpen} manually."); + } + } + + // Wait for the application to stop + await app.WaitForShutdownAsync(); } catch (Exception ex) { @@ -218,3 +282,35 @@ Log.Information("TelegramFileManager application shutting down"); Log.CloseAndFlush(); } + +// Helper method to open browser cross-platform +static void OpenBrowser(string url) +{ + try + { + Process.Start(url); + } + catch + { + // Windows + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + url = url.Replace("&", "^&"); + Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); + } + // Linux + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + Process.Start("xdg-open", url); + } + // macOS + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("open", url); + } + else + { + throw; + } + } +} diff --git a/TelegramDownloader/TelegramDownloader.csproj b/TelegramDownloader/TelegramDownloader.csproj index 6153719..7f0efe7 100644 --- a/TelegramDownloader/TelegramDownloader.csproj +++ b/TelegramDownloader/TelegramDownloader.csproj @@ -1,7 +1,7 @@  - 3.0.1.0 + 3.1.0.0 Mateo TelegramFileManager net10.0 From c6b965ff313db45e0c327a14e6841cbb26ab35e4 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Thu, 18 Dec 2025 13:15:22 +0100 Subject: [PATCH 2/7] feat: add pdf viewer --- .../Controllers/FileController.cs | 45 +- .../Pages/Modals/PdfViewerModal.razor | 467 ++++++++++++++++++ .../Pages/Partials/impl/FileManagerImpl.razor | 39 ++ .../Partials/impl/LocalFileManagerImpl.razor | 23 + 4 files changed, 573 insertions(+), 1 deletion(-) create mode 100644 TelegramDownloader/Pages/Modals/PdfViewerModal.razor diff --git a/TelegramDownloader/Controllers/FileController.cs b/TelegramDownloader/Controllers/FileController.cs index 2ad79e3..dfe6bdc 100644 --- a/TelegramDownloader/Controllers/FileController.cs +++ b/TelegramDownloader/Controllers/FileController.cs @@ -270,7 +270,50 @@ public async Task GetFile(string id, string? idChannel, string? i { FileDownloadName = fileName, EnableRangeProcessing = true - + + }; + } + + /// + /// View file inline (for PDF viewer, etc.) - doesn't trigger download + /// + [Route("ViewFile/{id}")] + public async Task ViewFile(string id, string? idChannel, string? idFile) + { + var fileName = id; + var mimeType = FileService.getMimeType(id.Split(".").Last()); + var file = _fs.ExistFileIntempFolder($"{idChannel}-{idFile}-{id}"); + if (file == null) + { + TL.Message idM = await _ts.getMessageFile(idChannel, Convert.ToInt32(idFile)); + ChatMessages cm = new ChatMessages(); + cm.message = idM; + string filePath = System.IO.Path.Combine(FileService.TEMPDIR, "_temp", $"{idChannel}-{idFile}-{id}"); + file = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite); + DownloadModel dm = new DownloadModel(); + dm.tis = _tis; + dm.startDate = DateTime.Now; + dm.path = filePath; + if (cm.message is Message msgBase) + { + if (msgBase.media is MessageMediaDocument mediaDoc && + mediaDoc.document is TL.Document doc) + { + dm._size = doc.size; + dm.name = doc.Filename; + } + } + dm.channelName = _ts.getChatName(Convert.ToInt64(idChannel)); + _tis.addToDownloadList(dm); + await _ts.DownloadFileAndReturn(cm, file, model: dm); + file.Position = 0; + } + + // Return file for inline viewing (no download header) + Response.Headers["Content-Disposition"] = $"inline; filename=\"{HttpUtility.UrlEncode(fileName)}\""; + return new FileStreamResult(file, mimeType) + { + EnableRangeProcessing = true }; } diff --git a/TelegramDownloader/Pages/Modals/PdfViewerModal.razor b/TelegramDownloader/Pages/Modals/PdfViewerModal.razor new file mode 100644 index 0000000..9ade3f6 --- /dev/null +++ b/TelegramDownloader/Pages/Modals/PdfViewerModal.razor @@ -0,0 +1,467 @@ +@inject IJSRuntime JSRuntime + + + +
+
+ + +
+
+ +
+
+
@GetDisplayFileName()
+ PDF Viewer +
+
+ + +
+
+ +
+ @if (isLoading) + { +
+
+ +
+ Loading PDF... +
+ } + + @if (!string.IsNullOrEmpty(fileUrl)) + { + + } + else if (!isLoading) + { +
+
+ +
+ Unable to load PDF +
+ } +
+
+
+ +@code { + private bool isVisible = false; + private bool isLoading = false; + private string fileUrl = ""; + private string fileName = ""; + + public async Task ShowModal(string url, string name = "") + { + fileUrl = url; + fileName = !string.IsNullOrEmpty(name) ? name : ExtractFileName(url); + isLoading = true; + isVisible = true; + StateHasChanged(); + } + + public async Task OnHideModalClick() + { + isVisible = false; + StateHasChanged(); + + // Small delay before clearing data to allow animation + await Task.Delay(300); + fileUrl = ""; + fileName = ""; + isLoading = false; + } + + private async Task OnOverlayClick() + { + await OnHideModalClick(); + } + + private void OnIframeLoaded() + { + isLoading = false; + StateHasChanged(); + } + + private async Task OpenInNewTab() + { + if (!string.IsNullOrEmpty(fileUrl)) + { + await JSRuntime.InvokeVoidAsync("open", fileUrl, "_blank"); + } + } + + private async Task DownloadPdf() + { + if (!string.IsNullOrEmpty(fileUrl)) + { + // Create a temporary link to trigger download + await JSRuntime.InvokeVoidAsync("eval", $@" + (function() {{ + var a = document.createElement('a'); + a.href = '{fileUrl}'; + a.download = '{fileName}'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }})(); + "); + } + } + + private string ExtractFileName(string url) + { + if (string.IsNullOrEmpty(url)) return "Document.pdf"; + try + { + var uri = new Uri(url); + var path = uri.LocalPath; + return System.IO.Path.GetFileName(path); + } + catch + { + return "Document.pdf"; + } + } + + private string GetDisplayFileName() + { + if (string.IsNullOrEmpty(fileName)) return "PDF Viewer"; + return fileName.Length > 60 ? fileName.Substring(0, 57) + "..." : fileName; + } +} diff --git a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor index 0b56c0b..0f9a784 100644 --- a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor +++ b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor @@ -104,6 +104,7 @@ + @@ -122,6 +123,7 @@ public static bool hasVirtualScroll { get; set; } = false; LocalFileManager lfm { get; set; } private VideoPlayerModal videoPlayer { get; set; } = default!; + private PdfViewerModal pdfViewer { get; set; } = default!; private MediaUrlModal mediaUrlModal { get; set; } = default!; @@ -370,6 +372,13 @@ return; } + if (args != null && args.FileDetails != null && args.FileDetails.IsFile && isPdfFile(args.FileDetails.Type.ToLower())) + { + var pdfUrl = await getPdfViewUrl(args.FileDetails.Id, args.FileDetails.Name, args.FileDetails.Size); + await pdfViewer.ShowModal(pdfUrl, args.FileDetails.Name); + return; + } + string filePath = getTempFileMediaUrl(args.FileDetails.Id, args.FileDetails.Name); await JSRuntime.InvokeVoidAsync("open", filePath, "_blank"); @@ -756,6 +765,13 @@ return; } + if (isPdfFile(args.FileDetails.Type?.ToLower() ?? "")) + { + var pdfUrl = await getPdfViewUrl(args.FileDetails.Id, args.FileDetails.Name, args.FileDetails.Size); + await pdfViewer.ShowModal(pdfUrl, args.FileDetails.Name); + return; + } + string filePath = getTempFileMediaUrl(args.FileDetails.Id, args.FileDetails.Name); await JSRuntime.InvokeVoidAsync("open", filePath, "_blank"); } @@ -947,6 +963,11 @@ return new List { ".mp3", ".ogg", ".flac", ".aac", ".wav" }.Contains(fileExtension); } + private bool isPdfFile(string fileExtension) + { + return fileExtension == ".pdf"; + } + private async Task playAudio(FileManagerDirectoryContent fileDetails) { await JSRuntime.InvokeVoidAsync("openAudioPlayerModal", await getMediaUrl(fileDetails.Id, fileDetails.Name, fileDetails.Size), null, fileDetails.Name); @@ -989,6 +1010,24 @@ return await getMediaUrl(idFile, name, size); } + private async Task getPdfViewUrl(string idFile, String name, long size) + { + BsonFileManagerModel item = null; + if (isShared) + item = await fs.getSharedItemById(id: idFile, collection: bsi.CollectionId); + else + item = await fs.getItemById(id, idFile); + string localdir = MyNavigationManager.BaseUri; + + // Use ViewFile endpoint which serves with Content-Disposition: inline + if (item != null && item.MessageId != null) + { + return new System.Uri(Path.Combine(localdir, "api/file/ViewFile", name).Replace("\\", "/") + $"?idChannel={(isShared ? bsi.ChannelId : id)}&idFile={item.MessageId}").AbsoluteUri; + } + // Fallback to stream endpoint + return new System.Uri(Path.Combine(localdir, "api/file/GetFileStream", isShared ? bsi.ChannelId : id, idFile, name).Replace("\\", "/")).AbsoluteUri; + } + private string getTempFileMediaUrl(string idFile, String name) { string localdir = MyNavigationManager.BaseUri; diff --git a/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor b/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor index 1f73bf7..e37ffaf 100644 --- a/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor +++ b/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor @@ -74,6 +74,7 @@ + @code { @@ -91,6 +92,7 @@ private MediaUrlModal mediaUrlModal { get; set; } = default!; private VideoPlayerModal videoPlayer { get; set; } = default!; + private PdfViewerModal pdfViewer { get; set; } = default!; NotificationModel nm = new NotificationModel(); public string[] ContextItems = new string[] { "Open", "Delete", "Download", "Rename", "Details", "Copy Media Url", "Add to Playlist" }; @@ -421,6 +423,11 @@ string path = getLocalVideoUrl(args.FileDetails.FilterPath, args.FileDetails.Name); videoPlayer.ShowModal(path); } + else if (args != null && args.FileDetails != null && args.FileDetails.IsFile && isPdfFile(args.FileDetails.Type.ToLower())) + { + string path = getLocalFileUrl(args.FileDetails.FilterPath, args.FileDetails.Name); + await pdfViewer.ShowModal(path, args.FileDetails.Name); + } } private async Task playAudio(FileOpenEventArgs args) @@ -512,6 +519,17 @@ return new List { ".mkv", ".avi", ".wmv", ".ts", ".mts", ".m2ts", ".vob", ".divx", ".xvid", ".3gp" }.Contains(fileExtension); } + private bool isPdfFile(string fileExtension) + { + return fileExtension == ".pdf"; + } + + private string getLocalFileUrl(string filterPath, string name) + { + string localdir = MyNavigationManager.BaseUri + FileService.STATICRELATIVELOCALDIR.Replace("\\", "/"); + return new System.Uri((Path.Combine(localdir, filterPath.Substring(1).Replace("\\", "/"), name)).Replace("\\", "/")).AbsoluteUri; + } + private string getLocalVideoUrl(string filterPath, string name) { var extension = Path.GetExtension(name).ToLowerInvariant(); @@ -675,6 +693,11 @@ string path = getLocalVideoUrl(args.FileDetails.FilterPath, args.FileDetails.Name); videoPlayer.ShowModal(path); } + else if (isPdfFile(fileType)) + { + string path = getLocalFileUrl(args.FileDetails.FilterPath, args.FileDetails.Name); + await pdfViewer.ShowModal(path, args.FileDetails.Name); + } } private async Task OnMobileBeforeDownloadAsync(MfmDownloadEventArgs args) From cba494aa8473d4daf86acdf8452add68363e8a54 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Thu, 18 Dec 2025 13:57:32 +0100 Subject: [PATCH 3/7] feat: add image viewer --- .../Pages/Modals/ImageViewerModal.razor | 879 ++++++++++++++++++ .../Pages/Partials/impl/FileManagerImpl.razor | 143 +++ .../Partials/impl/LocalFileManagerImpl.razor | 117 +++ 3 files changed, 1139 insertions(+) create mode 100644 TelegramDownloader/Pages/Modals/ImageViewerModal.razor diff --git a/TelegramDownloader/Pages/Modals/ImageViewerModal.razor b/TelegramDownloader/Pages/Modals/ImageViewerModal.razor new file mode 100644 index 0000000..a165d09 --- /dev/null +++ b/TelegramDownloader/Pages/Modals/ImageViewerModal.razor @@ -0,0 +1,879 @@ +@inject IJSRuntime JSRuntime + + + +
+
+ +
+
+
+ +
+
+
@GetDisplayFileName()
+ Image Viewer +
+
+
+ + + +
+
+ + +
+ @if (isLoading) + { +
+
+ Loading image... +
+ } + + @if (!string.IsNullOrEmpty(currentImageUrl)) + { +
1 ? "can-drag" : "")" + style="transform: scale(@zoomLevel.ToString(System.Globalization.CultureInfo.InvariantCulture)) rotate(@(rotation)deg) translate(@(translateX.ToString(System.Globalization.CultureInfo.InvariantCulture))px, @(translateY.ToString(System.Globalization.CultureInfo.InvariantCulture))px);"> + @currentFileName +
+ } + + + @if (images.Count > 1) + { + + + } +
+ + +
+ +
+ + +
+ + + @if (images.Count > 1) + { +
+ @(currentIndex + 1) / @images.Count +
+ } + + +
+ + @(Math.Round(zoomLevel * 100))% + + +
+
+
+
+ +@code { + private bool isVisible = false; + private bool isLoading = false; + private bool imageLoaded = false; + private string currentImageUrl = ""; + private string currentFileName = ""; + private int currentIndex = 0; + private List images = new List(); + private ElementReference overlayElement; + + // Zoom state + private double zoomLevel = 1.0; + private const double MinZoom = 0.25; + private const double MaxZoom = 5.0; + private const double ZoomStep = 0.25; + + // Rotation state + private int rotation = 0; + + // Pan/drag state + private bool isDragging = false; + private double translateX = 0; + private double translateY = 0; + private double dragStartX = 0; + private double dragStartY = 0; + private double dragStartTranslateX = 0; + private double dragStartTranslateY = 0; + + private bool CanGoPrevious => currentIndex > 0; + private bool CanGoNext => currentIndex < images.Count - 1; + + public class ImageInfo + { + public string Url { get; set; } = ""; + public string FileName { get; set; } = ""; + } + + /// + /// Show modal with a single image + /// + public async Task ShowModal(string url, string fileName = "") + { + await ShowModal(new List { new ImageInfo { Url = url, FileName = fileName } }, 0); + } + + /// + /// Show modal with multiple images and starting index + /// + public async Task ShowModal(List imageList, int startIndex = 0) + { + images = imageList ?? new List(); + currentIndex = Math.Max(0, Math.Min(startIndex, images.Count - 1)); + + if (images.Count > 0) + { + LoadCurrentImage(); + } + + isVisible = true; + StateHasChanged(); + + // Focus for keyboard navigation + await Task.Delay(100); + try + { + await JSRuntime.InvokeVoidAsync("eval", "document.querySelector('.image-viewer-overlay')?.focus()"); + } + catch { } + } + + private void LoadCurrentImage() + { + if (currentIndex >= 0 && currentIndex < images.Count) + { + var image = images[currentIndex]; + currentImageUrl = image.Url; + currentFileName = !string.IsNullOrEmpty(image.FileName) ? image.FileName : ExtractFileName(image.Url); + isLoading = true; + imageLoaded = false; + + // Reset zoom and rotation for new image + ResetTransform(); + } + } + + private void ResetTransform() + { + zoomLevel = 1.0; + rotation = 0; + translateX = 0; + translateY = 0; + isDragging = false; + } + + public async Task OnHideModalClick() + { + isVisible = false; + StateHasChanged(); + + await Task.Delay(300); + currentImageUrl = ""; + currentFileName = ""; + images.Clear(); + currentIndex = 0; + isLoading = false; + imageLoaded = false; + ResetTransform(); + } + + private async Task OnOverlayClick() + { + await OnHideModalClick(); + } + + private void OnImageAreaClick() + { + // Click on image area (not buttons) - could toggle UI visibility + } + + private void OnImageLoaded() + { + isLoading = false; + imageLoaded = true; + StateHasChanged(); + } + + private void OnImageError() + { + isLoading = false; + imageLoaded = true; // Still show to display broken image + StateHasChanged(); + } + + #region Navigation + + private async Task PreviousImage() + { + if (CanGoPrevious) + { + currentIndex--; + LoadCurrentImage(); + StateHasChanged(); + } + } + + private async Task NextImage() + { + if (CanGoNext) + { + currentIndex++; + LoadCurrentImage(); + StateHasChanged(); + } + } + + #endregion + + #region Zoom + + private void ZoomIn() + { + if (zoomLevel < MaxZoom) + { + zoomLevel = Math.Min(MaxZoom, zoomLevel + ZoomStep); + StateHasChanged(); + } + } + + private void ZoomOut() + { + if (zoomLevel > MinZoom) + { + zoomLevel = Math.Max(MinZoom, zoomLevel - ZoomStep); + // Reset pan when zooming out to fit + if (zoomLevel <= 1) + { + translateX = 0; + translateY = 0; + } + StateHasChanged(); + } + } + + private void ResetZoom() + { + zoomLevel = 1.0; + translateX = 0; + translateY = 0; + rotation = 0; + StateHasChanged(); + } + + private void OnMouseWheel(WheelEventArgs e) + { + if (e.DeltaY < 0) + { + ZoomIn(); + } + else if (e.DeltaY > 0) + { + ZoomOut(); + } + } + + #endregion + + #region Rotation + + private void RotateLeft() + { + rotation = (rotation - 90) % 360; + if (rotation < 0) rotation += 360; + StateHasChanged(); + } + + private void RotateRight() + { + rotation = (rotation + 90) % 360; + StateHasChanged(); + } + + #endregion + + #region Pan/Drag + + private void OnMouseDown(MouseEventArgs e) + { + if (zoomLevel > 1) + { + isDragging = true; + dragStartX = e.ClientX; + dragStartY = e.ClientY; + dragStartTranslateX = translateX; + dragStartTranslateY = translateY; + } + } + + private void OnMouseMove(MouseEventArgs e) + { + if (isDragging && zoomLevel > 1) + { + // Adjust for zoom level to make pan feel natural + var deltaX = (e.ClientX - dragStartX) / zoomLevel; + var deltaY = (e.ClientY - dragStartY) / zoomLevel; + + translateX = dragStartTranslateX + deltaX; + translateY = dragStartTranslateY + deltaY; + StateHasChanged(); + } + } + + private void OnMouseUp(MouseEventArgs e) + { + isDragging = false; + } + + #endregion + + #region Keyboard + + private async Task HandleKeyDown(KeyboardEventArgs e) + { + switch (e.Key) + { + case "ArrowLeft": + await PreviousImage(); + break; + case "ArrowRight": + await NextImage(); + break; + case "Escape": + await OnHideModalClick(); + break; + case "Home": + if (images.Count > 0) + { + currentIndex = 0; + LoadCurrentImage(); + StateHasChanged(); + } + break; + case "End": + if (images.Count > 0) + { + currentIndex = images.Count - 1; + LoadCurrentImage(); + StateHasChanged(); + } + break; + case "+": + case "=": + ZoomIn(); + break; + case "-": + case "_": + ZoomOut(); + break; + case "0": + ResetZoom(); + break; + case "l": + case "L": + RotateLeft(); + break; + case "r": + case "R": + RotateRight(); + break; + } + } + + #endregion + + #region Actions + + private async Task OpenInNewTab() + { + if (!string.IsNullOrEmpty(currentImageUrl)) + { + await JSRuntime.InvokeVoidAsync("open", currentImageUrl, "_blank"); + } + } + + private async Task DownloadImage() + { + if (!string.IsNullOrEmpty(currentImageUrl)) + { + await JSRuntime.InvokeVoidAsync("eval", $@" + (function() {{ + var a = document.createElement('a'); + a.href = '{currentImageUrl}'; + a.download = '{currentFileName}'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }})(); + "); + } + } + + #endregion + + #region Helpers + + private string ExtractFileName(string url) + { + if (string.IsNullOrEmpty(url)) return "Image"; + try + { + var uri = new Uri(url); + var path = uri.LocalPath; + return System.IO.Path.GetFileName(path); + } + catch + { + return "Image"; + } + } + + private string GetDisplayFileName() + { + if (string.IsNullOrEmpty(currentFileName)) return "Image Viewer"; + return currentFileName.Length > 50 ? currentFileName.Substring(0, 47) + "..." : currentFileName; + } + + #endregion +} diff --git a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor index 0f9a784..c9a4c84 100644 --- a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor +++ b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor @@ -105,6 +105,7 @@ + @@ -124,6 +125,7 @@ LocalFileManager lfm { get; set; } private VideoPlayerModal videoPlayer { get; set; } = default!; private PdfViewerModal pdfViewer { get; set; } = default!; + private ImageViewerModal imageViewer { get; set; } = default!; private MediaUrlModal mediaUrlModal { get; set; } = default!; @@ -379,6 +381,12 @@ return; } + if (args != null && args.FileDetails != null && args.FileDetails.IsFile && isImageFile(args.FileDetails.Type.ToLower())) + { + await OpenImageViewer(args.FileDetails); + return; + } + string filePath = getTempFileMediaUrl(args.FileDetails.Id, args.FileDetails.Name); await JSRuntime.InvokeVoidAsync("open", filePath, "_blank"); @@ -772,6 +780,12 @@ return; } + if (isImageFile(args.FileDetails.Type?.ToLower() ?? "")) + { + await OpenImageViewerMobile(args.FileDetails); + return; + } + string filePath = getTempFileMediaUrl(args.FileDetails.Id, args.FileDetails.Name); await JSRuntime.InvokeVoidAsync("open", filePath, "_blank"); } @@ -968,11 +982,122 @@ return fileExtension == ".pdf"; } + private bool isImageFile(string fileExtension) + { + return new List { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ".ico", ".tiff", ".tif" }.Contains(fileExtension); + } + private async Task playAudio(FileManagerDirectoryContent fileDetails) { await JSRuntime.InvokeVoidAsync("openAudioPlayerModal", await getMediaUrl(fileDetails.Id, fileDetails.Name, fileDetails.Size), null, fileDetails.Name); } + private async Task OpenImageViewer(FileManagerDirectoryContent clickedImage) + { + try + { + // Get current path from desktop file manager + var currentPath = fm?.Path ?? "/"; + + // Get all files in current folder + var response = isShared + ? await fs.GetFilesPath(DbService.SHARED_DB_NAME, currentPath, null, bsi.CollectionId) + : await fs.GetFilesPath(id, currentPath, null); + + // Filter to only images and build ImageInfo list + var imageFiles = response?.Files? + .Where(f => f.IsFile && isImageFile(f.Type?.ToLower() ?? "")) + .OrderBy(f => f.Name) + .ToList() ?? new List(); + + if (imageFiles.Count == 0) + { + // Fallback: just show the clicked image + var url = await getImageViewUrl(clickedImage.Id, clickedImage.Name, clickedImage.Size); + await imageViewer.ShowModal(url, clickedImage.Name); + return; + } + + // Build list of ImageInfo + var imageInfoList = new List(); + int startIndex = 0; + + for (int i = 0; i < imageFiles.Count; i++) + { + var file = imageFiles[i]; + var url = await getImageViewUrl(file.Id, file.Name, file.Size); + imageInfoList.Add(new ImageViewerModal.ImageInfo { Url = url, FileName = file.Name }); + + if (file.Id == clickedImage.Id) + { + startIndex = i; + } + } + + await imageViewer.ShowModal(imageInfoList, startIndex); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error opening image viewer"); + // Fallback: just show the clicked image + var url = await getImageViewUrl(clickedImage.Id, clickedImage.Name, clickedImage.Size); + await imageViewer.ShowModal(url, clickedImage.Name); + } + } + + private async Task OpenImageViewerMobile(FileManagerDirectoryContent clickedImage) + { + try + { + // Get current path from mobile file manager + var currentPath = mobileFileManager?.Path ?? "/"; + + // Get all files in current folder + var response = isShared + ? await fs.GetFilesPath(DbService.SHARED_DB_NAME, currentPath, null, bsi.CollectionId) + : await fs.GetFilesPath(id, currentPath, null); + + // Filter to only images and build ImageInfo list + var imageFiles = response?.Files? + .Where(f => f.IsFile && isImageFile(f.Type?.ToLower() ?? "")) + .OrderBy(f => f.Name) + .ToList() ?? new List(); + + if (imageFiles.Count == 0) + { + // Fallback: just show the clicked image + var url = await getImageViewUrl(clickedImage.Id, clickedImage.Name, clickedImage.Size); + await imageViewer.ShowModal(url, clickedImage.Name); + return; + } + + // Build list of ImageInfo + var imageInfoList = new List(); + int startIndex = 0; + + for (int i = 0; i < imageFiles.Count; i++) + { + var file = imageFiles[i]; + var url = await getImageViewUrl(file.Id, file.Name, file.Size); + imageInfoList.Add(new ImageViewerModal.ImageInfo { Url = url, FileName = file.Name }); + + if (file.Id == clickedImage.Id) + { + startIndex = i; + } + } + + await imageViewer.ShowModal(imageInfoList, startIndex); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error opening image viewer (mobile)"); + // Fallback: just show the clicked image + var url = await getImageViewUrl(clickedImage.Id, clickedImage.Name, clickedImage.Size); + await imageViewer.ShowModal(url, clickedImage.Name); + } + } + private async Task ShowMediaURLModal(string idFile, String name, long size, bool forcePreDownload = false) { mediaUrlModal.url = await getMediaUrl(idFile, name, size, forcePreDownload); @@ -1028,6 +1153,24 @@ return new System.Uri(Path.Combine(localdir, "api/file/GetFileStream", isShared ? bsi.ChannelId : id, idFile, name).Replace("\\", "/")).AbsoluteUri; } + private async Task getImageViewUrl(string idFile, String name, long size) + { + BsonFileManagerModel item = null; + if (isShared) + item = await fs.getSharedItemById(id: idFile, collection: bsi.CollectionId); + else + item = await fs.getItemById(id, idFile); + string localdir = MyNavigationManager.BaseUri; + + // Use ViewFile endpoint which serves with Content-Disposition: inline + if (item != null && item.MessageId != null) + { + return new System.Uri(Path.Combine(localdir, "api/file/ViewFile", name).Replace("\\", "/") + $"?idChannel={(isShared ? bsi.ChannelId : id)}&idFile={item.MessageId}").AbsoluteUri; + } + // Fallback to stream endpoint + return new System.Uri(Path.Combine(localdir, "api/file/GetFileStream", isShared ? bsi.ChannelId : id, idFile, name).Replace("\\", "/")).AbsoluteUri; + } + private string getTempFileMediaUrl(string idFile, String name) { string localdir = MyNavigationManager.BaseUri; diff --git a/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor b/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor index e37ffaf..2b38433 100644 --- a/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor +++ b/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor @@ -75,6 +75,7 @@ + @code { @@ -93,6 +94,7 @@ private MediaUrlModal mediaUrlModal { get; set; } = default!; private VideoPlayerModal videoPlayer { get; set; } = default!; private PdfViewerModal pdfViewer { get; set; } = default!; + private ImageViewerModal imageViewer { get; set; } = default!; NotificationModel nm = new NotificationModel(); public string[] ContextItems = new string[] { "Open", "Delete", "Download", "Rename", "Details", "Copy Media Url", "Add to Playlist" }; @@ -428,6 +430,10 @@ string path = getLocalFileUrl(args.FileDetails.FilterPath, args.FileDetails.Name); await pdfViewer.ShowModal(path, args.FileDetails.Name); } + else if (args != null && args.FileDetails != null && args.FileDetails.IsFile && isImageFile(args.FileDetails.Type.ToLower())) + { + await OpenLocalImageViewer(args.FileDetails); + } } private async Task playAudio(FileOpenEventArgs args) @@ -524,6 +530,113 @@ return fileExtension == ".pdf"; } + private bool isImageFile(string fileExtension) + { + return new List { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".svg", ".ico", ".tiff", ".tif" }.Contains(fileExtension); + } + + private async Task OpenLocalImageViewer(FileManagerDirectoryContent clickedImage) + { + try + { + // Get current path from desktop file manager + var currentPath = FileManager?.Path ?? "/"; + + // Get all files in current folder using the file operations API + var response = await CallFileOperationsAsync("read", currentPath, null); + + // Filter to only images and build ImageInfo list + var imageFiles = response?.Files? + .Where(f => f.IsFile && isImageFile(f.Type?.ToLower() ?? "")) + .OrderBy(f => f.Name) + .ToList() ?? new List(); + + if (imageFiles.Count == 0) + { + // Fallback: just show the clicked image + string url = getLocalFileUrl(clickedImage.FilterPath, clickedImage.Name); + await imageViewer.ShowModal(url, clickedImage.Name); + return; + } + + // Build list of ImageInfo + var imageInfoList = new List(); + int startIndex = 0; + + for (int i = 0; i < imageFiles.Count; i++) + { + var file = imageFiles[i]; + string url = getLocalFileUrl(file.FilterPath, file.Name); + imageInfoList.Add(new ImageViewerModal.ImageInfo { Url = url, FileName = file.Name }); + + if (file.Name == clickedImage.Name) + { + startIndex = i; + } + } + + await imageViewer.ShowModal(imageInfoList, startIndex); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error opening local image viewer"); + // Fallback: just show the clicked image + string url = getLocalFileUrl(clickedImage.FilterPath, clickedImage.Name); + await imageViewer.ShowModal(url, clickedImage.Name); + } + } + + private async Task OpenLocalImageViewerMobile(FileManagerDirectoryContent clickedImage) + { + try + { + // Get current path from mobile file manager + var currentPath = mobileFileManager?.Path ?? "/"; + + // Get all files in current folder using the file operations API + var response = await CallFileOperationsAsync("read", currentPath, null); + + // Filter to only images and build ImageInfo list + var imageFiles = response?.Files? + .Where(f => f.IsFile && isImageFile(f.Type?.ToLower() ?? "")) + .OrderBy(f => f.Name) + .ToList() ?? new List(); + + if (imageFiles.Count == 0) + { + // Fallback: just show the clicked image + string url = getLocalFileUrl(clickedImage.FilterPath, clickedImage.Name); + await imageViewer.ShowModal(url, clickedImage.Name); + return; + } + + // Build list of ImageInfo + var imageInfoList = new List(); + int startIndex = 0; + + for (int i = 0; i < imageFiles.Count; i++) + { + var file = imageFiles[i]; + string url = getLocalFileUrl(file.FilterPath, file.Name); + imageInfoList.Add(new ImageViewerModal.ImageInfo { Url = url, FileName = file.Name }); + + if (file.Name == clickedImage.Name) + { + startIndex = i; + } + } + + await imageViewer.ShowModal(imageInfoList, startIndex); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error opening local image viewer (mobile)"); + // Fallback: just show the clicked image + string url = getLocalFileUrl(clickedImage.FilterPath, clickedImage.Name); + await imageViewer.ShowModal(url, clickedImage.Name); + } + } + private string getLocalFileUrl(string filterPath, string name) { string localdir = MyNavigationManager.BaseUri + FileService.STATICRELATIVELOCALDIR.Replace("\\", "/"); @@ -698,6 +811,10 @@ string path = getLocalFileUrl(args.FileDetails.FilterPath, args.FileDetails.Name); await pdfViewer.ShowModal(path, args.FileDetails.Name); } + else if (isImageFile(fileType)) + { + await OpenLocalImageViewerMobile(args.FileDetails); + } } private async Task OnMobileBeforeDownloadAsync(MfmDownloadEventArgs args) From 408799bffa788c07dbf0709b50d6a0dc1b870135 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Thu, 18 Dec 2025 14:05:37 +0100 Subject: [PATCH 4/7] feat: search input focus --- .../MobileFileManager/MobileFileManager.razor | 2 +- .../MobileFileManager/MobileFileManager.razor.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor index f6b3a4b..7088a30 100644 --- a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor +++ b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor @@ -89,7 +89,7 @@ - + @if (!string.IsNullOrEmpty(SearchText)) { + + + + +@code { + [Parameter] public string ChannelId { get; set; } = string.Empty; + [Parameter] public string ChannelName { get; set; } = string.Empty; + [Inject] protected ToastService ToastService { get; set; } = default!; + + private Modal modal = default!; + private string ConfirmationInput { get; set; } = string.Empty; + private bool isDeleting = false; + + private bool IsInputValid => !string.IsNullOrEmpty(ConfirmationInput) && + ConfirmationInput.Trim() == ChannelId; + + public async Task ShowAsync() + { + ConfirmationInput = string.Empty; + isDeleting = false; + await modal.ShowAsync(); + } + + private async Task OnHideModalClick() + { + await modal.HideAsync(); + } + + private async Task OnDeleteClick() + { + if (!IsInputValid || isDeleting) + return; + + isDeleting = true; + StateHasChanged(); + + try + { + await DbService.deleteDatabase(ChannelId); + await modal.HideAsync(); + + ToastService.Notify(new ToastMessage(ToastType.Success, $"Database for channel '{ChannelName}' has been deleted successfully.") + { + Title = "Database Deleted", + AutoHide = true + }); + + // Navigate to home page + NavigationManager.NavigateTo("/fetchdata", forceLoad: true); + } + catch (Exception ex) + { + ToastService.Notify(new ToastMessage(ToastType.Danger, $"Error deleting database: {ex.Message}") + { + Title = "Error", + AutoHide = true + }); + isDeleting = false; + StateHasChanged(); + } + } +} diff --git a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor index 7088a30..fee57d4 100644 --- a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor +++ b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor @@ -1,6 +1,7 @@ @using Syncfusion.Blazor.FileManager @using TelegramDownloader.Models @inject ILogger Logger +@inject IJSRuntime JSRuntime
@* Header con breadcrumb y acciones *@ @@ -497,7 +498,9 @@

New folder

- +
diff --git a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs index 89b3282..2b2b9a4 100644 --- a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs +++ b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; using Syncfusion.Blazor.FileManager; using System.Timers; @@ -637,8 +638,9 @@ private async Task PasteItems() { Id = CurrentFolder.Id, Name = CurrentFolder.Name, - FilterPath = NormalizePath(CurrentFolder.FilterPath ?? ""), - FilterId = CurrentFolder.FilterId, + // Preserve empty FilterPath for root folder - don't normalize it to "/" + FilterPath = string.IsNullOrEmpty(CurrentFolder.FilterPath) ? "" : NormalizePath(CurrentFolder.FilterPath), + FilterId = CurrentFolder.FilterId ?? "", IsFile = false }; } @@ -780,11 +782,33 @@ private void CloseRenameDialog() #region New Folder - private void CreateNewFolder() + private async Task CreateNewFolder() { ShowFabMenu = false; NewFolderName = "New Folder"; ShowNewFolderDialog = true; + StateHasChanged(); + + // Wait for the dialog to render, then focus and select all text + await Task.Delay(50); + try + { + await newFolderInput.FocusAsync(); + await JSRuntime.InvokeVoidAsync("eval", "document.activeElement.select()"); + } + catch { } + } + + private async Task OnNewFolderKeyDown(KeyboardEventArgs e) + { + if (e.Key == "Enter") + { + await ConfirmNewFolder(); + } + else if (e.Key == "Escape") + { + CloseNewFolderDialog(); + } } private async Task ConfirmNewFolder() From 05eca8f25aef656278dfa754a9c6bb874230aee1 Mon Sep 17 00:00:00 2001 From: mateofuentespombo Date: Thu, 18 Dec 2025 17:22:52 +0100 Subject: [PATCH 7/7] feat: add general local file manager --- TelegramDownloader/Pages/LocalFiles.razor | 17 +++++++++++++++++ TelegramDownloader/Shared/NavMenu.razor | 9 +++++++++ 2 files changed, 26 insertions(+) create mode 100644 TelegramDownloader/Pages/LocalFiles.razor diff --git a/TelegramDownloader/Pages/LocalFiles.razor b/TelegramDownloader/Pages/LocalFiles.razor new file mode 100644 index 0000000..557ad56 --- /dev/null +++ b/TelegramDownloader/Pages/LocalFiles.razor @@ -0,0 +1,17 @@ +@page "/local-files" + + + +

+ Local File Manager +

+ + diff --git a/TelegramDownloader/Shared/NavMenu.razor b/TelegramDownloader/Shared/NavMenu.razor index e0e774a..ce832f6 100644 --- a/TelegramDownloader/Shared/NavMenu.razor +++ b/TelegramDownloader/Shared/NavMenu.razor @@ -555,6 +555,9 @@ + @@ -875,6 +878,12 @@ uriHelper.NavigateTo(route, forceLoad: true, replace: false); } + private void OpenLocalFiles() + { + ToggleNavMenu(); + uriHelper.NavigateTo("/local-files"); + } + public async Task searchChat() { await OnInitializedAsync();