From 8cb459c83219f24f4f19f7bd24525fce6fde1d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Wed, 18 Feb 2026 14:21:53 +0300 Subject: [PATCH] qwen edit --- BookReader/App.xaml.cs | 8 +- BookReader/Constants.cs | 65 +++++++++++++ .../Converters/BoolToVisibilityConverter.cs | 16 ---- .../Converters/ProgressToWidthConverter.cs | 2 +- BookReader/MauiProgram.cs | 1 + BookReader/Models/AppSettings.cs | 15 +-- BookReader/Resources/Styles/AppStyles.xaml | 1 - BookReader/Services/BookParserService.cs | 8 +- BookReader/Services/CalibreWebService.cs | 6 +- BookReader/Services/DatabaseService.cs | 95 ++++++++++--------- BookReader/Services/IDatabaseService.cs | 22 ++--- BookReader/Services/INavigationService.cs | 11 +++ BookReader/Services/NavigationService.cs | 34 +++++++ BookReader/Services/Result.cs | 45 +++++++++ BookReader/ViewModels/BookshelfViewModel.cs | 19 ++-- .../ViewModels/CalibreLibraryViewModel.cs | 13 ++- BookReader/ViewModels/ReaderViewModel.cs | 11 ++- BookReader/ViewModels/SettingsViewModel.cs | 13 ++- BookReader/Views/BookshelfPage.xaml | 5 +- BookReader/Views/BookshelfPage.xaml.cs | 9 +- BookReader/Views/ReaderPage.xaml.cs | 13 +-- 21 files changed, 287 insertions(+), 125 deletions(-) create mode 100644 BookReader/Constants.cs delete mode 100644 BookReader/Converters/BoolToVisibilityConverter.cs create mode 100644 BookReader/Services/INavigationService.cs create mode 100644 BookReader/Services/NavigationService.cs create mode 100644 BookReader/Services/Result.cs diff --git a/BookReader/App.xaml.cs b/BookReader/App.xaml.cs index 516e50c..64de8ea 100644 --- a/BookReader/App.xaml.cs +++ b/BookReader/App.xaml.cs @@ -21,22 +21,22 @@ public partial class App : Application protected override async void OnStart() { base.OnStart(); - await InitializeDatabaseAsync(); + await InitializeDatabaseAsync(CancellationToken.None); } protected override async void OnResume() { base.OnResume(); - await InitializeDatabaseAsync(); + await InitializeDatabaseAsync(CancellationToken.None); } - private async Task InitializeDatabaseAsync() + private async Task InitializeDatabaseAsync(CancellationToken ct = default) { if (_isInitialized) return; try { - await _databaseService.InitializeAsync(); + await _databaseService.InitializeAsync(ct); _isInitialized = true; } catch (Exception ex) diff --git a/BookReader/Constants.cs b/BookReader/Constants.cs new file mode 100644 index 0000000..9fe2fa7 --- /dev/null +++ b/BookReader/Constants.cs @@ -0,0 +1,65 @@ +namespace BookReader; + +public static class Constants +{ + // UI Constants + public static class UI + { + public const int ProgressBarMaxWidth = 120; + public const int CoverImageHeight = 150; + public const int BookGridSpan = 3; + public const int HorizontalItemSpacing = 10; + public const int VerticalItemSpacing = 15; + } + + // Reader Constants + public static class Reader + { + public const int DefaultFontSize = 18; + public const int MinFontSize = 12; + public const int MaxFontSize = 40; + public const int Base64ChunkSize = 400_000; + public const int TouchZoneLeftPercent = 30; + public const int TouchZoneRightPercent = 30; + public const int TouchZoneCenterPercent = 40; + public const int SwipeMinDistance = 50; + public const int SwipeMaxDurationMs = 500; + } + + // Database Constants + public static class Database + { + public const string DatabaseFileName = "bookreader.db3"; + public const int RetryCount = 3; + public const int RetryBaseDelayMs = 100; + } + + // File Constants + public static class Files + { + public const string BooksFolder = "Books"; + public const string DefaultCoverImage = "default_cover.png"; + public const string EpubExtension = ".epub"; + public const string Fb2Extension = ".fb2"; + public const string Fb2ZipExtension = ".fb2.zip"; + } + + // Network Constants + public static class Network + { + public const int HttpClientTimeoutSeconds = 30; + public const int DownloadBufferSize = 8192; + public const int CalibrePageSize = 20; + } + + // Storage Keys + public static class StorageKeys + { + public const string CalibreUrl = "CalibreUrl"; + public const string CalibreUsername = "CalibreUsername"; + public const string DefaultFontSize = "DefaultFontSize"; + public const string DefaultFontFamily = "DefaultFontFamily"; + public const string Theme = "Theme"; + public const string Brightness = "Brightness"; + } +} diff --git a/BookReader/Converters/BoolToVisibilityConverter.cs b/BookReader/Converters/BoolToVisibilityConverter.cs deleted file mode 100644 index 88ffd50..0000000 --- a/BookReader/Converters/BoolToVisibilityConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Globalization; - -namespace BookReader.Converters; - -public class BoolToVisibilityConverter : IValueConverter -{ - public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is true; - } - - public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) - { - return value is true; - } -} \ No newline at end of file diff --git a/BookReader/Converters/ProgressToWidthConverter.cs b/BookReader/Converters/ProgressToWidthConverter.cs index 11aff2c..0ceec23 100644 --- a/BookReader/Converters/ProgressToWidthConverter.cs +++ b/BookReader/Converters/ProgressToWidthConverter.cs @@ -8,7 +8,7 @@ public class ProgressToWidthConverter : IValueConverter { if (value is double progress) { - var maxWidth = 120.0; + var maxWidth = (double)Constants.UI.ProgressBarMaxWidth; if (parameter is string paramStr && double.TryParse(paramStr, out var max)) maxWidth = max; return progress * maxWidth; diff --git a/BookReader/MauiProgram.cs b/BookReader/MauiProgram.cs index feb2dd5..c766326 100644 --- a/BookReader/MauiProgram.cs +++ b/BookReader/MauiProgram.cs @@ -30,6 +30,7 @@ public static class MauiProgram builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // HTTP Client for Calibre builder.Services.AddHttpClient(); diff --git a/BookReader/Models/AppSettings.cs b/BookReader/Models/AppSettings.cs index 2e29425..0ca3891 100644 --- a/BookReader/Models/AppSettings.cs +++ b/BookReader/Models/AppSettings.cs @@ -1,4 +1,5 @@ -using SQLite; +using BookReader; +using SQLite; namespace BookReader.Models; @@ -12,12 +13,12 @@ public class AppSettings public static class SettingsKeys { - public const string CalibreUrl = "CalibreUrl"; - public const string CalibreUsername = "CalibreUsername"; - public const string DefaultFontSize = "DefaultFontSize"; - public const string DefaultFontFamily = "DefaultFontFamily"; - public const string Theme = "Theme"; - public const string Brightness = "Brightness"; + public const string CalibreUrl = Constants.StorageKeys.CalibreUrl; + public const string CalibreUsername = Constants.StorageKeys.CalibreUsername; + public const string DefaultFontSize = Constants.StorageKeys.DefaultFontSize; + public const string DefaultFontFamily = Constants.StorageKeys.DefaultFontFamily; + public const string Theme = Constants.StorageKeys.Theme; + public const string Brightness = Constants.StorageKeys.Brightness; } public static class SecureStorageKeys diff --git a/BookReader/Resources/Styles/AppStyles.xaml b/BookReader/Resources/Styles/AppStyles.xaml index 0660ebf..7105e8f 100644 --- a/BookReader/Resources/Styles/AppStyles.xaml +++ b/BookReader/Resources/Styles/AppStyles.xaml @@ -10,7 +10,6 @@ - diff --git a/BookReader/Services/BookParserService.cs b/BookReader/Services/BookParserService.cs index 857b4ae..ba27d2b 100644 --- a/BookReader/Services/BookParserService.cs +++ b/BookReader/Services/BookParserService.cs @@ -46,10 +46,10 @@ public class BookParserService : IBookParserService switch (extension) { - case ".epub": + case Constants.Files.EpubExtension: await ParseEpubMetadataAsync(book); break; - case ".fb2": + case Constants.Files.Fb2Extension: await ParseFb2MetadataAsync(book); break; default: @@ -97,10 +97,10 @@ public class BookParserService : IBookParserService try { string xml; - if (book.FilePath.EndsWith(".fb2.zip", StringComparison.OrdinalIgnoreCase)) + if (book.FilePath.EndsWith(Constants.Files.Fb2ZipExtension, StringComparison.OrdinalIgnoreCase)) { using var zip = ZipFile.OpenRead(book.FilePath); - var entry = zip.Entries.FirstOrDefault(e => e.Name.EndsWith(".fb2", StringComparison.OrdinalIgnoreCase)); + var entry = zip.Entries.FirstOrDefault(e => e.Name.EndsWith(Constants.Files.Fb2Extension, StringComparison.OrdinalIgnoreCase)); if (entry != null) { using var stream = entry.Open(); diff --git a/BookReader/Services/CalibreWebService.cs b/BookReader/Services/CalibreWebService.cs index 3c94352..1e2d475 100644 --- a/BookReader/Services/CalibreWebService.cs +++ b/BookReader/Services/CalibreWebService.cs @@ -15,7 +15,7 @@ public class CalibreWebService : ICalibreWebService public CalibreWebService(HttpClient httpClient) { _httpClient = httpClient; - _httpClient.Timeout = TimeSpan.FromSeconds(30); + _httpClient.Timeout = TimeSpan.FromSeconds(Constants.Network.HttpClientTimeoutSeconds); } public void Configure(string url, string username, string password) @@ -46,7 +46,7 @@ public class CalibreWebService : ICalibreWebService } } - public async Task> GetBooksAsync(string? searchQuery = null, int page = 0, int pageSize = 20) + public async Task> GetBooksAsync(string? searchQuery = null, int page = 0, int pageSize = Constants.Network.CalibrePageSize) { var books = new List(); @@ -125,7 +125,7 @@ public class CalibreWebService : ICalibreWebService using var contentStream = await response.Content.ReadAsStreamAsync(); using var fileStream = File.Create(filePath); - var buffer = new byte[8192]; + var buffer = new byte[Constants.Network.DownloadBufferSize]; int read; while ((read = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0) diff --git a/BookReader/Services/DatabaseService.cs b/BookReader/Services/DatabaseService.cs index b4f1db4..d823c81 100644 --- a/BookReader/Services/DatabaseService.cs +++ b/BookReader/Services/DatabaseService.cs @@ -9,38 +9,41 @@ public class DatabaseService : IDatabaseService { private SQLiteAsyncConnection? _database; private readonly string _dbPath; - private readonly AsyncRetryPolicy _retryPolicy; + private readonly ResiliencePipeline _retryPipeline; public DatabaseService() { _dbPath = Path.Combine(FileSystem.AppDataDirectory, "bookreader.db3"); - // Polly retry policy: 3 попытки с экспоненциальной задержкой - _retryPolicy = Policy - .Handle() - .Or(ex => ex.Message.Contains("database is locked") || ex.Message.Contains("busy")) - .WaitAndRetryAsync( - retryCount: 3, - sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)), - onRetry: (exception, timeSpan, retryNumber, context) => + // Polly 8.x retry pipeline: 3 попытки с экспоненциальной задержкой + _retryPipeline = new ResiliencePipelineBuilder() + .AddRetry(new RetryStrategyOptions + { + ShouldHandle = new PredicateBuilder().Handle() + .Handle(ex => ex.Message.Contains("database is locked") || ex.Message.Contains("busy")), + MaxRetryAttempts = 3, + DelayGenerator = context => new ValueTask(TimeSpan.FromMilliseconds(100 * Math.Pow(2, context.AttemptNumber))), + OnRetry = context => { - System.Diagnostics.Debug.WriteLine($"[Database] Retry {retryNumber} after {timeSpan.TotalMilliseconds}ms: {exception.Message}"); + System.Diagnostics.Debug.WriteLine($"[Database] Retry {context.AttemptNumber + 1} after {context.RetryDelay.TotalMilliseconds}ms: {context.Outcome.Exception?.Message}"); + return default; } - ); + }) + .Build(); } - public async Task InitializeAsync() + public async Task InitializeAsync(CancellationToken ct = default) { if (_database != null) return; _database = new SQLiteAsyncConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache); - await _retryPolicy.ExecuteAsync(async () => + await _retryPipeline.ExecuteAsync(async (token) => { await _database!.CreateTableAsync(); await _database!.CreateTableAsync(); await _database!.CreateTableAsync(); - }); + }, ct); } private async Task EnsureInitializedAsync() @@ -50,39 +53,39 @@ public class DatabaseService : IDatabaseService } // Books - public async Task> GetAllBooksAsync() + public async Task> GetAllBooksAsync(CancellationToken ct = default) { await EnsureInitializedAsync(); - return await _retryPolicy.ExecuteAsync(async () => - await _database!.Table().OrderByDescending(b => b.LastRead).ToListAsync()); + return await _retryPipeline.ExecuteAsync(async (token) => + await _database!.Table().OrderByDescending(b => b.LastRead).ToListAsync(), ct); } - public async Task GetBookByIdAsync(int id) + public async Task GetBookByIdAsync(int id, CancellationToken ct = default) { await EnsureInitializedAsync(); - return await _retryPolicy.ExecuteAsync(async () => - await _database!.Table().Where(b => b.Id == id).FirstOrDefaultAsync()); + return await _retryPipeline.ExecuteAsync(async (token) => + await _database!.Table().Where(b => b.Id == id).FirstOrDefaultAsync(), ct); } - public async Task SaveBookAsync(Book book) + public async Task SaveBookAsync(Book book, CancellationToken ct = default) { await EnsureInitializedAsync(); - return await _retryPolicy.ExecuteAsync(async () => + return await _retryPipeline.ExecuteAsync(async (token) => { if (book.Id != 0) return await _database!.UpdateAsync(book); return await _database!.InsertAsync(book); - }); + }, ct); } - public async Task UpdateBookAsync(Book book) + public async Task UpdateBookAsync(Book book, CancellationToken ct = default) { await EnsureInitializedAsync(); - return await _retryPolicy.ExecuteAsync(async () => - await _database!.UpdateAsync(book)); + return await _retryPipeline.ExecuteAsync(async (token) => + await _database!.UpdateAsync(book), ct); } - public async Task DeleteBookAsync(Book book) + public async Task DeleteBookAsync(Book book, CancellationToken ct = default) { await EnsureInitializedAsync(); @@ -93,28 +96,28 @@ public class DatabaseService : IDatabaseService } // Delete progress records - await _retryPolicy.ExecuteAsync(async () => - await _database!.Table().DeleteAsync(p => p.BookId == book.Id)); + await _retryPipeline.ExecuteAsync(async (token) => + await _database!.Table().DeleteAsync(p => p.BookId == book.Id), ct); - return await _retryPolicy.ExecuteAsync(async () => - await _database!.DeleteAsync(book)); + return await _retryPipeline.ExecuteAsync(async (token) => + await _database!.DeleteAsync(book), ct); } // Settings - public async Task GetSettingAsync(string key) + public async Task GetSettingAsync(string key, CancellationToken ct = default) { await EnsureInitializedAsync(); - return await _retryPolicy.ExecuteAsync(async () => + return await _retryPipeline.ExecuteAsync(async (token) => { var setting = await _database!.Table().Where(s => s.Key == key).FirstOrDefaultAsync(); return setting?.Value; - }); + }, ct); } - public async Task SetSettingAsync(string key, string value) + public async Task SetSettingAsync(string key, string value, CancellationToken ct = default) { await EnsureInitializedAsync(); - await _retryPolicy.ExecuteAsync(async () => + await _retryPipeline.ExecuteAsync(async (token) => { var existing = await _database!.Table().Where(s => s.Key == key).FirstOrDefaultAsync(); if (existing != null) @@ -126,37 +129,37 @@ public class DatabaseService : IDatabaseService { await _database.InsertAsync(new AppSettings { Key = key, Value = value }); } - }); + }, ct); } - public async Task> GetAllSettingsAsync() + public async Task> GetAllSettingsAsync(CancellationToken ct = default) { await EnsureInitializedAsync(); - return await _retryPolicy.ExecuteAsync(async () => + return await _retryPipeline.ExecuteAsync(async (token) => { var settings = await _database!.Table().ToListAsync(); return settings.ToDictionary(s => s.Key, s => s.Value); - }); + }, ct); } // Reading Progress - public async Task SaveProgressAsync(ReadingProgress progress) + public async Task SaveProgressAsync(ReadingProgress progress, CancellationToken ct = default) { await EnsureInitializedAsync(); - await _retryPolicy.ExecuteAsync(async () => + await _retryPipeline.ExecuteAsync(async (token) => { progress.Timestamp = DateTime.UtcNow; await _database!.InsertAsync(progress); - }); + }, ct); } - public async Task GetLatestProgressAsync(int bookId) + public async Task GetLatestProgressAsync(int bookId, CancellationToken ct = default) { await EnsureInitializedAsync(); - return await _retryPolicy.ExecuteAsync(async () => + return await _retryPipeline.ExecuteAsync(async (token) => await _database!.Table() .Where(p => p.BookId == bookId) .OrderByDescending(p => p.Timestamp) - .FirstOrDefaultAsync()); + .FirstOrDefaultAsync(), ct); } } \ No newline at end of file diff --git a/BookReader/Services/IDatabaseService.cs b/BookReader/Services/IDatabaseService.cs index 18a52e3..1708d8c 100644 --- a/BookReader/Services/IDatabaseService.cs +++ b/BookReader/Services/IDatabaseService.cs @@ -6,21 +6,21 @@ namespace BookReader.Services; public interface IDatabaseService { - Task InitializeAsync(); + Task InitializeAsync(CancellationToken ct = default); // Books - Task> GetAllBooksAsync(); - Task GetBookByIdAsync(int id); - Task SaveBookAsync(Book book); - Task UpdateBookAsync(Book book); - Task DeleteBookAsync(Book book); + Task> GetAllBooksAsync(CancellationToken ct = default); + Task GetBookByIdAsync(int id, CancellationToken ct = default); + Task SaveBookAsync(Book book, CancellationToken ct = default); + Task UpdateBookAsync(Book book, CancellationToken ct = default); + Task DeleteBookAsync(Book book, CancellationToken ct = default); // Settings - Task GetSettingAsync(string key); - Task SetSettingAsync(string key, string value); - Task> GetAllSettingsAsync(); + Task GetSettingAsync(string key, CancellationToken ct = default); + Task SetSettingAsync(string key, string value, CancellationToken ct = default); + Task> GetAllSettingsAsync(CancellationToken ct = default); // Reading Progress - Task SaveProgressAsync(ReadingProgress progress); - Task GetLatestProgressAsync(int bookId); + Task SaveProgressAsync(ReadingProgress progress, CancellationToken ct = default); + Task GetLatestProgressAsync(int bookId, CancellationToken ct = default); } \ No newline at end of file diff --git a/BookReader/Services/INavigationService.cs b/BookReader/Services/INavigationService.cs new file mode 100644 index 0000000..3c3cda1 --- /dev/null +++ b/BookReader/Services/INavigationService.cs @@ -0,0 +1,11 @@ +namespace BookReader.Services; + +public interface INavigationService +{ + Task GoToAsync(string route); + Task GoToAsync(string route, IDictionary parameters); + Task GoBackAsync(); + Task DisplayAlertAsync(string title, string message, string cancel); + Task DisplayAlertAsync(string title, string message, string accept, string cancel); + Task DisplayActionSheetAsync(string title, string cancel, params string[] actions); +} diff --git a/BookReader/Services/NavigationService.cs b/BookReader/Services/NavigationService.cs new file mode 100644 index 0000000..e8876e6 --- /dev/null +++ b/BookReader/Services/NavigationService.cs @@ -0,0 +1,34 @@ +namespace BookReader.Services; + +public class NavigationService : INavigationService +{ + public Task GoToAsync(string route) + { + return Shell.Current.GoToAsync(route); + } + + public Task GoToAsync(string route, IDictionary parameters) + { + return Shell.Current.GoToAsync(route, parameters); + } + + public Task GoBackAsync() + { + return Shell.Current.GoToAsync(".."); + } + + public Task DisplayAlertAsync(string title, string message, string cancel) + { + return Shell.Current.DisplayAlertAsync(title, message, cancel); + } + + public Task DisplayAlertAsync(string title, string message, string accept, string cancel) + { + return Shell.Current.DisplayAlertAsync(title, message, accept, cancel); + } + + public async Task DisplayActionSheetAsync(string title, string cancel, params string[] actions) + { + return await Shell.Current.DisplayActionSheetAsync(title, cancel, null, actions); + } +} diff --git a/BookReader/Services/Result.cs b/BookReader/Services/Result.cs new file mode 100644 index 0000000..9082e35 --- /dev/null +++ b/BookReader/Services/Result.cs @@ -0,0 +1,45 @@ +namespace BookReader.Services; + +public class Result +{ + public bool IsSuccess { get; } + public T? Value { get; } + public string? ErrorMessage { get; } + public Exception? Exception { get; } + + private Result(bool isSuccess, T? value, string? errorMessage, Exception? exception) + { + IsSuccess = isSuccess; + Value = value; + ErrorMessage = errorMessage; + Exception = exception; + } + + public static Result Success(T value) => new(true, value, null, null); + public static Result Failure(string errorMessage) => new(false, default, errorMessage, null); + public static Result Failure(Exception exception) => new(false, default, exception.Message, exception); + + public TResult Match(Func onSuccess, Func onFailure) => + IsSuccess ? onSuccess(Value!) : onFailure(ErrorMessage!); +} + +public class Result +{ + public bool IsSuccess { get; } + public string? ErrorMessage { get; } + public Exception? Exception { get; } + + private Result(bool isSuccess, string? errorMessage, Exception? exception) + { + IsSuccess = isSuccess; + ErrorMessage = errorMessage; + Exception = exception; + } + + public static Result Success() => new(true, null, null); + public static Result Failure(string errorMessage) => new(false, errorMessage, null); + public static Result Failure(Exception exception) => new(false, exception.Message, exception); + + public TResult Match(Func onSuccess, Func onFailure) => + IsSuccess ? onSuccess() : onFailure(ErrorMessage!); +} diff --git a/BookReader/ViewModels/BookshelfViewModel.cs b/BookReader/ViewModels/BookshelfViewModel.cs index b11a92d..4a6a3f9 100644 --- a/BookReader/ViewModels/BookshelfViewModel.cs +++ b/BookReader/ViewModels/BookshelfViewModel.cs @@ -11,6 +11,7 @@ public partial class BookshelfViewModel : BaseViewModel private readonly IDatabaseService _databaseService; private readonly IBookParserService _bookParserService; private readonly ISettingsService _settingsService; + private readonly INavigationService _navigationService; public ObservableCollection Books { get; } = new(); @@ -20,11 +21,13 @@ public partial class BookshelfViewModel : BaseViewModel public BookshelfViewModel( IDatabaseService databaseService, IBookParserService bookParserService, - ISettingsService settingsService) + ISettingsService settingsService, + INavigationService navigationService) { _databaseService = databaseService; _bookParserService = bookParserService; _settingsService = settingsService; + _navigationService = navigationService; Title = "My Library"; } @@ -75,7 +78,7 @@ public partial class BookshelfViewModel : BaseViewModel var extension = Path.GetExtension(result.FileName).ToLowerInvariant(); if (extension != ".epub" && extension != ".fb2") { - await Shell.Current.DisplayAlert("Error", "Only EPUB and FB2 formats are supported.", "OK"); + await _navigationService.DisplayAlertAsync("Error", "Only EPUB and FB2 formats are supported.", "OK"); return; } @@ -101,7 +104,7 @@ public partial class BookshelfViewModel : BaseViewModel } catch (Exception ex) { - await Shell.Current.DisplayAlert("Error", $"Failed to add book: {ex.Message}", "OK"); + await _navigationService.DisplayAlertAsync("Error", $"Failed to add book: {ex.Message}", "OK"); } finally { @@ -115,7 +118,7 @@ public partial class BookshelfViewModel : BaseViewModel { if (book == null) return; - var confirm = await Shell.Current.DisplayAlert("Delete Book", + var confirm = await _navigationService.DisplayAlertAsync("Delete Book", $"Are you sure you want to delete \"{book.Title}\"?", "Delete", "Cancel"); if (!confirm) return; @@ -128,7 +131,7 @@ public partial class BookshelfViewModel : BaseViewModel } catch (Exception ex) { - await Shell.Current.DisplayAlert("Error", $"Failed to delete book: {ex.Message}", "OK"); + await _navigationService.DisplayAlertAsync("Error", $"Failed to delete book: {ex.Message}", "OK"); } } @@ -142,18 +145,18 @@ public partial class BookshelfViewModel : BaseViewModel { "Book", book } }; - await Shell.Current.GoToAsync("reader", navigationParameter); + await _navigationService.GoToAsync("reader", navigationParameter); } [RelayCommand] public async Task OpenSettingsAsync() { - await Shell.Current.GoToAsync("settings"); + await _navigationService.GoToAsync("settings"); } [RelayCommand] public async Task OpenCalibreLibraryAsync() { - await Shell.Current.GoToAsync("calibre"); + await _navigationService.GoToAsync("calibre"); } } \ No newline at end of file diff --git a/BookReader/ViewModels/CalibreLibraryViewModel.cs b/BookReader/ViewModels/CalibreLibraryViewModel.cs index 709284c..b56fa27 100644 --- a/BookReader/ViewModels/CalibreLibraryViewModel.cs +++ b/BookReader/ViewModels/CalibreLibraryViewModel.cs @@ -12,6 +12,7 @@ public partial class CalibreLibraryViewModel : BaseViewModel private readonly IBookParserService _bookParserService; private readonly IDatabaseService _databaseService; private readonly ISettingsService _settingsService; + private readonly INavigationService _navigationService; public ObservableCollection Books { get; } = new(); @@ -30,12 +31,14 @@ public partial class CalibreLibraryViewModel : BaseViewModel ICalibreWebService calibreWebService, IBookParserService bookParserService, IDatabaseService databaseService, - ISettingsService settingsService) + ISettingsService settingsService, + INavigationService navigationService) { _calibreWebService = calibreWebService; _bookParserService = bookParserService; _databaseService = databaseService; _settingsService = settingsService; + _navigationService = navigationService; Title = "Calibre Library"; } @@ -73,7 +76,7 @@ public partial class CalibreLibraryViewModel : BaseViewModel } catch (Exception ex) { - await Shell.Current.DisplayAlert("Error", $"Failed to load library: {ex.Message}", "OK"); + await _navigationService.DisplayAlertAsync("Error", $"Failed to load library: {ex.Message}", "OK"); } finally { @@ -136,11 +139,11 @@ public partial class CalibreLibraryViewModel : BaseViewModel await _databaseService.UpdateBookAsync(book); DownloadStatus = "Download complete!"; - await Shell.Current.DisplayAlert("Success", $"\"{calibreBook.Title}\" has been added to your library.", "OK"); + await _navigationService.DisplayAlertAsync("Success", $"\"{calibreBook.Title}\" has been added to your library.", "OK"); } catch (Exception ex) { - await Shell.Current.DisplayAlert("Error", $"Failed to download: {ex.Message}", "OK"); + await _navigationService.DisplayAlertAsync("Error", $"Failed to download: {ex.Message}", "OK"); } finally { @@ -152,6 +155,6 @@ public partial class CalibreLibraryViewModel : BaseViewModel [RelayCommand] public async Task OpenSettingsAsync() { - await Shell.Current.GoToAsync("settings"); + await _navigationService.GoToAsync("settings"); } } \ No newline at end of file diff --git a/BookReader/ViewModels/ReaderViewModel.cs b/BookReader/ViewModels/ReaderViewModel.cs index 98fb1da..3452114 100644 --- a/BookReader/ViewModels/ReaderViewModel.cs +++ b/BookReader/ViewModels/ReaderViewModel.cs @@ -11,6 +11,7 @@ public partial class ReaderViewModel : BaseViewModel { private readonly IDatabaseService _databaseService; private readonly ISettingsService _settingsService; + private readonly INavigationService _navigationService; [ObservableProperty] private Book? _book; @@ -81,11 +82,15 @@ public partial class ReaderViewModel : BaseViewModel public event Action? OnJavaScriptRequested; public event Action? OnBookReady; - public ReaderViewModel(IDatabaseService databaseService, ISettingsService settingsService) + public ReaderViewModel( + IDatabaseService databaseService, + ISettingsService settingsService, + INavigationService navigationService) { _databaseService = databaseService; _settingsService = settingsService; - _fontSize = 18; + _navigationService = navigationService; + _fontSize = Constants.Reader.DefaultFontSize; } public async Task InitializeAsync() @@ -104,7 +109,7 @@ public partial class ReaderViewModel : BaseViewModel return; } - var savedFontSize = await _settingsService.GetIntAsync(SettingsKeys.DefaultFontSize, 18); + var savedFontSize = await _settingsService.GetIntAsync(SettingsKeys.DefaultFontSize, Constants.Reader.DefaultFontSize); var savedFontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif"); FontSize = savedFontSize; diff --git a/BookReader/ViewModels/SettingsViewModel.cs b/BookReader/ViewModels/SettingsViewModel.cs index 8da61ec..63bf0bb 100644 --- a/BookReader/ViewModels/SettingsViewModel.cs +++ b/BookReader/ViewModels/SettingsViewModel.cs @@ -9,6 +9,7 @@ public partial class SettingsViewModel : BaseViewModel { private readonly ISettingsService _settingsService; private readonly ICalibreWebService _calibreWebService; + private readonly INavigationService _navigationService; [ObservableProperty] private string _calibreUrl = string.Empty; @@ -20,7 +21,7 @@ public partial class SettingsViewModel : BaseViewModel private string _calibrePassword = string.Empty; [ObservableProperty] - private int _defaultFontSize = 18; + private int _defaultFontSize = Constants.Reader.DefaultFontSize; [ObservableProperty] private string _defaultFontFamily = "serif"; @@ -42,10 +43,14 @@ public partial class SettingsViewModel : BaseViewModel 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 36, 40 }; - public SettingsViewModel(ISettingsService settingsService, ICalibreWebService calibreWebService) + public SettingsViewModel( + ISettingsService settingsService, + ICalibreWebService calibreWebService, + INavigationService navigationService) { _settingsService = settingsService; _calibreWebService = calibreWebService; + _navigationService = navigationService; Title = "Settings"; } @@ -55,7 +60,7 @@ public partial class SettingsViewModel : BaseViewModel CalibreUrl = await _settingsService.GetAsync(SettingsKeys.CalibreUrl); CalibreUsername = await _settingsService.GetAsync(SettingsKeys.CalibreUsername); CalibrePassword = await _settingsService.GetSecurePasswordAsync(); - DefaultFontSize = await _settingsService.GetIntAsync(SettingsKeys.DefaultFontSize, 18); + DefaultFontSize = await _settingsService.GetIntAsync(SettingsKeys.DefaultFontSize, Constants.Reader.DefaultFontSize); DefaultFontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif"); } @@ -73,7 +78,7 @@ public partial class SettingsViewModel : BaseViewModel _calibreWebService.Configure(CalibreUrl, CalibreUsername, CalibrePassword); } - await Shell.Current.DisplayAlert("Settings", "Settings saved successfully.", "OK"); + await _navigationService.DisplayAlertAsync("Settings", "Settings saved successfully.", "OK"); } [RelayCommand] diff --git a/BookReader/Views/BookshelfPage.xaml b/BookReader/Views/BookshelfPage.xaml index ca276b3..1f95a66 100644 --- a/BookReader/Views/BookshelfPage.xaml +++ b/BookReader/Views/BookshelfPage.xaml @@ -3,15 +3,14 @@ xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:vm="clr-namespace:BookReader.ViewModels" xmlns:models="clr-namespace:BookReader.Models" - xmlns:converters="clr-namespace:BookReader.Converters" + xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit" x:Class="BookReader.Views.BookshelfPage" x:DataType="vm:BookshelfViewModel" Title="{Binding Title}" Shell.NavBarIsVisible="True"> - - + diff --git a/BookReader/Views/BookshelfPage.xaml.cs b/BookReader/Views/BookshelfPage.xaml.cs index 3128645..7e78a01 100644 --- a/BookReader/Views/BookshelfPage.xaml.cs +++ b/BookReader/Views/BookshelfPage.xaml.cs @@ -1,3 +1,4 @@ +using BookReader.Services; using BookReader.ViewModels; namespace BookReader.Views; @@ -5,11 +6,13 @@ namespace BookReader.Views; public partial class BookshelfPage : ContentPage { private readonly BookshelfViewModel _viewModel; + private readonly INavigationService _navigationService; - public BookshelfPage(BookshelfViewModel viewModel) + public BookshelfPage(BookshelfViewModel viewModel, INavigationService navigationService) { InitializeComponent(); _viewModel = viewModel; + _navigationService = navigationService; BindingContext = viewModel; } @@ -21,7 +24,7 @@ public partial class BookshelfPage : ContentPage private async void OnMenuClicked(object? sender, EventArgs e) { - var action = await DisplayActionSheet("Menu", "Cancel", null, + var action = await _navigationService.DisplayActionSheetAsync("Menu", "Cancel", "⚙️ Settings", "☁️ Calibre Library", "ℹ️ About"); switch (action) @@ -33,7 +36,7 @@ public partial class BookshelfPage : ContentPage await _viewModel.OpenCalibreLibraryCommand.ExecuteAsync(null); break; case "ℹ️ About": - await DisplayAlert("About", "BookReader v1.0\nEPUB & FB2 Reader", "OK"); + await _navigationService.DisplayAlertAsync("About", "BookReader v1.0\nEPUB & FB2 Reader", "OK"); break; } } diff --git a/BookReader/Views/ReaderPage.xaml.cs b/BookReader/Views/ReaderPage.xaml.cs index 84f6db5..c52155e 100644 --- a/BookReader/Views/ReaderPage.xaml.cs +++ b/BookReader/Views/ReaderPage.xaml.cs @@ -1,3 +1,4 @@ +using BookReader.Services; using BookReader.ViewModels; using Newtonsoft.Json.Linq; @@ -6,16 +7,16 @@ namespace BookReader.Views; public partial class ReaderPage : ContentPage { private readonly ReaderViewModel _viewModel; + private readonly INavigationService _navigationService; private bool _isBookLoaded; private readonly List _chapterData = new(); - private IDispatcherTimer? _pollTimer; - private IDispatcherTimer? _progressTimer; private bool _isActive; - public ReaderPage(ReaderViewModel viewModel) + public ReaderPage(ReaderViewModel viewModel, INavigationService navigationService) { InitializeComponent(); _viewModel = viewModel; + _navigationService = navigationService; BindingContext = viewModel; _viewModel.OnJavaScriptRequested += OnJavaScriptRequested; } @@ -72,7 +73,7 @@ public partial class ReaderPage : ContentPage if (!File.Exists(book.FilePath)) { - await DisplayAlert("Error", "Book file not found", "OK"); + await _navigationService.DisplayAlertAsync("Error", "Book file not found", "OK"); return; } @@ -124,7 +125,7 @@ public partial class ReaderPage : ContentPage catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[Reader] Load error: {ex.Message}\n{ex.StackTrace}"); - await DisplayAlert("Error", $"Failed to load book: {ex.Message}", "OK"); + await _navigationService.DisplayAlertAsync("Error", $"Failed to load book: {ex.Message}", "OK"); } } @@ -304,7 +305,7 @@ public partial class ReaderPage : ContentPage private async void OnBackToLibrary(object? sender, EventArgs e) { await SaveCurrentProgress(); - await Shell.Current.GoToAsync(".."); + await _navigationService.GoBackAsync(); } // ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========