qwen edit
This commit is contained in:
@@ -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
65
BookReader/Constants.cs
Normal 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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(
|
|
||||||
retryCount: 3,
|
|
||||||
sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)),
|
|
||||||
onRetry: (exception, timeSpan, retryNumber, context) =>
|
|
||||||
{
|
{
|
||||||
System.Diagnostics.Debug.WriteLine($"[Database] Retry {retryNumber} after {timeSpan.TotalMilliseconds}ms: {exception.Message}");
|
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 {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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
11
BookReader/Services/INavigationService.cs
Normal file
11
BookReader/Services/INavigationService.cs
Normal 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);
|
||||||
|
}
|
||||||
34
BookReader/Services/NavigationService.cs
Normal file
34
BookReader/Services/NavigationService.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
BookReader/Services/Result.cs
Normal file
45
BookReader/Services/Result.cs
Normal 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!);
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========
|
// ========== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ==========
|
||||||
|
|||||||
Reference in New Issue
Block a user