qwen edit

This commit is contained in:
Курнат Андрей
2026-02-18 14:05:32 +03:00
parent 49d19cdbdb
commit 55c620f5a3
12 changed files with 184 additions and 44 deletions

View File

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

View File

@@ -31,6 +31,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="VersOne.Epub" Version="3.3.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.3" />
<PackageReference Include="Polly" Version="8.5.0" />
</ItemGroup>
<ItemGroup>

24
BookReader/BookReader.sln Normal file
View File

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

View File

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

View File

@@ -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();
// Запускаем генерацию с динамическим размером

View File

@@ -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<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) =>
{
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<Book>();
await _database.CreateTableAsync<AppSettings>();
await _database.CreateTableAsync<ReadingProgress>();
await _retryPolicy.ExecuteAsync(async () =>
{
await _database!.CreateTableAsync<Book>();
await _database!.CreateTableAsync<AppSettings>();
await _database!.CreateTableAsync<ReadingProgress>();
});
}
private async Task EnsureInitializedAsync()
@@ -34,27 +53,33 @@ public class DatabaseService : IDatabaseService
public async Task<List<Book>> GetAllBooksAsync()
{
await EnsureInitializedAsync();
return await _database!.Table<Book>().OrderByDescending(b => b.LastRead).ToListAsync();
return await _retryPolicy.ExecuteAsync(async () =>
await _database!.Table<Book>().OrderByDescending(b => b.LastRead).ToListAsync());
}
public async Task<Book?> GetBookByIdAsync(int id)
{
await EnsureInitializedAsync();
return await _database!.Table<Book>().Where(b => b.Id == id).FirstOrDefaultAsync();
return await _retryPolicy.ExecuteAsync(async () =>
await _database!.Table<Book>().Where(b => b.Id == id).FirstOrDefaultAsync());
}
public async Task<int> SaveBookAsync(Book book)
{
await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () =>
{
if (book.Id != 0)
return await _database!.UpdateAsync(book);
return await _database!.InsertAsync(book);
});
}
public async Task<int> UpdateBookAsync(Book book)
{
await EnsureInitializedAsync();
return await _database!.UpdateAsync(book);
return await _retryPolicy.ExecuteAsync(async () =>
await _database!.UpdateAsync(book));
}
public async Task<int> DeleteBookAsync(Book book)
@@ -68,22 +93,29 @@ public class DatabaseService : IDatabaseService
}
// Delete progress records
await _database!.Table<ReadingProgress>().DeleteAsync(p => p.BookId == book.Id);
await _retryPolicy.ExecuteAsync(async () =>
await _database!.Table<ReadingProgress>().DeleteAsync(p => p.BookId == book.Id));
return await _database!.DeleteAsync(book);
return await _retryPolicy.ExecuteAsync(async () =>
await _database!.DeleteAsync(book));
}
// Settings
public async Task<string?> GetSettingAsync(string key)
{
await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () =>
{
var setting = await _database!.Table<AppSettings>().Where(s => s.Key == key).FirstOrDefaultAsync();
return setting?.Value;
});
}
public async Task SetSettingAsync(string key, string value)
{
await EnsureInitializedAsync();
await _retryPolicy.ExecuteAsync(async () =>
{
var existing = await _database!.Table<AppSettings>().Where(s => s.Key == key).FirstOrDefaultAsync();
if (existing != null)
{
@@ -94,29 +126,37 @@ public class DatabaseService : IDatabaseService
{
await _database.InsertAsync(new AppSettings { Key = key, Value = value });
}
});
}
public async Task<Dictionary<string, string>> GetAllSettingsAsync()
{
await EnsureInitializedAsync();
return await _retryPolicy.ExecuteAsync(async () =>
{
var settings = await _database!.Table<AppSettings>().ToListAsync();
return settings.ToDictionary(s => s.Key, s => s.Value);
});
}
// Reading Progress
public async Task SaveProgressAsync(ReadingProgress progress)
{
await EnsureInitializedAsync();
await _retryPolicy.ExecuteAsync(async () =>
{
progress.Timestamp = DateTime.UtcNow;
await _database!.InsertAsync(progress);
});
}
public async Task<ReadingProgress?> GetLatestProgressAsync(int bookId)
{
await EnsureInitializedAsync();
return await _database!.Table<ReadingProgress>()
return await _retryPolicy.ExecuteAsync(async () =>
await _database!.Table<ReadingProgress>()
.Where(p => p.BookId == bookId)
.OrderByDescending(p => p.Timestamp)
.FirstOrDefaultAsync();
.FirstOrDefaultAsync());
}
}

View File

@@ -7,4 +7,9 @@ public interface ISettingsService
Task<int> GetIntAsync(string key, int defaultValue = 0);
Task SetIntAsync(string key, int value);
Task<Dictionary<string, string>> GetAllAsync();
// Secure storage for sensitive data
Task SetSecurePasswordAsync(string password);
Task<string> GetSecurePasswordAsync();
Task ClearSecurePasswordAsync();
}

View File

@@ -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<string> GetSecurePasswordAsync()
{
return await SecureStorage.Default.GetAsync(SecureStorageKeys.CalibrePassword);
}
public async Task ClearSecurePasswordAsync()
{
SecureStorage.Default.Remove(SecureStorageKeys.CalibrePassword);
await Task.CompletedTask;
}
}

View File

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

View File

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

View File

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

View File

@@ -40,6 +40,7 @@ public partial class ReaderPage : ContentPage
protected override async void OnDisappearing()
{
_isActive = false;
_viewModel.OnJavaScriptRequested -= OnJavaScriptRequested;
base.OnDisappearing();
await SaveCurrentProgress();
}