@@ -1116,6 +1154,12 @@
return $"{size:0.##} {sizes[order]}";
}
+ private int GetMaxMemorySplitSize()
+ {
+ // MemorySplitSizeGB must be <= SplitSize and <= 4
+ return Math.Min(Model?.SplitSize ?? 4, 4);
+ }
+
public void Dispose()
{
_disposed = true;
diff --git a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor
index e8794bd..036be6a 100644
--- a/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor
+++ b/TelegramDownloader/Pages/Partials/impl/FileManagerImpl.razor
@@ -99,7 +99,10 @@
OnAddToPlaylist="OnMobileAddToPlaylistAsync"
CanSaveToPlaylist="true"
OnSaveToPlaylist="OnMobileSaveToPlaylistAsync"
- OnPathChanged="OnMobilePathChanged" />
+ OnPathChanged="OnMobilePathChanged"
+ OnFilterChanged="OnMobileFilterChanged"
+ InitialSearch="@_initialSearch"
+ InitialFilters="@_initialFilters" />
}
@@ -169,6 +172,12 @@
private bool _firstRenderComplete = false;
private bool _needsRefresh = false;
+ // URL state for filters and search
+ private string _initialSearch = string.Empty;
+ private HashSet
_initialFilters = new();
+ private string _currentSearch = string.Empty;
+ private HashSet _currentFilters = new();
+
protected override async Task OnParametersSetAsync()
{
string currentBsiId = bsi?.Id;
@@ -252,14 +261,32 @@
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"];
- if (string.IsNullOrEmpty(pathFromUrl))
- return;
+ // 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 /
- var decodedPath = Uri.UnescapeDataString(pathFromUrl);
- if (!decodedPath.EndsWith("/"))
- decodedPath += "/";
+ string decodedPath = "/";
+ if (!string.IsNullOrEmpty(pathFromUrl))
+ {
+ decodedPath = Uri.UnescapeDataString(pathFromUrl);
+ if (!decodedPath.EndsWith("/"))
+ decodedPath += "/";
+ }
// Handle mobile file manager
if (_isMobileView && mobileFileManager != null)
@@ -283,10 +310,7 @@
{
try
{
- // Update URL without triggering navigation
- var baseUri = MyNavigationManager.Uri.Split('?')[0];
- var newUrl = path == "/" ? baseUri : $"{baseUri}?path={Uri.EscapeDataString(path)}";
- MyNavigationManager.NavigateTo(newUrl, forceLoad: false, replace: true);
+ UpdateUrlWithState(path, _currentSearch, _currentFilters);
}
catch (Exception ex)
{
@@ -294,6 +318,50 @@
}
}
+ private void OnMobileFilterChanged(MfmFilterChangedEventArgs args)
+ {
+ try
+ {
+ _currentSearch = args.SearchText;
+ _currentFilters = args.TypeFilters;
+
+ // Get current path from mobile file manager
+ var currentPath = mobileFileManager?.Path ?? "/";
+ UpdateUrlWithState(currentPath, _currentSearch, _currentFilters);
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError(ex, "Error updating URL with filters");
+ }
+ }
+
+ private void UpdateUrlWithState(string path, string search, HashSet filters)
+ {
+ var baseUri = MyNavigationManager.Uri.Split('?')[0];
+ 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
+ if (filters.Count > 0)
+ {
+ queryParams.Add($"filters={Uri.EscapeDataString(string.Join(",", filters))}");
+ }
+
+ var newUrl = queryParams.Count > 0 ? $"{baseUri}?{string.Join("&", queryParams)}" : baseUri;
+ MyNavigationManager.NavigateTo(newUrl, forceLoad: false, replace: true);
+ }
+
public async Task RefreshFileManager()
{
if (fm != null)
diff --git a/TelegramDownloader/Program.cs b/TelegramDownloader/Program.cs
index db90738..a5bcd0d 100644
--- a/TelegramDownloader/Program.cs
+++ b/TelegramDownloader/Program.cs
@@ -173,6 +173,18 @@
// Add controllers for Mobile API
builder.Services.AddControllers();
+// CORS for PWA and mobile apps
+builder.Services.AddCors(options =>
+{
+ options.AddPolicy("MobilePolicy", policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader()
+ .WithExposedHeaders("Content-Length", "Content-Range", "Accept-Ranges");
+ });
+});
+
// Swagger/OpenAPI for Mobile API documentation
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
@@ -319,6 +331,9 @@
c.RoutePrefix = "api-docs";
});
+// CORS must be before routing
+app.UseCors("MobilePolicy");
+
// API Key authentication middleware for mobile endpoints
app.UseMiddleware();
diff --git a/TelegramDownloader/Services/Streams/FileChunkStream.cs b/TelegramDownloader/Services/Streams/FileChunkStream.cs
new file mode 100644
index 0000000..a637cbe
--- /dev/null
+++ b/TelegramDownloader/Services/Streams/FileChunkStream.cs
@@ -0,0 +1,139 @@
+using System;
+using System.IO;
+
+namespace TelegramDownloader.Services.Streams
+{
+ ///
+ /// A read-only stream that provides access to a specific chunk of a file.
+ /// This allows uploading large files in memory-efficient chunks without
+ /// creating temporary split files on disk.
+ ///
+ public class FileChunkStream : Stream
+ {
+ private readonly FileStream _sourceStream;
+ private readonly long _chunkStart;
+ private readonly long _chunkLength;
+ private long _position;
+ private bool _disposed;
+
+ ///
+ /// Creates a new FileChunkStream for a specific portion of a file.
+ ///
+ /// Path to the source file
+ /// Starting byte position of the chunk
+ /// Length of the chunk in bytes
+ public FileChunkStream(string filePath, long chunkStart, long chunkLength)
+ {
+ _sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ _chunkStart = chunkStart;
+ _chunkLength = chunkLength;
+ _position = 0;
+
+ // Validate parameters
+ if (chunkStart < 0 || chunkStart >= _sourceStream.Length)
+ throw new ArgumentOutOfRangeException(nameof(chunkStart), "Chunk start is out of file bounds");
+
+ // Adjust chunk length if it exceeds file end
+ if (chunkStart + chunkLength > _sourceStream.Length)
+ _chunkLength = _sourceStream.Length - chunkStart;
+
+ // Position source stream at chunk start
+ _sourceStream.Seek(_chunkStart, SeekOrigin.Begin);
+ }
+
+ public override bool CanRead => true;
+ public override bool CanSeek => true;
+ public override bool CanWrite => false;
+ public override long Length => _chunkLength;
+
+ public override long Position
+ {
+ get => _position;
+ set
+ {
+ if (value < 0 || value > _chunkLength)
+ throw new ArgumentOutOfRangeException(nameof(value));
+ _position = value;
+ _sourceStream.Seek(_chunkStart + _position, SeekOrigin.Begin);
+ }
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(FileChunkStream));
+
+ // Calculate how many bytes we can read
+ long remainingInChunk = _chunkLength - _position;
+ if (remainingInChunk <= 0)
+ return 0;
+
+ int bytesToRead = (int)Math.Min(count, remainingInChunk);
+ int bytesRead = _sourceStream.Read(buffer, offset, bytesToRead);
+ _position += bytesRead;
+ return bytesRead;
+ }
+
+ public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(FileChunkStream));
+
+ // Calculate how many bytes we can read
+ long remainingInChunk = _chunkLength - _position;
+ if (remainingInChunk <= 0)
+ return 0;
+
+ int bytesToRead = (int)Math.Min(count, remainingInChunk);
+ int bytesRead = await _sourceStream.ReadAsync(buffer, offset, bytesToRead, cancellationToken);
+ _position += bytesRead;
+ return bytesRead;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ long newPosition = origin switch
+ {
+ SeekOrigin.Begin => offset,
+ SeekOrigin.Current => _position + offset,
+ SeekOrigin.End => _chunkLength + offset,
+ _ => throw new ArgumentException("Invalid SeekOrigin", nameof(origin))
+ };
+
+ if (newPosition < 0 || newPosition > _chunkLength)
+ throw new ArgumentOutOfRangeException(nameof(offset), "Seek position is out of chunk bounds");
+
+ _position = newPosition;
+ _sourceStream.Seek(_chunkStart + _position, SeekOrigin.Begin);
+ return _position;
+ }
+
+ public override void Flush()
+ {
+ // Read-only stream, nothing to flush
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotSupportedException("FileChunkStream is read-only");
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotSupportedException("FileChunkStream is read-only");
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ if (disposing)
+ {
+ _sourceStream?.Dispose();
+ }
+ _disposed = true;
+ }
+ base.Dispose(disposing);
+ }
+ }
+}
diff --git a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs
index 670beac..fa663a1 100644
--- a/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs
+++ b/TelegramDownloader/Shared/MobileFileManager/MobileFileManager.razor.cs
@@ -128,6 +128,16 @@ public partial class MobileFileManager : ComponentBase
[Parameter]
public EventCallback OnPathChanged { get; set; }
+ [Parameter]
+ public EventCallback OnFilterChanged { get; set; }
+
+ // Initial values from URL
+ [Parameter]
+ public string InitialSearch { get; set; } = string.Empty;
+
+ [Parameter]
+ public HashSet InitialFilters { get; set; } = new();
+
#endregion
#region State
@@ -212,6 +222,19 @@ private List DisplayFiles
protected override async Task OnInitializedAsync()
{
_previousId = Id;
+
+ // Initialize from URL parameters
+ if (!string.IsNullOrEmpty(InitialSearch))
+ {
+ SearchText = InitialSearch;
+ ShowSearch = true;
+ }
+
+ if (InitialFilters.Count > 0)
+ {
+ SelectedTypeFilters = new HashSet(InitialFilters);
+ }
+
await LoadFiles();
}
@@ -1203,6 +1226,7 @@ await InvokeAsync(async () =>
{
await LoadFiles();
}
+ await NotifyFilterChanged();
StateHasChanged();
});
};
@@ -1214,6 +1238,7 @@ private async Task ClearSearchText()
SearchText = string.Empty;
ResetPagination();
await LoadFiles();
+ await NotifyFilterChanged();
}
private async Task CloseSearch()
@@ -1222,6 +1247,7 @@ private async Task CloseSearch()
SearchText = string.Empty;
ResetPagination();
await LoadFiles();
+ await NotifyFilterChanged();
}
#endregion
@@ -1339,12 +1365,23 @@ private void ClearTypeFilters()
StateHasChanged();
}
- private void ApplyFiltersAndClose()
+ private async Task ApplyFiltersAndClose()
{
ShowFilterDialog = false;
+ await NotifyFilterChanged();
StateHasChanged();
}
+ private async Task NotifyFilterChanged()
+ {
+ var args = new MfmFilterChangedEventArgs
+ {
+ SearchText = SearchText,
+ TypeFilters = new HashSet(SelectedTypeFilters)
+ };
+ await OnFilterChanged.InvokeAsync(args);
+ }
+
private void UploadFiles()
{
ShowFabMenu = false;
diff --git a/TelegramDownloader/Shared/MobileFileManager/MobileFileManagerEventArgs.cs b/TelegramDownloader/Shared/MobileFileManager/MobileFileManagerEventArgs.cs
index 76316d4..d408a41 100644
--- a/TelegramDownloader/Shared/MobileFileManager/MobileFileManagerEventArgs.cs
+++ b/TelegramDownloader/Shared/MobileFileManager/MobileFileManagerEventArgs.cs
@@ -124,4 +124,10 @@ public class MfmSaveToPlaylistEventArgs
public string ChannelId { get; set; } = string.Empty;
public bool IsMultiple => Files.Length > 0;
}
+
+ public class MfmFilterChangedEventArgs
+ {
+ public string SearchText { get; set; } = string.Empty;
+ public HashSet TypeFilters { get; set; } = new();
+ }
}
diff --git a/TelegramDownloader/TelegramDownloader.csproj b/TelegramDownloader/TelegramDownloader.csproj
index 92a6543..bfb71a7 100644
--- a/TelegramDownloader/TelegramDownloader.csproj
+++ b/TelegramDownloader/TelegramDownloader.csproj
@@ -1,7 +1,7 @@
๏ปฟ
- 3.3.0.0
+ 3.4.0.0
Mateo
TelegramFileManager
net10.0
diff --git a/global.json b/global.json
deleted file mode 100644
index 40584df..0000000
--- a/global.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "sdk": {
- "version": "9.0.100",
- "rollForward": "latestMinor",
- "allowPrerelease": false
- }
-}