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/Data/FileService.cs b/TelegramDownloader/Data/FileService.cs index f1babbb..5d6098c 100644 --- a/TelegramDownloader/Data/FileService.cs +++ b/TelegramDownloader/Data/FileService.cs @@ -527,9 +527,10 @@ public async Task oneItemDeleteAsync(string dbName, Syncfusion.Blazor.FileManage { if (!item.IsFile) { - var result = await copyAllDirectoryFiles(dbName, item.Id, args.TargetData, args.TargetPath + item.Name + "/"); + // Pass args.IsCopy to handle internal files correctly (delete originals when moving) + var result = await copyAllDirectoryFiles(dbName, item.Id, args.TargetData, args.TargetPath + item.Name + "/", args.IsCopy); lista.Add(result.toFileManagerContentInCopy()); - + // Note: copyAllDirectoryFiles now handles deletion of the folder when IsCopy=false } else { @@ -539,7 +540,6 @@ public async Task oneItemDeleteAsync(string dbName, Syncfusion.Blazor.FileManage { await _db.deleteEntry(dbName, item.Id); } - } await _db.addBytesToFolder(dbName, item.ParentId, item.Size); } @@ -573,12 +573,10 @@ public async Task oneItemDeleteAsync(string dbName, Syncfusion.Blazor.FileManage { if (!item.IsFile) { - var result = await copyAllDirectoryFiles(dbName, item.Id, targetData, targetPath + item.Name + "/"); + // Pass isCopy to handle internal files correctly (delete originals when moving) + var result = await copyAllDirectoryFiles(dbName, item.Id, targetData, targetPath + item.Name + "/", isCopy); lista.Add(result.toFileManagerContentInCopy()); - if (!isCopy) - { - await _db.deleteEntry(dbName, item.Id); - } + // Note: copyAllDirectoryFiles now handles deletion of the folder when isCopy=false } else { @@ -626,11 +624,9 @@ private async Task copyAllDirectoryFiles(string dbName, st } else { - await copyAllDirectoryFiles(dbName, file.Id, result.toFileManagerContent(), targetPath + file.Name + "/"); - if (!isCopy) - { - await _db.deleteEntry(dbName, file.Id); - } + // Pass isCopy to recursive call to handle nested folders correctly + await copyAllDirectoryFiles(dbName, file.Id, result.toFileManagerContent(), targetPath + file.Name + "/", isCopy); + // Note: recursive call handles deletion when isCopy=false, no need to delete here } } if (!isCopy) diff --git a/TelegramDownloader/Data/db/DbService.cs b/TelegramDownloader/Data/db/DbService.cs index 37877b4..9920566 100644 --- a/TelegramDownloader/Data/db/DbService.cs +++ b/TelegramDownloader/Data/db/DbService.cs @@ -489,10 +489,12 @@ public async Task copyItem(string dbName, string sourceId, result.Id = null; result.ParentId = target.Id; result.DateModified = DateTime.Now; - result.FilterId = target.FilterId + target.Id + "/"; + result.FilterId = (target.FilterId ?? "") + target.Id + "/"; result.ParentId = target.Id; - result.FilterPath = target.FilterPath == "" ? "/" : target.FilterPath + target.Name + "/"; - result.FilePath = isFile ? targetPath + result.Name : targetPath; + // Both empty string and "/" indicate root folder + var isRootTarget = string.IsNullOrEmpty(target.FilterPath) || target.FilterPath == "/"; + result.FilterPath = isRootTarget ? "/" : target.FilterPath + target.Name + "/"; + result.FilePath = isFile ? targetPath + result.Name : targetPath.TrimEnd('/'); await getDatabase(dbName).GetCollection(collectionName).InsertOneAsync(result); return result; diff --git a/TelegramDownloader/Pages/FileManager.razor b/TelegramDownloader/Pages/FileManager.razor index ebd94ff..3fb2307 100644 --- a/TelegramDownloader/Pages/FileManager.razor +++ b/TelegramDownloader/Pages/FileManager.razor @@ -50,6 +50,10 @@ } } + + + Delete Database + @if (webDavService.IsRunning) @@ -71,6 +75,7 @@ + @code { private Modals.ImportDataModal ImportModal { get; set; } @@ -78,6 +83,7 @@ [Parameter] public string id { get; set; } private MediaUrlModal mediaUrlModal { get; set; } = default!; + private DeleteChannelModal deleteChannelModal { get; set; } = default!; private WebbDavService webDavService = GeneralConfigStatic.config.webDav.webDavService ?? new WebbDavService(); private String webDavUrl = ""; @@ -156,4 +162,9 @@ await InvokeAsync(StateHasChanged); } } + + public async Task openDeleteChannelModal() + { + await deleteChannelModal.ShowAsync(); + } } 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/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/Modals/InfoModals/DeleteChannelModal.razor b/TelegramDownloader/Pages/Modals/InfoModals/DeleteChannelModal.razor new file mode 100644 index 0000000..f1b8cd2 --- /dev/null +++ b/TelegramDownloader/Pages/Modals/InfoModals/DeleteChannelModal.razor @@ -0,0 +1,114 @@ +@using TelegramDownloader.Data.db +@inject IDbService DbService +@inject NavigationManager NavigationManager + + + + + +
+ +
@ChannelId
+
+ +
+ +
@ChannelName
+
+ +
+ +
+ + + @if (!string.IsNullOrEmpty(ConfirmationInput) && !IsInputValid) + { +
+ The channel ID does not match. +
+ } +
+
+ + + + +
+ +@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/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..c9a4c84 100644 --- a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor +++ b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor @@ -104,6 +104,8 @@ + + @@ -122,6 +124,8 @@ public static bool hasVirtualScroll { get; set; } = false; 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!; @@ -370,6 +374,19 @@ 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; + } + + 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"); @@ -756,6 +773,19 @@ 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; + } + + 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"); } @@ -947,11 +977,127 @@ return new List { ".mp3", ".ogg", ".flac", ".aac", ".wav" }.Contains(fileExtension); } + private bool isPdfFile(string fileExtension) + { + 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); @@ -989,6 +1135,42 @@ 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 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 1f73bf7..4363958 100644 --- a/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor +++ b/TelegramDownloader/Pages/Partials/impl/LocalFileManagerImpl.razor @@ -74,6 +74,8 @@ + + @code { @@ -91,6 +93,8 @@ 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" }; @@ -421,6 +425,15 @@ 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); + } + 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) @@ -512,6 +525,124 @@ return new List { ".mkv", ".avi", ".wmv", ".ts", ".mts", ".m2ts", ".vob", ".divx", ".xvid", ".3gp" }.Contains(fileExtension); } + private bool isPdfFile(string fileExtension) + { + 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("\\", "/"); + 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(); @@ -649,7 +780,10 @@ { try { - var response = await CallFileOperationsAsync("search", args.Path, null, searchString: args.SearchText); + // Wrap search text with wildcards for "contains" search behavior + // The FileManagerService uses wildcard pattern matching where * matches any sequence + var searchPattern = $"*{args.SearchText}*"; + var response = await CallFileOperationsAsync("search", args.Path, null, searchString: searchPattern); args.Response = response; } catch (Exception ex) @@ -675,6 +809,15 @@ 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); + } + else if (isImageFile(fileType)) + { + await OpenLocalImageViewerMobile(args.FileDetails); + } } private async Task OnMobileBeforeDownloadAsync(MfmDownloadEventArgs args) 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/Shared/MobileFileManager/MobileFileManager.razor b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor index f6b3a4b..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 *@ @@ -89,7 +90,7 @@ - + @if (!string.IsNullOrEmpty(SearchText)) { diff --git a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs index f6f2ecb..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; @@ -176,6 +177,8 @@ private List DisplayFiles private string NewFolderName { get; set; } = string.Empty; private ElementReference newFolderInput; + private ElementReference searchInput; + private bool ShowDetailsPanel { get; set; } = false; private FileManagerDirectoryContent? DetailsItem { get; set; } @@ -242,6 +245,8 @@ private async Task LoadFiles() await OnRead.InvokeAsync(args); + // Always update Files list and invalidate cache, even if response is null/empty + // This prevents showing stale data from previous folder if (args.Response?.Files != null) { Files = args.Response.Files.Select(f => @@ -249,8 +254,13 @@ private async Task LoadFiles() f.FilterPath = NormalizePath(f.FilterPath); return f; }).ToList(); - InvalidateDisplayFilesCache(); } + else + { + // Clear files if response is null to avoid showing stale data + Files = new List(); + } + InvalidateDisplayFilesCache(); if (args.Response?.CWD != null) { @@ -260,6 +270,11 @@ private async Task LoadFiles() CurrentFolder.FilterPath = NormalizePath(CurrentFolder.FilterPath); } } + else + { + // Clear CurrentFolder if not in response + CurrentFolder = null; + } } finally { @@ -454,13 +469,30 @@ private async Task OnFileClick(FileManagerDirectoryContent file) private async Task NavigateToFolder(FileManagerDirectoryContent folder) { - var basePath = CurrentPath; - if (!basePath.EndsWith("/")) + string newPath; + + // If folder has FilterPath (e.g., from search results), use it to build correct path + // FilterPath contains the parent path where the folder is located + if (!string.IsNullOrEmpty(folder.FilterPath) && folder.FilterPath != "/") { - basePath += "/"; + // FilterPath is the parent path, so we append the folder name + var parentPath = NormalizePath(folder.FilterPath); + if (!parentPath.EndsWith("/")) + { + parentPath += "/"; + } + newPath = parentPath + folder.Name + "/"; + } + else + { + // Normal navigation from current folder + var basePath = CurrentPath; + if (!basePath.EndsWith("/")) + { + basePath += "/"; + } + newPath = basePath + folder.Name + "/"; } - - var newPath = basePath + folder.Name + "/"; if (newPath == CurrentPath) { @@ -468,6 +500,13 @@ private async Task NavigateToFolder(FileManagerDirectoryContent folder) return; } + // Clear search when navigating to a folder + if (ShowSearch) + { + ShowSearch = false; + SearchText = string.Empty; + } + CurrentPath = newPath; ClearSelection(); ResetPagination(); @@ -599,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 }; } @@ -742,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() @@ -1018,7 +1080,7 @@ private bool IsAudioFile(FileManagerDirectoryContent file) #region Search - private void ToggleSearch() + private async Task ToggleSearch() { ShowSearch = !ShowSearch; ShowMoreMenu = false; @@ -1027,6 +1089,17 @@ private void ToggleSearch() SearchText = string.Empty; } StateHasChanged(); + + // Focus on search input after showing + if (ShowSearch) + { + await Task.Delay(50); // Small delay to ensure the input is rendered + try + { + await searchInput.FocusAsync(); + } + catch { /* Input may not be rendered yet */ } + } } private async Task OnSearchKeyUp(KeyboardEventArgs e) @@ -1050,6 +1123,7 @@ await InvokeAsync(async () => }; await OnSearching.InvokeAsync(searchArgs); + // Always update Files and invalidate cache to avoid showing stale data if (searchArgs.Response?.Files != null) { // Normalize FilterPath in search results @@ -1058,8 +1132,13 @@ await InvokeAsync(async () => f.FilterPath = NormalizePath(f.FilterPath); return f; }).ToList(); - InvalidateDisplayFilesCache(); } + else + { + // No results or error - show empty list + Files = new List(); + } + InvalidateDisplayFilesCache(); } else { 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(); 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