qwen edit

This commit is contained in:
Курнат Андрей
2026-02-18 14:21:53 +03:00
parent 55c620f5a3
commit 8cb459c832
21 changed files with 287 additions and 125 deletions

View File

@@ -21,22 +21,22 @@ public partial class App : Application
protected override async void OnStart() protected override async void OnStart()
{ {
base.OnStart(); base.OnStart();
await InitializeDatabaseAsync(); await InitializeDatabaseAsync(CancellationToken.None);
} }
protected override async void OnResume() protected override async void OnResume()
{ {
base.OnResume(); base.OnResume();
await InitializeDatabaseAsync(); await InitializeDatabaseAsync(CancellationToken.None);
} }
private async Task InitializeDatabaseAsync() private async Task InitializeDatabaseAsync(CancellationToken ct = default)
{ {
if (_isInitialized) return; if (_isInitialized) return;
try try
{ {
await _databaseService.InitializeAsync(); await _databaseService.InitializeAsync(ct);
_isInitialized = true; _isInitialized = true;
} }
catch (Exception ex) catch (Exception ex)

65
BookReader/Constants.cs Normal file
View File

@@ -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";
}
}

View File

@@ -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;
}
}

View File

@@ -8,7 +8,7 @@ public class ProgressToWidthConverter : IValueConverter
{ {
if (value is double progress) 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)) if (parameter is string paramStr && double.TryParse(paramStr, out var max))
maxWidth = max; maxWidth = max;
return progress * maxWidth; return progress * maxWidth;

View File

@@ -30,6 +30,7 @@ public static class MauiProgram
builder.Services.AddSingleton<IDatabaseService, DatabaseService>(); builder.Services.AddSingleton<IDatabaseService, DatabaseService>();
builder.Services.AddSingleton<IBookParserService, BookParserService>(); builder.Services.AddSingleton<IBookParserService, BookParserService>();
builder.Services.AddSingleton<ISettingsService, SettingsService>(); builder.Services.AddSingleton<ISettingsService, SettingsService>();
builder.Services.AddSingleton<INavigationService, NavigationService>();
// HTTP Client for Calibre // HTTP Client for Calibre
builder.Services.AddHttpClient<ICalibreWebService, CalibreWebService>(); builder.Services.AddHttpClient<ICalibreWebService, CalibreWebService>();

View File

@@ -1,4 +1,5 @@
using SQLite; using BookReader;
using SQLite;
namespace BookReader.Models; namespace BookReader.Models;
@@ -12,12 +13,12 @@ public class AppSettings
public static class SettingsKeys public static class SettingsKeys
{ {
public const string CalibreUrl = "CalibreUrl"; public const string CalibreUrl = Constants.StorageKeys.CalibreUrl;
public const string CalibreUsername = "CalibreUsername"; public const string CalibreUsername = Constants.StorageKeys.CalibreUsername;
public const string DefaultFontSize = "DefaultFontSize"; public const string DefaultFontSize = Constants.StorageKeys.DefaultFontSize;
public const string DefaultFontFamily = "DefaultFontFamily"; public const string DefaultFontFamily = Constants.StorageKeys.DefaultFontFamily;
public const string Theme = "Theme"; public const string Theme = Constants.StorageKeys.Theme;
public const string Brightness = "Brightness"; public const string Brightness = Constants.StorageKeys.Brightness;
} }
public static class SecureStorageKeys public static class SecureStorageKeys

View File

@@ -10,7 +10,6 @@
<!-- Custom Converters --> <!-- Custom Converters -->
<converters:ProgressToWidthConverter x:Key="ProgressToWidthConverter" /> <converters:ProgressToWidthConverter x:Key="ProgressToWidthConverter" />
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:ByteArrayToImageSourceConverter x:Key="ByteArrayToImageConverter" /> <converters:ByteArrayToImageSourceConverter x:Key="ByteArrayToImageConverter" />
<!-- Colors --> <!-- Colors -->

View File

