289 lines
9.3 KiB
C#
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");
|
|
}
|
|
}
|