qwen edit
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
24
BookReader/BookReader.sln
Normal 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
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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();
|
||||
// Запускаем генерацию с динамическим размером
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ public partial class ReaderPage : ContentPage
|
||||
protected override async void OnDisappearing()
|
||||
{
|
||||
_isActive = false;
|
||||
_viewModel.OnJavaScriptRequested -= OnJavaScriptRequested;
|
||||
base.OnDisappearing();
|
||||
await SaveCurrentProgress();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user