@@ -46,10 +46,10 @@ public class BookParserService : IBookParserService
switch (extension) switch (extension)
{ {
case ".epub": case Constants.Files.EpubExtension:
await ParseEpubMetadataAsync(book); await ParseEpubMetadataAsync(book);
break; break;
case ".fb2": case Constants.Files.Fb2Extension:
await ParseFb2MetadataAsync(book); await ParseFb2MetadataAsync(book);
break; break;
default: default:
@@ -97,10 +97,10 @@ public class BookParserService : IBookParserService
try try
{ {
string xml; 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); 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) if (entry != null)
{ {
using var stream = entry.Open(); using var stream = entry.Open();

View File

@@ -15,7 +15,7 @@ public class CalibreWebService : ICalibreWebService
public CalibreWebService(HttpClient httpClient) public CalibreWebService(HttpClient httpClient)
{ {
_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) public void Configure(string url, string username, string password)
@@ -46,7 +46,7 @@ public class CalibreWebService : ICalibreWebService
} }
} }
public async Task<List<CalibreBook>> GetBooksAsync(string? searchQuery = null, int page = 0, int pageSize = 20) public async Task<List<CalibreBook>> GetBooksAsync(string? searchQuery = null, int page = 0, int pageSize = Constants.Network.CalibrePageSize)
{ {
var books = new List<CalibreBook>(); var books = new List<CalibreBook>();
@@ -125,7 +125,7 @@ public class CalibreWebService : ICalibreWebService
using var contentStream = await response.Content.ReadAsStreamAsync(); using var contentStream = await response.Content.ReadAsStreamAsync();
using var fileStream = File.Create(filePath); using var fileStream = File.Create(filePath);
var buffer = new byte[8192]; var buffer = new byte[Constants.Network.DownloadBufferSize];
int read; int read;
while ((read = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0) while ((read = await contentStream.ReadAsync(buffer, 0, buffer.Length)) > 0)

View File

@@ -9,38 +9,41 @@ public class DatabaseService : IDatabaseService
{ {
private SQLiteAsyncConnection? _database; private SQLiteAsyncConnection? _database;
private readonly string _dbPath; private readonly string _dbPath;
private readonly AsyncRetryPolicy _retryPolicy; private readonly ResiliencePipeline _retryPipeline;
public DatabaseService() public DatabaseService()
{ {
_dbPath = Path.Combine(FileSystem.AppDataDirectory, "bookreader.db3"); _dbPath = Path.Combine(FileSystem.AppDataDirectory, "bookreader.db3");
// Polly retry policy: 3 попытки с экспоненциальной задержкой // Polly 8.x retry pipeline: 3 попытки с экспоненциальной задержкой
_retryPolicy = Policy _retryPipeline = new ResiliencePipelineBuilder()
.Handle<SQLiteException>() .AddRetry(new RetryStrategyOptions
.Or<Exception>(ex => ex.Message.Contains("database is locked") || ex.Message.Contains("busy")) {
.WaitAndRetryAsync( ShouldHandle = new PredicateBuilder().Handle<SQLiteException>()
retryCount: 3, .Handle<Exception>(ex => ex.Message.Contains("database is locked") || ex.Message.Contains("busy")),
sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)), MaxRetryAttempts = 3,
onRetry: (exception, timeSpan, retryNumber, context) => DelayGenerator = context => new ValueTask<TimeSpan?>(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; if (_database != null) return;
_database = new SQLiteAsyncConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache); _database = new SQLiteAsyncConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache);
await _retryPolicy.ExecuteAsync(async () => await _retryPipeline.ExecuteAsync(async (token) =>
{ {
await _database!.CreateTableAsync<Book>(); await _database!.CreateTableAsync<Book>();
await _database!.CreateTableAsync<AppSettings>(); await _database!.CreateTableAsync<AppSettings>();
await _database!.CreateTableAsync<ReadingProgress>(); await _database!.CreateTableAsync<ReadingProgress>();
}); }, ct);
} }
private async Task EnsureInitializedAsync() private async Task EnsureInitializedAsync()
@@ -50,39 +53,39 @@ public class DatabaseService : IDatabaseService
} }
// Books // Books
public async Task<List<Book>> GetAllBooksAsync() public async Task<List<Book>> GetAllBooksAsync(CancellationToken ct = default)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () => return await _retryPipeline.ExecuteAsync(async (token) =>
await _database!.Table<Book>().OrderByDescending(b => b.LastRead).ToListAsync()); await _database!.Table<Book>().OrderByDescending(b => b.LastRead).ToListAsync(), ct);
} }
public async Task<Book?> GetBookByIdAsync(int id) public async Task<Book?> GetBookByIdAsync(int id, CancellationToken ct = default)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () => return await _retryPipeline.ExecuteAsync(async (token) =>
await _database!.Table<Book>().Where(b => b.Id == id).FirstOrDefaultAsync()); await _database!.Table<Book>().Where(b => b.Id == id).FirstOrDefaultAsync(), ct);
} }
public async Task<int> SaveBookAsync(Book book) public async Task<int> SaveBookAsync(Book book, CancellationToken ct = default)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () => return await _retryPipeline.ExecuteAsync(async (token) =>
{ {
if (book.Id != 0) if (book.Id != 0)
return await _database!.UpdateAsync(book); return await _database!.UpdateAsync(book);
return await _database!.InsertAsync(book); return await _database!.InsertAsync(book);
}); }, ct);
} }
public async Task<int> UpdateBookAsync(Book book) public async Task<int> UpdateBookAsync(Book book, CancellationToken ct = default)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () => return await _retryPipeline.ExecuteAsync(async (token) =>
await _database!.UpdateAsync(book)); await _database!.UpdateAsync(book), ct);
} }
public async Task<int> DeleteBookAsync(Book book) public async Task<int> DeleteBookAsync(Book book, CancellationToken ct = default)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
@@ -93,28 +96,28 @@ public class DatabaseService : IDatabaseService
} }
// Delete progress records // Delete progress records
await _retryPolicy.ExecuteAsync(async () => await _retryPipeline.ExecuteAsync(async (token) =>
await _database!.Table<ReadingProgress>().DeleteAsync(p => p.BookId == book.Id)); await _database!.Table<ReadingProgress>().DeleteAsync(p => p.BookId == book.Id), ct);
return await _retryPolicy.ExecuteAsync(async () => return await _retryPipeline.ExecuteAsync(async (token) =>
await _database!.DeleteAsync(book)); await _database!.DeleteAsync(book), ct);
} }
// Settings // Settings
public async Task<string?> GetSettingAsync(string key) public async Task<string?> GetSettingAsync(string key, CancellationToken ct = default)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () => return await _retryPipeline.ExecuteAsync(async (token) =>
{ {
var setting = await _database!.Table<AppSettings>().Where(s => s.Key == key).FirstOrDefaultAsync(); var setting = await _database!.Table<AppSettings>().Where(s => s.Key == key).FirstOrDefaultAsync();
return setting?.Value; 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 EnsureInitializedAsync();
await _retryPolicy.ExecuteAsync(async () => await _retryPipeline.ExecuteAsync(async (token) =>
{ {
var existing = await _database!.Table<AppSettings>().Where(s => s.Key == key).FirstOrDefaultAsync(); var existing = await _database!.Table<AppSettings>().Where(s => s.Key == key).FirstOrDefaultAsync();
if (existing != null) if (existing != null)
@@ -126,37 +129,37 @@ public class DatabaseService : IDatabaseService
{ {
await _database.InsertAsync(new AppSettings { Key = key, Value = value }); await _database.InsertAsync(new AppSettings { Key = key, Value = value });
} }
}); }, ct);
} }
public async Task<Dictionary<string, string>> GetAllSettingsAsync() public async Task<Dictionary<string, string>> GetAllSettingsAsync(CancellationToken ct = default)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () => return await _retryPipeline.ExecuteAsync(async (token) =>
{ {
var settings = await _database!.Table<AppSettings>().ToListAsync(); var settings = await _database!.Table<AppSettings>().ToListAsync();
return settings.ToDictionary(s => s.Key, s => s.Value); return settings.ToDictionary(s => s.Key, s => s.Value);
}); }, ct);
} }
// Reading Progress // Reading Progress
public async Task SaveProgressAsync(ReadingProgress progress) public async Task SaveProgressAsync(ReadingProgress progress, CancellationToken ct = default)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
await _retryPolicy.ExecuteAsync(async () => await _retryPipeline.ExecuteAsync(async (token) =>
{ {
progress.Timestamp = DateTime.UtcNow; progress.Timestamp = DateTime.UtcNow;
await _database!.InsertAsync(progress); await _database!.InsertAsync(progress);
}); }, ct);
} }
public async Task<ReadingProgress?> GetLatestProgressAsync(int bookId) public async Task<ReadingProgress?> GetLatestProgressAsync(int bookId, CancellationToken ct = default)
{ {
await EnsureInitializedAsync(); await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () => return await _retryPipeline.ExecuteAsync(async (token) =>
await _database!.Table<ReadingProgress>() await _database!.Table<ReadingProgress>()
.Where(p => p.BookId == bookId) .Where(p => p.BookId == bookId)
.OrderByDescending(p => p.Timestamp) .OrderByDescending(p => p.Timestamp)
.FirstOrDefaultAsync()); .FirstOrDefaultAsync(), ct);
} }
} }

