diff --git a/BookReader/App.xaml.cs b/BookReader/App.xaml.cs
index fa5295e..516e50c 100644
--- a/BookReader/App.xaml.cs
+++ b/BookReader/App.xaml.cs
@@ -4,16 +4,44 @@ namespace BookReader;
public partial class App : Application
{
+ private readonly IDatabaseService _databaseService;
+ private bool _isInitialized;
+
public App(IDatabaseService databaseService)
{
InitializeComponent();
-
- // Initialize database
- Task.Run(async () => await databaseService.InitializeAsync()).Wait();
+ _databaseService = databaseService;
}
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}
+
+ protected override async void OnStart()
+ {
+ base.OnStart();
+ await InitializeDatabaseAsync();
+ }
+
+ protected override async void OnResume()
+ {
+ base.OnResume();
+ await InitializeDatabaseAsync();
+ }
+
+ private async Task InitializeDatabaseAsync()
+ {
+ if (_isInitialized) return;
+
+ try
+ {
+ await _databaseService.InitializeAsync();
+ _isInitialized = true;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[App] Database initialization error: {ex.Message}");
+ }
+ }
}
\ No newline at end of file
diff --git a/BookReader/BookReader.csproj b/BookReader/BookReader.csproj
index 1f87f06..6aeb8a8 100644
--- a/BookReader/BookReader.csproj
+++ b/BookReader/BookReader.csproj
@@ -31,6 +31,7 @@
+
diff --git a/BookReader/BookReader.sln b/BookReader/BookReader.sln
new file mode 100644
index 0000000..a3b7eca
--- /dev/null
+++ b/BookReader/BookReader.sln
@@ -0,0 +1,24 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.2.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookReader", "BookReader.csproj", "{D0445465-3484-0365-406D-0D3744906997}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D0445465-3484-0365-406D-0D3744906997}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D0445465-3484-0365-406D-0D3744906997}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D0445465-3484-0365-406D-0D3744906997}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D0445465-3484-0365-406D-0D3744906997}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {49109510-50D7-44BA-B746-6E5BC1E08F6B}
+ EndGlobalSection
+EndGlobal
diff --git a/BookReader/Models/AppSettings.cs b/BookReader/Models/AppSettings.cs
index acb104e..2e29425 100644
--- a/BookReader/Models/AppSettings.cs
+++ b/BookReader/Models/AppSettings.cs
@@ -14,9 +14,13 @@ public static class SettingsKeys
{
public const string CalibreUrl = "CalibreUrl";
public const string CalibreUsername = "CalibreUsername";
- public const string CalibrePassword = "CalibrePassword";
public const string DefaultFontSize = "DefaultFontSize";
public const string DefaultFontFamily = "DefaultFontFamily";
public const string Theme = "Theme";
public const string Brightness = "Brightness";
+}
+
+public static class SecureStorageKeys
+{
+ public const string CalibrePassword = "calibre_password_secure";
}
\ No newline at end of file
diff --git a/BookReader/Resources/Raw/wwwroot/index.html b/BookReader/Resources/Raw/wwwroot/index.html
index b6e4f3f..b86587c 100644
--- a/BookReader/Resources/Raw/wwwroot/index.html
+++ b/BookReader/Resources/Raw/wwwroot/index.html
@@ -217,7 +217,7 @@
// 5. Ограничиваем значения для стабильности epub.js
// Минимум 400 (чтобы не плодить тысячи локаций на маленьких текстах)
// Максимум 1500 (чтобы избежать "залипания" на больших экранах)
- const finalSize = Math.max(400, Math.min(1500, charactersPerScreen));
+ const finalSize = Math.max(400, Math.min(1000, charactersPerScreen));
debugLog(`Расчет локации: Экран ${width}x${height}, Шрифт ${fontSize}px => Размер локации: ${finalSize}`);
@@ -324,7 +324,6 @@
state.totalPages = state.book.locations.length();
sendMessage('bookReady', { totalPages: state.totalPages });
} else {
- debugLog("Кэша нет, считаем в фоне...");
// Используем setTimeout, чтобы не блокировать поток отрисовки
const dynamicSize = calculateOptimalLocationSize();
// Запускаем генерацию с динамическим размером
diff --git a/BookReader/Services/DatabaseService.cs b/BookReader/Services/DatabaseService.cs
index 95a61c0..b4f1db4 100644
--- a/BookReader/Services/DatabaseService.cs
+++ b/BookReader/Services/DatabaseService.cs
@@ -1,4 +1,6 @@
using BookReader.Models;
+using Polly;
+using Polly.Retry;
using SQLite;
namespace BookReader.Services;
@@ -7,10 +9,24 @@ public class DatabaseService : IDatabaseService
{
private SQLiteAsyncConnection? _database;
private readonly string _dbPath;
+ private readonly AsyncRetryPolicy _retryPolicy;
public DatabaseService()
{
_dbPath = Path.Combine(FileSystem.AppDataDirectory, "bookreader.db3");
+
+ // Polly retry policy: 3 попытки с экспоненциальной задержкой
+ _retryPolicy = Policy
+ .Handle()
+ .Or(ex => ex.Message.Contains("database is locked") || ex.Message.Contains("busy"))
+ .WaitAndRetryAsync(
+ retryCount: 3,
+ sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt)),
+ onRetry: (exception, timeSpan, retryNumber, context) =>
+ {
+ System.Diagnostics.Debug.WriteLine($"[Database] Retry {retryNumber} after {timeSpan.TotalMilliseconds}ms: {exception.Message}");
+ }
+ );
}
public async Task InitializeAsync()
@@ -19,9 +35,12 @@ public class DatabaseService : IDatabaseService
_database = new SQLiteAsyncConnection(_dbPath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache);
- await _database.CreateTableAsync();
- await _database.CreateTableAsync();
- await _database.CreateTableAsync();
+ await _retryPolicy.ExecuteAsync(async () =>
+ {
+ await _database!.CreateTableAsync();
+ await _database!.CreateTableAsync();
+ await _database!.CreateTableAsync();
+ });
}
private async Task EnsureInitializedAsync()
@@ -34,27 +53,33 @@ public class DatabaseService : IDatabaseService
public async Task> GetAllBooksAsync()
{
await EnsureInitializedAsync();
- return await _database!.Table().OrderByDescending(b => b.LastRead).ToListAsync();
+ return await _retryPolicy.ExecuteAsync(async () =>
+ await _database!.Table().OrderByDescending(b => b.LastRead).ToListAsync());
}
public async Task GetBookByIdAsync(int id)
{
await EnsureInitializedAsync();
- return await _database!.Table().Where(b => b.Id == id).FirstOrDefaultAsync();
+ return await _retryPolicy.ExecuteAsync(async () =>
+ await _database!.Table().Where(b => b.Id == id).FirstOrDefaultAsync());
}
public async Task SaveBookAsync(Book book)
{
await EnsureInitializedAsync();
- if (book.Id != 0)
- return await _database!.UpdateAsync(book);
- return await _database!.InsertAsync(book);
+ return await _retryPolicy.ExecuteAsync(async () =>
+ {
+ if (book.Id != 0)
+ return await _database!.UpdateAsync(book);
+ return await _database!.InsertAsync(book);
+ });
}
public async Task UpdateBookAsync(Book book)
{
await EnsureInitializedAsync();
- return await _database!.UpdateAsync(book);
+ return await _retryPolicy.ExecuteAsync(async () =>
+ await _database!.UpdateAsync(book));
}
public async Task DeleteBookAsync(Book book)
@@ -68,55 +93,70 @@ public class DatabaseService : IDatabaseService
}
// Delete progress records
- await _database!.Table().DeleteAsync(p => p.BookId == book.Id);
+ await _retryPolicy.ExecuteAsync(async () =>
+ await _database!.Table().DeleteAsync(p => p.BookId == book.Id));
- return await _database!.DeleteAsync(book);
+ return await _retryPolicy.ExecuteAsync(async () =>
+ await _database!.DeleteAsync(book));
}
// Settings
public async Task GetSettingAsync(string key)
{
await EnsureInitializedAsync();
- var setting = await _database!.Table().Where(s => s.Key == key).FirstOrDefaultAsync();
- return setting?.Value;
+ return await _retryPolicy.ExecuteAsync(async () =>
+ {
+ var setting = await _database!.Table().Where(s => s.Key == key).FirstOrDefaultAsync();
+ return setting?.Value;
+ });
}
public async Task SetSettingAsync(string key, string value)
{
await EnsureInitializedAsync();
- var existing = await _database!.Table().Where(s => s.Key == key).FirstOrDefaultAsync();
- if (existing != null)
+ await _retryPolicy.ExecuteAsync(async () =>
{
- existing.Value = value;
- await _database.UpdateAsync(existing);
- }
- else
- {
- await _database.InsertAsync(new AppSettings { Key = key, Value = value });
- }
+ var existing = await _database!.Table().Where(s => s.Key == key).FirstOrDefaultAsync();
+ if (existing != null)
+ {
+ existing.Value = value;
+ await _database.UpdateAsync(existing);
+ }
+ else
+ {
+ await _database.InsertAsync(new AppSettings { Key = key, Value = value });
+ }
+ });
}
public async Task> GetAllSettingsAsync()
{
await EnsureInitializedAsync();
- var settings = await _database!.Table().ToListAsync();
- return settings.ToDictionary(s => s.Key, s => s.Value);
+ return await _retryPolicy.ExecuteAsync(async () =>
+ {
+ var settings = await _database!.Table().ToListAsync();
+ return settings.ToDictionary(s => s.Key, s => s.Value);
+ });
}
// Reading Progress
public async Task SaveProgressAsync(ReadingProgress progress)
{
await EnsureInitializedAsync();
- progress.Timestamp = DateTime.UtcNow;
- await _database!.InsertAsync(progress);
+ await _retryPolicy.ExecuteAsync(async () =>
+ {
+ progress.Timestamp = DateTime.UtcNow;
+ await _database!.InsertAsync(progress);
+ });
}
public async Task GetLatestProgressAsync(int bookId)
{
await EnsureInitializedAsync();
- return await _database!.Table()
- .Where(p => p.BookId == bookId)
- .OrderByDescending(p => p.Timestamp)
- .FirstOrDefaultAsync();
+ return await _retryPolicy.ExecuteAsync(async () =>
+ await _database!.Table()
+ .Where(p => p.BookId == bookId)
+ .OrderByDescending(p => p.Timestamp)
+ .FirstOrDefaultAsync());
}
}
\ No newline at end of file
diff --git a/BookReader/Services/ISettingsService.cs b/BookReader/Services/ISettingsService.cs
index 3daf552..520c3f6 100644
--- a/BookReader/Services/ISettingsService.cs
+++ b/BookReader/Services/ISettingsService.cs
@@ -7,4 +7,9 @@ public interface ISettingsService
Task GetIntAsync(string key, int defaultValue = 0);
Task SetIntAsync(string key, int value);
Task> GetAllAsync();
+
+ // Secure storage for sensitive data
+ Task SetSecurePasswordAsync(string password);
+ Task GetSecurePasswordAsync();
+ Task ClearSecurePasswordAsync();
}
\ No newline at end of file
diff --git a/BookReader/Services/SettingsService.cs b/BookReader/Services/SettingsService.cs
index 028bd85..e0b588d 100644
--- a/BookReader/Services/SettingsService.cs
+++ b/BookReader/Services/SettingsService.cs
@@ -1,4 +1,6 @@
-namespace BookReader.Services;
+using BookReader.Models;
+
+namespace BookReader.Services;
public class SettingsService : ISettingsService
{
@@ -37,4 +39,20 @@ public class SettingsService : ISettingsService
{
return await _databaseService.GetAllSettingsAsync();
}
+
+ public async Task SetSecurePasswordAsync(string password)
+ {
+ await SecureStorage.Default.SetAsync(SecureStorageKeys.CalibrePassword, password);
+ }
+
+ public async Task GetSecurePasswordAsync()
+ {
+ return await SecureStorage.Default.GetAsync(SecureStorageKeys.CalibrePassword);
+ }
+
+ public async Task ClearSecurePasswordAsync()
+ {
+ SecureStorage.Default.Remove(SecureStorageKeys.CalibrePassword);
+ await Task.CompletedTask;
+ }
}
\ No newline at end of file
diff --git a/BookReader/ViewModels/CalibreLibraryViewModel.cs b/BookReader/ViewModels/CalibreLibraryViewModel.cs
index 8187344..709284c 100644
--- a/BookReader/ViewModels/CalibreLibraryViewModel.cs
+++ b/BookReader/ViewModels/CalibreLibraryViewModel.cs
@@ -44,7 +44,7 @@ public partial class CalibreLibraryViewModel : BaseViewModel
{
var url = await _settingsService.GetAsync(SettingsKeys.CalibreUrl);
var username = await _settingsService.GetAsync(SettingsKeys.CalibreUsername);
- var password = await _settingsService.GetAsync(SettingsKeys.CalibrePassword);
+ var password = await _settingsService.GetSecurePasswordAsync();
IsConfigured = !string.IsNullOrWhiteSpace(url);
diff --git a/BookReader/ViewModels/ReaderViewModel.cs b/BookReader/ViewModels/ReaderViewModel.cs
index 5f9b0f3..98fb1da 100644
--- a/BookReader/ViewModels/ReaderViewModel.cs
+++ b/BookReader/ViewModels/ReaderViewModel.cs
@@ -90,6 +90,20 @@ public partial class ReaderViewModel : BaseViewModel
public async Task InitializeAsync()
{
+ // Валидация: книга должна быть загружена
+ if (Book == null)
+ {
+ System.Diagnostics.Debug.WriteLine("[ReaderViewModel] Book is null, cannot initialize");
+ return;
+ }
+
+ // Валидация: файл книги должен существовать
+ if (!File.Exists(Book.FilePath))
+ {
+ System.Diagnostics.Debug.WriteLine($"[ReaderViewModel] Book file not found: {Book.FilePath}");
+ return;
+ }
+
var savedFontSize = await _settingsService.GetIntAsync(SettingsKeys.DefaultFontSize, 18);
var savedFontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif");
@@ -145,15 +159,22 @@ public partial class ReaderViewModel : BaseViewModel
public async Task SaveLocationsAsync(string locations)
{
- if (Book == null) return;
+ if (Book == null)
+ {
+ System.Diagnostics.Debug.WriteLine("[ReaderViewModel] Cannot save locations: Book is null");
+ return;
+ }
Book.Locations = locations;
- // Сохраняем в базу данных
await _databaseService.UpdateBookAsync(Book);
}
public async Task SaveProgressAsync(double progress, string? cfi, string? chapter, int currentPage, int totalPages)
{
- if (Book == null) return;
+ if (Book == null)
+ {
+ System.Diagnostics.Debug.WriteLine("[ReaderViewModel] Cannot save progress: Book is null");
+ return;
+ }
// Важно: если CFI пустой, не перезаписываем старый прогресс (защита от багов JS)
if (string.IsNullOrEmpty(cfi) && progress <= 0) return;
@@ -165,7 +186,6 @@ public partial class ReaderViewModel : BaseViewModel
Book.TotalPages = totalPages;
Book.LastRead = DateTime.UtcNow;
- // Сохраняем в базу данных
await _databaseService.UpdateBookAsync(Book);
await _databaseService.SaveProgressAsync(new ReadingProgress
diff --git a/BookReader/ViewModels/SettingsViewModel.cs b/BookReader/ViewModels/SettingsViewModel.cs
index 172e928..8da61ec 100644
--- a/BookReader/ViewModels/SettingsViewModel.cs
+++ b/BookReader/ViewModels/SettingsViewModel.cs
@@ -54,7 +54,7 @@ public partial class SettingsViewModel : BaseViewModel
{
CalibreUrl = await _settingsService.GetAsync(SettingsKeys.CalibreUrl);
CalibreUsername = await _settingsService.GetAsync(SettingsKeys.CalibreUsername);
- CalibrePassword = await _settingsService.GetAsync(SettingsKeys.CalibrePassword);
+ CalibrePassword = await _settingsService.GetSecurePasswordAsync();
DefaultFontSize = await _settingsService.GetIntAsync(SettingsKeys.DefaultFontSize, 18);
DefaultFontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif");
}
@@ -64,7 +64,7 @@ public partial class SettingsViewModel : BaseViewModel
{
await _settingsService.SetAsync(SettingsKeys.CalibreUrl, CalibreUrl);
await _settingsService.SetAsync(SettingsKeys.CalibreUsername, CalibreUsername);
- await _settingsService.SetAsync(SettingsKeys.CalibrePassword, CalibrePassword);
+ await _settingsService.SetSecurePasswordAsync(CalibrePassword);
await _settingsService.SetIntAsync(SettingsKeys.DefaultFontSize, DefaultFontSize);
await _settingsService.SetAsync(SettingsKeys.DefaultFontFamily, DefaultFontFamily);
diff --git a/BookReader/Views/ReaderPage.xaml.cs b/BookReader/Views/ReaderPage.xaml.cs
index 4b5c18b..84f6db5 100644
--- a/BookReader/Views/ReaderPage.xaml.cs
+++ b/BookReader/Views/ReaderPage.xaml.cs
@@ -40,6 +40,7 @@ public partial class ReaderPage : ContentPage
protected override async void OnDisappearing()
{
_isActive = false;
+ _viewModel.OnJavaScriptRequested -= OnJavaScriptRequested;
base.OnDisappearing();
await SaveCurrentProgress();
}