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()
{
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)

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)
{
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;

View File

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

View File

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

View File

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

View File

@@ -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();

View File

@@ -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<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>();
@@ -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)

View File

@@ -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<SQLiteException>()
.Or<Exception>(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<SQLiteException>()
.Handle<Exception>(ex => ex.Message.Contains("database is locked") || ex.Message.Contains("busy")),
MaxRetryAttempts = 3,
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;
_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<AppSettings>();
await _database!.CreateTableAsync<ReadingProgress>();
});
}, ct);
}
private async Task EnsureInitializedAsync()
@@ -50,39 +53,39 @@ public class DatabaseService : IDatabaseService
}
// Books
public async Task<List<Book>> GetAllBooksAsync()
public async Task<List<Book>> GetAllBooksAsync(CancellationToken ct = default)
{
await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () =>
await _database!.Table<Book>().OrderByDescending(b => b.LastRead).ToListAsync());
return await _retryPipeline.ExecuteAsync(async (token) =>
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();
return await _retryPolicy.ExecuteAsync(async () =>
await _database!.Table<Book>().Where(b => b.Id == id).FirstOrDefaultAsync());
return await _retryPipeline.ExecuteAsync(async (token) =>
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();
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<int> UpdateBookAsync(Book book)
public async Task<int> 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<int> DeleteBookAsync(Book book)
public async Task<int> 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<ReadingProgress>().DeleteAsync(p => p.BookId == book.Id));
await _retryPipeline.ExecuteAsync(async (token) =>
await _database!.Table<ReadingProgress>().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<string?> GetSettingAsync(string key)
public async Task<string?> GetSettingAsync(string key, CancellationToken ct = default)
{
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();
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<AppSettings>().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<Dictionary<string, string>> GetAllSettingsAsync()
public async Task<Dictionary<string, string>> GetAllSettingsAsync(CancellationToken ct = default)
{
await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () =>
return await _retryPipeline.ExecuteAsync(async (token) =>
{
var settings = await _database!.Table<AppSettings>().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<ReadingProgress?> GetLatestProgressAsync(int bookId)
public async Task<ReadingProgress?> GetLatestProgressAsync(int bookId, CancellationToken ct = default)
{
await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () =>
return await _retryPipeline.ExecuteAsync(async (token) =>
await _database!.Table<ReadingProgress>()
.Where(p => p.BookId == bookId)
.OrderByDescending(p => p.Timestamp)
.FirstOrDefaultAsync());
.FirstOrDefaultAsync(), ct);
}
}

View File

@@ -6,21 +6,21 @@ namespace BookReader.Services;
public interface IDatabaseService
{
Task InitializeAsync();
Task InitializeAsync(CancellationToken ct = default);
// Books
Task<List<Book>> GetAllBooksAsync();
Task<Book?> GetBookByIdAsync(int id);
Task<int> SaveBookAsync(Book book);
Task<int> UpdateBookAsync(Book book);
Task<int> DeleteBookAsync(Book book);
Task<List<Book>> GetAllBooksAsync(CancellationToken ct = default);
Task<Book?> GetBookByIdAsync(int id, CancellationToken ct = default);
Task<int> SaveBookAsync(Book book, CancellationToken ct = default);
Task<int> UpdateBookAsync(Book book, CancellationToken ct = default);
Task<int> DeleteBookAsync(Book book, CancellationToken ct = default);
// Settings
Task<string?> GetSettingAsync(string key);
Task SetSettingAsync(string key, string value);
Task<Dictionary<string, string>> GetAllSettingsAsync();
Task<string?> GetSettingAsync(string key, CancellationToken ct = default);
Task SetSettingAsync(string key, string value, CancellationToken ct = default);
Task<Dictionary<string, string>> GetAllSettingsAsync(CancellationToken ct = default);
// Reading Progress
Task SaveProgressAsync(ReadingProgress progress);
Task<ReadingProgress?> GetLatestProgressAsync(int bookId);
Task SaveProgressAsync(ReadingProgress progress, CancellationToken ct = default);
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 IBookParserService _bookParserService;
private readonly ISettingsService _settingsService;
private readonly INavigationService _navigationService;
public ObservableCollection<Book> 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");
}
}

View File

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

View File

@@ -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<string>? 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;

View File

@@ -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]

View File

@@ -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">
<ContentPage.Resources>
<converters:ByteArrayToImageSourceConverter x:Key="ByteArrayToImageConverter" />
<converters:ProgressToWidthConverter x:Key="ProgressToWidthConverter" />
<toolkit:InvertedBoolConverter x:Key="InvertedBoolConverter" />
</ContentPage.Resources>
<Shell.TitleView>

View File

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

View File

@@ -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<JObject> _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();
}
// ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========