View File

@@ -6,21 +6,21 @@ namespace BookReader.Services;
public interface IDatabaseService public interface IDatabaseService
{ {
Task InitializeAsync(); Task InitializeAsync(CancellationToken ct = default);
// Books // Books
Task<List<Book>> GetAllBooksAsync(); Task<List<Book>> GetAllBooksAsync(CancellationToken ct = default);
Task<Book?> GetBookByIdAsync(int id); Task<Book?> GetBookByIdAsync(int id, CancellationToken ct = default);
Task<int> SaveBookAsync(Book book); Task<int> SaveBookAsync(Book book, CancellationToken ct = default);
Task<int> UpdateBookAsync(Book book); Task<int> UpdateBookAsync(Book book, CancellationToken ct = default);
Task<int> DeleteBookAsync(Book book); Task<int> DeleteBookAsync(Book book, CancellationToken ct = default);
// Settings // Settings
Task<string?> GetSettingAsync(string key); Task<string?> GetSettingAsync(string key, CancellationToken ct = default);
Task SetSettingAsync(string key, string value); Task SetSettingAsync(string key, string value, CancellationToken ct = default);
Task<Dictionary<string, string>> GetAllSettingsAsync(); Task<Dictionary<string, string>> GetAllSettingsAsync(CancellationToken ct = default);
// Reading Progress // Reading Progress
Task SaveProgressAsync(ReadingProgress progress); Task SaveProgressAsync(ReadingProgress progress, CancellationToken ct = default);
Task<ReadingProgress?> GetLatestProgressAsync(int bookId); Task<ReadingProgress?> GetLatestProgressAsync(int bookId, CancellationToken ct = default);
} }

