Files
BookReader/BookReader/ViewModels/ReaderViewModel.cs
Курнат Андрей 7393230696 edit_codex
2026-03-08 22:07:04 +03:00

289 lines
9.3 KiB
C#

using BookReader.Models;
using BookReader.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace BookReader.ViewModels;
[QueryProperty(nameof(Book), "Book")]
public partial class ReaderViewModel : BaseViewModel
{
private readonly IDatabaseService _databaseService;
private readonly ISettingsService _settingsService;
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;
[ObservableProperty]
private bool _isMenuVisible;
[ObservableProperty]
private bool _isChapterListVisible;
[ObservableProperty]
private int _fontSize;
[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();
[ObservableProperty]
private string? _selectedChapter;
[ObservableProperty]
private int _chapterCurrentPage = 1;
[ObservableProperty]
private int _chapterTotalPages = 1;
[ObservableProperty]
private int _currentPage = 1;
[ObservableProperty]
private int _totalPages = 100;
public string ChapterProgressText => ChapterTotalPages > 1
? $"Глава: {ChapterCurrentPage} из {ChapterTotalPages}"
: "Позиция внутри главы появится после перелистывания";
public string ProgressText => TotalPages == 100
? $"{CurrentPage}%"
: $"Стр. {CurrentPage} из {TotalPages}";
partial void OnChapterCurrentPageChanged(int value) => OnPropertyChanged(nameof(ChapterProgressText));
partial void OnChapterTotalPagesChanged(int value) => OnPropertyChanged(nameof(ChapterProgressText));
partial void OnCurrentPageChanged(int value) => OnPropertyChanged(nameof(ProgressText));
partial void OnTotalPagesChanged(int value) => OnPropertyChanged(nameof(ProgressText));
public List<string> AvailableFonts { get; } = new()
{
"serif",
"sans-serif",
"monospace",
"Georgia",
"Palatino",
"Times New Roman",
"Arial",
"Verdana",
"Courier New"
};
public List<int> AvailableFontSizes { get; } = new()
{
12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 36, 40
};
public event Action<string>? OnJavaScriptRequested;
public ReaderViewModel(
IDatabaseService databaseService,
ISettingsService settingsService)
{
_databaseService = databaseService;
_settingsService = settingsService;
_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;
}
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);
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]
public void ToggleMenu()
{
IsMenuVisible = !IsMenuVisible;
if (!IsMenuVisible)
{
IsChapterListVisible = false;
}
}
[RelayCommand]
public void HideMenu()
{
IsMenuVisible = false;
IsChapterListVisible = false;
}
[RelayCommand]
public void ToggleChapterList()
{
IsChapterListVisible = !IsChapterListVisible;
}
[RelayCommand]
public void ChangeFontSize(int size)
{
FontSize = size;
OnJavaScriptRequested?.Invoke($"setFontSize({size})");
_ = _settingsService.SetIntAsync(SettingsKeys.DefaultFontSize, size);
}
[RelayCommand]
public void ChangeFontFamily(string family)
{
FontFamily = 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;
}
OnJavaScriptRequested?.Invoke($"goToChapter('{EscapeJs(chapter)}')");
IsChapterListVisible = false;
IsMenuVisible = false;
}
public async Task SaveLocationsAsync(string locations)
{
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, bool force = false)
{
if (Book == null)
{
System.Diagnostics.Debug.WriteLine("[ReaderViewModel] Cannot save progress: Book is null");
return;
}
if (string.IsNullOrEmpty(cfi) && progress <= 0)
{
return;
}
CurrentPage = currentPage > 0 ? currentPage : CurrentPage;
TotalPages = totalPages > 0 ? totalPages : TotalPages;
Book.ReadingProgress = progress;
Book.LastCfi = cfi;
Book.LastChapter = chapter;
Book.CurrentPage = currentPage;
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
{
BookId = Book.Id,
Cfi = cfi,
Progress = progress,
CurrentPage = currentPage,
ChapterTitle = chapter
});
RememberPersistedProgress(progress, cfi, chapter, currentPage, totalPages);
}
public string? GetLastCfi() => Book?.LastCfi;
public string? GetLocations() => Book?.Locations;
private bool HasMeaningfulProgressChange(double progress, string? cfi, string? chapter, int currentPage, int totalPages)
{
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;
}
private void RememberPersistedProgress(double progress, string? cfi, string? chapter, int currentPage, int totalPages)
{
_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");
}
}