136 lines
6.1 KiB
Markdown
136 lines
6.1 KiB
Markdown
|
|
# Reading time estimates (ETA)
|
|||
|
|
|
|||
|
|
Komikku shows estimated reading time in two places:
|
|||
|
|
|
|||
|
|
| Surface | What it shows |
|
|||
|
|
|---------|----------------|
|
|||
|
|
| **Library** | Time-left badge per entry (unread chapters × average chapter duration). |
|
|||
|
|
| **Reader** | Series progress ETA (remaining time for the current series, including the active chapter). |
|
|||
|
|
|
|||
|
|
Both surfaces share the same **limits and defaults** via `ReadingEtaPreferences` → `ReadingEtaLimits`. Tunable in **Settings → Reader → Time estimate tuning** (`SettingsReaderScreen`, group `getReadingEtaTuningGroup`).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Very long chapters (one entry = one huge chapter)
|
|||
|
|
|
|||
|
|
**Reading itself has no artificial page cap.** The reader uses the real `totalPages` from the loaded chapter. If a source exposes an entire volume as a single chapter, that is one DB chapter with many pages — normal behaviour.
|
|||
|
|
|
|||
|
|
**Limits apply only to statistics and ETA**, not to loading or turning pages:
|
|||
|
|
|
|||
|
|
- Sessions shorter than ~2 s or longer than the configured *max trusted session* are ignored for speed stats.
|
|||
|
|
- When averaging read times, each chapter sample counts at most *max minutes per chapter sample* (default 45 min) so an idle tab does not inflate estimates.
|
|||
|
|
- History rows with more than *max pages per history sample* (default 2000) are skipped for **page-rate** priors (cross-series hints and ms/page blending).
|
|||
|
|
|
|||
|
|
For **very long chapters**, the reader ETA path that scales by **remaining pages × ms/page** can still show a long ETA if your real pace is slow — that is intentional. If estimates look **too short**, raise the stats/session caps in settings (e.g. 180 min sample cap, 1440 min trusted session, 5000+ pages sample).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Why there used to be two different logics
|
|||
|
|
|
|||
|
|
Before `f178a7919`, library and reader each duplicated constants in their ViewModels:
|
|||
|
|
|
|||
|
|
| Constant | Library (`LibraryScreenModel`) | Reader (`ReaderViewModel`) |
|
|||
|
|
|----------|-------------------------------|----------------------------|
|
|||
|
|
| Max trusted session | 1 h | 4 h |
|
|||
|
|
| Chapters sampled | 60 | 20 |
|
|||
|
|
|
|||
|
|
Same *idea* (trim outliers, cap per-chapter duration for averages), but **different numbers** → inconsistent badges vs reader ETA on the same manga.
|
|||
|
|
|
|||
|
|
The reader additionally had a **page-rate path** (`ReadingEtaPageRateEstimator` + cross-manga history); the library used **chapter-duration averages only**. That asymmetry is by design (library has no “current page”), but the **shared caps** are now unified.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Architecture (current)
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TB
|
|||
|
|
Prefs[ReadingEtaPreferences]
|
|||
|
|
Limits[ReadingEtaLimits]
|
|||
|
|
Lib[LibraryScreenModel]
|
|||
|
|
Reader[ReaderViewModel]
|
|||
|
|
Cross[GetCrossMangaPageRateHistorySamples]
|
|||
|
|
Est[ReadingEtaPageRateEstimator]
|
|||
|
|
|
|||
|
|
Prefs --> Limits
|
|||
|
|
Limits --> Lib
|
|||
|
|
Limits --> Reader
|
|||
|
|
Limits --> Cross
|
|||
|
|
Cross --> Reader
|
|||
|
|
Est --> Reader
|
|||
|
|
Prefs -->|limitsFlow| Lib
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Key types and files
|
|||
|
|
|
|||
|
|
| Piece | Path |
|
|||
|
|
|-------|------|
|
|||
|
|
| Defaults (code constants) | `domain/.../reading/ReadingEtaDefaults.kt` |
|
|||
|
|
| Effective limits snapshot | `domain/.../reading/ReadingEtaLimits.kt` |
|
|||
|
|
| User preferences | `domain/.../reading/service/ReadingEtaPreferences.kt` |
|
|||
|
|
| Page-rate blending | `domain/.../reading/ReadingEtaPageRateEstimator.kt` |
|
|||
|
|
| Cross-series history samples | `domain/.../history/interactor/GetCrossMangaPageRateHistorySamples.kt` |
|
|||
|
|
| Library ETA | `app/.../library/LibraryScreenModel.kt` |
|
|||
|
|
| Reader series ETA | `app/.../reader/ReaderViewModel.kt` |
|
|||
|
|
| Settings UI | `app/.../settings/screen/SettingsReaderScreen.kt` |
|
|||
|
|
|
|||
|
|
`ReadingEtaPreferences` is registered in `PreferenceModule`; `GetCrossMangaPageRateHistorySamples` takes it via Injekt (`DomainModule`).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## User settings
|
|||
|
|
|
|||
|
|
Preference keys (defaults in parentheses):
|
|||
|
|
|
|||
|
|
| Setting | Key | Default | Slider range (UI) |
|
|||
|
|
|---------|-----|---------|-------------------|
|
|||
|
|
| Max minutes per chapter sample | `pref_reading_eta_max_stats_min` | 45 | 5–180 |
|
|||
|
|
| Max trusted session length | `pref_reading_eta_max_trusted_session_min` | 240 (4 h) | 60–1440 |
|
|||
|
|
| Chapters sampled for averages | `pref_reading_eta_sample_chapter_limit` | 60 | 5–100 |
|
|||
|
|
| Max pages per history sample | `pref_reading_eta_max_pages_sample` | 2000 | 1000–50000 (slider 10–500 × 100) |
|
|||
|
|
|
|||
|
|
`ReadingEtaLimits.fromRaw` enforces:
|
|||
|
|
|
|||
|
|
- Trusted session ≥ stats cap (minutes).
|
|||
|
|
- Sample chapter count ∈ [5, 200].
|
|||
|
|
- Max pages ∈ [100, 50_000].
|
|||
|
|
|
|||
|
|
Strings: `i18n-kmk/.../base/strings.xml` and `de/strings.xml` (`pref_reading_eta_*`).
|
|||
|
|
|
|||
|
|
Reader-only toggles (unchanged): **Show series ETA**, **Persist series ETA** (`ReaderPreferences`).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## How estimates are computed
|
|||
|
|
|
|||
|
|
### Library badge
|
|||
|
|
|
|||
|
|
1. Load per-manga chapter read durations from history.
|
|||
|
|
2. Filter to trusted session range; cap each sample at *max chapter duration for stats*.
|
|||
|
|
3. Take the last *N* chapters (`etaSampleChapterLimit`), trim top/bottom 10% (when enough samples), average.
|
|||
|
|
4. Multiply by unread count (or `1` when `totalChapters == 1`).
|
|||
|
|
|
|||
|
|
Falls back to a **global** average across the visible library when a title has no usable samples.
|
|||
|
|
|
|||
|
|
### Reader series ETA
|
|||
|
|
|
|||
|
|
Two paths; the UI uses **page-based ETA when available**, else **chapter-based**:
|
|||
|
|
|
|||
|
|
1. **Page-based** (`calculatePageBasedEtaMillis`): ms/page from recent chapters in this series + optional blend with cross-manga priors (`ReadingEtaPageRateEstimator`). Projects `remainingPages × ms/page` plus whole chapters ahead using average pages/chapter.
|
|||
|
|
2. **Chapter-based**: average/projected chapter duration × remaining chapters, with projection from current session or progress.
|
|||
|
|
|
|||
|
|
Cross-manga priors: up to `CROSS_MANGA_PAGE_RATE_SAMPLE_LIMIT` (500) recent completed chapters from **other** series, preferring similar page counts (±15%, then ±30% band).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Release
|
|||
|
|
|
|||
|
|
Shipped in tag **`v1.13.6-forgejo.38`** — commit `feat(reading-eta): unify library and reader limits; make caps tunable`.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Maintainer notes
|
|||
|
|
|
|||
|
|
- When changing defaults, update `ReadingEtaDefaults`, `ReadingEtaPreferences` default getters, and the settings slider ranges if needed.
|
|||
|
|
- Keep library and reader on `readingEtaPreferences.currentLimits()` / `limitsFlow()` — do not reintroduce local `MIN_TRACKED_*` constants in ViewModels.
|
|||
|
|
- Page caps affect **stats only**; do not use them to limit `UpsertHistory` unless product intent changes.
|