View File

@@ -0,0 +1,11 @@
namespace BookReader.Services;
public interface INavigationService
{
Task GoToAsync(string route);
Task GoToAsync(string route, IDictionary<string, object> parameters);
Task GoBackAsync();
Task DisplayAlertAsync(string title, string message, string cancel);
Task<bool> DisplayAlertAsync(string title, string message, string accept, string cancel);
Task<string?> DisplayActionSheetAsync(string title, string cancel, params string[] actions);
}

View File

@@ -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<string, object> 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<bool> DisplayAlertAsync(string title, string message, string accept, string cancel)
{
return Shell.Current.DisplayAlertAsync(title, message, accept, cancel);
}
public async Task<string?> DisplayActionSheetAsync(string title, string cancel, params string[] actions)
{
return await Shell.Current.DisplayActionSheetAsync(title, cancel, null, actions);
}
}

View File

@@ -0,0 +1,45 @@
namespace BookReader.Services;
public class Result<T>
{
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<T> Success(T value) => new(true, value, null, null);
public static Result<T> Failure(string errorMessage) => new(false, default, errorMessage, null);
public static Result<T> Failure(Exception exception) => new(false, default, exception.Message, exception);
public TResult Match<TResult>(Func<T, TResult> onSuccess, Func<string, TResult> 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<TResult>(Func<TResult> onSuccess, Func<string, TResult> onFailure) =>
IsSuccess ? onSuccess() : onFailure(ErrorMessage!);
}

View File

@@ -11,6 +11,7 @@ public partial class BookshelfViewModel : BaseViewModel
private readonly IDatabaseService _databaseService; private readonly IDatabaseService _databaseService;
private readonly IBookParserService _bookParserService; private readonly IBookParserService _bookParserService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly INavigationService _navigationService;
public ObservableCollection<Book> Books { get; } = new(); public ObservableCollection<Book> Books { get; } = new();
@@ -20,11 +21,13 @@ public partial class BookshelfViewModel : BaseViewModel
public BookshelfViewModel( public BookshelfViewModel(
IDatabaseService databaseService, IDatabaseService databaseService,
IBookParserService bookParserService, IBookParserService bookParserService,
ISettingsService settingsService) ISettingsService settingsService,
INavigationService navigationService)
{ {
_databaseService = databaseService; _databaseService = databaseService;
_bookParserService = bookParserService; _bookParserService = bookParserService;
_settingsService = settingsService; _settingsService = settingsService;
_navigationService = navigationService;
Title = "My Library"; Title = "My Library";
} }
@@ -75,7 +78,7 @@ public partial class BookshelfViewModel : BaseViewModel
var extension = Path.GetExtension(result.FileName).ToLowerInvariant(); var extension = Path.GetExtension(result.FileName).ToLowerInvariant();
if (extension != ".epub" && extension != ".fb2") 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; return;
} }
@@ -101,7 +104,7 @@ public partial class BookshelfViewModel : BaseViewModel
} }
catch (Exception ex) 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 finally
{ {
@@ -115,7 +118,7 @@ public partial class BookshelfViewModel : BaseViewModel
{ {
if (book == null) return; 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"); $"Are you sure you want to delete \"{book.Title}\"?", "Delete", "Cancel");
if (!confirm) return; if (!confirm) return;
@@ -128,7 +131,7 @@ public partial class BookshelfViewModel : BaseViewModel
} }
catch (Exception ex) 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 } { "Book", book }
}; };
await Shell.Current.GoToAsync("reader", navigationParameter); await _navigationService.GoToAsync("reader", navigationParameter);
} }
[RelayCommand] [RelayCommand]
public async Task OpenSettingsAsync() public async Task OpenSettingsAsync()
{ {
await Shell.Current.GoToAsync("settings"); await _navigationService.GoToAsync("settings");
} }
[RelayCommand] [RelayCommand]
public async Task OpenCalibreLibraryAsync() public async Task OpenCalibreLibraryAsync()
{ {
await Shell.Current.GoToAsync("calibre"); await _navigationService.GoToAsync("calibre");
} }
} }

