From 55c620f5a33c3e9bf6c471eee65c9b962ff5c349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Wed, 18 Feb 2026 14:05:32 +0300 Subject: [PATCH] qwen edit --- BookReader/App.xaml.cs | 34 +++++- BookReader/BookReader.csproj | 1 + BookReader/BookReader.sln | 24 +++++ BookReader/Models/AppSettings.cs | 6 +- BookReader/Resources/Raw/wwwroot/index.html | 3 +- BookReader/Services/DatabaseService.cs | 100 ++++++++++++------ BookReader/Services/ISettingsService.cs | 5 + BookReader/Services/SettingsService.cs | 20 +++- .../ViewModels/CalibreLibraryViewModel.cs | 2 +- BookReader/ViewModels/ReaderViewModel.cs | 28 ++++- BookReader/ViewModels/SettingsViewModel.cs | 4 +- BookReader/Views/ReaderPage.xaml.cs | 1 + 12 files changed, 184 insertions(+), 44 deletions(-) create mode 100644 BookReader/BookReader.sln 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(); }