edit_codex

This commit is contained in:
Курнат Андрей
2026-03-08 22:07:04 +03:00
parent f50c918f10
commit 7393230696
28 changed files with 1686 additions and 1117 deletions

View File

@@ -1,5 +1,4 @@
using Android.Graphics.Fonts;
using BookReader.Models;
using BookReader.Models;
using BookReader.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -11,7 +10,13 @@ public partial class ReaderViewModel : BaseViewModel
{
private readonly IDatabaseService _databaseService;
private readonly ISettingsService _settingsService;
private readonly INavigationService _navigationService;
private double _lastPersistedProgress = -1;
private string _lastPersistedCfi = string.Empty;
private string _lastPersistedChapter = string.Empty;
private int _lastPersistedCurrentPage = -1;
private int _lastPersistedTotalPages = -1;
private DateTime _lastPersistedAt = DateTime.MinValue;
[ObservableProperty]
private Book? _book;
@@ -28,6 +33,12 @@ public partial class ReaderViewModel : BaseViewModel
[ObservableProperty]
private string _fontFamily = "serif";
[ObservableProperty]
private string _readerTheme = Constants.Reader.DefaultTheme;
[ObservableProperty]
private double _brightness = Constants.Reader.DefaultBrightness;
[ObservableProperty]
private List<string> _chapters = new();
@@ -44,19 +55,18 @@ public partial class ReaderViewModel : BaseViewModel
private int _currentPage = 1;
[ObservableProperty]
private int _totalPages = 1;
private int _totalPages = 100;
// Это свойство будет обновляться автоматически при изменении любого из полей выше
public string ChapterProgressText => $"{ChapterCurrentPage} из {ChapterTotalPages}";
public string ChapterProgressText => ChapterTotalPages > 1
? $"Глава: {ChapterCurrentPage} из {ChapterTotalPages}"
: "Позиция внутри главы появится после перелистывания";
// Это свойство показывает процент прогресса
public string ProgressText => $"{CurrentPage}%";
public string ProgressText => TotalPages == 100
? $"{CurrentPage}%"
: $"Стр. {CurrentPage} из {TotalPages}";
// Чтобы ChapterProgressText уведомлял интерфейс, добавим частичные методы (особенность Toolkit)
partial void OnChapterCurrentPageChanged(int value) => OnPropertyChanged(nameof(ChapterProgressText));
partial void OnChapterTotalPagesChanged(int value) => OnPropertyChanged(nameof(ChapterProgressText));
// Чтобы ProgressText уведомлял интерфейс, добавим частичные методы (особенность Toolkit)
partial void OnCurrentPageChanged(int value) => OnPropertyChanged(nameof(ProgressText));
partial void OnTotalPagesChanged(int value) => OnPropertyChanged(nameof(ProgressText));
@@ -78,42 +88,40 @@ public partial class ReaderViewModel : BaseViewModel
12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 36, 40
};
// Events for the view to subscribe to
public event Action<string>? OnJavaScriptRequested;
public event Action? OnBookReady;
public ReaderViewModel(
IDatabaseService databaseService,
ISettingsService settingsService,
INavigationService navigationService)
ISettingsService settingsService)
{
_databaseService = databaseService;
_settingsService = settingsService;
_navigationService = navigationService;
_fontSize = Constants.Reader.DefaultFontSize;
}
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, Constants.Reader.DefaultFontSize);
var savedFontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif");
FontSize = await _settingsService.GetIntAsync(SettingsKeys.DefaultFontSize, Constants.Reader.DefaultFontSize);
FontFamily = await _settingsService.GetAsync(SettingsKeys.DefaultFontFamily, "serif");
ReaderTheme = await _settingsService.GetAsync(SettingsKeys.Theme, Constants.Reader.DefaultTheme);
Brightness = await _settingsService.GetDoubleAsync(SettingsKeys.Brightness, Constants.Reader.DefaultBrightness);
FontSize = savedFontSize;
FontFamily = savedFontFamily;
CurrentPage = Book.CurrentPage > 0 ? Book.CurrentPage : 1;
TotalPages = Book.TotalPages > 0 ? Book.TotalPages : 100;
RememberPersistedProgress(Book.ReadingProgress, Book.LastCfi, Book.LastChapter, Book.CurrentPage, Book.TotalPages);
}
[RelayCommand]
@@ -121,7 +129,9 @@ public partial class ReaderViewModel : BaseViewModel
{
IsMenuVisible = !IsMenuVisible;
if (!IsMenuVisible)
{
IsChapterListVisible = false;
}
}
[RelayCommand]
@@ -149,14 +159,34 @@ public partial class ReaderViewModel : BaseViewModel
public void ChangeFontFamily(string family)
{
FontFamily = family;
OnJavaScriptRequested?.Invoke($"setFontFamily('{family}')");
OnJavaScriptRequested?.Invoke($"setFontFamily('{EscapeJs(family)}')");
_ = _settingsService.SetAsync(SettingsKeys.DefaultFontFamily, family);
}
[RelayCommand]
public void ChangeReaderTheme(string theme)
{
ReaderTheme = theme;
OnJavaScriptRequested?.Invoke($"setReaderTheme('{EscapeJs(theme)}')");
_ = _settingsService.SetAsync(SettingsKeys.Theme, theme);
}
[RelayCommand]
public void ChangeBrightness(double brightness)
{
Brightness = Math.Clamp(brightness, Constants.Reader.MinBrightness, Constants.Reader.MaxBrightness);
OnJavaScriptRequested?.Invoke($"setBrightness({Brightness.ToString(System.Globalization.CultureInfo.InvariantCulture)})");
_ = _settingsService.SetDoubleAsync(SettingsKeys.Brightness, Brightness);
}
[RelayCommand]
public void GoToChapter(string chapter)
{
if (string.IsNullOrEmpty(chapter)) return;
if (string.IsNullOrEmpty(chapter))
{
return;
}
OnJavaScriptRequested?.Invoke($"goToChapter('{EscapeJs(chapter)}')");
IsChapterListVisible = false;
IsMenuVisible = false;
@@ -169,11 +199,12 @@ public partial class ReaderViewModel : BaseViewModel
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)
public async Task SaveProgressAsync(double progress, string? cfi, string? chapter, int currentPage, int totalPages, bool force = false)
{
if (Book == null)
{
@@ -181,8 +212,13 @@ public partial class ReaderViewModel : BaseViewModel
return;
}
// Важно: если CFI пустой, не перезаписываем старый прогресс (защита от багов JS)
if (string.IsNullOrEmpty(cfi) && progress <= 0) return;
if (string.IsNullOrEmpty(cfi) && progress <= 0)
{
return;
}
CurrentPage = currentPage > 0 ? currentPage : CurrentPage;
TotalPages = totalPages > 0 ? totalPages : TotalPages;
Book.ReadingProgress = progress;
Book.LastCfi = cfi;
@@ -191,6 +227,23 @@ public partial class ReaderViewModel : BaseViewModel
Book.TotalPages = totalPages;
Book.LastRead = DateTime.UtcNow;
var hasMeaningfulChange = HasMeaningfulProgressChange(progress, cfi, chapter, currentPage, totalPages);
if (!force)
{
var throttle = TimeSpan.FromSeconds(Constants.Reader.ProgressSaveThrottleSeconds);
var shouldPersistNow = hasMeaningfulChange &&
(DateTime.UtcNow - _lastPersistedAt >= throttle || Math.Abs(progress - _lastPersistedProgress) >= 0.02 || currentPage != _lastPersistedCurrentPage);
if (!shouldPersistNow)
{
return;
}
}
else if (!hasMeaningfulChange)
{
return;
}
await _databaseService.UpdateBookAsync(Book);
await _databaseService.SaveProgressAsync(new ReadingProgress
@@ -201,30 +254,35 @@ public partial class ReaderViewModel : BaseViewModel
CurrentPage = currentPage,
ChapterTitle = chapter
});
RememberPersistedProgress(progress, cfi, chapter, currentPage, totalPages);
}
public string GetBookFilePath()
public string? GetLastCfi() => Book?.LastCfi;
public string? GetLocations() => Book?.Locations;
private bool HasMeaningfulProgressChange(double progress, string? cfi, string? chapter, int currentPage, int totalPages)
{
return Book?.FilePath ?? string.Empty;
return Math.Abs(progress - _lastPersistedProgress) >= 0.005 ||
string.Equals(cfi ?? string.Empty, _lastPersistedCfi, StringComparison.Ordinal) == false ||
string.Equals(chapter ?? string.Empty, _lastPersistedChapter, StringComparison.Ordinal) == false ||
currentPage != _lastPersistedCurrentPage ||
totalPages != _lastPersistedTotalPages;
}
public string GetBookFormat()
private void RememberPersistedProgress(double progress, string? cfi, string? chapter, int currentPage, int totalPages)
{
return Book?.Format ?? "epub";
}
public string? GetLastCfi()
{
return Book?.LastCfi;
}
public string? GetLocations()
{
return Book?.Locations;
_lastPersistedProgress = progress;
_lastPersistedCfi = cfi ?? string.Empty;
_lastPersistedChapter = chapter ?? string.Empty;
_lastPersistedCurrentPage = currentPage;
_lastPersistedTotalPages = totalPages;
_lastPersistedAt = DateTime.UtcNow;
}
private static string EscapeJs(string value)
{
return value.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\n", "\\n").Replace("\r", "\\r");
}
}
}