View File

@@ -12,6 +12,7 @@ public partial class CalibreLibraryViewModel : BaseViewModel
private readonly IBookParserService _bookParserService; private readonly IBookParserService _bookParserService;
private readonly IDatabaseService _databaseService; private readonly IDatabaseService _databaseService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly INavigationService _navigationService;
public ObservableCollection<CalibreBook> Books { get; } = new(); public ObservableCollection<CalibreBook> Books { get; } = new();
@@ -30,12 +31,14 @@ public partial class CalibreLibraryViewModel : BaseViewModel
ICalibreWebService calibreWebService, ICalibreWebService calibreWebService,
IBookParserService bookParserService, IBookParserService bookParserService,
IDatabaseService databaseService, IDatabaseService databaseService,
ISettingsService settingsService) ISettingsService settingsService,
INavigationService navigationService)
{ {
_calibreWebService = calibreWebService; _calibreWebService = calibreWebService;
_bookParserService = bookParserService; _bookParserService = bookParserService;
_databaseService = databaseService; _databaseService = databaseService;
_settingsService = settingsService; _settingsService = settingsService;
_navigationService = navigationService;
Title = "Calibre Library"; Title = "Calibre Library";
} }
@@ -73,7 +76,7 @@ public partial class CalibreLibraryViewModel : BaseViewModel
} }
catch (Exception ex) 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 finally
{ {
@@ -136,11 +139,11 @@ public partial class CalibreLibraryViewModel : BaseViewModel
await _databaseService.UpdateBookAsync(book); await _databaseService.UpdateBookAsync(book);
DownloadStatus = "Download complete!"; 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) 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 finally
{ {
@@ -152,6 +155,6 @@ public partial class CalibreLibraryViewModel : BaseViewModel
[RelayCommand] [RelayCommand]
public async Task OpenSettingsAsync() public async Task OpenSettingsAsync()
{ {
await Shell.Current.GoToAsync("settings"); await _navigationService.GoToAsync("settings");
} }
} }

View File

@@ -11,6 +11,7 @@ public partial class ReaderViewModel : BaseViewModel
{ {
private readonly IDatabaseService _databaseService; private readonly IDatabaseService _databaseService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly INavigationService _navigationService;
[ObservableProperty] [ObservableProperty]
private Book? _book; private Book? _book;
@@ -81,11 +82,15 @@ public partial class ReaderViewModel : BaseViewModel
public event Action<string>? OnJavaScriptRequested; public event Action<string>? OnJavaScriptRequested;
public event Action? OnBookReady; public event Action? OnBookReady;
public ReaderViewModel(IDatabaseService databaseService, ISettingsService settingsService) public ReaderViewModel(
IDatabaseService databaseService,
ISettingsService settingsService,
INavigationService navigationService)
{ {
_databaseService = databaseService; _databaseService = databaseService;
_settingsService = settingsService; _settingsService = settingsService;
_fontSize = 18; _navigationService = navigationService;
_fontSize = Constants.Reader.DefaultFontSize;
} }
public async Task InitializeAsync() public async Task InitializeAsync()
@@ -104,7 +109,7 @@ public partial class ReaderViewModel : BaseViewModel
return; 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"); var savedFontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif");
FontSize = savedFontSize; FontSize = savedFontSize;

View File

@@ -9,6 +9,7 @@ public partial class SettingsViewModel : BaseViewModel
{ {
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly ICalibreWebService _calibreWebService; private readonly ICalibreWebService _calibreWebService;
private readonly INavigationService _navigationService;
[ObservableProperty] [ObservableProperty]
private string _calibreUrl = string.Empty; private string _calibreUrl = string.Empty;
@@ -20,7 +21,7 @@ public partial class SettingsViewModel : BaseViewModel
private string _calibrePassword = string.Empty; private string _calibrePassword = string.Empty;
[ObservableProperty] [ObservableProperty]
private int _defaultFontSize = 18; private int _defaultFontSize = Constants.Reader.DefaultFontSize;
[ObservableProperty] [ObservableProperty]
private string _defaultFontFamily = "serif"; 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 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; _settingsService = settingsService;
_calibreWebService = calibreWebService; _calibreWebService = calibreWebService;
_navigationService = navigationService;
Title = "Settings"; Title = "Settings";
} }
@@ -55,7 +60,7 @@ public partial class SettingsViewModel : BaseViewModel
CalibreUrl = await _settingsService.GetAsync(SettingsKeys.CalibreUrl); CalibreUrl = await _settingsService.GetAsync(SettingsKeys.CalibreUrl);
CalibreUsername = await _settingsService.GetAsync(SettingsKeys.CalibreUsername); CalibreUsername = await _settingsService.GetAsync(SettingsKeys.CalibreUsername);
CalibrePassword = await _settingsService.GetSecurePasswordAsync(); 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"); DefaultFontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif");
} }
@@ -73,7 +78,7 @@ public partial class SettingsViewModel : BaseViewModel
_calibreWebService.Configure(CalibreUrl, CalibreUsername, CalibrePassword); _calibreWebService.Configure(CalibreUrl, CalibreUsername, CalibrePassword);
} }
await Shell.Current.DisplayAlert("Settings", "Settings saved successfully.", "OK"); await _navigationService.DisplayAlertAsync("Settings", "Settings saved successfully.", "OK");
} }
[RelayCommand] [RelayCommand]

View File

@@ -3,15 +3,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:BookReader.ViewModels" xmlns:vm="clr-namespace:BookReader.ViewModels"
xmlns:models="clr-namespace:BookReader.Models" 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:Class="BookReader.Views.BookshelfPage"
x:DataType="vm:BookshelfViewModel" x:DataType="vm:BookshelfViewModel"
Title="{Binding Title}" Title="{Binding Title}"
Shell.NavBarIsVisible="True"> Shell.NavBarIsVisible="True">
<ContentPage.Resources> <ContentPage.Resources>
<converters:ByteArrayToImageSourceConverter x:Key="ByteArrayToImageConverter" /> <toolkit:InvertedBoolConverter x:Key="InvertedBoolConverter" />
<converters:ProgressToWidthConverter x:Key="ProgressToWidthConverter" />
</ContentPage.Resources> </ContentPage.Resources>
<Shell.TitleView> <Shell.TitleView>

View File

@@ -1,3 +1,4 @@
using BookReader.Services;
using BookReader.ViewModels; using BookReader.ViewModels;
namespace BookReader.Views; namespace BookReader.Views;
@@ -5,11 +6,13 @@ namespace BookReader.Views;
public partial class BookshelfPage : ContentPage public partial class BookshelfPage : ContentPage
{ {
private readonly BookshelfViewModel _viewModel; private readonly BookshelfViewModel _viewModel;
private readonly INavigationService _navigationService;
public BookshelfPage(BookshelfViewModel viewModel) public BookshelfPage(BookshelfViewModel viewModel, INavigationService navigationService)
{ {
InitializeComponent(); InitializeComponent();
_viewModel = viewModel; _viewModel = viewModel;
_navigationService = navigationService;
BindingContext = viewModel; BindingContext = viewModel;
} }
@@ -21,7 +24,7 @@ public partial class BookshelfPage : ContentPage
private async void OnMenuClicked(object? sender, EventArgs e) 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"); "⚙️ Settings", "☁️ Calibre Library", " About");
switch (action) switch (action)
@@ -33,7 +36,7 @@ public partial class BookshelfPage : ContentPage
await _viewModel.OpenCalibreLibraryCommand.ExecuteAsync(null); await _viewModel.OpenCalibreLibraryCommand.ExecuteAsync(null);
break; break;
case " About": case " About":
await DisplayAlert("About", "BookReader v1.0\nEPUB & FB2 Reader", "OK"); await _navigationService.DisplayAlertAsync("About", "BookReader v1.0\nEPUB & FB2 Reader", "OK");
break; break;
} }
} }

View File

@@ -1,3 +1,4 @@
using BookReader.Services;
using BookReader.ViewModels; using BookReader.ViewModels;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@@ -6,16 +7,16 @@ namespace BookReader.Views;
public partial class ReaderPage : ContentPage public partial class ReaderPage : ContentPage
{ {
private readonly ReaderViewModel _viewModel; private readonly ReaderViewModel _viewModel;
private readonly INavigationService _navigationService;
private bool _isBookLoaded; private bool _isBookLoaded;
private readonly List<JObject> _chapterData = new(); private readonly List<JObject> _chapterData = new();
private IDispatcherTimer? _pollTimer;
private IDispatcherTimer? _progressTimer;
private bool _isActive; private bool _isActive;
public ReaderPage(ReaderViewModel viewModel) public ReaderPage(ReaderViewModel viewModel, INavigationService navigationService)
{ {
InitializeComponent(); InitializeComponent();
_viewModel = viewModel; _viewModel = viewModel;
_navigationService = navigationService;
BindingContext = viewModel; BindingContext = viewModel;
_viewModel.OnJavaScriptRequested += OnJavaScriptRequested; _viewModel.OnJavaScriptRequested += OnJavaScriptRequested;
} }
@@ -72,7 +73,7 @@ public partial class ReaderPage : ContentPage
if (!File.Exists(book.FilePath)) if (!File.Exists(book.FilePath))
{ {
await DisplayAlert("Error", "Book file not found", "OK"); await _navigationService.DisplayAlertAsync("Error", "Book file not found", "OK");
return; return;
} }
@@ -124,7 +125,7 @@ public partial class ReaderPage : ContentPage
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine($"[Reader] Load error: {ex.Message}\n{ex.StackTrace}"); 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) private async void OnBackToLibrary(object? sender, EventArgs e)
{ {
await SaveCurrentProgress(); await SaveCurrentProgress();
await Shell.Current.GoToAsync(".."); await _navigationService.GoBackAsync();
} }
// ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ========== // ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========