diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index d09f4f6..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/AGENTS.md b/AGENTS.md
deleted file mode 100644
index 6f0bc26..0000000
--- a/AGENTS.md
+++ /dev/null
@@ -1,76 +0,0 @@
-# Book Reader — Episteme fork + bookshelf-api/ABS
-
-> Форк `Aryan-Raj3112/episteme` под bookshelf-api + Audiobookshelf + Material You 3 (Myne-style UI).
-
-## Stack
-
-- **Язык:** Kotlin 100%
-- **UI:** Jetpack Compose + Material3 (Myne-style: round cards, dynamic color, minimal)
-- **DI:** Dagger Hilt
-- **БД:** Room
-- **Настройки:** DataStore
-- **Сеть:** Retrofit + OkHttp + Kotlinx Serialization
-- **Аудио:** ExoPlayer (Media3) + MediaSession
-- **Фоновые задачи:** WorkManager
-- **Читалка:** Episteme paginatedreader (CSS-движок, ContentBlock/Page модель)
-- **Форматы:** EPUB, FB2, PDF, MOBI/AZW3, DOCX, ODT, TXT, Markdown, HTML, comics
-- **minSdk:** 26 (Episteme default)
-- **compileSdk/targetSdk:** 36
-
-## Origin
-
-- **upstream:** `https://github.com/Aryan-Raj3112/episteme.git` (AGPL-3.0)
-- **origin:** `https://git.dueattendant149.org/Atte149/book-reader.git` (Forgejo)
-- **Лицензия:** AGPL-3.0 (наследуется от Episteme)
-
-## Goal
-
-- Взять зрелый reader engine из Episteme (пагинация, все форматы, TTS, annotations)
-- Redesign UI под Myne-стиль (Material You 3, round cards, dynamic color)
-- Заменить backend (OPDS/Gutenberg/LocalFolder) на bookshelf-api + ABS
-- Добавить фичи из Book's Story: audio player, RSVP, Librarr search, TTS jobs, cache, progress sync, upload
-
-## Layout
-
-- `app/src/main/` — Android app (после KMP→Android упрощения)
-- `app/src/oss/` — OSS flavor (без проприетарных ML Kit/cloud)
-- `app/src/releaseOffline/` — offline-only flavor
-- ~~`desktopApp/`~~ — удалён (Android-only)
-- ~~`shared/`~~ — слит в `app/src/main/` (после Этапа 1)
-
-## Backend
-
-- **bookshelf-api:** `https://books.dueattendant149.org/api/v1/*`
- - `GET /api/v1/books/libraries`
- - `GET /api/v1/books/{item_id}`
- - `GET /api/v1/books/library/{id}/items`
- - `POST /api/v1/search`
- - `POST /api/v1/tts`, `GET /api/v1/tts/{job_id}`
- - `POST /api/v1/download`, `GET /api/v1/download/list`
-- **Audiobookshelf (напрямую):**
- - `GET /api/items/{id}/file/{ino}`
- - `GET /api/items/{id}/cover`
- - `GET /api/items/{id}/download`
-
-## WARNs
-
-- **Не пушить в `upstream` (Aryan-Raj3112/episteme).** Только в `origin` (Forgejo).
-- **AGPL-3.0** — если публикуем, исходный код должен быть открыт (включая server-side modifications если есть network use).
-- **`pro` flavor** содержит проприетарные компоненты (ML Kit OCR, cloud sync) — не использовать, только `oss`.
-- **Секреты** (ABS token, API keys) — только в `.env` / DataStore, никогда в коде.
-
-## Build
-
-```bash
-./gradlew :app:assembleOssDebug
-```
-
-## Status
-
-- Phase 0: clone + package rename — completed
-- Phase 1: KMP → Android-only — pending
-- Phase 2: backend swap (bookshelf-api + ABS) — pending
-- Phase 3: UI redesign (Myne-style) — pending
-- Phase 4: features from Book's Story (audio/RSVP/search/TTS/cache) — pending
-- Phase 5: reader engine integration — pending
-- Phase 6: build, test, deploy — pending
\ No newline at end of file
diff --git a/README.md b/README.md
index b92844f..a6af8ee 100644
--- a/README.md
+++ b/README.md
@@ -5,63 +5,62 @@
Episteme Reader
- A modern, offline-first, privacy-focused document and e-book reader for Android and desktop, built with Kotlin Multiplatform and Compose.
+ A modern, offline‑first, privacy‑focused document & e‑book reader for Android, built with Kotlin and Jetpack Compose.
-
+
-
-
-
-
-
- Android
- |
-
-
-
- Desktop
- |
-
-
+
## Overview
-Episteme Reader is a customizable reader for documents, e-books, comics, and text-heavy files. The app is designed around local-first reading, deep typography controls, flexible layouts, and a consistent Kotlin Multiplatform core across Android and desktop.
+Episteme Reader is a comprehensive, customizable reader for documents and e-books on Android. It features a modern Jetpack Compose UI, powerful reading tools, and extensive theming.
-The same core reading experience is available across editions. The main differences are distribution channel, network access, and whether proprietary online services are included.
+To best serve different user preferences regarding privacy and network usage, Episteme Reader is available in three distinct editions:
+* **PlayStore Version:** The full-featured release which includes proprietary code and features.
+* **OSS Version (GitHub/F-Droid):** A fully open-source build.
+* **OSS Offline Version (GitHub):** A strictly offline build with network permissions completely removed.
-## Core Features
+---
-Available across supported editions unless noted in the edition table:
+## Feature Comparison
-* **Formats:** PDF, EPUB, MOBI/AZW3, FB2, DOCX, ODT/FODT, TXT, Markdown, HTML, and comic archives.
-* **Reading modes:** Paginated reading, vertical scroll, PDF multi-tab reading, PDF reflow, auto-scroll, and musician mode.
-* **PDF tools:** Ink annotations, highlighting, erasing, text annotations, and reading-focused PDF controls.
-* **Customization:** App themes, reader themes, custom local fonts, typography controls, spacing, margins, and layout tuning.
-* **Library tools:** Local folder sync, library organization, bookmarks, progress tracking, and file management.
-* **Accessibility:** System text-to-speech, app language selection, and reader settings that adapt to different reading preferences.
+### 📚 Supported Formats
+| Feature | PlayStore | OSS | OSS Offline |
+| :--- | :---: | :---: | :---: |
+| **Documents:** PDF, DOCX, ODT/FODT | ✅ | ✅ | ✅ |
+| **E-books & Text:** EPUB, MOBI, AZW3, FB2, MD, HTML, TXT | ✅ | ✅ | ✅ |
+| **Comics:** CBZ, CBR, CB7 | ✅ | ✅ | ✅ |
+| **View-Only:** CSV, TSV, JSON, XML, Logs, Code Files | ✅ | ✅ | ✅ |
-## Editions
+### 📖 Core Reading Experience
+| Feature | PlayStore | OSS | OSS Offline |
+| :--- | :---: | :---: | :---: |
+| **Display Modes:** Paginated & Vertical Scroll | ✅ | ✅ | ✅ |
+| **PDF Multi-Tab Reading & Reflow** | ✅ | ✅ | ✅ |
+| **PDF Annotations:** Ink (Pen, Highlight, Erase) & Text | ✅ | ✅ | ✅ |
+| **App-wide Customization & Reader Theming** | ✅ | ✅ | ✅ |
+| **Custom Fonts:** Local Import (TTF and OTF) | ✅ | ✅ | ✅ |
+| **Typography Control** | ✅ | ✅ | ✅ |
+| **Auto-Scroll & Musician Mode** | ✅ | ✅ | ✅ |
+| **System Text-to-Speech (TTS)** | ✅ | ✅ | ✅ |
-| Edition | Platform | Network access | Distribution | Notes |
-|---|---|---|---|---|
-| **Play Store** | Android | Online-capable | Google Play | Full Android release with proprietary extras such as ML Kit OCR, cloud sync, AI tools, cloud TTS, and PDF bubble zoom. |
-| **OSS** | Android | Online-capable | [epistemereader.com](https://epistemereader.com), GitHub, F-Droid, Obtainium | Fully open-source Android build with OPDS, downloadable fonts, and BYOK access to AI and cloud features. |
-| **OSS Offline** | Android | Offline-only | [epistemereader.com](https://epistemereader.com), GitHub, Obtainium | Open-source Android build with network permissions removed. |
-| **Standard** | Windows desktop | Online-capable | [epistemereader.com](https://epistemereader.com), GitHub | Full-featured desktop release with the shared KMP reader core and online-capable services. |
-| **Offline** | Windows desktop | Offline-only | [epistemereader.com](https://epistemereader.com), GitHub | Desktop build focused on local reading with online services disabled. |
+### ⚙️ Advanced & Network Features
+| Feature | PlayStore | OSS (F-Droid) | OSS Offline |
+| :--- | :---: | :---: | :---: |
+| **Local Folder Sync & Library Management** | ✅ | ✅ | ✅ |
+| **Download Google Fonts** | ✅ | ✅ | ❌ |
+| **OPDS Catalog Support** | ✅ | ✅ | ❌ |
+| **PDF Bubble Zoom Magnifier** | ✅ | ❌ | ❌ |
+| **ML Kit OCR** (Scanned PDF Text Selection) | ✅ | ❌ | ❌ |
+| **Cross-Device Cloud Sync** | ✅ | ❌ | ❌ |
+| **AI Tools** (Summaries, Story Recap, Dictionary) | ✅ | 🔜 | ❌ |
+| **Cloud Text-to-Speech** | ✅ | 🔜 | ❌ |
-Future desktop platforms can use the same Standard and Offline model as support expands.
-
-## Languages
-
-Episteme Reader currently supports: English, Arabic, Belarusian, German, Spanish, Estonian, French, Hindi, Indonesian, Italian, Japanese, Korean, Dutch, Polish, Portuguese (Brazil), Russian, Turkish, Ukrainian, Vietnamese, and Chinese Simplified.
-
-Want Episteme Reader in another language? Please request it through [GitHub Issues](https://github.com/Aryan-Raj3112/episteme/issues/new/choose) or start a thread in [Discussions](https://github.com/Aryan-Raj3112/episteme/discussions).
+---
## Building from Source
@@ -71,7 +70,7 @@ Want Episteme Reader in another language? Please request it through [GitHub Issu
cd episteme
```
-2. Build Android:
+2. Build:
* Open in Android Studio and run the `ossDebug` or `ossOfflineDebug` variant, or
* Build from the command line:
```bash
@@ -80,56 +79,43 @@ Want Episteme Reader in another language? Please request it through [GitHub Issu
The APK will be generated at:
`app/build/outputs/apk/oss/debug/Episteme-oss-v{version}-oss-debug.apk`
-3. Build desktop:
- ```bash
- ./gradlew :desktopApp:packageReleaseDistributionForCurrentOS
- ```
- For the offline desktop build, pass:
- ```bash
- ./gradlew :desktopApp:packageReleaseDistributionForCurrentOS -PdesktopFlavor=oss
- ```
-
## Open Source Libraries
-Powered by the Kotlin, Android, and desktop OSS ecosystem:
-
-* **Core and UI:** Kotlin Multiplatform, Compose Multiplatform, AndroidX, Jetpack Compose, Kotlinx Serialization
-* **Document engines:** PdfiumAndroidKt, PDFium, libmobi
-* **Parsers:** Jsoup, Flexmark, Apache Commons Compress
-* **Media and image loading:** Coil, Media3
-* **Utilities:** Room, Timber, JNA
+Powered by the Android OSS ecosystem:
+* **Core & UI:** AndroidX, Jetpack Compose, Kotlinx Serialization
+* **Document Engines:** PdfiumAndroidKt (PDF), libmobi (MOBI/AZW3)
+* **Parsers:** Jsoup (HTML/EPUB), Flexmark (Markdown)
+* **Media & Image Loading:** Coil, Media3 (ExoPlayer)
+* **Utilities:** Room (Database), Timber (Logging)
## Contributors
| Contributor | Contribution |
|---|---|
-|
[CCerrer](https://github.com/CCerrer) | Testing and QA |
-|
[ottozumkeller](https://github.com/ottozumkeller) | German translation |
-|
[TURBOKANTR](https://github.com/TURBOKANTR) | Turkish translation |
-|
[eyadalkordy24](https://github.com/eyadalkordy24) | Arabic translation |
-|
[berebara](https://github.com/berebara) | Russian translation |
-|
[mh4ckt3mh4ckt1c4s](https://github.com/mh4ckt3mh4ckt1c4s) | French translation |
+|
[CCerrer](https://github.com/CCerrer) | Testing & QA |
+|
[ottozumkeller](https://github.com/ottozumkeller) | Translation (German) |
+|
[TURBOKANTR](https://github.com/TURBOKANTR) | Translation (Turkish) |
+|
[eyadalkordy24](https://github.com/eyadalkordy24) | Translation (Arabic) |
+|
[berebara](https://github.com/berebara) | Translation (Russian) |
+|
[mh4ckt3mh4ckt1c4s](https://github.com/mh4ckt3mh4ckt1c4s) | Translation (French) |
-## Supporters
+## Translations
-Thank you to the people helping keep Episteme Reader moving:
+Help translate Episteme Reader into your native language! [Weblate](https://hosted.weblate.org/engage/episteme/) is used to manage localization.
-| Supporter | Platform |
-|---|---|
-|
[Zorklo](https://github.com/Zorklo) | GitHub Sponsors |
-
-## Support the Project
-
-Help make Episteme Reader better:
-
-* [Sponsor on GitHub](https://github.com/sponsors/Aryan-Raj3112)
-* [Support on Patreon](https://www.patreon.com/c/epistemereader)
-* Star the repository to help visibility
-* Report bugs or request features via [GitHub Issues](https://github.com/Aryan-Raj3112/episteme/issues/new/choose)
-* Share feedback in [Discussions](https://github.com/Aryan-Raj3112/episteme/discussions)
-* Leave a review on the [Google Play Store](https://play.google.com/store/apps/details?id=com.aryan.reader)
-* Tell a friend
+[](https://hosted.weblate.org/engage/episteme/)
## License
-Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). See the [LICENSE](LICENSE) file.
+Licensed under the GNU Affero General Public License v3.0 (AGPL‑3.0). See the [LICENSE](LICENSE) file.
+
+## Support the Project
+
+Help make Episteme Reader even better:
+
+* ❤️ [Support on Patreon](https://www.patreon.com/c/epistemereader)
+* ⭐ Star the repository to help visibility
+* 🐞 Report bugs or request features via [GitHub Issues](https://github.com/Aryan-Raj3112/episteme/issues/new/choose)
+* 💬 Share feedback in [Discussions](https://github.com/Aryan-Raj3112/episteme/discussions)
+* ✍️ Leave a review on the [Google Play Store](https://play.google.com/store/apps/details?id=com.aryan.reader)
+* 📣 Tell a friend!
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 0c234e9..8d29580 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -9,7 +9,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
- alias(libs.plugins.kotlin.serialization)
+ id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20"
alias(libs.plugins.kotlin.ksp)
id("com.diffplug.spotless") version "8.2.1"
alias(libs.plugins.kover)
@@ -50,15 +50,15 @@ kotlin {
}
android {
- namespace = "org.dueattendant149.bookreader"
+ namespace = "com.aryan.reader"
compileSdk = 36
defaultConfig {
- applicationId = "org.dueattendant149.bookreader"
+ applicationId = "com.aryan.reader"
minSdk = 26
targetSdk = 35
- versionCode = 54
- versionName = "1.0.50"
+ versionCode = 53
+ versionName = "1.0.49"
resourceConfigurations += configuredAppLocaleTags()
.map { it.toAndroidResourceConfiguration() }
@@ -171,7 +171,6 @@ android {
testOptions {
unitTests.isReturnDefaultValues = true
unitTests.all {
- it.maxHeapSize = "4g"
it.jvmArgs("-Xss2m")
}
}
@@ -244,6 +243,8 @@ dependencies {
ksp(libs.androidx.room.compiler)
+ implementation("androidx.compose.material:material-icons-extended:1.7.8")
+
implementation("androidx.appcompat:appcompat:1.7.1")
//noinspection GradleDependency (Updating these might cause the custom toolbox in pagination to break)
diff --git a/app/src/androidTest/assets/epub/reader_test_book.epub b/app/src/androidTest/assets/epub/reader_test_book.epub
deleted file mode 100644
index 3858f26..0000000
Binary files a/app/src/androidTest/assets/epub/reader_test_book.epub and /dev/null differ
diff --git a/app/src/androidTest/fixtures/epub/README.md b/app/src/androidTest/fixtures/epub/README.md
deleted file mode 100644
index 01b7040..0000000
--- a/app/src/androidTest/fixtures/epub/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# EPUB UI Test Fixture
-
-`reader_test_book.epub` is a deterministic EPUB fixture for Android instrumentation and UI tests.
-
-Use it for reader flows that need stable EPUB content:
-
-- open/import EPUB through a `content://` URI
-- restore reading position
-- navigate with chapter anchors
-- create or verify bookmarks
-- search unique fixture markers
-- exercise highlight and CFI-related flows
-
-The generated EPUB is stored at:
-
-`app/src/androidTest/assets/epub/reader_test_book.epub`
-
-The source files live in:
-
-`app/src/androidTest/fixtures/epub/reader_test_book`
-
-Regenerate the EPUB after editing the source:
-
-```powershell
-python app/src/androidTest/fixtures/epub/build_reader_test_book.py
-```
-
-Tests should copy the asset from the instrumentation context, not the target app context:
-
-```kotlin
-val testContext = InstrumentationRegistry.getInstrumentation().context
-testContext.assets.open("epub/reader_test_book.epub")
-```
-
-Current basic UI coverage using this fixture lives in:
-
-`app/src/androidTest/java/com/aryan/reader/epubreader/EpubReaderScreenTest.kt`
-
-Stable markers intentionally embedded in the book:
-
-- `POSITION_TARGET_ALPHA`
-- `HIGHLIGHT_TARGET_BRAVO`
-- `CFI_TARGET_CHARLIE`
-- `SEARCH_TARGET_DELTA`
-- `BOOKMARK_TARGET_ECHO`
-- `POSITION_TARGET_FOXTROT`
-- `ANNOTATION_TARGET_GOLF`
-- `CFI_TARGET_HOTEL`
diff --git a/app/src/androidTest/fixtures/epub/build_reader_test_book.py b/app/src/androidTest/fixtures/epub/build_reader_test_book.py
deleted file mode 100644
index f5ab2fb..0000000
--- a/app/src/androidTest/fixtures/epub/build_reader_test_book.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from __future__ import annotations
-
-from pathlib import Path
-from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile, ZipInfo
-
-
-ROOT = Path(__file__).resolve().parent
-SOURCE_DIR = ROOT / "reader_test_book"
-OUTPUT = ROOT.parents[1] / "assets" / "epub" / "reader_test_book.epub"
-FIXED_TIMESTAMP = (2026, 1, 1, 0, 0, 0)
-
-
-def add_file(epub: ZipFile, source: Path, archive_name: str, compression: int) -> None:
- info = ZipInfo(archive_name, FIXED_TIMESTAMP)
- info.compress_type = compression
- info.external_attr = 0o644 << 16
- epub.writestr(info, source.read_bytes())
-
-
-def main() -> None:
- OUTPUT.parent.mkdir(parents=True, exist_ok=True)
-
- with ZipFile(OUTPUT, "w") as epub:
- info = ZipInfo("mimetype", FIXED_TIMESTAMP)
- info.compress_type = ZIP_STORED
- info.external_attr = 0o644 << 16
- epub.writestr(info, b"application/epub+zip")
-
- for source in sorted(SOURCE_DIR.rglob("*")):
- if not source.is_file() or source.name == "mimetype":
- continue
- archive_name = source.relative_to(SOURCE_DIR).as_posix()
- add_file(epub, source, archive_name, ZIP_DEFLATED)
-
- print(f"Wrote {OUTPUT}")
-
-
-if __name__ == "__main__":
- main()
diff --git a/app/src/androidTest/fixtures/epub/reader_test_book/META-INF/container.xml b/app/src/androidTest/fixtures/epub/reader_test_book/META-INF/container.xml
deleted file mode 100644
index fe5cbeb..0000000
--- a/app/src/androidTest/fixtures/epub/reader_test_book/META-INF/container.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/chapters/chapter-01.xhtml b/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/chapters/chapter-01.xhtml
deleted file mode 100644
index d17d58f..0000000
--- a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/chapters/chapter-01.xhtml
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
-
-
- Chapter One
-
-
-
-
- Chapter One: Stable Opening
- This EPUB is intentionally plain so Android UI tests can rely on stable text, stable IDs, and stable chapter order.
- POSITION_TARGET_ALPHA appears near the start of chapter one. Use this marker for first-position and restore-position checks.
- HIGHLIGHT_TARGET_BRAVO is a short highlight target. It is surrounded by ordinary words so selection handles have context.
- CFI_TARGET_CHARLIE sits inside a paragraph with a fixed element id. It can be used for CFI and locator assertions.
- Chapter one filler paragraph 01 keeps the document tall enough for scroll and pagination tests.
- Chapter one filler paragraph 02 keeps the document tall enough for scroll and pagination tests.
- Chapter one filler paragraph 03 keeps the document tall enough for scroll and pagination tests.
- Chapter one filler paragraph 04 keeps the document tall enough for scroll and pagination tests.
- Chapter one filler paragraph 05 keeps the document tall enough for scroll and pagination tests.
- Chapter one filler paragraph 06 keeps the document tall enough for scroll and pagination tests.
- Chapter one filler paragraph 07 keeps the document tall enough for scroll and pagination tests.
- Chapter one filler paragraph 08 keeps the document tall enough for scroll and pagination tests.
- END_OF_CHAPTER_ONE_MARKER
-
-
-
diff --git a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/chapters/chapter-02.xhtml b/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/chapters/chapter-02.xhtml
deleted file mode 100644
index c5acd90..0000000
--- a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/chapters/chapter-02.xhtml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
- Chapter Two
-
-
-
-
- Chapter Two: Search And Bookmarks
- SEARCH_TARGET_DELTA appears exactly once in the book. Use it for search result navigation.
- BOOKMARK_TARGET_ECHO is positioned near the top of chapter two for bookmark add, list, and return flows.
- POSITION_TARGET_FOXTROT is lower in chapter two and is useful for persistence tests after a chapter change.
- Chapter two filler paragraph 01 provides enough height for gesture based navigation.
- Chapter two filler paragraph 02 provides enough height for gesture based navigation.
- Chapter two filler paragraph 03 provides enough height for gesture based navigation.
- Chapter two filler paragraph 04 provides enough height for gesture based navigation.
- Chapter two filler paragraph 05 provides enough height for gesture based navigation.
- Chapter two filler paragraph 06 provides enough height for gesture based navigation.
- Chapter two filler paragraph 07 provides enough height for gesture based navigation.
- Chapter two filler paragraph 08 provides enough height for gesture based navigation.
- END_OF_CHAPTER_TWO_MARKER
-
-
-
diff --git a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/chapters/chapter-03.xhtml b/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/chapters/chapter-03.xhtml
deleted file mode 100644
index 73b68a0..0000000
--- a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/chapters/chapter-03.xhtml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- Chapter Three
-
-
-
-
- Chapter Three: Annotation Targets
- ANNOTATION_TARGET_GOLF is reserved for tests that verify highlight or note persistence across app restarts.
- CFI_TARGET_HOTEL is a second fixed CFI target in a later chapter.
- The following SVG is local to the EPUB and can be used later for image rendering checks.
-
-
- Local SVG image for EPUB resource resolution.
-
- Chapter three filler paragraph 01 rounds out the fixture.
- Chapter three filler paragraph 02 rounds out the fixture.
- Chapter three filler paragraph 03 rounds out the fixture.
- END_OF_CHAPTER_THREE_MARKER
-
-
-
diff --git a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/images/fixture-diagram.svg b/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/images/fixture-diagram.svg
deleted file mode 100644
index 730dbff..0000000
--- a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/images/fixture-diagram.svg
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
diff --git a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/nav.xhtml b/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/nav.xhtml
deleted file mode 100644
index 14a8470..0000000
--- a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/nav.xhtml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- Reader Android UI Test Book
-
-
-
-
-
-
-
diff --git a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/package.opf b/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/package.opf
deleted file mode 100644
index 7bdfd8f..0000000
--- a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/package.opf
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
- urn:uuid:reader-android-ui-test-epub
- Reader Android UI Test Book
- Reader Test Fixtures
- en
- 2026-01-01T00:00:00Z
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/styles/reader-test.css b/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/styles/reader-test.css
deleted file mode 100644
index 5799725..0000000
--- a/app/src/androidTest/fixtures/epub/reader_test_book/OEBPS/styles/reader-test.css
+++ /dev/null
@@ -1,21 +0,0 @@
-body {
- font-family: serif;
- line-height: 1.5;
-}
-
-h1 {
- font-size: 1.4em;
-}
-
-p {
- margin: 0 0 1em 0;
-}
-
-.fixture-note {
- border-left: 0.25em solid #4a90e2;
- padding-left: 0.75em;
-}
-
-.target {
- font-weight: bold;
-}
diff --git a/app/src/androidTest/fixtures/epub/reader_test_book/mimetype b/app/src/androidTest/fixtures/epub/reader_test_book/mimetype
deleted file mode 100644
index 403c4f0..0000000
--- a/app/src/androidTest/fixtures/epub/reader_test_book/mimetype
+++ /dev/null
@@ -1 +0,0 @@
-application/epub+zip
diff --git a/app/src/androidTest/java/com/aryan/reader/AppNavigationTest.kt b/app/src/androidTest/java/com/aryan/reader/AppNavigationTest.kt
new file mode 100644
index 0000000..d7fd42a
--- /dev/null
+++ b/app/src/androidTest/java/com/aryan/reader/AppNavigationTest.kt
@@ -0,0 +1,146 @@
+// AppNavigationTest.kt
+package com.aryan.reader
+
+import android.net.Uri
+import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
+import androidx.compose.material3.windowsizeclass.WindowSizeClass
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.ComposeNavigator
+import androidx.navigation.testing.TestNavHostController
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.aryan.reader.epub.EpubBook
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AppNavigationTest {
+
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private lateinit var navController: TestNavHostController
+ private val fakeUiState = MutableStateFlow(ReaderScreenState())
+
+ // Mock ViewModel that uses the fake state
+ private val fakeViewModel: MainViewModel = object : MainViewModel(
+ ApplicationProvider.getApplicationContext()
+ ) {
+ override val uiState = fakeUiState
+ override fun clearSelectedFile() {
+ fakeUiState.value = fakeUiState.value.copy(
+ selectedFileType = null,
+ selectedPdfUri = null,
+ selectedEpubBook = null
+ )
+ }
+ }
+
+ @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
+ @Before
+ fun setup() {
+ composeTestRule.setContent {
+ navController = TestNavHostController(LocalContext.current)
+ navController.navigatorProvider.addNavigator(ComposeNavigator())
+ AppNavigation(
+ navController = navController,
+ windowSizeClass = WindowSizeClass.calculateFromSize(DpSize(400.dp, 800.dp)),
+ viewModel = fakeViewModel
+ )
+ }
+ }
+
+ @Test
+ fun appNavigation_defaultStartDestination_isMainRoute() {
+ val currentRoute = navController.currentBackStackEntry?.destination?.route
+ assertEquals(AppDestinations.MAIN_ROUTE, currentRoute)
+ }
+
+ @Test
+ fun appNavigation_whenPdfSelected_navigatesToPdfViewer() {
+ // Trigger state change
+ fakeUiState.value = ReaderScreenState(
+ selectedFileType = FileType.PDF,
+ selectedPdfUri = Uri.parse("content://dummy.pdf")
+ )
+
+ // Let compose recompose and run LaunchedEffect
+ composeTestRule.waitForIdle()
+
+ val currentRoute = navController.currentBackStackEntry?.destination?.route
+ assertEquals(AppDestinations.PDF_VIEWER_ROUTE, currentRoute)
+ }
+
+ @Test
+ fun appNavigation_whenPptxSelected_navigatesToPdfViewer() {
+ fakeUiState.value = ReaderScreenState(
+ selectedFileType = FileType.PPTX,
+ selectedPdfUri = Uri.parse("content://dummy.pptx")
+ )
+
+ composeTestRule.waitForIdle()
+
+ val currentRoute = navController.currentBackStackEntry?.destination?.route
+ assertEquals(AppDestinations.PDF_VIEWER_ROUTE, currentRoute)
+ }
+
+ @Test
+ fun appNavigation_whenEpubSelected_navigatesToEpubReader() {
+ // Trigger state change
+ fakeUiState.value = ReaderScreenState(
+ selectedFileType = FileType.EPUB,
+ selectedEpubBook = EpubBook(
+ fileName = "dummy.epub",
+ title = "Dummy Book",
+ author = "Author",
+ language = "en",
+ coverImage = null
+ )
+ )
+
+ composeTestRule.waitForIdle()
+
+ val currentRoute = navController.currentBackStackEntry?.destination?.route
+ assertEquals(AppDestinations.EPUB_READER_ROUTE, currentRoute)
+ }
+
+ @Test
+ fun appNavigation_whenFileCleared_navigatesBackToMain() {
+ // First, navigate to PDF viewer
+ fakeUiState.value = ReaderScreenState(
+ selectedFileType = FileType.PDF,
+ selectedPdfUri = Uri.parse("content://dummy.pdf")
+ )
+ composeTestRule.waitForIdle()
+ assertEquals(AppDestinations.PDF_VIEWER_ROUTE, navController.currentBackStackEntry?.destination?.route)
+
+ // Then, trigger the clear action (simulating onNavigateBack)
+ fakeViewModel.clearSelectedFile()
+ composeTestRule.waitForIdle()
+
+ val currentRoute = navController.currentBackStackEntry?.destination?.route
+ assertEquals(AppDestinations.MAIN_ROUTE, currentRoute)
+ }
+
+ @Test
+ fun appNavigation_whenUnknownFileTypeSelected_navigatesBackToMain() {
+ fakeUiState.value = ReaderScreenState(
+ selectedFileType = FileType.PDF,
+ selectedPdfUri = Uri.parse("content://dummy.pdf")
+ )
+ composeTestRule.waitForIdle()
+ assertEquals(AppDestinations.PDF_VIEWER_ROUTE, navController.currentBackStackEntry?.destination?.route)
+
+ fakeUiState.value = ReaderScreenState(selectedFileType = FileType.UNKNOWN)
+ composeTestRule.waitForIdle()
+
+ assertEquals(AppDestinations.MAIN_ROUTE, navController.currentBackStackEntry?.destination?.route)
+ }
+}
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/MainDispatcherRule.kt b/app/src/androidTest/java/com/aryan/reader/MainDispatcherRule.kt
similarity index 95%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/MainDispatcherRule.kt
rename to app/src/androidTest/java/com/aryan/reader/MainDispatcherRule.kt
index f45fabe..1cf2f5b 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/MainDispatcherRule.kt
+++ b/app/src/androidTest/java/com/aryan/reader/MainDispatcherRule.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/ChapterWebViewBridgeTest.kt b/app/src/androidTest/java/com/aryan/reader/epubreader/ChapterWebViewBridgeTest.kt
similarity index 67%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/ChapterWebViewBridgeTest.kt
rename to app/src/androidTest/java/com/aryan/reader/epubreader/ChapterWebViewBridgeTest.kt
index 9586e38..cf9996f 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/ChapterWebViewBridgeTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/epubreader/ChapterWebViewBridgeTest.kt
@@ -1,19 +1,13 @@
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.json.JSONArray
import org.json.JSONObject
import org.junit.Rule
import org.junit.Test
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
@OptIn(ExperimentalCoroutinesApi::class)
class ChapterWebViewBridgeTest {
@@ -26,8 +20,7 @@ class ChapterWebViewBridgeTest {
var receivedCfi = ""
val bridge = CfiJsBridge(
onCfiReady = { cfi -> receivedCfi = cfi },
- onCfiForBookmarkReady = {},
- onScrollFinishedCallback = {}
+ onCfiForBookmarkReady = {}
)
val cfi = "/4/2[chapter1]/6:10"
@@ -46,8 +39,7 @@ class ChapterWebViewBridgeTest {
var receivedCfi = ""
val bridge = CfiJsBridge(
onCfiReady = { cfi -> receivedCfi = cfi },
- onCfiForBookmarkReady = {},
- onScrollFinishedCallback = {}
+ onCfiForBookmarkReady = {}
)
val invalidJson = "this is not json"
@@ -62,8 +54,7 @@ class ChapterWebViewBridgeTest {
var receivedCfi: String? = null
val bridge = CfiJsBridge(
onCfiReady = { cfi -> receivedCfi = cfi },
- onCfiForBookmarkReady = {},
- onScrollFinishedCallback = {}
+ onCfiForBookmarkReady = {}
)
val jsonResponse = JSONObject().apply {
@@ -78,51 +69,29 @@ class ChapterWebViewBridgeTest {
}
@Test
- fun ttsJsBridge_onStructuredTextExtracted_callsHandlerWithJson() {
- val latch = CountDownLatch(1)
+ fun ttsJsBridge_onStructuredTextExtracted_callsHandlerWithJson() = runTest {
var receivedJson: String? = null
- val bridgeScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
- try {
- val bridge = TtsJsBridge(
- scope = bridgeScope,
- ttsStructuredTextHandler = { json ->
- receivedJson = json
- latch.countDown()
- }
- )
- val jsonPayload = "[{\"text\":\"Hello world\",\"cfi\":\"/4/2\"}]"
-
- bridge.onStructuredTextExtracted(jsonPayload)
-
- assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue()
- assertThat(receivedJson).isEqualTo(jsonPayload)
- } finally {
- bridgeScope.cancel()
- }
+ val bridge = TtsJsBridge(
+ scope = this,
+ ttsStructuredTextHandler = { json -> receivedJson = json }
+ )
+ val jsonPayload = "[{\"text\":\"Hello world\",\"cfi\":\"/4/2\"}]"
+ bridge.onStructuredTextExtracted(jsonPayload)
+ advanceUntilIdle()
+ assertThat(receivedJson).isEqualTo(jsonPayload)
}
@Test
- fun ttsJsBridge_onStructuredTextExtracted_withEmptyJson_callsHandlerWithEmptyArray() {
- val latch = CountDownLatch(1)
+ fun ttsJsBridge_onStructuredTextExtracted_withEmptyJson_callsHandlerWithEmptyArray() = runTest {
var receivedJson: String? = null
- val bridgeScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
- try {
- val bridge = TtsJsBridge(
- scope = bridgeScope,
- ttsStructuredTextHandler = { json ->
- receivedJson = json
- latch.countDown()
- }
- )
- val jsonPayload = ""
-
- bridge.onStructuredTextExtracted(jsonPayload)
-
- assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue()
- assertThat(receivedJson).isEqualTo("[]")
- } finally {
- bridgeScope.cancel()
- }
+ val bridge = TtsJsBridge(
+ scope = this,
+ ttsStructuredTextHandler = { json -> receivedJson = json }
+ )
+ val jsonPayload = ""
+ bridge.onStructuredTextExtracted(jsonPayload)
+ advanceUntilIdle()
+ assertThat(receivedJson).isEqualTo("[]")
}
@Test
@@ -184,4 +153,4 @@ class ChapterWebViewBridgeTest {
advanceUntilIdle()
assertThat(receivedContent).isEqualTo(content)
}
-}
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderBookmarkTest.kt b/app/src/androidTest/java/com/aryan/reader/epubreader/EpubReaderBookmarkTest.kt
similarity index 97%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderBookmarkTest.kt
rename to app/src/androidTest/java/com/aryan/reader/epubreader/EpubReaderBookmarkTest.kt
index 2788375..36e67ca 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderBookmarkTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/epubreader/EpubReaderBookmarkTest.kt
@@ -1,9 +1,9 @@
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.dueattendant149.bookreader.epub.EpubChapter
+import com.aryan.reader.epub.EpubChapter
import com.google.common.truth.Truth.assertThat
import org.json.JSONArray
import org.json.JSONObject
diff --git a/app/src/androidTest/java/com/aryan/reader/epubreader/EpubReaderLogicTest.kt b/app/src/androidTest/java/com/aryan/reader/epubreader/EpubReaderLogicTest.kt
new file mode 100644
index 0000000..a310b2d
--- /dev/null
+++ b/app/src/androidTest/java/com/aryan/reader/epubreader/EpubReaderLogicTest.kt
@@ -0,0 +1,204 @@
+// app/src/androidTest/java/com/aryan/reader/epubreader/EpubReaderLogicTest.kt
+package com.aryan.reader.epubreader
+
+import android.content.Context
+import timber.log.Timber
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontWeight
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.aryan.reader.SearchResult
+import com.aryan.reader.epub.EpubBook
+import com.aryan.reader.epub.EpubChapter
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.jsoup.Jsoup
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import kotlin.math.max
+import kotlin.math.min
+
+@RunWith(AndroidJUnit4::class)
+class EpubReaderLogicTest {
+
+ private lateinit var context: Context
+ private lateinit var testDir: File
+ private lateinit var mockEpubBook: EpubBook
+
+ @Before
+ fun setup() {
+ context = ApplicationProvider.getApplicationContext()
+ testDir = File(context.cacheDir, "test_epub").apply { mkdirs() }
+
+ // Create dummy chapter files
+ val chapter1File = File(testDir, "chapter1.html")
+ chapter1File.writeText("A simple Test case.
")
+
+ val chapter2File = File(testDir, "chapter2.html")
+ chapter2File.writeText("Another test case here.
The word Test appears twice.
")
+
+ mockEpubBook = EpubBook(
+ fileName = "test.epub",
+ title = "Test Book",
+ author = "Tester",
+ language = "en",
+ coverImage = null,
+ extractionBasePath = testDir.absolutePath,
+ chapters = listOf(
+ EpubChapter(
+ chapterId = "ch1",
+ absPath = chapter1File.absolutePath,
+ title = "Chapter 1",
+ htmlFilePath = "chapter1.html",
+ plainTextContent = "",
+ htmlContent = ""
+ ),
+ EpubChapter(
+ chapterId = "ch2",
+ absPath = chapter2File.absolutePath,
+ title = "Chapter 2",
+ htmlFilePath = "chapter2.html",
+ plainTextContent = "",
+ htmlContent = ""
+ )
+ )
+ )
+ }
+
+ @After
+ fun tearDown() {
+ testDir.deleteRecursively()
+ }
+
+ private suspend fun searchEpub(book: EpubBook, query: String): List {
+ val TAG = "EpubReaderLogicTest"
+ Timber.d("Starting search for query: '$query'")
+ return withContext(Dispatchers.IO) {
+ val results = mutableListOf()
+ book.chapters.forEachIndexed { chapterIndex, chapter ->
+ try {
+ val fullPath = "${book.extractionBasePath}/${chapter.htmlFilePath}"
+ Timber.d("Chapter ${chapterIndex + 1}: Checking path '$fullPath'")
+ val htmlFile = File(fullPath)
+ if (!htmlFile.exists()) {
+ Timber.e("File does not exist: $fullPath")
+ return@forEachIndexed
+ }
+
+ val doc = Jsoup.parse(htmlFile, "UTF-8")
+ val bodyChildren = doc.body().children().toList()
+ val chunks = bodyChildren.chunked(20)
+
+ chunks.forEachIndexed { chunkIndex, chunkOfElements ->
+ val chunkHtml = chunkOfElements.joinToString(separator = "\n") { it.outerHtml() }
+ val content = Jsoup.parse(chunkHtml).text()
+ var lastIndex = -1
+
+ while (true) {
+ lastIndex = content.indexOf(query, startIndex = lastIndex + 1, ignoreCase = true)
+ if (lastIndex == -1) break
+
+ Timber.d("Found potential match for '$query' at index $lastIndex.")
+ val isWordStart = lastIndex == 0 || !content[lastIndex - 1].isLetterOrDigit()
+ Timber.d("Is it a word start? -> $isWordStart")
+ if (isWordStart) {
+ Timber.d("Match is a word start. Adding to results.")
+ val snippetStart = max(0, lastIndex - 35)
+ val snippetEnd = min(content.length, lastIndex + query.length + 35)
+ val rawSnippet = content.substring(snippetStart, snippetEnd)
+ val annotatedSnippet = buildAnnotatedString {
+ append(rawSnippet)
+ val highlightStart = content.indexOf(query, lastIndex, ignoreCase = true) - snippetStart
+ val highlightEnd = highlightStart + query.length
+ addStyle(
+ style = SpanStyle(fontWeight = FontWeight.Bold),
+ start = highlightStart,
+ end = highlightEnd
+ )
+ }
+ results.add(
+ SearchResult(
+ locationInSource = chapterIndex,
+ locationTitle = chapter.title,
+ snippet = annotatedSnippet,
+ query = query,
+ occurrenceIndexInLocation = results.count { it.locationInSource == chapterIndex },
+ chunkIndex = chunkIndex
+ )
+ )
+ }
+ }
+ }
+ } catch (e: Exception) {
+ Timber.e("Error during search in chapter ${chapter.title}", e)
+ throw e
+ }
+ }
+ Timber.d("Search finished. Total results found: ${results.size}")
+ results
+ }
+ }
+
+ @Test
+ fun search_findsCorrectResults() = runBlocking {
+ val results = searchEpub(mockEpubBook, "case")
+ assertThat(results).hasSize(2)
+ assertThat(results.count { it.locationTitle == "Chapter 1" }).isEqualTo(1)
+ assertThat(results.count { it.locationTitle == "Chapter 2" }).isEqualTo(1)
+ }
+
+ @Test
+ fun search_isCaseInsensitive() = runBlocking {
+ val results = searchEpub(mockEpubBook, "test")
+ assertThat(results).hasSize(3)
+ assertThat(results[0].locationTitle).isEqualTo("Chapter 1")
+ assertThat(results[1].locationTitle).isEqualTo("Chapter 2")
+ assertThat(results[2].locationTitle).isEqualTo("Chapter 2")
+ }
+
+ @Test
+ fun search_noResultsFound() = runBlocking {
+ val results = searchEpub(mockEpubBook, "nonexistent")
+ assertThat(results).isEmpty()
+ }
+
+ @Test
+ fun search_createsCorrectSnippetHighlight() = runBlocking {
+ val query = "Test"
+ mockEpubBook.chapters.first()
+ val content = "A simple Test case."
+ val annotatedString = buildAnnotatedStringWithHighlight(content, query)
+
+ val spanStyles = annotatedString.spanStyles
+ assertThat(spanStyles).hasSize(1)
+
+ val style = spanStyles.first().item
+ assertThat(style.fontWeight).isEqualTo(FontWeight.Bold)
+
+ val start = spanStyles.first().start
+ val end = spanStyles.first().end
+ assertThat(annotatedString.substring(start, end)).isEqualTo(query)
+ }
+
+ @Suppress("SameParameterValue")
+ private fun buildAnnotatedStringWithHighlight(content: String, query: String): AnnotatedString {
+ return buildAnnotatedString {
+ append(content)
+ val highlightStart = content.indexOf(query, ignoreCase = true)
+ if (highlightStart != -1) {
+ addStyle(
+ style = SpanStyle(fontWeight = FontWeight.Bold),
+ start = highlightStart,
+ end = highlightStart + query.length
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/tts/MainDispatcherRule.kt b/app/src/androidTest/java/com/aryan/reader/epubreader/MainDispatcherRule.kt
similarity index 95%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/tts/MainDispatcherRule.kt
rename to app/src/androidTest/java/com/aryan/reader/epubreader/MainDispatcherRule.kt
index e46b599..1f804a0 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/tts/MainDispatcherRule.kt
+++ b/app/src/androidTest/java/com/aryan/reader/epubreader/MainDispatcherRule.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.tts
+package com.aryan.reader.epubreader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/CssParserTest.kt b/app/src/androidTest/java/com/aryan/reader/paginatedreader/CssParserTest.kt
similarity index 72%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/CssParserTest.kt
rename to app/src/androidTest/java/com/aryan/reader/paginatedreader/CssParserTest.kt
index cab1c9e..c6fe395 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/CssParserTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/paginatedreader/CssParserTest.kt
@@ -1,5 +1,5 @@
// CssParserTest.kt
-package org.dueattendant149.bookreader.paginatedreader
+package com.aryan.reader.paginatedreader
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.isSpecified
@@ -17,66 +17,49 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class CssParserTest {
- private val dummyConstraints = androidx.compose.ui.unit.Constraints()
- private val baseFontSize = 16f
- private val density = 1f
-
- private fun parseTextColor(value: String): Color? {
- val result = CssParser.parse(
- cssContent = "p { color: $value; }",
- cssPath = null,
- baseFontSizeSp = baseFontSize,
- density = density,
- constraints = dummyConstraints,
- isDarkTheme = false
- )
- return result.rules.byTag["p"]
- ?.firstOrNull()
- ?.style
- ?.spanStyle
- ?.color
- ?.takeIf { it.isSpecified }
- }
-
@Test
fun parseColor_handlesNamedColorsCorrectly() {
- assertThat(parseTextColor("red")).isEqualTo(Color.Red)
- assertThat(parseTextColor("black")).isEqualTo(Color.Black)
- assertThat(parseTextColor("transparent")).isEqualTo(Color.Transparent)
+ assertThat(CssParser.parseColor("red")).isEqualTo(Color.Red)
+ assertThat(CssParser.parseColor("black")).isEqualTo(Color.Black)
+ assertThat(CssParser.parseColor("transparent")).isEqualTo(Color.Transparent)
}
@Test
fun parseColor_handles3DigitHexCodes() {
- assertThat(parseTextColor("#F0C")).isEqualTo(Color(0xFFFF00CC))
+ assertThat(CssParser.parseColor("#F0C")).isEqualTo(Color(0xFFFF00CC))
}
@Test
fun parseColor_handles6DigitHexCodes() {
- assertThat(parseTextColor("#FF00CC")).isEqualTo(Color(0xFFFF00CC))
+ assertThat(CssParser.parseColor("#FF00CC")).isEqualTo(Color(0xFFFF00CC))
}
@Test
fun parseColor_handles8DigitHexCodes() {
- assertThat(parseTextColor("#80FF00CC")).isEqualTo(Color(128, 255, 0, 204))
+ assertThat(CssParser.parseColor("#80FF00CC")).isEqualTo(Color(0x80FF00CC))
}
@Test
fun parseColor_handlesRgbFunction() {
- assertThat(parseTextColor("rgb(255, 0, 204)")).isEqualTo(Color(255, 0, 204))
+ assertThat(CssParser.parseColor("rgb(255, 0, 204)")).isEqualTo(Color(255, 0, 204))
}
@Test
fun parseColor_handlesRgbaFunction() {
- assertThat(parseTextColor("rgba(255, 0, 204, 0.5)")).isEqualTo(Color(255, 0, 204, 128))
+ assertThat(CssParser.parseColor("rgba(255, 0, 204, 0.5)")).isEqualTo(Color(255, 0, 204, 128))
}
@Test
fun parseColor_returnsNullForInvalidInput() {
- assertThat(parseTextColor("not a color")).isNull()
- assertThat(parseTextColor("#12345")).isNull()
- assertThat(parseTextColor("rgb(1,2)")).isNull()
+ assertThat(CssParser.parseColor("not a color")).isNull()
+ assertThat(CssParser.parseColor("#12345")).isNull()
+ assertThat(CssParser.parseColor("rgb(1,2)")).isNull()
}
+ private val dummyConstraints = androidx.compose.ui.unit.Constraints()
+ private val baseFontSize = 16f
+ private val density = 1f
+
@Test
fun parse_handlesSimpleRule() {
val css = "p { color: red; }"
@@ -137,12 +120,12 @@ class CssParserTest {
}
p { color: black; }
""".trimIndent()
- val result = CssParser.parse(css, "OEBPS/styles/style.css", baseFontSize, density, dummyConstraints, isDarkTheme = false)
+ val result = CssParser.parse(css, "/some/path/style.css", baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(result.rules.byTag).containsKey("p")
assertThat(result.fontFaces).hasSize(1)
val fontFace = result.fontFaces.first()
assertThat(fontFace.fontFamily).isEqualTo("mycustomfont")
- assertThat(fontFace.src).isEqualTo("OEBPS/fonts/myfont.ttf")
+ assertThat(fontFace.src).isEqualTo("/some/fonts/myfont.ttf")
assertThat(fontFace.fontWeight).isEqualTo(FontWeight.Bold)
assertThat(fontFace.fontStyle).isEqualTo(FontStyle.Normal)
}
@@ -207,11 +190,10 @@ class CssParserTest {
val css = "div { border: 2px solid red; }"
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val style = result.rules.byTag["div"]?.first()?.style?.blockStyle
- val expectedBorder = BorderStyle(width = 2.dp, color = Color.Red, style = "solid")
- assertThat(style?.borderTop).isEqualTo(expectedBorder)
- assertThat(style?.borderRight).isEqualTo(expectedBorder)
- assertThat(style?.borderBottom).isEqualTo(expectedBorder)
- assertThat(style?.borderLeft).isEqualTo(expectedBorder)
+ assertThat(style?.border).isNotNull()
+ assertThat(style?.border?.width).isEqualTo(2.dp)
+ assertThat(style?.border?.style).isEqualTo("solid")
+ assertThat(style?.border?.color).isEqualTo(Color.Red)
}
@Test
@@ -280,70 +262,9 @@ class CssParserTest {
url("font.ttf") format("truetype");
}
""".trimIndent()
- val result = CssParser.parse(css, "OEBPS/css/style.css", baseFontSize, density, dummyConstraints, isDarkTheme = false)
+ val result = CssParser.parse(css, "/css/style.css", baseFontSize, density, dummyConstraints, isDarkTheme = false)
assertThat(result.fontFaces).hasSize(1)
- assertThat(result.fontFaces.first().src).isEqualTo("OEBPS/css/font.otf")
- }
-
- @Test
- fun parse_handlesNestedMediaAndCalcVariables() {
- val css = """
- :root { --gap: 12px; }
- @media screen and (min-width: 300px) {
- p { margin-left: calc(var(--gap) + 8px); color: hsl(120 100% 25%); }
- }
- """.trimIndent()
-
- val result = CssParser.parse(
- css,
- null,
- baseFontSize,
- density,
- androidx.compose.ui.unit.Constraints(maxWidth = 500),
- isDarkTheme = false
- )
-
- val style = result.rules.byTag["p"]!!.first().style
- assertThat(style.blockStyle.margin.left).isEqualTo(20.dp)
- assertThat(style.spanStyle.color).isEqualTo(Color(0, 128, 0))
- }
-
- @Test
- fun parse_preservesBeforeAfterPseudoElementRules() {
- val css = "p::before { content: 'Note: '; color: red; } p { color: blue; }"
- val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
-
- val pseudoRule = result.rules.otherComplex.single { it.pseudoElement == "before" }
- assertThat(pseudoRule.selector.selector).isEqualTo("p")
- assertThat(pseudoRule.style.content).isEqualTo("'Note: '")
- assertThat(pseudoRule.style.spanStyle.color).isEqualTo(Color.Red)
- assertThat(result.rules.byTag["p"]!!.single().style.spanStyle.color).isEqualTo(Color.Blue)
- }
-
- @Test
- fun parse_handlesModernRgbSlashAlphaAndCssHexAlpha() {
- assertThat(parseTextColor("rgb(255 0 204 / 50%)")).isEqualTo(Color(255, 0, 204, 128))
- assertThat(parseTextColor("#ff00cc80")).isEqualTo(Color(255, 0, 204, 128))
- }
-
- @Test
- fun parse_backgroundShorthandExtractsColorAndImage() {
- val css = "section { background: #ffeecc url('../images/paper.png') repeat; }"
- val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
- val style = result.rules.byTag["section"]!!.first().style.blockStyle
-
- assertThat(style.backgroundColor).isEqualTo(Color(255, 238, 204))
- assertThat(style.backgroundImage).isEqualTo("../images/paper.png")
- }
-
- @Test
- fun parse_listStyleShorthandExtractsMarkerTypeAndImage() {
- val css = "ul { list-style: square url('../images/bullet.png') outside; }"
- val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
- val style = result.rules.byTag["ul"]!!.first().style.blockStyle
-
- assertThat(style.listStyleType).isEqualTo("square")
- assertThat(style.listStyleImage).isEqualTo("../images/bullet.png")
+ assertThat(result.fontFaces.first().src).isEqualTo("/css/font.otf")
}
@Test
@@ -406,10 +327,10 @@ class CssParserTest {
}
@Test
- fun parse_lineHeightPreservesUnitlessMultiplier() {
+ fun parse_lineHeightClampsSmallEmValues() {
val css = "p { line-height: 1.1; }" // This is treated as 1.1em
val result = CssParser.parse(css, null, baseFontSize, density, dummyConstraints, isDarkTheme = false)
val style = result.rules.byTag["p"]?.first()?.style
- assertThat(style?.paragraphStyle?.lineHeight).isEqualTo(1.1.em)
+ assertThat(style?.paragraphStyle?.lineHeight).isEqualTo(2.0.em)
}
-}
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/HtmlParserTest.kt b/app/src/androidTest/java/com/aryan/reader/paginatedreader/HtmlParserTest.kt
similarity index 84%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/HtmlParserTest.kt
rename to app/src/androidTest/java/com/aryan/reader/paginatedreader/HtmlParserTest.kt
index 16c3f9b..dcfe42e 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/HtmlParserTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/paginatedreader/HtmlParserTest.kt
@@ -1,8 +1,9 @@
// HtmlParserTest.kt
-package org.dueattendant149.bookreader.paginatedreader
+package com.aryan.reader.paginatedreader
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -40,7 +41,7 @@ class HtmlParserTest {
val allRules = cssRules?.let { userAgentRules.merge(it) } ?: userAgentRules
- return androidHtmlToSemanticBlocks(
+ return htmlToSemanticBlocks(
html = "$html", // Wrap in body to match real usage
cssRules = allRules, // Use the combined list of rules
textStyle = defaultTextStyle,
@@ -96,61 +97,7 @@ class HtmlParserTest {
assertThat(blocks).hasSize(1)
val pBlock = blocks.first() as SemanticParagraph
val blockStyle = pBlock.style.spanStyle
- assertThat(blockStyle.color).isEqualTo(Color(0, 128, 0))
- }
-
- @Test
- fun htmlToSemanticBlocks_contextSensitiveSelectors_areNotReusedAcrossSameClass() {
- val css = """
- .warning p.note { color: red; }
- .safe p.note { color: blue; }
- """.trimIndent()
- val cssRules = CssParser.parse(css, null, 16f, 1f, defaultConstraints, isDarkTheme = false).rules
-
- val blocks = parse(
- """
-
-
- """.trimIndent(),
- cssRules = cssRules
- )
-
- val first = blocks[0] as SemanticParagraph
- val second = blocks[1] as SemanticParagraph
- assertThat(first.style.spanStyle.color).isEqualTo(Color.Red)
- assertThat(second.style.spanStyle.color).isEqualTo(Color.Blue)
- }
-
- @Test
- fun htmlToSemanticBlocks_generatedBeforeContent_isMaterializedIntoText() {
- val css = "p.note::before { content: 'Note: '; color: red; }"
- val cssRules = CssParser.parse(css, null, 16f, 1f, defaultConstraints, isDarkTheme = false).rules
-
- val blocks = parse("Remember this
", cssRules = cssRules)
-
- val paragraph = blocks.single() as SemanticParagraph
- assertThat(paragraph.text).isEqualTo("Note: Remember this")
- val generatedSpan = paragraph.spans.first { it.tag == "::before" }
- assertThat(generatedSpan.start).isEqualTo(0)
- assertThat(generatedSpan.end).isEqualTo("Note: ".length)
- assertThat(generatedSpan.style.spanStyle.color).isEqualTo(Color.Red)
- }
-
- @Test
- fun htmlToSemanticBlocks_backgroundImageUrl_isResolvedIntoBlockStyle() {
- val imageRelativeSrc = "images/paper.png"
- val chapterParentDir = File(defaultChapterPath).parent ?: ""
- val imageFile = File(File(defaultExtractionPath, chapterParentDir), imageRelativeSrc).canonicalFile
- imageFile.parentFile?.mkdirs()
- imageFile.createNewFile()
- imageFile.deleteOnExit()
- val css = "p.paper { background-image: url('$imageRelativeSrc'); }"
- val cssRules = CssParser.parse(css, null, 16f, 1f, defaultConstraints, isDarkTheme = false).rules
-
- val blocks = parse("Text over paper
", cssRules = cssRules)
-
- val paragraph = blocks.single() as SemanticParagraph
- assertThat(paragraph.style.blockStyle.backgroundImage).isEqualTo(imageFile.absolutePath)
+ assertThat(blockStyle.color).isEqualTo(Color.Green)
}
@Test
@@ -250,7 +197,7 @@ class HtmlParserTest {
}
@Test
- fun htmlToSemanticBlocks_complexInlineText_isPreserved() {
+ fun htmlToSemanticBlocks_complexInlineFormatting_isPreserved() {
val html = "This is bold and italic text.
"
val blocks = parse(html)
@@ -258,6 +205,17 @@ class HtmlParserTest {
val pBlock = blocks.first() as SemanticParagraph
assertThat(pBlock.text).isEqualTo("This is bold and italic text.")
+
+ // Find the range for "bold" and check its style
+ val boldRange = pBlock.spans.find { pBlock.text.substring(it.start, it.end) == "bold" }
+ assertThat(boldRange).isNotNull()
+ assertThat(boldRange!!.style.spanStyle.fontWeight).isEqualTo(FontWeight.Bold)
+
+ // Find the range for "italic" and check its style
+ val italicRange =
+ pBlock.spans.find { pBlock.text.substring(it.start, it.end) == "italic" }
+ assertThat(italicRange).isNotNull()
+ assertThat(italicRange!!.style.spanStyle.fontStyle).isEqualTo(androidx.compose.ui.text.font.FontStyle.Italic)
}
@Test
@@ -284,14 +242,15 @@ class HtmlParserTest {
}
@Test
- fun htmlToSemanticBlocks_beforePseudoElementContent_isIncludedInParagraphText() {
+ fun htmlToSemanticBlocks_pseudoElements_areIgnoredByTheParser() {
val css = "p::before { content: \"Note: \"; }"
val cssRules = CssParser.parse(css, null, 16f, 1f, defaultConstraints, isDarkTheme = false).rules
val blocks = parse("This is a test.
", cssRules = cssRules)
+ // The parser now ignores pseudo-elements, so only the paragraph content should be parsed.
assertThat(blocks).hasSize(1)
val pBlock = blocks[0] as SemanticParagraph
- assertThat(pBlock.text).isEqualTo("Note: This is a test.")
+ assertThat(pBlock.text).isEqualTo("This is a test.")
}
@Test
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/MainDispatcherRule.kt b/app/src/androidTest/java/com/aryan/reader/paginatedreader/MainDispatcherRule.kt
similarity index 96%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/MainDispatcherRule.kt
rename to app/src/androidTest/java/com/aryan/reader/paginatedreader/MainDispatcherRule.kt
index d773bbe..a6d31f8 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/MainDispatcherRule.kt
+++ b/app/src/androidTest/java/com/aryan/reader/paginatedreader/MainDispatcherRule.kt
@@ -1,5 +1,5 @@
// MainDispatcherRule.kt
-package org.dueattendant149.bookreader.pdf
+package com.aryan.reader.paginatedreader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/PaginatedReaderDataTest.kt b/app/src/androidTest/java/com/aryan/reader/paginatedreader/PaginatedReaderDataTest.kt
similarity index 91%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/PaginatedReaderDataTest.kt
rename to app/src/androidTest/java/com/aryan/reader/paginatedreader/PaginatedReaderDataTest.kt
index 59935bb..dd56c8e 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/PaginatedReaderDataTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/paginatedreader/PaginatedReaderDataTest.kt
@@ -1,5 +1,5 @@
// PaginatedReaderDataTest.kt
-package org.dueattendant149.bookreader.paginatedreader
+package com.aryan.reader.paginatedreader
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.ParagraphStyle
@@ -75,7 +75,7 @@ class PaginatedReaderDataTest {
margin = BoxBorders(bottom = 10.dp, left = 10.dp),
width = 200.dp,
backgroundColor = Color.Black,
- borderTop = BorderStyle(width = 1.dp, color = Color.Red)
+ border = BorderStyle(width = 1.dp, color = Color.Red)
)
val merged = baseStyle.merge(overrideStyle)
@@ -95,8 +95,8 @@ class PaginatedReaderDataTest {
// Other properties
assertThat(merged.width).isEqualTo(200.dp)
assertThat(merged.backgroundColor).isEqualTo(Color.Black)
- assertThat(merged.borderTop).isNotNull()
- assertThat(merged.borderTop?.width).isEqualTo(1.dp)
+ assertThat(merged.border).isNotNull()
+ assertThat(merged.border?.width).isEqualTo(1.dp)
}
@Test
@@ -115,9 +115,6 @@ class PaginatedReaderDataTest {
assertThat(merged.margin.top).isEqualTo(5.dp)
assertThat(merged.width).isEqualTo(100.dp)
assertThat(merged.backgroundColor).isEqualTo(Color.White)
- assertThat(merged.borderTop).isNull()
- assertThat(merged.borderRight).isNull()
- assertThat(merged.borderBottom).isNull()
- assertThat(merged.borderLeft).isNull()
+ assertThat(merged.border).isNull()
}
-}
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/aryan/reader/paginatedreader/PaginatedReaderViewModelTest.kt b/app/src/androidTest/java/com/aryan/reader/paginatedreader/PaginatedReaderViewModelTest.kt
new file mode 100644
index 0000000..c839650
--- /dev/null
+++ b/app/src/androidTest/java/com/aryan/reader/paginatedreader/PaginatedReaderViewModelTest.kt
@@ -0,0 +1,304 @@
+// PaginatedReaderViewModelTest.kt
+package com.aryan.reader.paginatedreader
+
+import android.content.Context
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.text.TextMeasurer
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.aryan.reader.SearchResult
+import com.aryan.reader.epub.EpubBook
+import com.aryan.reader.epub.EpubChapter
+import com.aryan.reader.paginatedreader.data.BookCacheDao
+import com.aryan.reader.paginatedreader.data.BookCacheDatabase
+import com.aryan.reader.paginatedreader.data.BookProcessingWorker
+import com.google.common.truth.Truth.assertThat
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.unmockkAll
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+private class FakePaginator(
+ initiallyLoading: Boolean,
+ initialPageCount: Int,
+ initialGeneration: Int
+) : IPaginator {
+ override var isLoading by mutableStateOf(initiallyLoading)
+ override var totalPageCount by mutableIntStateOf(initialPageCount)
+ override var generation by mutableIntStateOf(initialGeneration)
+ override val pageShiftRequest: Flow = emptyFlow()
+
+ var lastNavigatedHref: String? = null
+ var lastNavigatedChapter: String? = null
+
+ override fun getPageContent(pageIndex: Int): Page? = null
+ override fun getChapterPathForPage(pageIndex: Int): String? = null
+ override fun getPlainTextForChapter(chapterIndex: Int): String? = null
+
+ override fun navigateToHref(
+ currentChapterAbsPath: String,
+ href: String,
+ onNavigationComplete: (pageIndex: Int) -> Unit
+ ) {
+ lastNavigatedChapter = currentChapterAbsPath
+ lastNavigatedHref = href
+ }
+
+ override fun findPageForSearchResult(
+ result: SearchResult,
+ onResult: (Int) -> Unit
+ ) = Unit
+
+ // Add stubs for the other missing interface members
+ override fun findPageForCfi(chapterIndex: Int, cfi: String, onResult: (Int) -> Unit) = Unit
+ override fun findPageForCfiAndOffset(
+ chapterIndex: Int,
+ cfi: String,
+ charOffset: Int
+ ): Int? {
+ return null
+ }
+
+ override fun findChapterIndexForPage(pageIndex: Int): Int? = null
+ override fun getCfiForPage(pageIndex: Int): String? = null
+ override fun onUserScrolledTo(pageIndex: Int) = Unit
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+class PaginatedReaderViewModelTest {
+
+ @get:Rule
+ val mainDispatcherRule = MainDispatcherRule()
+
+ private lateinit var viewModel: PaginatedReaderViewModel
+ private lateinit var fakePaginator: FakePaginator
+
+ @Before
+ fun setUp() {
+ viewModel = PaginatedReaderViewModel()
+ fakePaginator = FakePaginator(
+ initiallyLoading = true,
+ initialPageCount = 0,
+ initialGeneration = 0
+ )
+ viewModel.setPaginatorForTest(fakePaginator)
+ }
+
+ @After
+ fun tearDown() {
+ unmockkAll()
+ }
+
+ @Test
+ fun uiState_reflectsPaginatorInitialState() = runTest {
+ val initialState = viewModel.uiState.value
+ assertThat(initialState.isLoading).isTrue()
+ assertThat(initialState.totalPageCount).isEqualTo(0)
+ assertThat(initialState.generation).isEqualTo(0)
+ }
+
+ @Test
+ fun uiState_updatesWhenPaginatorIsLoadingChanges() = runTest {
+ assertThat(viewModel.uiState.value.isLoading).isTrue()
+
+ fakePaginator.isLoading = false
+ Snapshot.sendApplyNotifications()
+ advanceUntilIdle()
+
+ assertThat(viewModel.uiState.value.isLoading).isFalse()
+ }
+
+ @Test
+ fun uiState_updatesWhenPaginatorTotalPageCountChanges() = runTest {
+ assertThat(viewModel.uiState.value.totalPageCount).isEqualTo(0)
+
+ fakePaginator.totalPageCount = 123
+ Snapshot.sendApplyNotifications()
+ advanceUntilIdle()
+
+ assertThat(viewModel.uiState.value.totalPageCount).isEqualTo(123)
+ }
+
+ @Test
+ fun uiState_updatesWhenPaginatorGenerationChanges() = runTest {
+ assertThat(viewModel.uiState.value.generation).isEqualTo(0)
+
+ fakePaginator.generation = 5
+ Snapshot.sendApplyNotifications()
+ advanceUntilIdle()
+
+ assertThat(viewModel.uiState.value.generation).isEqualTo(5)
+ }
+
+ @Test
+ fun onLinkClick_callsPaginatorNavigateToHrefWithCorrectArguments() {
+ val currentChapter = "chapter1.xhtml"
+ val href = "#section2"
+
+ viewModel.onLinkClick(currentChapter, href) {}
+
+ assertThat(fakePaginator.lastNavigatedChapter).isEqualTo(currentChapter)
+ assertThat(fakePaginator.lastNavigatedHref).isEqualTo(href)
+ }
+ @Test
+ fun initialize_createsARealPaginatorAndUpdateState() = runTest {
+ // Arrange
+ val viewModel = PaginatedReaderViewModel() // Create a fresh ViewModel
+ val context = ApplicationProvider.getApplicationContext()
+ val textMeasurer = mockk(relaxed = true)
+ val constraints = Constraints(maxWidth = 1080, maxHeight = 1920)
+ val textStyle = TextStyle.Default
+ val density = Density(1f)
+ val mathMLRenderer = mockk(relaxed = true)
+ val testBook = EpubBook(
+ fileName = "test.epub",
+ title = "Test Book",
+ author = "Test Author",
+ language = "en",
+ coverImage = null,
+ chapters = listOf(
+ EpubChapter(
+ chapterId = "ch1",
+ title = "Chapter 1",
+ htmlFilePath = "ch1.html",
+ absPath = "/ops/ch1.html",
+ htmlContent = "Some content
",
+ plainTextContent = "Some content"
+ )
+ ),
+ css = mapOf("/ops/style.css" to "p {color: red;}"),
+ extractionBasePath = ""
+ )
+
+ // Mock dependencies for BookPaginator
+ val mockDao = mockk(relaxed = true)
+ coEvery { mockDao.getProcessedBook(any()) } returns null // Simulate cache miss
+
+ val mockDb = mockk()
+ every { mockDb.bookCacheDao() } returns mockDao
+
+ mockkObject(BookCacheDatabase.Companion)
+ every { BookCacheDatabase.getDatabase(any()) } returns mockDb
+
+ mockkObject(BookProcessingWorker.Companion)
+ every { BookProcessingWorker.enqueue(any(), any(), any(), any(), any(), any()) } returns Unit
+
+ // Pre-condition check
+ assertThat(viewModel.uiState.value.isLoading).isTrue()
+ assertThat(viewModel.paginator).isNull()
+
+ // Act
+ viewModel.initialize(
+ book = testBook,
+ textMeasurer = textMeasurer,
+ textConstraints = constraints,
+ textStyle = textStyle,
+ density = density,
+ isDarkTheme = false,
+ context = context,
+ initialChapterToPaginate = 0,
+ mathMLRenderer = mathMLRenderer
+ )
+ advanceUntilIdle() // Allow coroutines to complete
+
+ // Assert
+ assertThat(viewModel.paginator).isInstanceOf(BookPaginator::class.java)
+ assertThat(viewModel.uiState.value.isLoading).isFalse()
+ assertThat(viewModel.uiState.value.totalPageCount).isGreaterThan(0)
+ }
+
+ @Test
+ fun initialize_isIdempotent() = runTest {
+ // Arrange
+ val viewModel = PaginatedReaderViewModel()
+ val context = ApplicationProvider.getApplicationContext()
+ val textMeasurer = mockk(relaxed = true)
+ val constraints = Constraints(maxWidth = 1080, maxHeight = 1920)
+ val textStyle = TextStyle.Default
+ val density = Density(1f)
+ val mathMLRenderer = mockk(relaxed = true)
+ val testBook = EpubBook(
+ fileName = "test.epub",
+ title = "Test Book",
+ author = "Test Author",
+ language = "en",
+ coverImage = null,
+ chapters = listOf(
+ EpubChapter(
+ chapterId = "ch1",
+ title = "Chapter 1",
+ htmlFilePath = "ch1.html",
+ absPath = "/ops/ch1.html",
+ htmlContent = "Some content
",
+ plainTextContent = "Some content"
+ )
+ ),
+ css = mapOf("/ops/style.css" to "p {color: red;}"),
+ extractionBasePath = ""
+ )
+
+ // Mock dependencies
+ val mockDao = mockk(relaxed = true)
+ coEvery { mockDao.getProcessedBook(any()) } returns null
+ val mockDb = mockk()
+ every { mockDb.bookCacheDao() } returns mockDao
+ mockkObject(BookCacheDatabase.Companion)
+ every { BookCacheDatabase.getDatabase(any()) } returns mockDb
+ mockkObject(BookProcessingWorker.Companion)
+ every { BookProcessingWorker.enqueue(any(), any(), any(), any(), any(), any()) } returns Unit
+
+ // Act
+ viewModel.initialize(
+ book = testBook,
+ textMeasurer = textMeasurer,
+ textConstraints = constraints,
+ textStyle = textStyle,
+ density = density,
+ isDarkTheme = false,
+ context = context,
+ initialChapterToPaginate = 0,
+ mathMLRenderer = mathMLRenderer
+ )
+ advanceUntilIdle()
+
+ val firstPaginator = viewModel.paginator
+ assertThat(firstPaginator).isNotNull()
+
+ // Act again
+ viewModel.initialize(
+ book = testBook,
+ textMeasurer = textMeasurer,
+ textConstraints = constraints,
+ textStyle = textStyle,
+ density = density,
+ isDarkTheme = false,
+ context = context,
+ initialChapterToPaginate = 0,
+ mathMLRenderer = mathMLRenderer
+ )
+ advanceUntilIdle()
+
+ // Assert
+ val secondPaginator = viewModel.paginator
+ assertThat(secondPaginator).isSameInstanceAs(firstPaginator)
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/PaginatorTest.kt b/app/src/androidTest/java/com/aryan/reader/paginatedreader/PaginatorTest.kt
similarity index 70%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/PaginatorTest.kt
rename to app/src/androidTest/java/com/aryan/reader/paginatedreader/PaginatorTest.kt
index c74943c..b04283f 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/PaginatorTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/paginatedreader/PaginatorTest.kt
@@ -1,13 +1,11 @@
// PaginatorTest.kt
-package org.dueattendant149.bookreader.paginatedreader
+package com.aryan.reader.paginatedreader
-import android.os.Build
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth.assertThat
+import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -44,52 +42,14 @@ class FakeSplittableMeasurementProvider(
}
return null
}
-
- override suspend fun split(block: TableBlock, availableHeight: Int): Pair? = null
-
- override suspend fun split(block: FlexContainerBlock, availableHeight: Int): Pair? = null
}
@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
class PaginatorTest {
private val testDensity = Density(density = 1f, fontScale = 1f)
private val pageHeight = 1000
- private fun List.withoutMeasuredHeights(): List {
- return map { it.withoutMeasuredHeight() }
- }
-
- private fun ContentBlock.withoutMeasuredHeight(): ContentBlock {
- return when (this) {
- is ParagraphBlock -> copy(expectedHeight = 0)
- is ImageBlock -> copy(expectedHeight = 0)
- is HeaderBlock -> copy(expectedHeight = 0)
- is SpacerBlock -> copy(expectedHeight = 0)
- is QuoteBlock -> copy(expectedHeight = 0)
- is ListItemBlock -> copy(expectedHeight = 0)
- is TableBlock -> copy(
- rows = rows.map { row ->
- row.map { cell ->
- cell.copy(content = cell.content.withoutMeasuredHeights())
- }
- },
- expectedHeight = 0
- )
- is MathBlock -> copy(expectedHeight = 0)
- is WrappingContentBlock -> copy(
- floatedImage = floatedImage.copy(expectedHeight = 0),
- paragraphsToWrap = paragraphsToWrap.map { it.copy(expectedHeight = 0) },
- expectedHeight = 0
- )
- is FlexContainerBlock -> copy(
- children = children.withoutMeasuredHeights(),
- expectedHeight = 0
- )
- }
- }
-
@Test
fun paginate_givenEmptyBlocks_createsZeroPages() = runTest {
val pages = paginate(emptyList(), pageHeight, FakeSplittableMeasurementProvider(emptyMap()), testDensity)
@@ -125,53 +85,8 @@ class PaginatorTest {
val pages = paginate(blocks, pageHeight, measurementProvider, testDensity)
assertThat(pages).hasSize(2)
- assertThat(pages[0].content.withoutMeasuredHeights()).containsExactly(block1)
- assertThat(pages[1].content.withoutMeasuredHeights()).containsExactly(block2)
- }
-
- @Test
- fun paginate_honorsBreakBeforePage() = runTest {
- val block1 = ParagraphBlock(content = AnnotatedString("Before"), blockIndex = 0)
- val block2 = ParagraphBlock(
- content = AnnotatedString("After"),
- style = BlockStyle(breakBefore = "page"),
- blockIndex = 1
- )
-
- val pages = paginate(
- listOf(block1, block2),
- pageHeight,
- FakeSplittableMeasurementProvider(mapOf(block1 to 100, block2 to 100)),
- testDensity
- )
-
- assertThat(pages).hasSize(2)
- assertThat(pages[0].content.withoutMeasuredHeights()).containsExactly(block1)
- assertThat(pages[1].content.withoutMeasuredHeights()).containsExactly(block2)
- }
-
- @Test
- fun paginate_breakInsideAvoidPreventsParagraphSplit() = runTest {
- val block1 = ParagraphBlock(
- content = AnnotatedString("Keep together"),
- style = BlockStyle(breakInside = "avoid"),
- blockIndex = 0
- )
- val part1 = block1.copy(content = AnnotatedString("Keep"))
- val part2 = block1.copy(content = AnnotatedString("together"))
-
- val pages = paginate(
- listOf(block1),
- pageHeight = 400,
- measurementProvider = FakeSplittableMeasurementProvider(
- heights = mapOf(block1 to 800, part1 to 300, part2 to 500),
- splittableParagraphs = mapOf(block1 to (part1 to part2))
- ),
- density = testDensity
- )
-
- assertThat(pages).hasSize(1)
- assertThat(pages[0].content.withoutMeasuredHeights()).containsExactly(block1)
+ assertThat(pages[0].content).containsExactly(block1)
+ assertThat(pages[1].content).containsExactly(block2)
}
@Test
@@ -195,8 +110,8 @@ class PaginatorTest {
val pages = paginate(blocks, pageHeight, measurementProvider, testDensity)
assertThat(pages).hasSize(2)
- assertThat(pages[0].content.withoutMeasuredHeights()).containsExactly(block1, part1).inOrder()
- assertThat(pages[1].content.withoutMeasuredHeights()).containsExactly(part2)
+ assertThat(pages[0].content).containsExactly(block1, part1).inOrder()
+ assertThat(pages[1].content).containsExactly(part2)
}
@Test
@@ -221,8 +136,8 @@ class PaginatorTest {
val pages = paginate(listOf(originalWrapper), pageHeight, measurementProvider, testDensity)
assertThat(pages).hasSize(2)
- assertThat(pages[0].content.withoutMeasuredHeights()).containsExactly(splitWrapper)
- assertThat(pages[1].content.withoutMeasuredHeights()).containsExactly(para2)
+ assertThat(pages[0].content).containsExactly(splitWrapper)
+ assertThat(pages[1].content).containsExactly(para2)
}
@@ -243,8 +158,8 @@ class PaginatorTest {
val pages = paginate(blocks, pageHeight, measurementProvider, testDensity)
assertThat(pages).hasSize(2)
- assertThat(pages[0].content.withoutMeasuredHeights()).containsExactly(block1)
- assertThat(pages[1].content.withoutMeasuredHeights()).containsExactly(unsplittableBlock)
+ assertThat(pages[0].content).containsExactly(block1)
+ assertThat(pages[1].content).containsExactly(unsplittableBlock)
}
@Test
@@ -257,7 +172,7 @@ class PaginatorTest {
val pages = paginate(blocks, pageHeight, measurementProvider, testDensity)
assertThat(pages).hasSize(1)
- assertThat(pages[0].content.withoutMeasuredHeights()).containsExactly(oversizedBlock)
+ assertThat(pages[0].content).containsExactly(oversizedBlock)
}
@Test
@@ -333,8 +248,8 @@ class PaginatorTest {
val pages = paginate(blocks, pageHeight, measurementProvider, testDensity)
assertThat(pages).hasSize(2)
- assertThat(pages[0].content.withoutMeasuredHeights()).containsExactly(block1)
- assertThat(pages[1].content.withoutMeasuredHeights()).containsExactly(splittableBlock) // Was not split
+ assertThat(pages[0].content).containsExactly(block1)
+ assertThat(pages[1].content).containsExactly(splittableBlock) // Was not split
}
@Test
@@ -356,7 +271,8 @@ class PaginatorTest {
// Set page height so that a split is attempted.
val pages = paginate(blocks, 150, measurementProvider, testDensity)
assertThat(pages).hasSize(1)
- // Empty split heads are skipped so pagination keeps only the remaining content.
- assertThat(pages[0].content.withoutMeasuredHeights()).containsExactly(part2)
+ // The page should be empty because part1 was empty, and the original block was re-added
+ // to the remaining list. The next page then contains the full block.
+ assertThat(pages[0].content).containsExactly(part2)
}
-}
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/StyleUtilsTest.kt b/app/src/androidTest/java/com/aryan/reader/paginatedreader/StyleUtilsTest.kt
similarity index 98%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/StyleUtilsTest.kt
rename to app/src/androidTest/java/com/aryan/reader/paginatedreader/StyleUtilsTest.kt
index c108d87..9fddc60 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/StyleUtilsTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/paginatedreader/StyleUtilsTest.kt
@@ -1,5 +1,5 @@
// StyleUtilsTest.kt
-package org.dueattendant149.bookreader.paginatedreader
+package com.aryan.reader.paginatedreader
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isUnspecified
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/MainDispatcherRule.kt b/app/src/androidTest/java/com/aryan/reader/pdf/MainDispatcherRule.kt
similarity index 95%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/MainDispatcherRule.kt
rename to app/src/androidTest/java/com/aryan/reader/pdf/MainDispatcherRule.kt
index aa3941d..afbbc75 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/MainDispatcherRule.kt
+++ b/app/src/androidTest/java/com/aryan/reader/pdf/MainDispatcherRule.kt
@@ -1,5 +1,5 @@
// MainDispatcherRule.kt
-package org.dueattendant149.bookreader.paginatedreader
+package com.aryan.reader.pdf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfAnnotationTest.kt b/app/src/androidTest/java/com/aryan/reader/pdf/PdfAnnotationTest.kt
similarity index 69%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfAnnotationTest.kt
rename to app/src/androidTest/java/com/aryan/reader/pdf/PdfAnnotationTest.kt
index 985a186..894d567 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfAnnotationTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/pdf/PdfAnnotationTest.kt
@@ -1,5 +1,5 @@
// PdfAnnotationTest.kt
-package org.dueattendant149.bookreader.pdf
+package com.aryan.reader.pdf
import android.content.Context
import android.content.Intent
@@ -11,7 +11,6 @@ import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.assertIsNotSelected
import androidx.compose.ui.test.click
import androidx.compose.ui.test.junit4.createEmptyComposeRule
-import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
@@ -20,9 +19,7 @@ import androidx.core.content.FileProvider
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.dueattendant149.bookreader.MainActivity
-import org.dueattendant149.bookreader.R
-import com.google.common.truth.Truth.assertThat
+import com.aryan.reader.MainActivity
import org.junit.After
import org.junit.Before
import org.junit.Rule
@@ -41,12 +38,6 @@ class PdfAnnotationTest {
private var currentPdfFile: File? = null
private var scenario: ActivityScenario? = null
private val samplePdfUri: Uri by lazy { copyAssetToCache(context, "sample.pdf") }
- private fun text(resId: Int): String = context.getString(resId)
- private fun dockTag(resId: Int): String = "DockItem_${text(resId)}"
-
- private fun assertNoNodeWithTag(tag: String) {
- assertThat(composeTestRule.onAllNodesWithTag(tag).fetchSemanticsNodes()).isEmpty()
- }
private fun createPdfViewIntent(context: Context, uri: Uri): Intent {
return Intent(context, MainActivity::class.java).apply {
@@ -64,7 +55,7 @@ class PdfAnnotationTest {
context.getSharedPreferences("epub_reader_settings", Context.MODE_PRIVATE)
.edit().clear().commit()
- scenario = ActivityScenario.launch(createPdfViewIntent(context, samplePdfUri))
+ scenario = ActivityScenario.launch(createPdfViewIntent(context, samplePdfUri))
waitForDocumentLoad()
}
@@ -84,11 +75,11 @@ class PdfAnnotationTest {
}
private fun enterEditMode() {
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_toggle_editing_mode))
+ composeTestRule.onNodeWithContentDescription("Toggle Drawing Mode")
.assertIsDisplayed()
.performClick()
composeTestRule.waitForIdle()
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_close_edit_mode)).assertIsDisplayed()
+ composeTestRule.onNodeWithContentDescription("Close Edit Mode").assertIsDisplayed()
}
private fun tapOutsidePopup() {
@@ -120,13 +111,13 @@ class PdfAnnotationTest {
enterEditMode()
// Verify Dock Items exist using new Tags
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_pen)).assertIsDisplayed()
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_highlighter)).assertIsDisplayed()
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_eraser)).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("DockItem_Pen").assertIsDisplayed()
+ composeTestRule.onNodeWithTag("DockItem_Highlighter").assertIsDisplayed()
+ composeTestRule.onNodeWithTag("DockItem_Eraser").assertIsDisplayed()
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_close_edit_mode)).performClick()
+ composeTestRule.onNodeWithContentDescription("Close Edit Mode").performClick()
composeTestRule.waitForIdle()
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_toggle_editing_mode)).assertIsDisplayed()
+ composeTestRule.onNodeWithContentDescription("Toggle Drawing Mode").assertIsDisplayed()
}
// --- TOOL LOGIC TESTS ---
@@ -136,38 +127,38 @@ class PdfAnnotationTest {
enterEditMode()
// 1. Select Highlighter
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_highlighter)).performClick()
+ composeTestRule.onNodeWithTag("DockItem_Highlighter").performClick()
composeTestRule.waitForIdle()
// 2. Verify selection state
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_highlighter)).assertIsSelected()
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_pen)).assertIsNotSelected()
+ composeTestRule.onNodeWithTag("DockItem_Highlighter").assertIsSelected()
+ composeTestRule.onNodeWithTag("DockItem_Pen").assertIsNotSelected()
// 3. Exit Edit Mode
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_close_edit_mode)).performClick()
+ composeTestRule.onNodeWithContentDescription("Close Edit Mode").performClick()
composeTestRule.waitForIdle()
// 4. Re-enter Edit Mode
enterEditMode()
// 5. Verify Highlighter is STILL selected (Persistence)
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_highlighter)).assertIsSelected()
+ composeTestRule.onNodeWithTag("DockItem_Highlighter").assertIsSelected()
}
@Test
- fun testEraserSettingsPopupOpensWhenAlreadySelected() {
+ fun testEraserHasNoPopup() {
enterEditMode()
// Select Eraser
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_eraser)).performClick()
+ composeTestRule.onNodeWithTag("DockItem_Eraser").performClick()
composeTestRule.waitForIdle()
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_eraser)).assertIsSelected()
+ composeTestRule.onNodeWithTag("DockItem_Eraser").assertIsSelected()
- // Click Eraser again to open its settings popup.
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_eraser)).performClick()
+ // Click Eraser AGAIN (Should NOT open popup)
+ composeTestRule.onNodeWithTag("DockItem_Eraser").performClick()
composeTestRule.waitForIdle()
- composeTestRule.onNodeWithTag("ToolSettingsPopup").assertIsDisplayed()
+ composeTestRule.onNodeWithTag("ToolSettingsPopup").assertDoesNotExist()
}
// --- SETTINGS POPUP TESTS ---
@@ -178,7 +169,7 @@ class PdfAnnotationTest {
// 1. Pen is default. Click Pen ONCE to open Settings.
// (Clicking twice would toggle it off, which caused previous failures)
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_pen)).performClick()
+ composeTestRule.onNodeWithTag("DockItem_Pen").performClick()
composeTestRule.waitForIdle()
// 2. Verify Popup Displayed
@@ -195,7 +186,7 @@ class PdfAnnotationTest {
// 5. Dismiss Settings
tapOutsidePopup()
- assertNoNodeWithTag("ToolSettingsPopup")
+ composeTestRule.onNodeWithTag("ToolSettingsPopup").assertDoesNotExist()
}
@Test
@@ -203,7 +194,7 @@ class PdfAnnotationTest {
enterEditMode()
// Open Settings for Pen (Default selected, so one click opens settings)
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_pen)).performClick()
+ composeTestRule.onNodeWithTag("DockItem_Pen").performClick()
composeTestRule.waitForIdle()
// Test Palette Click (Index 1)
@@ -217,7 +208,7 @@ class PdfAnnotationTest {
tapOutsidePopup()
// Quick verification that settings didn't crash app
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_pen)).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("DockItem_Pen").assertIsDisplayed()
}
// --- UNDO/REDO TESTS ---
@@ -226,7 +217,7 @@ class PdfAnnotationTest {
fun testDrawingEnablesUndo() {
enterEditMode()
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_undo))
+ composeTestRule.onNodeWithContentDescription("Undo")
.assertIsDisplayed()
.assertIsNotEnabled()
@@ -236,7 +227,7 @@ class PdfAnnotationTest {
}
composeTestRule.waitForIdle()
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_undo)).assertIsEnabled()
+ composeTestRule.onNodeWithContentDescription("Undo").assertIsEnabled()
}
@Test
@@ -249,8 +240,8 @@ class PdfAnnotationTest {
}
composeTestRule.waitForIdle()
- val undoNode = composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_undo))
- val redoNode = composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_redo))
+ val undoNode = composeTestRule.onNodeWithContentDescription("Undo")
+ val redoNode = composeTestRule.onNodeWithContentDescription("Redo")
undoNode.assertIsEnabled()
redoNode.assertIsNotEnabled()
@@ -277,7 +268,7 @@ class PdfAnnotationTest {
enterEditMode()
// 1. Drag Dock to make it floating (using Pen icon as handle)
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_pen)).performTouchInput {
+ composeTestRule.onNodeWithTag("DockItem_Pen").performTouchInput {
down(center)
advanceEventTime(600) // Long press
// Drag UP significantly
@@ -287,20 +278,20 @@ class PdfAnnotationTest {
composeTestRule.waitForIdle()
// 2. Minimize (Eye icon)
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_toggle_visibility)).performClick()
+ composeTestRule.onNodeWithContentDescription("Toggle Visibility").performClick()
composeTestRule.waitForIdle()
// 3. Verify Dock items are hidden
- assertNoNodeWithTag(dockTag(R.string.content_desc_pen))
+ composeTestRule.onNodeWithTag("DockItem_Pen").assertDoesNotExist()
// 4. Verify "Show Dock" floating button is visible
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_show_dock)).assertIsDisplayed()
+ composeTestRule.onNodeWithContentDescription("Show Dock").assertIsDisplayed()
// 5. Restore
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_show_dock)).performClick()
+ composeTestRule.onNodeWithContentDescription("Show Dock").performClick()
composeTestRule.waitForIdle()
// 6. Verify Dock items return
- composeTestRule.onNodeWithTag(dockTag(R.string.content_desc_pen)).assertIsDisplayed()
+ composeTestRule.onNodeWithTag("DockItem_Pen").assertIsDisplayed()
}
-}
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfCoverGeneratorTest.kt b/app/src/androidTest/java/com/aryan/reader/pdf/PdfCoverGeneratorTest.kt
similarity index 98%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfCoverGeneratorTest.kt
rename to app/src/androidTest/java/com/aryan/reader/pdf/PdfCoverGeneratorTest.kt
index 074b0c9..137dad8 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfCoverGeneratorTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/pdf/PdfCoverGeneratorTest.kt
@@ -1,5 +1,5 @@
// app/src/androidTest/java/com/aryan/reader/pdf/PdfCoverGeneratorTest.kt
-package org.dueattendant149.bookreader.pdf
+package com.aryan.reader.pdf
import android.content.Context
import android.net.Uri
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfHelperTest.kt b/app/src/androidTest/java/com/aryan/reader/pdf/PdfHelperTest.kt
similarity index 98%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfHelperTest.kt
rename to app/src/androidTest/java/com/aryan/reader/pdf/PdfHelperTest.kt
index ade8317..376c1d1 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfHelperTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/pdf/PdfHelperTest.kt
@@ -1,5 +1,5 @@
// app/src/androidTest/java/com/aryan/reader/pdf/PdfHelperTest.kt
-package org.dueattendant149.bookreader.pdf
+package com.aryan.reader.pdf
import android.graphics.Rect
import androidx.test.ext.junit.runners.AndroidJUnit4
diff --git a/app/src/androidTest/java/com/aryan/reader/pdf/PdfViewerScreenTest.kt b/app/src/androidTest/java/com/aryan/reader/pdf/PdfViewerScreenTest.kt
new file mode 100644
index 0000000..2c1cf34
--- /dev/null
+++ b/app/src/androidTest/java/com/aryan/reader/pdf/PdfViewerScreenTest.kt
@@ -0,0 +1,313 @@
+package com.aryan.reader.pdf
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.ui.test.assert
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertTextContains
+import androidx.compose.ui.test.hasTestTag
+import androidx.compose.ui.test.hasText
+import androidx.compose.ui.test.junit4.createEmptyComposeRule
+import androidx.compose.ui.test.onAllNodesWithText
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTextInput
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipe
+import androidx.core.content.FileProvider
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.rule.GrantPermissionRule
+import com.aryan.reader.MainActivity
+import org.junit.After
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.io.File
+import java.util.UUID
+
+@RunWith(AndroidJUnit4::class)
+class PdfViewerScreenTest {
+
+ @get:Rule
+ val composeTestRule = createEmptyComposeRule()
+
+ @get:Rule
+ val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.POST_NOTIFICATIONS)
+
+ @org.junit.Before
+ fun setup() {
+ val context = ApplicationProvider.getApplicationContext()
+ context.getSharedPreferences("epub_reader_settings", Context.MODE_PRIVATE)
+ .edit()
+ .clear()
+ .commit()
+ }
+
+ private fun createPdfViewIntent(context: Context, uri: Uri): Intent {
+ return Intent(context, MainActivity::class.java).apply {
+ action = Intent.ACTION_VIEW
+ data = uri
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ }
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ private var currentPdfFile: File? = null
+
+ private val samplePdfUri: Uri by lazy { copyAssetToCache(context, "sample.pdf") }
+
+ @get:Rule
+ val activityRule = ActivityScenarioRule(createPdfViewIntent(context, samplePdfUri))
+
+ @After
+ fun tearDown() {
+ currentPdfFile?.let {
+ if (it.exists()) it.delete()
+ }
+ }
+
+ private fun waitForDocumentLoad(pageText: String = "Page 1 of 4") {
+ composeTestRule.waitUntil(timeoutMillis = 15_000) {
+ composeTestRule
+ .onAllNodesWithText(pageText)
+ .fetchSemanticsNodes().size == 1
+ }
+ }
+
+ private fun ensurePaginationMode() {
+ composeTestRule.onNodeWithContentDescription("More Options").performClick()
+ composeTestRule.onNodeWithText("Reading Mode: Paginated").performClick()
+ composeTestRule.waitForIdle()
+ }
+
+ @Test
+ fun documentLoadsAndDisplaysCorrectPageCount() {
+ waitForDocumentLoad()
+ composeTestRule.onNodeWithTag("PageNumberIndicator")
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun tableOfContents_displaysEmptyState() {
+ waitForDocumentLoad()
+
+ composeTestRule.onNodeWithTag("TocButton").performClick()
+
+ composeTestRule.onNodeWithText("Chapters are not available for this book.").assertIsDisplayed()
+ }
+
+ @Suppress("SameParameterValue")
+ private fun copyAssetToCache(context: Context, assetName: String): Uri {
+ val uniqueName = "${UUID.randomUUID()}_$assetName"
+ val file = File(context.cacheDir, uniqueName)
+
+ currentPdfFile = file
+
+ if (file.exists()) file.delete()
+ context.assets.open(assetName).use { inputStream ->
+ file.outputStream().use { outputStream ->
+ inputStream.copyTo(outputStream)
+ }
+ }
+ return FileProvider.getUriForFile(
+ context,
+ "${context.packageName}.provider",
+ file
+ )
+ }
+
+ @Test
+ fun bookmarkFunctionality_addNavigateAndDelete() {
+ waitForDocumentLoad()
+
+ ensurePaginationMode()
+
+ composeTestRule.onNodeWithText("Page 1 of 4").assertIsDisplayed()
+
+ try {
+ composeTestRule.onRoot().performTouchInput { swipe(start = this.centerRight, end = this.centerLeft, durationMillis = 300) }
+ composeTestRule.onRoot().performClick()
+ composeTestRule.waitUntil(5_000) {
+ composeTestRule.onAllNodesWithText("Page 2 of 4").fetchSemanticsNodes().isNotEmpty()
+ }
+ composeTestRule.onNodeWithText("Page 2 of 4").assertIsDisplayed()
+ } catch (e: Exception) {
+ throw e
+ }
+
+ try {
+ composeTestRule.onNodeWithContentDescription("More Options").performClick()
+ composeTestRule.onNodeWithText("Bookmark this page").performClick()
+ composeTestRule.waitForIdle()
+ } catch (e: Exception) {
+ throw e
+ }
+
+ try {
+ composeTestRule.onRoot().performTouchInput { swipe(start = this.centerRight, end = this.centerLeft, durationMillis = 300) }
+ composeTestRule.onRoot().performClick()
+ composeTestRule.waitUntil(5_000) {
+ composeTestRule.onAllNodesWithText("Page 3 of 4").fetchSemanticsNodes().isNotEmpty()
+ }
+ composeTestRule.onNodeWithText("Page 3 of 4").assertIsDisplayed()
+ } catch (e: Exception) {
+ throw e
+ }
+
+ try {
+ composeTestRule.onNodeWithTag("TocButton").performClick()
+ composeTestRule.onNodeWithTag("BookmarksTab").performClick()
+ composeTestRule.waitForIdle()
+ composeTestRule.onNodeWithTag("BookmarkItem_1").assertIsDisplayed()
+ .assert(hasText("Page 2", substring = true))
+ } catch (e: Exception) {
+ throw e
+ }
+
+ try {
+ composeTestRule.onNodeWithTag("BookmarkItem_1").performClick()
+ composeTestRule.waitForIdle()
+ composeTestRule.waitUntil(5_000) {
+ composeTestRule.onAllNodes(hasTestTag("PageNumberIndicator").and(hasText("Page 2 of 4"))).fetchSemanticsNodes().size == 1
+ }
+ composeTestRule.onNode(hasTestTag("PageNumberIndicator").and(hasText("Page 2 of 4"))).assertIsDisplayed()
+
+ } catch (e: Exception) {
+ throw e
+ }
+
+ try {
+ composeTestRule.onNodeWithTag("TocButton").performClick()
+ composeTestRule.onNodeWithTag("BookmarksTab").performClick()
+ composeTestRule.waitForIdle()
+ } catch (e: Exception) {
+ throw e
+ }
+
+ try {
+ composeTestRule.onNodeWithContentDescription("More options for bookmark").performClick()
+ composeTestRule.onNodeWithText("Delete").performClick()
+ } catch (e: Exception) {
+ throw e
+ }
+
+ try {
+ composeTestRule.onNodeWithText("Delete", useUnmergedTree = true).performClick()
+ } catch (e: Exception) {
+ throw e
+ }
+
+ try {
+ composeTestRule.onNodeWithTag("BookmarkItem_1").assertDoesNotExist()
+ composeTestRule.onNodeWithText("You haven't added any bookmarks yet.").assertIsDisplayed()
+ } catch (e: Exception) {
+ throw e
+ }
+ }
+
+ @Test
+ fun sliderNavigation_opensAndDisplaysCorrectly() {
+ waitForDocumentLoad()
+
+ composeTestRule.onNodeWithContentDescription("Navigate with slider").performClick()
+ composeTestRule.onNodeWithContentDescription("Exit slider navigation").assertIsDisplayed()
+ composeTestRule.onNodeWithText("1 / 4").assertIsDisplayed()
+ }
+
+ @Test
+ fun displayMode_switchesToVerticalScroll() {
+ waitForDocumentLoad()
+
+ // Ensure we are in Pagination mode first to test the switch
+ ensurePaginationMode()
+
+ // Verify Vertical Scroll component is NOT displayed initially
+ composeTestRule.onNodeWithTag("PdfVerticalScroll").assertDoesNotExist()
+
+ // Switch to Vertical Scroll
+ composeTestRule.onNodeWithContentDescription("More Options").performClick()
+ composeTestRule.onNodeWithText("Reading Mode: Vertical scroll").performClick()
+
+ composeTestRule.waitForIdle()
+
+ // Verify Vertical Scroll component IS displayed
+ composeTestRule.onNodeWithTag("PdfVerticalScroll").assertIsDisplayed()
+
+ // Switch back to Pagination
+ ensurePaginationMode()
+
+ // Verify Vertical Scroll component is gone
+ composeTestRule.onNodeWithTag("PdfVerticalScroll").assertDoesNotExist()
+ }
+
+ @Test
+ fun search_uiOpensAndAcceptsQuery() {
+ waitForDocumentLoad()
+
+ // Click search button
+ composeTestRule.onNodeWithTag("SearchButton").performClick()
+
+ composeTestRule.onNodeWithText("English, Spanish, French, etc.").performClick()
+
+ // Verify text field appears
+ composeTestRule.onNodeWithTag("SearchTextField").assertIsDisplayed()
+
+ // Enter text
+ composeTestRule.onNodeWithTag("SearchTextField").performTextInput("test query")
+
+ // Verify text exists in the field
+ composeTestRule.onNodeWithTag("SearchTextField").assertTextContains("test query")
+
+ // Close search
+ composeTestRule.onNodeWithContentDescription("Close Search").performClick()
+
+ // Verify text field is gone
+ composeTestRule.onNodeWithTag("SearchTextField").assertDoesNotExist()
+ }
+
+ @Test
+ fun fullScreen_togglesVisibility() {
+ waitForDocumentLoad()
+
+ // Click enter full screen button
+ composeTestRule.onNodeWithContentDescription("Enter Full Screen").performClick()
+
+ // Verify exit full screen button appears
+ composeTestRule.onNodeWithContentDescription("Exit Full Screen").assertIsDisplayed()
+
+ // Click exit full screen
+ composeTestRule.onNodeWithContentDescription("Exit Full Screen").performClick()
+
+ // Verify exit button is gone and enter button returns
+ composeTestRule.onNodeWithContentDescription("Exit Full Screen").assertDoesNotExist()
+ composeTestRule.onNodeWithContentDescription("Enter Full Screen").assertIsDisplayed()
+ }
+
+ @Test
+ fun darkMode_togglesState() {
+ waitForDocumentLoad()
+
+ // Initial state: Light mode (default from cleared prefs), so button says "Enable Dark Mode"
+ composeTestRule.onNodeWithContentDescription("Enable Dark Mode").assertIsDisplayed()
+
+ // Toggle On
+ composeTestRule.onNodeWithContentDescription("Enable Dark Mode").performClick()
+
+ // State changed: Now button says "Disable Dark Mode"
+ composeTestRule.onNodeWithContentDescription("Disable Dark Mode").assertIsDisplayed()
+
+ // Toggle Off
+ composeTestRule.onNodeWithContentDescription("Disable Dark Mode").performClick()
+
+ // State changed back
+ composeTestRule.onNodeWithContentDescription("Enable Dark Mode").assertIsDisplayed()
+ }
+}
\ No newline at end of file
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/tts/BaseTtsSynthesizerTest.kt b/app/src/androidTest/java/com/aryan/reader/tts/BaseTtsSynthesizerTest.kt
similarity index 98%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/tts/BaseTtsSynthesizerTest.kt
rename to app/src/androidTest/java/com/aryan/reader/tts/BaseTtsSynthesizerTest.kt
index d5f7292..9b97fdc 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/tts/BaseTtsSynthesizerTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/tts/BaseTtsSynthesizerTest.kt
@@ -1,5 +1,5 @@
// BaseTtsSynthesizerTest.kt
-package org.dueattendant149.bookreader.tts
+package com.aryan.reader.tts
import android.speech.tts.TextToSpeech
import androidx.test.core.app.ApplicationProvider
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/MainDispatcherRule.kt b/app/src/androidTest/java/com/aryan/reader/tts/MainDispatcherRule.kt
similarity index 94%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/MainDispatcherRule.kt
rename to app/src/androidTest/java/com/aryan/reader/tts/MainDispatcherRule.kt
index 0545722..1b2d937 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/MainDispatcherRule.kt
+++ b/app/src/androidTest/java/com/aryan/reader/tts/MainDispatcherRule.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.tts
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/tts/TtsUtilsTest.kt b/app/src/androidTest/java/com/aryan/reader/tts/TtsUtilsTest.kt
similarity index 98%
rename from app/src/androidTest/java/com/dueattendant149/bookreader/reader/tts/TtsUtilsTest.kt
rename to app/src/androidTest/java/com/aryan/reader/tts/TtsUtilsTest.kt
index 6749d89..a338488 100644
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/tts/TtsUtilsTest.kt
+++ b/app/src/androidTest/java/com/aryan/reader/tts/TtsUtilsTest.kt
@@ -1,5 +1,5 @@
// TtsUtilsTest.kt
-package org.dueattendant149.bookreader.tts
+package com.aryan.reader.tts
import com.google.common.truth.Truth.assertThat
import org.junit.Test
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/AppNavigationTest.kt b/app/src/androidTest/java/com/dueattendant149/bookreader/reader/AppNavigationTest.kt
deleted file mode 100644
index 6c41fd6..0000000
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/AppNavigationTest.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package org.dueattendant149.bookreader
-
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.dueattendant149.bookreader.shared.FileType
-import org.dueattendant149.bookreader.shared.ReaderFeatureSurface
-import org.dueattendant149.bookreader.shared.ReaderPlatform
-import org.dueattendant149.bookreader.shared.SharedFileCapabilities
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class AppNavigationTest {
-
- @Test
- fun appDestinations_useStableReaderRoutes() {
- assertThat(AppDestinations.MAIN_ROUTE).isEqualTo("main")
- assertThat(AppDestinations.PDF_VIEWER_ROUTE).isEqualTo("pdf_viewer")
- assertThat(AppDestinations.EPUB_READER_ROUTE).isEqualTo("epub_reader")
- }
-
- @Test
- fun androidReaderSurface_mapsPdfBackedTypesToPdfViewer() {
- val mappedSurfaces = listOf(
- FileType.PDF,
- FileType.CBZ,
- FileType.CBR,
- FileType.CB7,
- FileType.CBT,
- FileType.PPTX
- ).associateWith { it.readerSurfaceOnAndroid() }
-
- assertThat(mappedSurfaces).containsExactly(
- FileType.PDF, ReaderFeatureSurface.PDF_VIEWER,
- FileType.CBZ, ReaderFeatureSurface.PDF_VIEWER,
- FileType.CBR, ReaderFeatureSurface.PDF_VIEWER,
- FileType.CB7, ReaderFeatureSurface.PDF_VIEWER,
- FileType.CBT, ReaderFeatureSurface.PDF_VIEWER,
- FileType.PPTX, ReaderFeatureSurface.PDF_VIEWER
- )
- }
-
- @Test
- fun androidReaderSurface_mapsTextBackedTypesToEpubReader() {
- val mappedSurfaces = listOf(
- FileType.EPUB,
- FileType.MOBI,
- FileType.MD,
- FileType.TXT,
- FileType.HTML,
- FileType.FB2,
- FileType.DOCX,
- FileType.ODT,
- FileType.FODT
- ).associateWith { it.readerSurfaceOnAndroid() }
-
- assertThat(mappedSurfaces).containsExactly(
- FileType.EPUB, ReaderFeatureSurface.EPUB_READER,
- FileType.MOBI, ReaderFeatureSurface.EPUB_READER,
- FileType.MD, ReaderFeatureSurface.EPUB_READER,
- FileType.TXT, ReaderFeatureSurface.EPUB_READER,
- FileType.HTML, ReaderFeatureSurface.EPUB_READER,
- FileType.FB2, ReaderFeatureSurface.EPUB_READER,
- FileType.DOCX, ReaderFeatureSurface.EPUB_READER,
- FileType.ODT, ReaderFeatureSurface.EPUB_READER,
- FileType.FODT, ReaderFeatureSurface.EPUB_READER
- )
- }
-
- @Test
- fun androidReaderSurface_returnsNullForUnknownFileType() {
- assertThat(FileType.UNKNOWN.readerSurfaceOnAndroid()).isNull()
- }
-
- @Test
- fun appNavBackInterceptor_onlyHandlesResumedNonReaderBackStackEntries() {
- assertThat(
- shouldInterceptAppNavBack(
- currentRoute = AppDestinations.PRO_SCREEN_ROUTE,
- hasPreviousBackStackEntry = true,
- isCurrentEntryResumed = true
- )
- ).isTrue()
- assertThat(
- shouldInterceptAppNavBack(
- currentRoute = AppDestinations.MAIN_ROUTE,
- hasPreviousBackStackEntry = true,
- isCurrentEntryResumed = true
- )
- ).isFalse()
- assertThat(
- shouldInterceptAppNavBack(
- currentRoute = AppDestinations.PDF_VIEWER_ROUTE,
- hasPreviousBackStackEntry = true,
- isCurrentEntryResumed = true
- )
- ).isFalse()
- assertThat(
- shouldInterceptAppNavBack(
- currentRoute = AppDestinations.PRO_SCREEN_ROUTE,
- hasPreviousBackStackEntry = false,
- isCurrentEntryResumed = true
- )
- ).isFalse()
- assertThat(
- shouldInterceptAppNavBack(
- currentRoute = AppDestinations.PRO_SCREEN_ROUTE,
- hasPreviousBackStackEntry = true,
- isCurrentEntryResumed = false
- )
- ).isFalse()
- }
-
- private fun FileType.readerSurfaceOnAndroid(): ReaderFeatureSurface? {
- return SharedFileCapabilities.surfaceFor(this, ReaderPlatform.ANDROID)
- }
-}
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/HomeRecentFileCardTest.kt b/app/src/androidTest/java/com/dueattendant149/bookreader/reader/HomeRecentFileCardTest.kt
deleted file mode 100644
index d32e298..0000000
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/HomeRecentFileCardTest.kt
+++ /dev/null
@@ -1,126 +0,0 @@
-package org.dueattendant149.bookreader
-
-import android.content.Context
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onAllNodesWithContentDescription
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTouchInput
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.dueattendant149.bookreader.data.RecentFileItem
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class HomeRecentFileCardTest {
-
- @get:Rule
- val composeTestRule = createComposeRule()
-
- private val context: Context = ApplicationProvider.getApplicationContext()
-
- @Test
- fun recentFileCardShowsProgressAndUnavailableState() {
- val item = recentBook(
- bookId = "home_unavailable_epub",
- title = "Unavailable Field Guide",
- author = "Casey Example",
- progress = 42f,
- isAvailable = false
- )
-
- setRecentFileCard(item = item)
-
- composeTestRule.onNodeWithTag("HomeRecentFileCard_home_unavailable_epub").assertIsDisplayed()
- composeTestRule.onNodeWithText("Unavailable Field Guide").assertIsDisplayed()
- composeTestRule.onNodeWithText("Casey Example").assertIsDisplayed()
- composeTestRule.onNodeWithText("42%").assertIsDisplayed()
- composeTestRule.onAllNodesWithContentDescription(text(R.string.not_available_locally))[0]
- .assertIsDisplayed()
- }
-
- @Test
- fun recentFileCardClickLongClickAndSelectedOverlayWork() {
- val item = recentBook(
- bookId = "home_selected_pdf",
- title = "Selected Position Notes",
- author = "Morgan Example",
- progress = 7f
- )
- var clicked = false
- var longClicked = false
-
- setRecentFileCard(
- item = item,
- isSelected = true,
- onClick = { clicked = true },
- onLongClick = { longClicked = true }
- )
-
- composeTestRule.onAllNodesWithContentDescription(text(R.string.content_desc_selected))[0]
- .assertIsDisplayed()
-
- composeTestRule.onNodeWithTag("HomeRecentFileCard_home_selected_pdf").performClick()
- composeTestRule.onNodeWithTag("HomeRecentFileCard_home_selected_pdf").performTouchInput {
- down(center)
- advanceEventTime(600)
- up()
- }
-
- assertThat(clicked).isTrue()
- assertThat(longClicked).isTrue()
- }
-
- private fun setRecentFileCard(
- item: RecentFileItem,
- isSelected: Boolean = false,
- isPinned: Boolean = false,
- onClick: () -> Unit = {},
- onLongClick: () -> Unit = {}
- ) {
- composeTestRule.setContent {
- MaterialTheme {
- RecentFileCard(
- item = item,
- isSelected = isSelected,
- isPinned = isPinned,
- onClick = onClick,
- onLongClick = onLongClick,
- isDownloading = false,
- usePdfFileNameAsDisplayName = false
- )
- }
- }
- }
-
- private fun recentBook(
- bookId: String,
- title: String,
- author: String,
- progress: Float,
- isAvailable: Boolean = true
- ): RecentFileItem {
- return RecentFileItem(
- bookId = bookId,
- uriString = "content://home-test/$bookId",
- type = FileType.EPUB,
- displayName = "$bookId.epub",
- timestamp = 1_000L,
- title = title,
- author = author,
- progressPercentage = progress,
- isRecent = true,
- isAvailable = isAvailable
- )
- }
-
- private fun text(resId: Int): String {
- return context.getString(resId)
- }
-}
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/LibraryScreenContentTest.kt b/app/src/androidTest/java/com/dueattendant149/bookreader/reader/LibraryScreenContentTest.kt
deleted file mode 100644
index 1321a82..0000000
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/LibraryScreenContentTest.kt
+++ /dev/null
@@ -1,400 +0,0 @@
-package org.dueattendant149.bookreader
-
-import android.content.Context
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onAllNodesWithText
-import androidx.compose.ui.test.onNodeWithContentDescription
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTextInput
-import androidx.compose.ui.test.performTouchInput
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.data.TagEntity
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalFoundationApi::class)
-@RunWith(AndroidJUnit4::class)
-class LibraryScreenContentTest {
-
- @get:Rule
- val composeTestRule = createComposeRule()
-
- private val context: Context = ApplicationProvider.getApplicationContext()
- private val focusTag = TagEntity(id = "tag_focus", name = "Focus", color = null, createdAt = 1L)
- private val libraryBooks = listOf(
- libraryBook(
- bookId = "pdf_beta",
- type = FileType.PDF,
- displayName = "beta.pdf",
- title = "Beta Manual",
- author = "Mira Example",
- timestamp = 3_000L,
- progress = 84f
- ),
- libraryBook(
- bookId = "epub_gamma",
- type = FileType.EPUB,
- displayName = "gamma.epub",
- title = "Gamma Field Notes",
- author = "Nora Example",
- timestamp = 2_000L,
- progress = 47f,
- tags = listOf(focusTag)
- ),
- libraryBook(
- bookId = "epub_alpha",
- type = FileType.EPUB,
- displayName = "alpha.epub",
- title = "Alpha Orchard",
- author = "Zara Example",
- timestamp = 1_000L,
- progress = 12f,
- tags = listOf(focusTag)
- )
- )
-
- @Test
- fun searchFiltersAndClearRestoresLibraryList() {
- setLibraryContent()
-
- composeTestRule.onNodeWithText("Beta Manual").assertIsDisplayed()
- composeTestRule.onNodeWithContentDescription(text(R.string.action_search)).performClick()
- composeTestRule.onNodeWithTag("LibrarySearchTextField").performTextInput("Gamma")
-
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithText("Gamma Field Notes").fetchSemanticsNodes().isNotEmpty()
- }
- assertNoText("Alpha Orchard")
- assertNoText("Beta Manual")
-
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_clear_query)).performClick()
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithText("Alpha Orchard").fetchSemanticsNodes().isNotEmpty() &&
- composeTestRule.onAllNodesWithText("Beta Manual").fetchSemanticsNodes().isNotEmpty()
- }
-
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_close_search)).performClick()
- composeTestRule.onNodeWithText(text(R.string.library_title)).assertIsDisplayed()
- }
-
- @Test
- fun searchMatchesAuthorAndTagNames() {
- setLibraryContent()
-
- composeTestRule.onNodeWithContentDescription(text(R.string.action_search)).performClick()
- composeTestRule.onNodeWithTag("LibrarySearchTextField").performTextInput("Zara")
-
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithText("Alpha Orchard").fetchSemanticsNodes().isNotEmpty()
- }
- assertNoText("Gamma Field Notes")
-
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_clear_query)).performClick()
- composeTestRule.onNodeWithTag("LibrarySearchTextField").performTextInput("Focus")
-
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithText("Alpha Orchard").fetchSemanticsNodes().isNotEmpty() &&
- composeTestRule.onAllNodesWithText("Gamma Field Notes").fetchSemanticsNodes().isNotEmpty()
- }
- assertNoText("Beta Manual")
- }
-
- @Test
- fun activeFileTypeFilterChipCanBeCleared() {
- setLibraryContent(initialFilters = LibraryFilters(fileTypes = setOf(FileType.EPUB)))
-
- composeTestRule.onNodeWithText("Alpha Orchard").assertIsDisplayed()
- composeTestRule.onNodeWithText("Gamma Field Notes").assertIsDisplayed()
- assertNoText("Beta Manual")
-
- composeTestRule.onNodeWithText(text(R.string.filter_types, FileType.EPUB.name)).performClick()
-
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithText("Beta Manual").fetchSemanticsNodes().isNotEmpty()
- }
- }
-
- @Test
- fun tagAndReadStatusFiltersUseSharedLibraryRules() {
- setLibraryContent(
- initialFilters = LibraryFilters(
- tagIds = setOf(focusTag.id),
- readStatus = ReadStatusFilter.IN_PROGRESS
- )
- )
-
- composeTestRule.onNodeWithText("Alpha Orchard").assertIsDisplayed()
- composeTestRule.onNodeWithText("Gamma Field Notes").assertIsDisplayed()
- assertNoText("Beta Manual")
- composeTestRule.onNodeWithText(text(R.string.filter_tags, focusTag.name)).assertIsDisplayed()
- composeTestRule.onNodeWithText(
- text(R.string.filter_status, text(ReadStatusFilter.IN_PROGRESS.labelRes))
- ).assertIsDisplayed()
- }
-
- @Test
- fun clearSelectionReturnsToNormalToolbar() {
- setLibraryContent()
-
- composeTestRule.onNodeWithTag("LibraryBookItem_epub_alpha").performTouchInput {
- down(center)
- advanceEventTime(600)
- up()
- }
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithText(text(R.string.items_selected_count, 1)).fetchSemanticsNodes().isNotEmpty()
- }
-
- composeTestRule.onNodeWithContentDescription(text(R.string.clear_selection)).performClick()
-
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithText(text(R.string.library_title)).fetchSemanticsNodes().isNotEmpty()
- }
- }
-
- @Test
- fun sortMenuSelectionReordersLibraryItems() {
- setLibraryContent()
-
- assertBookAbove("pdf_beta", "epub_alpha")
-
- composeTestRule.onNodeWithTag("LibrarySortButton").performClick()
- composeTestRule.onNodeWithText(text(SortOrder.TITLE_ASC.labelRes)).performClick()
-
- composeTestRule.waitUntil(5_000) {
- runCatching {
- bookTop("epub_alpha") < bookTop("pdf_beta")
- }.getOrDefault(false)
- }
- assertBookAbove("epub_alpha", "pdf_beta")
- }
-
- @Test
- fun longPressBookShowsContextualToolbarActions() {
- var tagClicked = false
- var pinClicked = false
- var infoClicked = false
- var selectAllClicked = false
- var deleteClicked = false
-
- setLibraryContent(
- onTagClick = { tagClicked = true },
- onPinClick = { pinClicked = true },
- onInfoClick = { infoClicked = true },
- onSelectAllClick = { selectAllClicked = true },
- onDeleteClick = { deleteClicked = true }
- )
-
- composeTestRule.onNodeWithTag("LibraryBookItem_epub_alpha").performTouchInput {
- down(center)
- advanceEventTime(600)
- up()
- }
-
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithText(text(R.string.items_selected_count, 1)).fetchSemanticsNodes().isNotEmpty()
- }
-
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_tag)).performClick()
- composeTestRule.onNodeWithContentDescription(text(R.string.pin_unpin)).performClick()
- composeTestRule.onNodeWithContentDescription(text(R.string.info)).performClick()
- composeTestRule.onNodeWithContentDescription(text(R.string.select_all)).performClick()
- composeTestRule.onNodeWithContentDescription(text(R.string.action_delete)).performClick()
-
- assertThat(tagClicked).isTrue()
- assertThat(pinClicked).isTrue()
- assertThat(infoClicked).isTrue()
- assertThat(selectAllClicked).isTrue()
- assertThat(deleteClicked).isTrue()
- }
-
- @Test
- fun shelvesTabShowsShelfRowsAndNewShelfAction() {
- val shelf = Shelf(
- id = "manual_favorites",
- name = "Manual Favorites",
- type = ShelfType.MANUAL,
- books = listOf(libraryBooks[0], libraryBooks[1])
- )
- var clickedShelfId: String? = null
- var longClickedShelfId: String? = null
- var newShelfClicked = false
-
- setLibraryContent(
- initialPage = 1,
- shelves = listOf(shelf),
- onShelfClick = { clickedShelfId = it.id },
- onShelfLongClick = { longClickedShelfId = it.id },
- onNewShelfClick = { newShelfClicked = true }
- )
-
- composeTestRule.onNodeWithTag("ShelfItem_manual_favorites").assertIsDisplayed()
- composeTestRule.onNodeWithText("Manual Favorites").assertIsDisplayed()
-
- composeTestRule.onNodeWithTag("ShelfItem_manual_favorites").performClick()
- composeTestRule.onNodeWithTag("ShelfItem_manual_favorites").performTouchInput {
- down(center)
- advanceEventTime(600)
- up()
- }
- composeTestRule.onNodeWithTag("LibraryNewShelfFab").performClick()
-
- assertThat(clickedShelfId).isEqualTo("manual_favorites")
- assertThat(longClickedShelfId).isEqualTo("manual_favorites")
- assertThat(newShelfClicked).isTrue()
- }
-
- private fun setLibraryContent(
- initialFilters: LibraryFilters = LibraryFilters(),
- initialPage: Int = 0,
- shelves: List = emptyList(),
- onTagClick: () -> Unit = {},
- onPinClick: () -> Unit = {},
- onInfoClick: () -> Unit = {},
- onSelectAllClick: () -> Unit = {},
- onDeleteClick: () -> Unit = {},
- onShelfClick: (Shelf) -> Unit = {},
- onShelfLongClick: (Shelf) -> Unit = {},
- onNewShelfClick: () -> Unit = {}
- ) {
- val searchQuery = mutableStateOf("")
- val isSearchActive = mutableStateOf(false)
- val filters = mutableStateOf(initialFilters)
- val sortOrder = mutableStateOf(SortOrder.RECENT)
- val selectedItems = mutableStateOf(emptySet())
-
- composeTestRule.setContent {
- val pagerState = rememberPagerState(
- initialPage = initialPage,
- pageCount = { 3 }
- )
- val visibleBooks = sortFiles(
- applyLibraryFilters(
- filterBySearch(libraryBooks, searchQuery.value),
- filters.value
- ),
- sortOrder.value
- )
-
- MaterialTheme {
- LibraryScreenContent(
- tabTitles = listOf(
- text(R.string.tab_all_books),
- text(R.string.tab_shelves),
- text(R.string.tab_folders)
- ),
- recentFiles = visibleBooks,
- rawLibraryFiles = libraryBooks,
- shelves = shelves,
- selectedItems = selectedItems.value,
- selectedShelves = emptySet(),
- sortOrder = sortOrder.value,
- libraryFilters = filters.value,
- allTags = listOf(focusTag),
- pinnedLibraryBookIds = emptySet(),
- pagerState = pagerState,
- scope = rememberCoroutineScope(),
- searchQuery = searchQuery.value,
- isSearchActive = isSearchActive.value,
- onSearchQueryChange = { searchQuery.value = it },
- onSearchActiveChange = { isSearchActive.value = it },
- onSortOrderChange = { sortOrder.value = it },
- onFilterClick = {},
- onClearFilters = { filters.value = LibraryFilters() },
- onRemoveFilter = { filters.value = it },
- onTagClick = onTagClick,
- onPinClick = onPinClick,
- onClearSelection = { selectedItems.value = emptySet() },
- onItemClick = {},
- onItemLongClick = { item -> selectedItems.value = setOf(item) },
- onInfoClick = onInfoClick,
- onSaveClick = null,
- onShareClick = null,
- onDeleteClick = onDeleteClick,
- onSelectAllClick = onSelectAllClick,
- onShelfClick = onShelfClick,
- onShelfLongClick = onShelfLongClick,
- onClearShelfSelection = {},
- onDeleteShelves = {},
- onNewShelfClick = onNewShelfClick,
- onSelectFileClick = {},
- onScanNowClick = {},
- onSyncMetadataClick = {},
- onSelectSyncFolderClick = {},
- onEditFolderFiltersClick = { _, _ -> },
- onDisconnectSyncFolderClick = {},
- downloadingBookIds = emptySet(),
- lastFolderScanTime = null,
- isLoading = false,
- isRefreshing = false,
- syncedFolders = emptyList(),
- onRemoveFolderClick = {},
- onFolderLocalSyncChange = { _, _, _ -> },
- onOpdsBookDownloaded = { _, _ -> },
- onStreamOpdsBook = { _, _ -> },
- onDeleteCatalogStreams = {},
- onSettingsClick = {},
- usePdfFileNameAsDisplayName = false
- )
- }
- }
- }
-
- private fun libraryBook(
- bookId: String,
- type: FileType,
- displayName: String,
- title: String,
- author: String,
- timestamp: Long,
- progress: Float,
- tags: List = emptyList()
- ): RecentFileItem {
- return RecentFileItem(
- bookId = bookId,
- uriString = "content://library-test/$bookId",
- type = type,
- displayName = displayName,
- timestamp = timestamp,
- title = title,
- author = author,
- progressPercentage = progress,
- isRecent = true,
- isAvailable = true,
- fileSize = timestamp * 10,
- tags = tags
- )
- }
-
- private fun assertBookAbove(upperBookId: String, lowerBookId: String) {
- assertThat(bookTop(upperBookId)).isLessThan(bookTop(lowerBookId))
- }
-
- private fun assertNoText(value: String) {
- assertThat(composeTestRule.onAllNodesWithText(value).fetchSemanticsNodes()).isEmpty()
- }
-
- private fun bookTop(bookId: String): Float {
- return composeTestRule
- .onNodeWithTag("LibraryBookItem_$bookId")
- .fetchSemanticsNode()
- .boundsInRoot
- .top
- }
-
- private fun text(resId: Int, vararg args: Any): String {
- return context.getString(resId, *args)
- }
-}
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderLogicTest.kt b/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderLogicTest.kt
deleted file mode 100644
index 131d3bd..0000000
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderLogicTest.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-package org.dueattendant149.bookreader.epubreader
-
-import android.content.Context
-import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import org.dueattendant149.bookreader.epub.EpubBook
-import org.dueattendant149.bookreader.epub.EpubChapter
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.io.File
-
-@RunWith(AndroidJUnit4::class)
-class EpubReaderLogicTest {
-
- private lateinit var context: Context
- private lateinit var testDir: File
- private lateinit var testBook: EpubBook
-
- @Before
- fun setup() {
- context = ApplicationProvider.getApplicationContext()
- testDir = File(context.cacheDir, "test_epub_search").apply {
- deleteRecursively()
- mkdirs()
- }
-
- val chapter1File = File(testDir, "chapter1.html").apply {
- writeText("A simple Test case.
")
- }
- val chapter2File = File(testDir, "chapter2.html").apply {
- writeText("Another test case here.
The word Test appears twice.
")
- }
-
- testBook = EpubBook(
- fileName = "test.epub",
- title = "Test Book",
- author = "Tester",
- language = "en",
- coverImage = null,
- extractionBasePath = testDir.absolutePath,
- chapters = listOf(
- EpubChapter(
- chapterId = "ch1",
- absPath = chapter1File.absolutePath,
- title = "Chapter 1",
- htmlFilePath = "chapter1.html",
- plainTextContent = "",
- htmlContent = ""
- ),
- EpubChapter(
- chapterId = "ch2",
- absPath = chapter2File.absolutePath,
- title = "Chapter 2",
- htmlFilePath = "chapter2.html",
- plainTextContent = "",
- htmlContent = ""
- )
- )
- )
- }
-
- @After
- fun tearDown() {
- testDir.deleteRecursively()
- }
-
- @Test
- fun search_findsCorrectResults() = runTest {
- val results = createEpubSearcher(testBook)("case")
-
- assertThat(results).hasSize(2)
- assertThat(results.count { it.locationTitle == "Chapter 1" }).isEqualTo(1)
- assertThat(results.count { it.locationTitle == "Chapter 2" }).isEqualTo(1)
- }
-
- @Test
- fun search_isCaseInsensitive() = runTest {
- val results = createEpubSearcher(testBook)("test")
-
- assertThat(results).hasSize(3)
- assertThat(results[0].locationTitle).isEqualTo("Chapter 1")
- assertThat(results[1].locationTitle).isEqualTo("Chapter 2")
- assertThat(results[2].locationTitle).isEqualTo("Chapter 2")
- }
-
- @Test
- fun search_noResultsFound() = runTest {
- val results = createEpubSearcher(testBook)("nonexistent")
-
- assertThat(results).isEmpty()
- }
-
- @Test
- fun search_createsCorrectSnippetHighlight() = runTest {
- val result = createEpubSearcher(testBook)("Test").first()
- val boldRanges = result.snippet.spanStyles.filter { it.item == SpanStyle(fontWeight = FontWeight.Bold) }
-
- assertThat(boldRanges).hasSize(1)
- val highlight = boldRanges.first()
- assertThat(result.snippet.text.substring(highlight.start, highlight.end)).isEqualTo("Test")
- }
-}
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderScreenTest.kt b/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderScreenTest.kt
deleted file mode 100644
index 499bfc3..0000000
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderScreenTest.kt
+++ /dev/null
@@ -1,818 +0,0 @@
-package org.dueattendant149.bookreader.epubreader
-
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertTextContains
-import androidx.compose.ui.test.click
-import androidx.compose.ui.test.hasSetTextAction
-import androidx.compose.ui.test.junit4.createEmptyComposeRule
-import androidx.compose.ui.test.onAllNodesWithContentDescription
-import androidx.compose.ui.test.onAllNodesWithTag
-import androidx.compose.ui.test.onAllNodesWithText
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTextClearance
-import androidx.compose.ui.test.performTextInput
-import androidx.compose.ui.test.performTouchInput
-import androidx.core.content.FileProvider
-import androidx.test.core.app.ActivityScenario
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import org.dueattendant149.bookreader.FileType
-import org.dueattendant149.bookreader.FileHasher
-import org.dueattendant149.bookreader.MainActivity
-import org.dueattendant149.bookreader.R
-import org.dueattendant149.bookreader.RenderMode
-import org.dueattendant149.bookreader.data.AppDatabase
-import org.dueattendant149.bookreader.data.RecentFileEntity
-import org.dueattendant149.bookreader.shared.EpubAnnotationSerializer
-import org.dueattendant149.bookreader.shared.ReaderLocator
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.runBlocking
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.io.File
-import java.util.UUID
-
-@RunWith(AndroidJUnit4::class)
-class EpubReaderScreenTest {
-
- @get:Rule
- val composeTestRule = createEmptyComposeRule()
-
- private val fixtureAssetName = "epub/reader_test_book.epub"
- private val fixtureBookTitle = "Reader Android UI Test Book"
- private val sanitizedFixtureBookTitle = "ReaderAndroidUITestBook"
- private val targetContext: Context = ApplicationProvider.getApplicationContext()
- private val instrumentationContext: Context = InstrumentationRegistry.getInstrumentation().context
- private var currentEpubFile: File? = null
- private var scenario: ActivityScenario? = null
- private lateinit var fixtureBookId: String
-
- @Before
- fun setup() {
- clearReaderPrefs()
- fixtureBookId = requireNotNull(
- runBlocking {
- FileHasher.calculateSha256 {
- instrumentationContext.assets.open(fixtureAssetName)
- }
- }
- )
- runBlocking {
- AppDatabase.getDatabase(targetContext)
- .recentFileDao()
- .deleteFilePermanently(listOf(fixtureBookId))
- }
- }
-
- @After
- fun tearDown() {
- scenario?.close()
- currentEpubFile?.let {
- if (it.exists()) it.delete()
- }
- }
-
- @Test
- fun fixtureEpub_opensReaderAndShowsBookTitle() {
- launchFixtureReader()
- waitForReader()
- showReaderChrome()
-
- waitForText(fixtureBookTitle)
- composeTestRule.onNodeWithText(fixtureBookTitle).assertIsDisplayed()
- assertThat(hasContentDescription(text(R.string.tooltip_search))).isTrue()
- assertThat(hasContentDescription(text(R.string.content_desc_chapters_menu))).isTrue()
- }
-
- @Test
- fun fixtureEpub_recordsInitialReadingPosition() {
- launchFixtureReader()
- waitForReader()
-
- waitForRecentFile(timeoutMillis = 30_000) { recentFile ->
- recentFile?.lastChapterIndex != null &&
- recentFile.locatorBlockIndex != null &&
- recentFile.locatorCharOffset != null &&
- recentFile.progressPercentage != null
- }
-
- val recentFile = readFixtureRecentFile()
- assertThat(recentFile?.lastChapterIndex).isAtLeast(0)
- assertThat(recentFile?.locatorBlockIndex).isAtLeast(0)
- assertThat(recentFile?.locatorCharOffset).isAtLeast(0)
- assertThat(recentFile?.progressPercentage).isAtLeast(0f)
- }
-
- @Test
- fun fixtureEpub_restoresSeededReadingPositionWithoutResettingToStart() {
- launchFixtureReader { fixtureUri ->
- seedFixtureRecentFile(
- uriString = fixtureUri.toString(),
- chapterIndex = 1,
- blockIndex = 1,
- charOffset = 0,
- progress = 45f
- )
- }
- waitForReader()
-
- waitForRecentFile(timeoutMillis = 30_000) { recentFile ->
- recentFile?.lastChapterIndex == 1 &&
- recentFile.locatorBlockIndex == 1 &&
- recentFile.locatorCharOffset == 0 &&
- (recentFile.progressPercentage ?: 0f) >= 45f
- }
- }
-
- @Test
- fun fixtureEpub_drawerShowsFixtureChapters() {
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.content_desc_chapters_menu))
-
- waitForText(text(R.string.tab_chapters))
- waitForTextContaining("Chapter One")
- waitForTextContaining("Chapter Two")
- waitForTextContaining("Chapter Three")
- }
-
- @Test
- fun fixtureEpub_drawerShowsFixtureImageCatalog() {
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.content_desc_chapters_menu))
- clickText(text(R.string.tab_images))
-
- waitForText("Fixture diagram")
- waitForTextContaining("Chapter Three")
- assertThat(hasContentDescription(text(R.string.content_desc_download_image))).isTrue()
- }
-
- @Test
- fun fixtureEpub_searchFindsUniqueFixtureMarker() {
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.tooltip_search))
- waitForTag("SearchTextField")
-
- composeTestRule.onNodeWithTag("SearchTextField").performTextInput("SEARCH_TARGET_DELTA")
- composeTestRule.onNodeWithTag("SearchTextField").assertTextContains("SEARCH_TARGET_DELTA")
-
- waitForTag("SearchResultItem_1", timeoutMillis = 20_000)
- composeTestRule.onNodeWithTag("SearchResultItem_1").assertIsDisplayed()
- }
-
- @Test
- fun fixtureEpub_searchNavigationPositionSurvivesReadingModeSwitches() {
- launchFixtureReader()
- waitForReader()
-
- navigateToFixtureSearchResult("SEARCH_TARGET_DELTA", expectedChapterIndex = 1)
- clickContentDescription(text(R.string.tooltip_close_search))
-
- waitForRecentFile(timeoutMillis = 30_000) { recentFile ->
- recentFile?.lastChapterIndex == 1 &&
- recentFile.locatorBlockIndex != null &&
- recentFile.locatorCharOffset != null &&
- recentFile.progressPercentage != null
- }
-
- openOverflowMenu()
- clickText(text(R.string.menu_change_reading_mode))
- clickText(text(R.string.menu_reading_mode_paginated))
- waitForRenderMode(RenderMode.PAGINATED)
-
- waitForRecentFile(timeoutMillis = 20_000) { recentFile ->
- recentFile?.lastChapterIndex == 1 &&
- (recentFile.progressPercentage ?: 0f) > 0f
- }
-
- openOverflowMenu()
- clickText(text(R.string.menu_change_reading_mode))
- clickText(text(R.string.menu_reading_mode_vertical_webview))
- waitForRenderMode(RenderMode.VERTICAL_SCROLL)
-
- waitForRecentFile(timeoutMillis = 20_000) { recentFile ->
- recentFile?.lastChapterIndex == 1 &&
- (recentFile.progressPercentage ?: 0f) > 0f
- }
- }
-
- @Test
- fun fixtureEpub_addsCurrentPageBookmarkPersistsAndDeletes() {
- launchFixtureReader()
- waitForReader()
-
- openOverflowMenu()
- clickText(text(R.string.menu_bookmark_this_page))
-
- waitForFixtureBookmarks(timeoutMillis = 20_000) { bookmarksJson ->
- parseBookmarksJson(bookmarksJson).isNotEmpty()
- }
-
- clickReaderControl(text(R.string.content_desc_chapters_menu))
- clickText(text(R.string.tab_bookmarks))
- waitForTextContaining("Chapter One")
- assertThat(hasContentDescription(text(R.string.content_desc_more_options_bookmark))).isTrue()
-
- clickContentDescription(text(R.string.content_desc_more_options_bookmark))
- clickText(text(R.string.action_delete))
- waitForText(text(R.string.dialog_delete_bookmark))
- clickText(text(R.string.action_delete))
-
- waitForText(text(R.string.no_bookmarks_yet))
- waitForFixtureBookmarks(timeoutMillis = 10_000) { bookmarksJson ->
- parseBookmarksJson(bookmarksJson).isEmpty()
- }
- }
-
- @Test
- fun fixtureEpub_drawerShowsSeededBookmarkAndSupportsRenameDelete() {
- seedFixtureBookmark()
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.content_desc_chapters_menu))
- clickText(text(R.string.tab_bookmarks))
-
- waitForText("BOOKMARK_TARGET_ECHO")
- waitForText("Chapter Two")
-
- clickContentDescription(text(R.string.content_desc_more_options_bookmark))
- clickText(text(R.string.action_rename))
- waitForText(text(R.string.dialog_rename_bookmark))
- composeTestRule.onNode(hasSetTextAction()).performTextInput("Renamed fixture bookmark")
- clickText(text(R.string.action_save))
- waitForText("Renamed fixture bookmark")
-
- clickContentDescription(text(R.string.content_desc_more_options_bookmark))
- clickText(text(R.string.action_delete))
- waitForText(text(R.string.dialog_delete_bookmark))
- clickText(text(R.string.action_delete))
- waitForText(text(R.string.no_bookmarks_yet))
- }
-
- @Test
- fun fixtureEpub_drawerShowsSeededAnnotationWithNoteAndFilter() {
- seedFixtureHighlight()
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.content_desc_chapters_menu))
- clickText(text(R.string.tab_annotations))
-
- waitForText("ANNOTATION_TARGET_GOLF")
- waitForText("Fixture note survives startup")
- waitForTextContaining("Chapter Three")
-
- clickText(text(R.string.filter_with_notes))
- waitForText("ANNOTATION_TARGET_GOLF")
- waitForText("Fixture note survives startup")
- }
-
- @Test
- fun fixtureEpub_seededAnnotationSupportsColorNoteAndDeletePersistence() {
- seedFixtureHighlight()
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.content_desc_chapters_menu))
- clickText(text(R.string.tab_annotations))
-
- waitForText("ANNOTATION_TARGET_GOLF")
- clickContentDescription(text(R.string.content_desc_options))
- composeTestRule.onNodeWithTag("HighlightColor_blue").performClick()
-
- waitForFixtureHighlights(timeoutMillis = 10_000) { highlightsJson ->
- parseHighlightsJson(highlightsJson).singleOrNull()?.color == HighlightColor.BLUE
- }
-
- clickContentDescription(text(R.string.content_desc_options))
- clickText(text(R.string.menu_edit_note))
- waitForText(text(R.string.action_save_note))
- composeTestRule.onNode(hasSetTextAction())
- .performTextClearance()
- composeTestRule.onNode(hasSetTextAction())
- .performTextInput("Updated fixture note")
- clickText(text(R.string.action_save_note))
-
- waitForText("Updated fixture note")
- waitForFixtureHighlights(timeoutMillis = 10_000) { highlightsJson ->
- parseHighlightsJson(highlightsJson).singleOrNull()?.note == "Updated fixture note"
- }
-
- clickContentDescription(text(R.string.content_desc_options))
- clickText(text(R.string.action_delete))
- waitForText(text(R.string.dialog_delete_highlight))
- clickText(text(R.string.action_delete))
-
- waitForText(text(R.string.no_highlights_yet))
- waitForFixtureHighlights(timeoutMillis = 10_000) { highlightsJson ->
- parseHighlightsJson(highlightsJson).isEmpty()
- }
- }
-
- @Test
- fun fixtureEpub_overflowSwitchesReadingModeAndTogglesPageOptions() {
- launchFixtureReader()
- waitForReader()
-
- openOverflowMenu()
- clickText(text(R.string.menu_change_reading_mode))
- clickText(text(R.string.menu_reading_mode_paginated))
- waitForRenderMode(RenderMode.PAGINATED)
- composeTestRule.waitForIdle()
-
- openOverflowMenu()
- clickText(text(R.string.menu_tap_to_turn_pages))
- assertReaderSettingEventually(
- prefsName = "epub_reader_settings",
- key = "tap_to_navigate_enabled",
- expected = true
- )
-
- openOverflowMenu()
- clickText(text(R.string.menu_realistic_page_turns))
- assertReaderSettingEventually(
- prefsName = "reader_prefs",
- key = "page_turn_animation_enabled",
- expected = true
- )
-
- openOverflowMenu()
- clickText(text(R.string.menu_keep_screen_on))
- assertReaderSettingEventually(
- prefsName = "reader_prefs",
- key = "keep_screen_on_enabled",
- expected = true
- )
- }
-
- @Test
- fun fixtureEpub_visualOptionsSheetPersistsProgressPosition() {
- launchFixtureReader()
- waitForReader()
-
- openOverflowMenu()
- clickText(text(R.string.menu_visual_options))
-
- waitForText(text(R.string.visual_options_system_ui))
- waitForText(text(R.string.visual_options_progress_bar))
- waitForText(text(R.string.visual_options_progress_bar_position))
- clickText(text(R.string.label_top))
-
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- targetContext.getSharedPreferences("epub_reader_settings", Context.MODE_PRIVATE)
- .getInt("reader_page_info_position", 0) == 1
- }
- }
-
- @Test
- fun fixtureEpub_formatPanelShowsControlsAndPersistsLocalFontSize() {
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.content_desc_text_formatting))
- waitForText(text(R.string.section_font_alignment))
- waitForText(text(R.string.section_layout_spacing))
- waitForText(text(R.string.label_font_size))
- waitForText(text(R.string.label_line_height))
- waitForText(text(R.string.label_paragraph_gap))
- waitForText(text(R.string.label_image_size))
- waitForText(text(R.string.label_horizontal_margin))
- waitForText(text(R.string.label_vertical_margin))
-
- composeTestRule.onAllNodesWithContentDescription(text(R.string.content_desc_select_mode))[0]
- .performClick()
- clickText(text(R.string.format_local))
-
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- targetContext.getSharedPreferences("epub_reader_settings", Context.MODE_PRIVATE)
- .getBoolean("format_is_local_$fixtureBookId", false)
- }
-
- composeTestRule.onAllNodesWithContentDescription(text(R.string.content_desc_increase))[0]
- .performClick()
-
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- targetContext.getSharedPreferences("epub_reader_settings", Context.MODE_PRIVATE)
- .getFloat("local_font_size_$fixtureBookId", 1.0f) > 1.0f
- }
- }
-
- @Test
- fun fixtureEpub_fontSelectionSheetPersistsFontFamily() {
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.content_desc_text_formatting))
- waitForText(text(R.string.section_font_alignment))
- clickContentDescription(text(R.string.content_desc_select_font_family))
-
- waitForText(text(R.string.select_font))
- waitForText(text(R.string.tab_presets))
- waitForText(text(R.string.tab_imported))
- clickText("Lato")
-
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- targetContext.getSharedPreferences("epub_reader_settings", Context.MODE_PRIVATE)
- .getString("reader_font_family", "original") == "lato"
- }
- }
-
- @Test
- fun fixtureEpub_themePanelShowsThemesAndPersistsSelection() {
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.tooltip_theme_desc))
-
- waitForText(text(R.string.reading_themes))
- waitForText(text(R.string.theme_solid_colors))
- waitForText("Light")
- waitForText("Dark")
- clickText("Sepia")
-
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- targetContext.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
- .getString(PREF_READER_THEME, "system") == "sepia"
- }
- }
-
- @Test
- fun fixtureEpub_drawerShowsEmptyBookmarkAndAnnotationStates() {
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.content_desc_chapters_menu))
- clickText(text(R.string.tab_bookmarks))
- waitForText(text(R.string.no_bookmarks_yet))
-
- clickText(text(R.string.tab_annotations))
- waitForText(text(R.string.no_highlights_yet))
- }
-
- @Test
- fun fixtureEpub_searchCanClearAndClose() {
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.tooltip_search))
- waitForTag("SearchTextField")
-
- composeTestRule.onNodeWithTag("SearchTextField")
- .performTextInput("SEARCH_TARGET_DELTA")
- waitForTextContaining("SEARCH_TARGET_DELTA")
-
- clickContentDescription(text(R.string.tooltip_clear_search))
- waitForNoContentDescription(text(R.string.tooltip_clear_search))
-
- clickContentDescription(text(R.string.tooltip_close_search))
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- hasContentDescription(text(R.string.tooltip_search))
- }
- }
-
- @Test
- fun fixtureEpub_dictionarySettingsShowsLookupSections() {
- launchFixtureReader()
- waitForReader()
-
- clickReaderControl(text(R.string.content_desc_dictionary_settings))
-
- waitForText(text(R.string.dict_lookup_settings))
- waitForText(text(R.string.tooltip_dictionary))
- waitForText(text(R.string.dict_translate))
- waitForText(text(R.string.tooltip_search))
- }
-
- @Test
- fun fixtureEpub_ttsReplacementSheetShowsGlobalAndBookScopes() {
- launchFixtureReader()
- waitForReader()
-
- openOverflowMenu()
- clickText(text(R.string.menu_tts_settings))
- clickText(text(R.string.menu_tts_word_replacements))
-
- waitForText(text(R.string.menu_tts_word_replacements))
- waitForText(fixtureBookTitle)
- waitForText(text(R.string.tts_replacements_tab_global))
- waitForText(text(R.string.tts_replacements_tab_this_book))
- waitForText(text(R.string.tts_replacements_enable))
- }
-
- private fun launchFixtureReader(beforeLaunch: (Uri) -> Unit = {}) {
- val fixtureUri = copyAndroidTestAssetToCache(fixtureAssetName)
- beforeLaunch(fixtureUri)
- scenario = ActivityScenario.launch(createEpubViewIntent(fixtureUri))
- }
-
- private fun clearReaderPrefs() {
- listOf(
- "epub_reader_settings",
- "epub_reader_bookmarks",
- "reader_prefs"
- ).forEach { prefsName ->
- targetContext.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
- .edit()
- .clear()
- .commit()
- }
- targetContext.getSharedPreferences("reader_user_prefs", Context.MODE_PRIVATE)
- .edit()
- .putString("render_mode", "VERTICAL_SCROLL")
- .commit()
- }
-
- private fun text(resId: Int): String = targetContext.getString(resId)
-
- private fun seedFixtureBookmark() {
- val bookmark = org.json.JSONObject().apply {
- put("cfi", "android-locator:1:1:0")
- put("chapterTitle", "Chapter Two")
- put("label", org.json.JSONObject.NULL)
- put("snippet", "BOOKMARK_TARGET_ECHO")
- put("pageInChapter", 1)
- put("totalPagesInChapter", 3)
- put("chapterIndex", 1)
- put(
- "locator",
- org.json.JSONObject().apply {
- put("chapterIndex", 1)
- put("chapterId", "chapter-02")
- put("pageIndex", 0)
- put("blockIndex", 1)
- put("charOffset", 0)
- put("textQuote", "BOOKMARK_TARGET_ECHO")
- put("cfi", "android-locator:1:1:0")
- }
- )
- }
-
- targetContext.getSharedPreferences("epub_reader_bookmarks", Context.MODE_PRIVATE)
- .edit()
- .putStringSet("bookmarks_cfi_$sanitizedFixtureBookTitle", setOf(bookmark.toString()))
- .commit()
- }
-
- private fun seedFixtureHighlight() {
- val highlight = UserHighlight(
- id = "fixture_annotation_golf",
- cfi = "android-locator:2:1:0",
- text = "ANNOTATION_TARGET_GOLF",
- color = HighlightColor.GREEN,
- chapterIndex = 2,
- note = "Fixture note survives startup",
- locator = ReaderLocator(
- chapterIndex = 2,
- chapterId = "chapter-03",
- blockIndex = 1,
- charOffset = 0,
- textQuote = "ANNOTATION_TARGET_GOLF",
- cfi = "android-locator:2:1:0"
- )
- )
-
- targetContext.getSharedPreferences("epub_reader_settings", Context.MODE_PRIVATE)
- .edit()
- .putString("highlights_data_$sanitizedFixtureBookTitle", highlightsToJson(listOf(highlight)))
- .commit()
- }
-
- private fun seedFixtureRecentFile(
- uriString: String,
- chapterIndex: Int,
- blockIndex: Int,
- charOffset: Int,
- progress: Float
- ) {
- val now = System.currentTimeMillis()
- runBlocking {
- AppDatabase.getDatabase(targetContext)
- .recentFileDao()
- .insertOrUpdateFile(
- RecentFileEntity(
- bookId = fixtureBookId,
- uriString = uriString,
- type = FileType.EPUB,
- displayName = fixtureBookTitle,
- timestamp = now,
- coverImagePath = null,
- title = fixtureBookTitle,
- author = "Fixture Author",
- lastChapterIndex = chapterIndex,
- lastPage = null,
- lastPositionCfi = "android-locator:$chapterIndex:$blockIndex:$charOffset",
- progressPercentage = progress,
- isRecent = true,
- isAvailable = true,
- lastModifiedTimestamp = now,
- isDeleted = false,
- locatorBlockIndex = blockIndex,
- locatorCharOffset = charOffset,
- bookmarks = null,
- sourceFolderUri = null,
- isReflowPreferred = false,
- customName = null,
- highlights = null,
- fileSize = 0L,
- fileContentModifiedTimestamp = 0L,
- seriesName = null,
- seriesIndex = null,
- description = null,
- folderTextMetadataParsed = false,
- folderCoverMetadataParsed = false,
- originalTitle = fixtureBookTitle,
- originalAuthor = "Fixture Author",
- originalSeriesName = null,
- originalSeriesIndex = null,
- originalDescription = null
- )
- )
- }
- }
-
- private fun createEpubViewIntent(uri: Uri): Intent {
- return Intent(targetContext, MainActivity::class.java).apply {
- action = Intent.ACTION_VIEW
- setDataAndType(uri, "application/epub+zip")
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- }
-
- private fun copyAndroidTestAssetToCache(assetName: String): Uri {
- val file = File(targetContext.cacheDir, "${UUID.randomUUID()}_reader_test_book.epub")
- currentEpubFile = file
-
- instrumentationContext.assets.open(assetName).use { inputStream ->
- file.outputStream().use { outputStream ->
- inputStream.copyTo(outputStream)
- }
- }
-
- return FileProvider.getUriForFile(
- targetContext,
- "${targetContext.packageName}.provider",
- file
- )
- }
-
- private fun navigateToFixtureSearchResult(query: String, expectedChapterIndex: Int) {
- clickReaderControl(text(R.string.tooltip_search))
- waitForTag("SearchTextField")
-
- composeTestRule.onNodeWithTag("SearchTextField").performTextInput(query)
- waitForTag("SearchResultItem_$expectedChapterIndex", timeoutMillis = 20_000)
- composeTestRule.onNodeWithTag("SearchResultItem_$expectedChapterIndex").performClick()
- }
-
- private fun waitForReader() {
- waitForTag("ReaderContainer", timeoutMillis = 30_000)
- }
-
- private fun showReaderChrome() {
- if (hasAnyReaderControl()) return
-
- composeTestRule.onRoot().performTouchInput { click(center) }
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- hasAnyReaderControl()
- }
- }
-
- private fun clickReaderControl(contentDescription: String) {
- showReaderChrome()
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- hasContentDescription(contentDescription)
- }
- composeTestRule.onAllNodesWithContentDescription(contentDescription)[0].performClick()
- }
-
- private fun clickContentDescription(contentDescription: String) {
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- hasContentDescription(contentDescription)
- }
- composeTestRule.onAllNodesWithContentDescription(contentDescription)[0].performClick()
- }
-
- private fun clickText(value: String) {
- composeTestRule.waitUntil(timeoutMillis = 10_000) {
- composeTestRule.onAllNodesWithText(value).fetchSemanticsNodes().isNotEmpty()
- }
- composeTestRule.onAllNodesWithText(value)[0].performClick()
- }
-
- private fun openOverflowMenu() {
- clickReaderControl(text(R.string.content_desc_more_options))
- }
-
- private fun hasAnyReaderControl(): Boolean {
- return hasContentDescription(text(R.string.tooltip_search)) ||
- hasContentDescription(text(R.string.content_desc_chapters_menu)) ||
- hasContentDescription(text(R.string.content_desc_more_options))
- }
-
- private fun hasContentDescription(contentDescription: String): Boolean {
- return composeTestRule
- .onAllNodesWithContentDescription(contentDescription)
- .fetchSemanticsNodes()
- .isNotEmpty()
- }
-
- private fun waitForTag(tag: String, timeoutMillis: Long = 10_000) {
- composeTestRule.waitUntil(timeoutMillis = timeoutMillis) {
- composeTestRule.onAllNodesWithTag(tag).fetchSemanticsNodes().isNotEmpty()
- }
- }
-
- private fun waitForText(value: String, timeoutMillis: Long = 10_000) {
- composeTestRule.waitUntil(timeoutMillis = timeoutMillis) {
- composeTestRule.onAllNodesWithText(value).fetchSemanticsNodes().isNotEmpty()
- }
- }
-
- private fun waitForTextContaining(value: String, timeoutMillis: Long = 10_000) {
- composeTestRule.waitUntil(timeoutMillis = timeoutMillis) {
- composeTestRule.onAllNodesWithText(value, substring = true).fetchSemanticsNodes().isNotEmpty()
- }
- }
-
- private fun waitForNoContentDescription(contentDescription: String, timeoutMillis: Long = 5_000) {
- composeTestRule.waitUntil(timeoutMillis = timeoutMillis) {
- composeTestRule
- .onAllNodesWithContentDescription(contentDescription)
- .fetchSemanticsNodes()
- .isEmpty()
- }
- }
-
- private fun waitForRenderMode(expected: RenderMode) {
- composeTestRule.waitUntil(timeoutMillis = 20_000) {
- targetContext.getSharedPreferences("reader_user_prefs", Context.MODE_PRIVATE)
- .getString("render_mode", RenderMode.VERTICAL_SCROLL.name) == expected.name
- }
- }
-
- private fun assertReaderSettingEventually(
- prefsName: String,
- key: String,
- expected: Boolean
- ) {
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- targetContext.getSharedPreferences(prefsName, Context.MODE_PRIVATE)
- .getBoolean(key, !expected) == expected
- }
- }
-
- private fun readFixtureRecentFile(): RecentFileEntity? {
- return runBlocking {
- AppDatabase.getDatabase(targetContext)
- .recentFileDao()
- .getFileByBookId(fixtureBookId)
- }
- }
-
- private fun waitForRecentFile(
- timeoutMillis: Long = 10_000,
- predicate: (RecentFileEntity?) -> Boolean
- ) {
- composeTestRule.waitUntil(timeoutMillis = timeoutMillis) {
- predicate(readFixtureRecentFile())
- }
- }
-
- private fun waitForFixtureBookmarks(
- timeoutMillis: Long = 10_000,
- predicate: (String?) -> Boolean
- ) {
- composeTestRule.waitUntil(timeoutMillis = timeoutMillis) {
- predicate(readFixtureRecentFile()?.bookmarks)
- }
- }
-
- private fun waitForFixtureHighlights(
- timeoutMillis: Long = 10_000,
- predicate: (String?) -> Boolean
- ) {
- composeTestRule.waitUntil(timeoutMillis = timeoutMillis) {
- predicate(readFixtureRecentFile()?.highlights)
- }
- }
-
- private fun parseBookmarksJson(rawJson: String?): Set {
- return EpubAnnotationSerializer.parseBookmarksJson(rawJson)
- }
-}
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/PaginatedReaderViewModelTest.kt b/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/PaginatedReaderViewModelTest.kt
deleted file mode 100644
index bde9ccc..0000000
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/PaginatedReaderViewModelTest.kt
+++ /dev/null
@@ -1,180 +0,0 @@
-package org.dueattendant149.bookreader.paginatedreader
-
-import android.content.Context
-import android.os.Build
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshots.Snapshot
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.TextMeasurer
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SdkSuppress
-import org.dueattendant149.bookreader.SearchResult
-import org.dueattendant149.bookreader.epub.EpubBook
-import com.google.common.truth.Truth.assertThat
-import io.mockk.mockk
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.test.advanceUntilIdle
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-private class FakePaginator(
- initiallyLoading: Boolean,
- initialPageCount: Int,
- initialGeneration: Int
-) : IPaginator {
- override var isLoading by mutableStateOf(initiallyLoading)
- override var totalPageCount by mutableIntStateOf(initialPageCount)
- override var generation by mutableIntStateOf(initialGeneration)
- override val pageShiftRequest: Flow = emptyFlow()
-
- var lastNavigatedHref: String? = null
- var lastNavigatedChapter: String? = null
-
- override fun getPageContent(pageIndex: Int): Page? = null
- override fun getChapterPathForPage(pageIndex: Int): String? = null
- override fun getPlainTextForChapter(chapterIndex: Int): String? = null
-
- override fun navigateToHref(
- currentChapterAbsPath: String,
- href: String,
- onNavigationComplete: (pageIndex: Int) -> Unit
- ) {
- lastNavigatedChapter = currentChapterAbsPath
- lastNavigatedHref = href
- }
-
- override fun findPageForSearchResult(
- result: SearchResult,
- onResult: (pageIndex: Int) -> Unit
- ) = Unit
-
- override fun findPageForAnchor(
- chapterIndex: Int,
- anchor: String?,
- onResult: (pageIndex: Int) -> Unit
- ) = Unit
-
- override fun findPageForCfi(chapterIndex: Int, cfi: String, onResult: (pageIndex: Int) -> Unit) = Unit
- override fun findPageForCfiAndOffset(chapterIndex: Int, cfi: String, charOffset: Int): Int? = null
- override fun findChapterIndexForPage(pageIndex: Int): Int? = null
- override fun getCfiForPage(pageIndex: Int): String? = null
- override fun onUserScrolledTo(pageIndex: Int) = Unit
- override fun getActiveAnchorForPage(pageIndex: Int, tocAnchors: List): String? = null
-}
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(AndroidJUnit4::class)
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
-class PaginatedReaderViewModelTest {
-
- @get:Rule
- val mainDispatcherRule = MainDispatcherRule()
-
- private lateinit var viewModel: PaginatedReaderViewModel
- private lateinit var fakePaginator: FakePaginator
-
- @Before
- fun setUp() {
- viewModel = PaginatedReaderViewModel()
- fakePaginator = FakePaginator(
- initiallyLoading = true,
- initialPageCount = 0,
- initialGeneration = 0
- )
- viewModel.setPaginatorForTest(fakePaginator)
- }
-
- @Test
- fun uiState_reflectsPaginatorInitialState() = runTest {
- val initialState = viewModel.uiState.value
- assertThat(initialState.isLoading).isTrue()
- assertThat(initialState.totalPageCount).isEqualTo(0)
- assertThat(initialState.generation).isEqualTo(0)
- }
-
- @Test
- fun uiState_updatesWhenPaginatorIsLoadingChanges() = runTest {
- assertThat(viewModel.uiState.value.isLoading).isTrue()
-
- fakePaginator.isLoading = false
- Snapshot.sendApplyNotifications()
- advanceUntilIdle()
-
- assertThat(viewModel.uiState.value.isLoading).isFalse()
- }
-
- @Test
- fun uiState_updatesWhenPaginatorTotalPageCountChanges() = runTest {
- assertThat(viewModel.uiState.value.totalPageCount).isEqualTo(0)
-
- fakePaginator.totalPageCount = 123
- Snapshot.sendApplyNotifications()
- advanceUntilIdle()
-
- assertThat(viewModel.uiState.value.totalPageCount).isEqualTo(123)
- }
-
- @Test
- fun uiState_updatesWhenPaginatorGenerationChanges() = runTest {
- assertThat(viewModel.uiState.value.generation).isEqualTo(0)
-
- fakePaginator.generation = 5
- Snapshot.sendApplyNotifications()
- advanceUntilIdle()
-
- assertThat(viewModel.uiState.value.generation).isEqualTo(5)
- }
-
- @Test
- fun onLinkClick_callsPaginatorNavigateToHrefWithCorrectArguments() {
- val currentChapter = "chapter1.xhtml"
- val href = "#section2"
-
- viewModel.onLinkClick(currentChapter, href) {}
-
- assertThat(fakePaginator.lastNavigatedChapter).isEqualTo(currentChapter)
- assertThat(fakePaginator.lastNavigatedHref).isEqualTo(href)
- }
-
- @Test
- fun initialize_whenPaginatorAlreadySet_keepsExistingPaginator() = runTest {
- val context = ApplicationProvider.getApplicationContext()
- val existingPaginator = viewModel.paginator
-
- viewModel.initialize(
- book = EpubBook(
- fileName = "test.epub",
- title = "Test Book",
- author = "Test Author",
- language = "en",
- coverImage = null
- ),
- textMeasurer = mockk(relaxed = true),
- textConstraints = Constraints(maxWidth = 1080, maxHeight = 1920),
- textStyle = TextStyle.Default,
- density = Density(1f),
- isDarkTheme = false,
- themeBackgroundColor = Color.White,
- themeTextColor = Color.Black,
- context = context,
- initialChapterToPaginate = 0,
- mathMLRenderer = mockk(relaxed = true),
- paragraphGapMultiplier = 1.0f
- )
- advanceUntilIdle()
-
- assertThat(viewModel.paginator).isSameInstanceAs(existingPaginator)
- }
-}
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/ReaderLinkHitTest.kt b/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/ReaderLinkHitTest.kt
deleted file mode 100644
index 11e4cc1..0000000
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/paginatedreader/ReaderLinkHitTest.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-package org.dueattendant149.bookreader.paginatedreader
-
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.buildAnnotatedString
-import androidx.compose.ui.text.rememberTextMeasurer
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.sp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class ReaderLinkHitTest {
- @get:Rule
- val composeTestRule = createComposeRule()
-
- @Test
- fun urlAnnotationAtPositionIgnoresSameLineSpaceAfterLink() {
- lateinit var text: AnnotatedString
- var layoutResult: TextLayoutResult? = null
-
- composeTestRule.setContent {
- val textMeasurer = rememberTextMeasurer()
- text = linkText("Open")
- layoutResult = textMeasurer.measure(
- text = text,
- style = TextStyle(fontSize = 24.sp),
- constraints = Constraints.fixedWidth(500)
- )
- }
-
- composeTestRule.waitForIdle()
-
- val layout = checkNotNull(layoutResult)
- val firstBox = layout.getBoundingBox(0)
- val lastBox = layout.getBoundingBox(text.length - 1)
- val y = (firstBox.top + firstBox.bottom) / 2f
-
- assertThat(text.readerUrlAnnotationAtPosition(layout, Offset(firstBox.left + 1f, y)))
- .isEqualTo(HREF)
- assertThat(text.readerUrlAnnotationAtPosition(layout, Offset(lastBox.right + 60f, y)))
- .isNull()
- }
-
- @Test
- fun urlAnnotationAtPositionAppliesTextStartOffsetForWrappedLineLayouts() {
- lateinit var fullText: AnnotatedString
- var lineLayoutResult: TextLayoutResult? = null
- val prefix = "Before "
- val label = "Open"
-
- composeTestRule.setContent {
- val textMeasurer = rememberTextMeasurer()
- fullText = buildAnnotatedString {
- append(prefix)
- append(label)
- addStringAnnotation("URL", HREF, prefix.length, prefix.length + label.length)
- }
- lineLayoutResult = textMeasurer.measure(
- text = fullText.subSequence(prefix.length, fullText.length),
- style = TextStyle(fontSize = 24.sp),
- constraints = Constraints.fixedWidth(500)
- )
- }
-
- composeTestRule.waitForIdle()
-
- val layout = checkNotNull(lineLayoutResult)
- val firstBox = layout.getBoundingBox(0)
- val lastBox = layout.getBoundingBox(label.length - 1)
- val y = (firstBox.top + firstBox.bottom) / 2f
-
- assertThat(
- fullText.readerUrlAnnotationAtPosition(
- layout = layout,
- position = Offset(firstBox.left + 1f, y),
- textStartOffset = prefix.length
- )
- ).isEqualTo(HREF)
- assertThat(
- fullText.readerUrlAnnotationAtPosition(
- layout = layout,
- position = Offset(lastBox.right + 60f, y),
- textStartOffset = prefix.length
- )
- ).isNull()
- }
-
- private fun linkText(label: String) = buildAnnotatedString {
- append(label)
- addStringAnnotation("URL", HREF, 0, label.length)
- }
-
- private companion object {
- const val HREF = "chapter.xhtml#target"
- }
-}
diff --git a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfViewerScreenTest.kt b/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfViewerScreenTest.kt
deleted file mode 100644
index 99c3931..0000000
--- a/app/src/androidTest/java/com/dueattendant149/bookreader/reader/pdf/PdfViewerScreenTest.kt
+++ /dev/null
@@ -1,258 +0,0 @@
-package org.dueattendant149.bookreader.pdf
-
-import android.Manifest
-import android.content.Context
-import android.content.Intent
-import android.net.Uri
-import androidx.compose.ui.test.assert
-import androidx.compose.ui.test.assertIsDisplayed
-import androidx.compose.ui.test.assertTextContains
-import androidx.compose.ui.test.hasText
-import androidx.compose.ui.test.junit4.createEmptyComposeRule
-import androidx.compose.ui.test.onAllNodesWithTag
-import androidx.compose.ui.test.onAllNodesWithText
-import androidx.compose.ui.test.onNodeWithContentDescription
-import androidx.compose.ui.test.onNodeWithTag
-import androidx.compose.ui.test.onNodeWithText
-import androidx.compose.ui.test.performClick
-import androidx.compose.ui.test.performTextInput
-import androidx.core.content.FileProvider
-import androidx.test.core.app.ActivityScenario
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.rule.GrantPermissionRule
-import org.dueattendant149.bookreader.MainActivity
-import org.dueattendant149.bookreader.R
-import com.google.common.truth.Truth.assertThat
-import org.junit.After
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import java.io.File
-import java.util.UUID
-
-@RunWith(AndroidJUnit4::class)
-class PdfViewerScreenTest {
-
- @get:Rule
- val composeTestRule = createEmptyComposeRule()
-
- @get:Rule
- val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.POST_NOTIFICATIONS)
-
- private val context: Context = ApplicationProvider.getApplicationContext()
- private var currentPdfFile: File? = null
- private var scenario: ActivityScenario? = null
-
- @Before
- fun setup() {
- context.getSharedPreferences("epub_reader_settings", Context.MODE_PRIVATE)
- .edit()
- .clear()
- .commit()
-
- val samplePdfUri = copyAssetToCache(context, "sample.pdf")
- scenario = ActivityScenario.launch(createPdfViewIntent(context, samplePdfUri))
- }
-
- @After
- fun tearDown() {
- scenario?.close()
- currentPdfFile?.let {
- if (it.exists()) it.delete()
- }
- }
-
- private fun text(resId: Int, vararg args: Any): String = context.getString(resId, *args)
-
- private fun assertNoNodeWithTag(tag: String) {
- assertThat(composeTestRule.onAllNodesWithTag(tag).fetchSemanticsNodes()).isEmpty()
- }
-
- private fun createPdfViewIntent(context: Context, uri: Uri): Intent {
- return Intent(context, MainActivity::class.java).apply {
- action = Intent.ACTION_VIEW
- data = uri
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- }
-
- private fun waitForDocumentLoad(pageText: String = text(R.string.page_of_pages, 1, 4)) {
- composeTestRule.waitUntil(timeoutMillis = 15_000) {
- composeTestRule
- .onAllNodesWithText(pageText)
- .fetchSemanticsNodes().isNotEmpty()
- }
- }
-
- private fun openMoreOptions() {
- composeTestRule.onNodeWithContentDescription(text(R.string.tooltip_more_options)).performClick()
- }
-
- private fun openNavigationDrawer() {
- composeTestRule.onNodeWithTag("TocButton").performClick()
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithText(text(R.string.tab_chapters)).fetchSemanticsNodes().isNotEmpty() &&
- composeTestRule.onAllNodesWithTag("BookmarksTab").fetchSemanticsNodes().isNotEmpty()
- }
- }
-
- private fun selectChaptersTabIfTabsPaneIsFirst() {
- if (composeTestRule.onAllNodesWithTag("TabsTab").fetchSemanticsNodes().isNotEmpty()) {
- composeTestRule.onNodeWithText(text(R.string.tab_chapters)).performClick()
- composeTestRule.waitForIdle()
- }
- }
-
- private fun selectBookmarksTab() {
- composeTestRule.onNodeWithTag("BookmarksTab").performClick()
- composeTestRule.waitForIdle()
- }
-
- private fun selectReadingMode(modeText: String) {
- openMoreOptions()
- composeTestRule.onNodeWithText(text(R.string.menu_change_reading_mode)).performClick()
- composeTestRule.onNodeWithText(modeText).performClick()
- composeTestRule.waitForIdle()
- }
-
- private fun ensurePaginationMode() {
- selectReadingMode(text(R.string.menu_reading_mode_paginated))
- }
-
- @Suppress("SameParameterValue")
- private fun copyAssetToCache(context: Context, assetName: String): Uri {
- val uniqueName = "${UUID.randomUUID()}_$assetName"
- val file = File(context.cacheDir, uniqueName)
-
- currentPdfFile = file
-
- if (file.exists()) file.delete()
- context.assets.open(assetName).use { inputStream ->
- file.outputStream().use { outputStream ->
- inputStream.copyTo(outputStream)
- }
- }
- return FileProvider.getUriForFile(
- context,
- "${context.packageName}.provider",
- file
- )
- }
-
- @Test
- fun documentLoadsAndDisplaysCorrectPageCount() {
- waitForDocumentLoad()
- composeTestRule.onNodeWithTag("PageNumberIndicator")
- .assertIsDisplayed()
- }
-
- @Test
- fun tableOfContentsButton_handlesTabsPaneAndOpensChaptersTab() {
- waitForDocumentLoad()
-
- openNavigationDrawer()
- selectChaptersTabIfTabsPaneIsFirst()
-
- composeTestRule.onNodeWithText(text(R.string.tab_chapters)).assertIsDisplayed()
- composeTestRule.onNodeWithTag("BookmarksTab").assertIsDisplayed()
- }
-
- @Test
- fun bookmarkFunctionality_addAndDeleteCurrentPage() {
- waitForDocumentLoad()
-
- openMoreOptions()
- composeTestRule.onNodeWithText(text(R.string.menu_bookmark_this_page)).performClick()
- composeTestRule.waitForIdle()
-
- openNavigationDrawer()
- selectBookmarksTab()
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithTag("BookmarkItem_0").fetchSemanticsNodes().isNotEmpty()
- }
- composeTestRule.onNodeWithTag("BookmarkItem_0").assertIsDisplayed()
- .assert(hasText(text(R.string.pdf_page_short, 1), substring = true))
-
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_more_options_bookmark)).performClick()
- composeTestRule.onNodeWithText(text(R.string.action_delete)).performClick()
- composeTestRule.waitForIdle()
- composeTestRule.onNodeWithText(text(R.string.action_delete), useUnmergedTree = true).performClick()
-
- composeTestRule.waitUntil(5_000) {
- composeTestRule.onAllNodesWithTag("BookmarkItem_0").fetchSemanticsNodes().isEmpty()
- }
- assertNoNodeWithTag("BookmarkItem_0")
- composeTestRule.onNodeWithText(text(R.string.no_bookmarks_yet)).assertIsDisplayed()
- }
-
- @Test
- fun sliderNavigation_opensAndDisplaysCorrectly() {
- waitForDocumentLoad()
-
- composeTestRule.onNodeWithContentDescription(text(R.string.content_desc_navigate_slider)).performClick()
-
- composeTestRule.onNodeWithText(text(R.string.page_format, 1, 4)).assertIsDisplayed()
- }
-
- @Test
- fun displayMode_switchesToVerticalScroll() {
- waitForDocumentLoad()
-
- ensurePaginationMode()
-
- assertNoNodeWithTag("PdfVerticalScroll")
-
- selectReadingMode(text(R.string.menu_reading_mode_vertical))
-
- composeTestRule.onNodeWithTag("PdfVerticalScroll").assertIsDisplayed()
-
- ensurePaginationMode()
-
- assertNoNodeWithTag("PdfVerticalScroll")
- }
-
- @Test
- fun displayModeSelectionPersistsReaderPreference() {
- waitForDocumentLoad()
-
- selectReadingMode(text(R.string.menu_reading_mode_paginated))
- waitForDisplayModePreference(DisplayMode.PAGINATION)
-
- selectReadingMode(text(R.string.menu_reading_mode_vertical))
- waitForDisplayModePreference(DisplayMode.VERTICAL_SCROLL)
- }
-
- @Test
- fun search_uiOpensAndAcceptsQuery() {
- waitForDocumentLoad()
-
- composeTestRule.onNodeWithTag("SearchButton").performClick()
- composeTestRule.waitForIdle()
-
- val ocrLanguageText = text(R.string.ocr_language_latin)
- if (composeTestRule.onAllNodesWithText(ocrLanguageText).fetchSemanticsNodes().isNotEmpty()) {
- composeTestRule.onNodeWithText(ocrLanguageText).performClick()
- composeTestRule.waitForIdle()
- }
-
- composeTestRule.onNodeWithTag("SearchTextField").assertIsDisplayed()
-
- composeTestRule.onNodeWithTag("SearchTextField").performTextInput("test query")
-
- composeTestRule.onNodeWithTag("SearchTextField").assertTextContains("test query")
-
- composeTestRule.onNodeWithContentDescription(text(R.string.tooltip_close_search)).performClick()
-
- assertNoNodeWithTag("SearchTextField")
- }
-
- private fun waitForDisplayModePreference(expected: DisplayMode) {
- composeTestRule.waitUntil(timeoutMillis = 5_000) {
- context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
- .getString(DISPLAY_MODE_KEY, DisplayMode.VERTICAL_SCROLL.name) == expected.name
- }
- }
-
-}
diff --git a/app/src/debug/java/com/dueattendant149/bookreader/reader/epubreader/EpubTestActivity.kt b/app/src/debug/java/com/aryan/reader/epubreader/EpubTestActivity.kt
similarity index 84%
rename from app/src/debug/java/com/dueattendant149/bookreader/reader/epubreader/EpubTestActivity.kt
rename to app/src/debug/java/com/aryan/reader/epubreader/EpubTestActivity.kt
index 9596429..1bfd9d1 100644
--- a/app/src/debug/java/com/dueattendant149/bookreader/reader/epubreader/EpubTestActivity.kt
+++ b/app/src/debug/java/com/aryan/reader/epubreader/EpubTestActivity.kt
@@ -1,12 +1,12 @@
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.RequiresApi
-import org.dueattendant149.bookreader.RenderMode
-import org.dueattendant149.bookreader.epub.EpubBook
+import com.aryan.reader.RenderMode
+import com.aryan.reader.epub.EpubBook
import kotlinx.serialization.json.Json
class EpubTestActivity : ComponentActivity() {
@@ -31,8 +31,8 @@ class EpubTestActivity : ComponentActivity() {
coverImagePath = null,
onRenderModeChange = {},
customFonts = TODO(),
- onImportFonts = TODO(), viewModel = TODO()
+ onImportFont = TODO(), viewModel = TODO()
)
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/debug/java/com/dueattendant149/bookreader/reader/epubreader/HiltTestActivity.kt b/app/src/debug/java/com/aryan/reader/epubreader/HiltTestActivity.kt
similarity index 89%
rename from app/src/debug/java/com/dueattendant149/bookreader/reader/epubreader/HiltTestActivity.kt
rename to app/src/debug/java/com/aryan/reader/epubreader/HiltTestActivity.kt
index 1f512af..99f56ed 100644
--- a/app/src/debug/java/com/dueattendant149/bookreader/reader/epubreader/HiltTestActivity.kt
+++ b/app/src/debug/java/com/aryan/reader/epubreader/HiltTestActivity.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import androidx.activity.ComponentActivity
diff --git a/app/src/debug/java/com/dueattendant149/bookreader/reader/ml/ComicPanelDetector.kt b/app/src/debug/java/com/aryan/reader/ml/ComicPanelDetector.kt
similarity index 99%
rename from app/src/debug/java/com/dueattendant149/bookreader/reader/ml/ComicPanelDetector.kt
rename to app/src/debug/java/com/aryan/reader/ml/ComicPanelDetector.kt
index 4e21c85..c2a0e3a 100644
--- a/app/src/debug/java/com/dueattendant149/bookreader/reader/ml/ComicPanelDetector.kt
+++ b/app/src/debug/java/com/aryan/reader/ml/ComicPanelDetector.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.ml
+package com.aryan.reader.ml
import android.graphics.Bitmap
import android.graphics.RectF
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c9a474e..4432b4a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -55,22 +55,7 @@
-
-
-
-
@@ -169,19 +154,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/assets/epub_reader.js b/app/src/main/assets/epub_reader.js
index 030e760..2075568 100644
--- a/app/src/main/assets/epub_reader.js
+++ b/app/src/main/assets/epub_reader.js
@@ -128,15 +128,8 @@
background-color: rgba(255, 236, 179, 0.8);
/* Semi-transparent Gold */
color: black !important;
- display: inline !important;
- text-align: initial !important;
- text-align-last: auto !important;
- letter-spacing: normal !important;
- word-spacing: normal !important;
padding: 0.1em 0;
border-radius: 3px;
- -webkit-box-decoration-break: clone;
- box-decoration-break: clone;
}
html.dark-theme span.tts-highlight {
@@ -1503,16 +1496,6 @@
};
const TTS_HIGHLIGHT_LOG_TAG = "TTS_HIGHLIGHT_DIAGNOSIS";
- const TTS_HIGHLIGHT_BLOCK_SELECTOR = "p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th";
-
- function getTtsHighlightBlock(node) {
- if (!node) {
- return document.body;
- }
-
- const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
- return (element && element.closest(TTS_HIGHLIGHT_BLOCK_SELECTOR)) || document.body;
- }
window.highlightFromCfi = function (cfi, textToHighlight, startOffset) {
console.log(`$ {
@@ -1573,10 +1556,9 @@
, Text content: '${(location.node.textContent || "").substring(0, 50)}...' `);
const baseNode = location.node;
- const highlightRoot = getTtsHighlightBlock(baseNode);
let remainingOffset = startOffset;
- const treeWalker = document.createTreeWalker(highlightRoot, NodeFilter.SHOW_TEXT, null, false);
+ const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
treeWalker.currentNode = baseNode;
let currentNode = baseNode.nodeType === Node.TEXT_NODE ? baseNode : treeWalker.nextNode();
@@ -1644,7 +1626,7 @@
} else {
remainingTextLength -= availableLength;
// Important: We need a fresh walker starting from the endNode to find the *next* text node reliably
- const nextNodeWalker = document.createTreeWalker(highlightRoot, NodeFilter.SHOW_TEXT, null, false);
+ const nextNodeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
nextNodeWalker.currentNode = endNode;
endNode = nextNodeWalker.nextNode();
endOffset = 0; // Start from the beginning of the next node
@@ -1695,14 +1677,11 @@
TTS_HIGHLIGHT_LOG_TAG
}
- : surroundContents failed, using same-block fallback. Error: $ {
+ : surroundContents failed, using fallback. Error: $ {
e.message
}
`);
- if (!highlightRoot.contains(range.commonAncestorContainer)) {
- return "JS: Highlight range escaped current block.";
- }
const contents = range.extractContents();
highlightSpan.appendChild(contents);
range.insertNode(highlightSpan);
@@ -2945,29 +2924,6 @@
};
const HL_LOG_TAG = "HIGHLIGHT_DEBUG";
- const HL_RENDER_LOG_TAG = "AndroidHighlightRenderDiag";
-
- function hlRenderPreview(value, maxLength) {
- if (value === undefined || value === null) return "";
- var text = String(value).replace(/\s+/g, " ");
- var max = maxLength || 100;
- return text.length > max ? text.substring(0, max) : text;
- }
-
- function hlRenderLocatorLabel(locator) {
- locator = locator || {};
- return "locatorChapter=" + (locator.chapterIndex === undefined || locator.chapterIndex === null ? "null" : locator.chapterIndex) +
- " locatorPage=" + (locator.pageIndex === undefined || locator.pageIndex === null ? "null" : locator.pageIndex) +
- " locatorOffsets=" + (locator.startOffset === undefined || locator.startOffset === null ? "null" : locator.startOffset) +
- ".." + (locator.endOffset === undefined || locator.endOffset === null ? "null" : locator.endOffset) +
- " locatorBlock=" + (locator.blockIndex === undefined || locator.blockIndex === null ? "null" : locator.blockIndex) +
- " locatorChar=" + (locator.charOffset === undefined || locator.charOffset === null ? "null" : locator.charOffset) +
- " locatorCfi=" + hlRenderPreview(locator.cfi || "", 120);
- }
-
- function hlRenderLog(message) {
- console.log(HL_RENDER_LOG_TAG + ": " + message);
- }
window.HighlightBridgeHelper = {
updateHighlightStyle: function (cfi, newColorClass, colorId) {
@@ -3324,13 +3280,11 @@
try {
var highlights = JSON.parse(jsonArrayString);
var self = this;
- hlRenderLog("webview_restore_start count=" + highlights.length);
highlights.forEach(function (h) {
- self.applyHighlightObject(h);
+ self.applyHighlight(h.cfi, h.text, h.cssClass);
});
} catch (e) {
- hlRenderLog("webview_restore_error error=" + hlRenderPreview(e && e.message ? e.message : e, 160));
console.log(
`$ {
HL_LOG_TAG
@@ -3341,376 +3295,136 @@
}
},
- applyHighlightObject: function (highlight) {
- if (!highlight) return;
- hlRenderLog(
- "webview_apply_object id=" + (highlight.id || "") +
- " cfi=" + hlRenderPreview(highlight.cfi || "", 120) +
- " textLen=" + String(highlight.text || "").length +
- " text='" + hlRenderPreview(highlight.text || "", 80) + "' " +
- hlRenderLocatorLabel(highlight.locator || {})
- );
- this.applyHighlight(highlight.cfi, highlight.text, highlight.cssClass, highlight.locator || null);
- },
-
- highlightTextRoot: function () {
- return document.getElementById("content-container") || document.body;
- },
-
- highlightTextNodes: function (root) {
- var nodes = [];
- if (!root) return nodes;
- var walker = document.createTreeWalker(
- root,
- NodeFilter.SHOW_TEXT,
- {
- acceptNode: function (node) {
- if (!node || !node.nodeValue) return NodeFilter.FILTER_REJECT;
- var parent = node.parentElement;
- if (!parent) return NodeFilter.FILTER_REJECT;
- if (parent.closest && parent.closest("script, style, noscript")) return NodeFilter.FILTER_REJECT;
- return NodeFilter.FILTER_ACCEPT;
- },
- },
- false,
- );
- while (walker.nextNode()) nodes.push(walker.currentNode);
- return nodes;
- },
-
- rangeFromTextOffsets: function (root, startOffset, endOffset) {
- var start = parseInt(startOffset, 10);
- var end = parseInt(endOffset, 10);
- if (!root || !isFinite(start) || !isFinite(end) || end <= start) {
- hlRenderLog(
- "webview_range_offsets_skip reason=invalid_offsets start=" + startOffset +
- " end=" + endOffset + " hasRoot=" + !!root
- );
- return null;
- }
-
- var nodes = this.highlightTextNodes(root);
- var cursor = 0;
- var startNode = null;
- var startInNode = 0;
- var endNode = null;
- var endInNode = 0;
-
- for (var i = 0; i < nodes.length; i++) {
- var value = nodes[i].nodeValue || "";
- var next = cursor + value.length;
- if (!startNode && start >= cursor && start <= next) {
- startNode = nodes[i];
- startInNode = Math.max(0, Math.min(value.length, start - cursor));
- }
- if (startNode && end >= cursor && end <= next) {
- endNode = nodes[i];
- endInNode = Math.max(0, Math.min(value.length, end - cursor));
- break;
- }
- cursor = next;
- }
-
- if (!startNode || !endNode) {
- hlRenderLog(
- "webview_range_offsets_skip reason=missing_boundary start=" + start +
- " end=" + end + " nodes=" + nodes.length + " textCursor=" + cursor
- );
- return null;
- }
- var range = document.createRange();
- range.setStart(startNode, startInNode);
- range.setEnd(endNode, endInNode);
- if (range.collapsed) {
- hlRenderLog("webview_range_offsets_skip reason=collapsed start=" + start + " end=" + end);
- return null;
- }
- hlRenderLog(
- "webview_range_offsets_result start=" + start + " end=" + end +
- " text='" + hlRenderPreview(range.toString(), 80) + "'"
- );
- return range;
- },
-
- rangeMatchesText: function (range, text) {
- if (!range || !text) return true;
- var actual = (range.toString() || "").trim();
- var expected = String(text || "").trim();
- if (!expected) return true;
- return actual === expected || actual.replace(/\s+/g, " ") === expected.replace(/\s+/g, " ");
- },
-
- rangeFromVisibleTextSearch: function (text, locator) {
- if (!text) {
- hlRenderLog("webview_text_search_skip reason=empty_text " + hlRenderLocatorLabel(locator));
- return null;
- }
- var root = this.highlightTextRoot();
- var nodes = this.highlightTextNodes(root);
- if (!nodes.length) {
- hlRenderLog("webview_text_search_skip reason=no_nodes " + hlRenderLocatorLabel(locator));
- return null;
- }
-
- var fullText = nodes.map(function (node) { return node.nodeValue || ""; }).join("");
- var expected = String(text);
- var candidates = [];
- var index = fullText.indexOf(expected);
- while (index !== -1) {
- candidates.push(index);
- index = fullText.indexOf(expected, index + 1);
- }
- if (!candidates.length) {
- var lowerFull = fullText.toLowerCase();
- var lowerExpected = expected.toLowerCase();
- index = lowerFull.indexOf(lowerExpected);
- while (index !== -1) {
- candidates.push(index);
- index = lowerFull.indexOf(lowerExpected, index + 1);
- }
- }
- if (!candidates.length && expected.length > 20) {
- var partial = expected.substring(0, Math.min(expected.length, 40));
- index = fullText.indexOf(partial);
- while (index !== -1) {
- candidates.push(index);
- index = fullText.indexOf(partial, index + 1);
- }
- }
- if (!candidates.length) {
- hlRenderLog(
- "webview_text_search_skip reason=no_candidates textLen=" + expected.length +
- " text='" + hlRenderPreview(expected, 80) + "' " + hlRenderLocatorLabel(locator)
- );
- return null;
- }
-
- var preferred = locator && locator.startOffset !== undefined && locator.startOffset !== null
- ? parseInt(locator.startOffset, 10)
- : candidates[0];
- if (!isFinite(preferred)) preferred = candidates[0];
- var best = candidates.reduce(function (currentBest, candidate) {
- return Math.abs(candidate - preferred) < Math.abs(currentBest - preferred) ? candidate : currentBest;
- }, candidates[0]);
- hlRenderLog(
- "webview_text_search_candidate count=" + candidates.length +
- " preferred=" + preferred + " best=" + best +
- " text='" + hlRenderPreview(expected, 80) + "' " + hlRenderLocatorLabel(locator)
- );
- return this.rangeFromTextOffsets(root, best, best + expected.length);
- },
-
- rangeFromLocator: function (locator, text) {
- if (!locator) {
- hlRenderLog("webview_locator_skip reason=missing_locator");
- return null;
- }
- if (locator.cfi && String(locator.cfi).charAt(0) === "/") {
- hlRenderLog(
- "webview_locator_skip reason=structural_cfi_prefers_cfi " +
- hlRenderLocatorLabel(locator)
- );
- return null;
- }
- var root = this.highlightTextRoot();
- var range = this.rangeFromTextOffsets(root, locator.startOffset, locator.endOffset);
- if (range && this.rangeMatchesText(range, text)) {
- hlRenderLog(
- "webview_locator_result matched=true text='" + hlRenderPreview(range.toString(), 80) + "' " +
- hlRenderLocatorLabel(locator)
- );
- return range;
- }
- hlRenderLog(
- "webview_locator_skip reason=" + (range ? "text_mismatch" : "range_missing") +
- " rangeText='" + hlRenderPreview(range ? range.toString() : "", 80) + "' " +
- "expected='" + hlRenderPreview(text || "", 80) + "' " + hlRenderLocatorLabel(locator)
- );
- return null;
- },
-
- rangeFromCfi: function (cfi, text) {
- if (!cfi || cfi.indexOf("desktop:") === 0) {
- hlRenderLog("webview_cfi_skip reason=unsupported cfi=" + hlRenderPreview(cfi || "", 120));
- return null;
- }
- var sourceCfi = String(cfi).split("|")[0];
- const location = window.getNodeAndOffsetFromCfi(sourceCfi);
- if (!location || !location.node) {
- hlRenderLog("webview_cfi_skip reason=missing_location cfi=" + hlRenderPreview(cfi, 120));
- return null;
- }
-
- let startNode = location.node;
- let startOffset = location.offset;
-
- if (startNode.nodeType === Node.TEXT_NODE) {
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
- walker.currentNode = startNode;
-
- while (startNode && startOffset >= startNode.nodeValue.length) {
- if (startOffset === startNode.nodeValue.length) {
- const next = walker.nextNode();
-
- if (next) {
- startOffset -= startNode.nodeValue.length;
- startNode = next;
- } else {
- break;
- }
- } else {
- startOffset -= startNode.nodeValue.length;
- startNode = walker.nextNode();
- }
- }
- }
-
- if (text && text.length > 0 && startNode && startNode.nodeType === Node.TEXT_NODE) {
- const nodeVal = startNode.nodeValue;
- const substring = nodeVal.substring(startOffset, startOffset + text.length);
-
- if (substring !== text && substring.trim() !== text.trim()) {
- hlRenderLog(
- "webview_cfi_text_mismatch cfi=" + hlRenderPreview(cfi, 120) +
- " startOffset=" + startOffset +
- " actual='" + hlRenderPreview(substring, 80) + "'" +
- " expected='" + hlRenderPreview(text, 80) + "'"
- );
- console.log("HIGHLIGHT_DEBUG: Text mismatch at CFI. Searching nearby.");
- const foundIndex = nodeVal.indexOf(text);
-
- if (foundIndex !== -1) {
- hlRenderLog(
- "webview_cfi_text_adjust reason=full_match oldStartOffset=" + startOffset +
- " newStartOffset=" + foundIndex + " cfi=" + hlRenderPreview(cfi, 120)
- );
- startOffset = foundIndex;
- } else {
- const partial = text.substring(0, Math.min(text.length, 20));
- const partialIndex = nodeVal.indexOf(partial);
-
- if (partialIndex !== -1) {
- hlRenderLog(
- "webview_cfi_text_adjust reason=partial_match oldStartOffset=" + startOffset +
- " newStartOffset=" + partialIndex + " cfi=" + hlRenderPreview(cfi, 120)
- );
- startOffset = partialIndex;
- }
- }
- }
- }
-
- if (!startNode) {
- hlRenderLog("webview_cfi_skip reason=missing_start_node cfi=" + hlRenderPreview(cfi, 120));
- return null;
- }
-
- const range = document.createRange();
-
- if (startNode.nodeType === Node.TEXT_NODE && startOffset > startNode.nodeValue.length) {
- startOffset = Math.max(0, startNode.nodeValue.length - 1);
- }
-
- range.setStart(startNode, startOffset);
-
- const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
- treeWalker.currentNode = startNode;
-
- let remainingLen = text.length;
- let endNode = treeWalker.currentNode;
- let endOffset = startOffset;
-
- while (remainingLen > 0 && endNode) {
- let avail = endNode.nodeValue.length - endOffset;
-
- if (avail >= remainingLen) {
- endOffset += remainingLen;
- remainingLen = 0;
- } else {
- remainingLen -= avail;
- endNode = treeWalker.nextNode();
- endOffset = 0;
- }
- }
-
- if (!endNode) {
- hlRenderLog("webview_cfi_skip reason=missing_end_node cfi=" + hlRenderPreview(cfi, 120));
- return null;
- }
- range.setEnd(endNode, endOffset);
- if (range.collapsed) {
- hlRenderLog("webview_cfi_skip reason=collapsed cfi=" + hlRenderPreview(cfi, 120));
- return null;
- }
- hlRenderLog(
- "webview_cfi_result cfi=" + hlRenderPreview(cfi, 120) +
- " text='" + hlRenderPreview(range.toString(), 80) + "'"
- );
- return range;
- },
-
- applyHighlight: function (cfi, text, cssClass, locator) {
+ applyHighlight: function (cfi, text, cssClass) {
try {
- cssClass = cssClass || "user-highlight-yellow";
var alreadyApplied = false;
var spans = document.querySelectorAll(`span[data-cfi]`);
for (var i = 0; i < spans.length; i++) {
- if (cfi && (spans[i].getAttribute("data-cfi") || "").split(";;").includes(cfi)) {
+ if ((spans[i].getAttribute("data-cfi") || "").split(";;").includes(cfi)) {
alreadyApplied = true;
break;
}
}
- if (alreadyApplied) {
- hlRenderLog(
- "webview_apply_skip reason=already_applied cfi=" + hlRenderPreview(cfi || "", 120) +
- " textLen=" + String(text || "").length + " " + hlRenderLocatorLabel(locator)
- );
- return;
+ if (alreadyApplied) return;
+
+ const location = window.getNodeAndOffsetFromCfi(cfi);
+ if (!location || !location.node) return;
+
+ let startNode = location.node;
+ let startOffset = location.offset;
+
+ if (startNode.nodeType === Node.TEXT_NODE) {
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
+ walker.currentNode = startNode;
+
+ while (startNode && startOffset >= startNode.nodeValue.length) {
+ if (startOffset === startNode.nodeValue.length) {
+ const next = walker.nextNode();
+
+ if (next) {
+ startOffset -= startNode.nodeValue.length;
+ startNode = next;
+ } else {
+ break;
+ }
+ } else {
+ startOffset -= startNode.nodeValue.length;
+ startNode = walker.nextNode();
+ }
+ }
}
- var hasPreciseLocator = locator &&
- locator.startOffset !== undefined && locator.startOffset !== null &&
- locator.endOffset !== undefined && locator.endOffset !== null &&
- parseInt(locator.endOffset, 10) > parseInt(locator.startOffset, 10);
- hlRenderLog(
- "webview_apply_start cfi=" + hlRenderPreview(cfi || "", 120) +
- " textLen=" + String(text || "").length +
- " cssClass=" + cssClass +
- " hasPreciseLocator=" + !!hasPreciseLocator + " " +
- hlRenderLocatorLabel(locator)
- );
- var rangeSource = "locator";
- var sourceCfi = (locator && locator.cfi) || cfi;
- var hasSourceCfi = sourceCfi && String(sourceCfi).charAt(0) === "/";
- var range = hasSourceCfi ? null : this.rangeFromLocator(locator, text);
- if (!range && hasSourceCfi) {
- rangeSource = "cfi";
- range = this.rangeFromCfi(sourceCfi, text || "");
+ // 1. Text Verification / Healing
+ if (text && text.length > 0 && startNode && startNode.nodeType === Node.TEXT_NODE) {
+ const nodeVal = startNode.nodeValue;
+ // Check if text matches at exact offset
+ const substring = nodeVal.substring(startOffset, startOffset + text.length);
+
+ // Allow for some whitespace looseness (trim comparison)
+ if (substring !== text && substring.trim() !== text.trim()) {
+ console.log(`$ {
+ HL_LOG_TAG
+ }
+
+ : Text mismatch at CFI. Searching nearby... Expected: '${text.substring(0, 10)}...', Found: '${substring.substring(0, 10)}...' `);
+
+ // Try finding the text in the whole node
+ const foundIndex = nodeVal.indexOf(text);
+
+ if (foundIndex !== -1) {
+ console.log(`$ {
+ HL_LOG_TAG
+ }
+
+ : Found text elsewhere in node. Adjusting offset from $ {
+ startOffset
+ }
+
+ to $ {
+ foundIndex
+ }
+
+ .`);
+ startOffset = foundIndex;
+ } else {
+ // Simple fuzzy: Try finding first 20 chars
+ const partial = text.substring(0, Math.min(text.length, 20));
+ const partialIndex = nodeVal.indexOf(partial);
+
+ if (partialIndex !== -1) {
+ console.log(`$ {
+ HL_LOG_TAG
+ }
+
+ : Found partial match. Adjusting offset.`);
+ startOffset = partialIndex;
+ }
+ }
+ }
}
- if (!range) {
- rangeSource = "locator";
- range = this.rangeFromLocator(locator, text);
+
+ if (!startNode) return;
+
+ const range = document.createRange();
+
+ // Set Start
+ if (startNode.nodeType === Node.TEXT_NODE) {
+ // Ensure offset is valid
+ if (startOffset > startNode.nodeValue.length) {
+ startOffset = Math.max(0, startNode.nodeValue.length - 1);
+ }
}
- if (!range && !hasPreciseLocator && !hasSourceCfi) {
- rangeSource = "text_search";
- range = this.rangeFromVisibleTextSearch(text || "", locator);
+
+ range.setStart(startNode, startOffset);
+
+ const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
+ treeWalker.currentNode = startNode;
+
+ let currentNode = treeWalker.currentNode;
+ let remainingOffset = startOffset;
+ let remainingLen = text.length;
+ let endNode = currentNode;
+ let endOffset = startOffset;
+
+ while (remainingLen > 0 && endNode) {
+ let avail = endNode.nodeValue.length - endOffset;
+
+ if (avail >= remainingLen) {
+ endOffset += remainingLen;
+ remainingLen = 0;
+ } else {
+ remainingLen -= avail;
+ endNode = treeWalker.nextNode();
+ endOffset = 0;
+ }
}
- if (!range) {
- hlRenderLog(
- "webview_apply_skip reason=no_range cfi=" + hlRenderPreview(cfi || "", 120) +
- " hasPreciseLocator=" + !!hasPreciseLocator + " " + hlRenderLocatorLabel(locator)
- );
- return;
+
+ if (endNode) {
+ range.setEnd(endNode, endOffset);
+ var normalizedRange = this.normalizeRangeBoundaries(range);
+ this.highlightRangeSafe(normalizedRange, cssClass, cfi);
}
- var normalizedRange = this.normalizeRangeBoundaries(range);
- this.highlightRangeSafe(normalizedRange, cssClass, cfi);
- hlRenderLog(
- "webview_apply_result applied=true cfi=" + hlRenderPreview(cfi || "", 120) +
- " source=" + rangeSource +
- " renderedText='" + hlRenderPreview(normalizedRange.toString(), 80) + "'"
- );
} catch (e) {
- hlRenderLog("webview_apply_error error=" + hlRenderPreview(e && e.message ? e.message : e, 160));
console.log(e);
}
},
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/AiSettingsScreen.kt b/app/src/main/java/com/aryan/reader/AiSettingsScreen.kt
similarity index 99%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/AiSettingsScreen.kt
rename to app/src/main/java/com/aryan/reader/AiSettingsScreen.kt
index 1a95ac5..ecf9c61 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/AiSettingsScreen.kt
+++ b/app/src/main/java/com/aryan/reader/AiSettingsScreen.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/AndroidFolderPathResolver.kt b/app/src/main/java/com/aryan/reader/AndroidFolderPathResolver.kt
similarity index 95%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/AndroidFolderPathResolver.kt
rename to app/src/main/java/com/aryan/reader/AndroidFolderPathResolver.kt
index 197ffef..c03bdeb 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/AndroidFolderPathResolver.kt
+++ b/app/src/main/java/com/aryan/reader/AndroidFolderPathResolver.kt
@@ -1,8 +1,8 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.net.Uri
import androidx.core.net.toUri
-import org.dueattendant149.bookreader.data.RecentFileItem
+import com.aryan.reader.data.RecentFileItem
import timber.log.Timber
class AndroidFolderPathResolver : FolderPathResolver {
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/AndroidSettingsHubModels.kt b/app/src/main/java/com/aryan/reader/AndroidSettingsHubModels.kt
similarity index 86%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/AndroidSettingsHubModels.kt
rename to app/src/main/java/com/aryan/reader/AndroidSettingsHubModels.kt
index 7cb6a84..f050e92 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/AndroidSettingsHubModels.kt
+++ b/app/src/main/java/com/aryan/reader/AndroidSettingsHubModels.kt
@@ -1,8 +1,8 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
-import org.dueattendant149.bookreader.shared.SharedFeaturePolicy
-import org.dueattendant149.bookreader.shared.SharedSettingsHubInput
-import org.dueattendant149.bookreader.shared.SharedSettingsPlatform
+import com.aryan.reader.shared.SharedFeaturePolicy
+import com.aryan.reader.shared.SharedSettingsHubInput
+import com.aryan.reader.shared.SharedSettingsPlatform
fun androidSettingsHubInput(
uiState: ReaderScreenState,
@@ -15,8 +15,6 @@ fun androidSettingsHubInput(
val supportsOssAiKeys = isOssBuild && !isOfflineBuild
val featurePolicy = if (isOfflineBuild) {
SharedFeaturePolicy.OssOffline
- } else if (isOssBuild) {
- SharedFeaturePolicy.OssOnline
} else {
SharedFeaturePolicy.Standard
}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/AndroidSharedStateBridge.kt b/app/src/main/java/com/aryan/reader/AndroidSharedStateBridge.kt
similarity index 91%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/AndroidSharedStateBridge.kt
rename to app/src/main/java/com/aryan/reader/AndroidSharedStateBridge.kt
index 65a002e..4f0bb68 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/AndroidSharedStateBridge.kt
+++ b/app/src/main/java/com/aryan/reader/AndroidSharedStateBridge.kt
@@ -1,14 +1,14 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.data.TagEntity
-import org.dueattendant149.bookreader.shared.AppAction as SharedAppAction
-import org.dueattendant149.bookreader.shared.LibraryAction as SharedLibraryAction
-import org.dueattendant149.bookreader.shared.SharedFolderPathResolver
-import org.dueattendant149.bookreader.shared.SharedLibraryProjectionInput
-import org.dueattendant149.bookreader.shared.SharedLibraryStateProjector
-import org.dueattendant149.bookreader.shared.SharedReaderScreenState
-import org.dueattendant149.bookreader.shared.reduce
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.data.TagEntity
+import com.aryan.reader.shared.AppAction as SharedAppAction
+import com.aryan.reader.shared.LibraryAction as SharedLibraryAction
+import com.aryan.reader.shared.SharedFolderPathResolver
+import com.aryan.reader.shared.SharedLibraryProjectionInput
+import com.aryan.reader.shared.SharedLibraryStateProjector
+import com.aryan.reader.shared.SharedReaderScreenState
+import com.aryan.reader.shared.reduce
internal object AndroidSharedStateBridge {
fun prepareLibraryProjection(
@@ -23,8 +23,7 @@ internal object AndroidSharedStateBridge {
val sharedInput = SharedLibraryProjectionInput(
state = projectionState.toSharedReaderScreenState(
rawBooks = taggedBooks,
- dbTags = input.dbTags,
- includeReaderAnnotations = false
+ dbTags = input.dbTags
),
booksFromStore = taggedBooks
.filterNot { it.bookId.endsWith("_reflow") }
@@ -196,8 +195,7 @@ internal object AndroidSharedStateBridge {
private fun ReaderScreenState.toBridgeSharedState(projectedState: ReaderScreenState): SharedReaderScreenState {
return toSharedReaderScreenState(
rawBooks = projectedState.rawLibraryFiles.ifEmpty { rawLibraryFiles },
- dbTags = projectedState.allTags.ifEmpty { allTags },
- includeReaderAnnotations = false
+ dbTags = projectedState.allTags.ifEmpty { allTags }
)
}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/AppFontResolver.kt b/app/src/main/java/com/aryan/reader/AppFontResolver.kt
similarity index 89%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/AppFontResolver.kt
rename to app/src/main/java/com/aryan/reader/AppFontResolver.kt
index eab48dc..597c650 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/AppFontResolver.kt
+++ b/app/src/main/java/com/aryan/reader/AppFontResolver.kt
@@ -1,8 +1,8 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
-import org.dueattendant149.bookreader.data.CustomFontEntity
+import com.aryan.reader.data.CustomFontEntity
import java.io.File
fun AppFontPreference.toAndroidAppFontFamily(customFonts: List): FontFamily? {
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/AppNavigation.kt b/app/src/main/java/com/aryan/reader/AppNavigation.kt
similarity index 90%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/AppNavigation.kt
rename to app/src/main/java/com/aryan/reader/AppNavigation.kt
index dd9963e..5f4bbee 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/AppNavigation.kt
+++ b/app/src/main/java/com/aryan/reader/AppNavigation.kt
@@ -17,11 +17,10 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.os.Build
import timber.log.Timber
-import androidx.activity.compose.BackHandler
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
@@ -59,14 +58,14 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
-import org.dueattendant149.bookreader.epubreader.EpubReaderScreen
-import org.dueattendant149.bookreader.feedback.FeedbackScreen
-import org.dueattendant149.bookreader.feedback.SupportProjectScreen
-import org.dueattendant149.bookreader.pdf.PdfViewerScreen
-import org.dueattendant149.bookreader.shared.ReaderFeatureSurface
-import org.dueattendant149.bookreader.tts.ReaderTtsMiniBar
-import org.dueattendant149.bookreader.tts.readerTtsMiniBarBottomPaddingDp
-import org.dueattendant149.bookreader.tts.shouldShowReaderTtsMiniBar
+import com.aryan.reader.epubreader.EpubReaderScreen
+import com.aryan.reader.feedback.FeedbackScreen
+import com.aryan.reader.feedback.SupportProjectScreen
+import com.aryan.reader.pdf.PdfViewerScreen
+import com.aryan.reader.shared.ReaderFeatureSurface
+import com.aryan.reader.tts.ReaderTtsMiniBar
+import com.aryan.reader.tts.readerTtsMiniBarBottomPaddingDp
+import com.aryan.reader.tts.shouldShowReaderTtsMiniBar
import kotlinx.coroutines.delay
object AppDestinations {
@@ -81,18 +80,6 @@ object AppDestinations {
const val SETTINGS_SCREEN_ROUTE = "settings_screen_route"
}
-fun shouldInterceptAppNavBack(
- currentRoute: String?,
- hasPreviousBackStackEntry: Boolean,
- isCurrentEntryResumed: Boolean
-): Boolean {
- if (!hasPreviousBackStackEntry || !isCurrentEntryResumed) return false
- return currentRoute != null &&
- currentRoute != AppDestinations.MAIN_ROUTE &&
- currentRoute != AppDestinations.PDF_VIEWER_ROUTE &&
- currentRoute != AppDestinations.EPUB_READER_ROUTE
-}
-
private fun NavHostController.isReadyForBackStackChange(): Boolean {
return currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED
}
@@ -179,11 +166,6 @@ fun AppNavigation(
val miniBarBottomPadding = readerTtsMiniBarBottomPaddingDp(
isOnMainRoute = currentRoute == AppDestinations.MAIN_ROUTE
).dp
- val shouldInterceptBack = shouldInterceptAppNavBack(
- currentRoute = currentRoute,
- hasPreviousBackStackEntry = navController.previousBackStackEntry != null,
- isCurrentEntryResumed = currentBackStackEntry?.lifecycle?.currentState == Lifecycle.State.RESUMED
- )
LaunchedEffect(currentRoute, uiState.selectedFileType, uiState.isLoading, uiState.selectedEpubBook, uiState.selectedPdfUri) {
if (!uiState.isLoading) {
@@ -213,24 +195,14 @@ fun AppNavigation(
}
Box(modifier = Modifier.fillMaxSize()) {
- BackHandler(enabled = shouldInterceptBack) {
- navController.popBackStackIfReady()
- }
-
NavHost(navController = navController, startDestination = AppDestinations.MAIN_ROUTE) {
composable(AppDestinations.MAIN_ROUTE) {
Timber.d("Navigating to Main Screen (${AppDestinations.MAIN_ROUTE}).")
- if (uiState.isTemporaryExternalOpen) {
- Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator()
- }
- } else {
- MainScreen(
- viewModel = viewModel,
- windowSizeClass = windowSizeClass,
- navController = navController
- )
- }
+ MainScreen(
+ viewModel = viewModel,
+ windowSizeClass = windowSizeClass,
+ navController = navController
+ )
}
// PDF Viewer Screen Composable
@@ -343,7 +315,7 @@ fun AppNavigation(
},
onRenderModeChange = viewModel::setRenderMode,
customFonts = customFonts,
- onImportFonts = viewModel::importFonts,
+ onImportFont = viewModel::importFont,
viewModel = viewModel
)
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/AppUiModels.kt b/app/src/main/java/com/aryan/reader/AppUiModels.kt
similarity index 81%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/AppUiModels.kt
rename to app/src/main/java/com/aryan/reader/AppUiModels.kt
index b69de1b..f5236d6 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/AppUiModels.kt
+++ b/app/src/main/java/com/aryan/reader/AppUiModels.kt
@@ -1,20 +1,20 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.net.Uri
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.data.TagEntity
-import org.dueattendant149.bookreader.epub.CalibreBundleResult
-import org.dueattendant149.bookreader.epub.EpubBook
-import org.dueattendant149.bookreader.paginatedreader.Locator
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.data.TagEntity
+import com.aryan.reader.epub.CalibreBundleResult
+import com.aryan.reader.epub.EpubBook
+import com.aryan.reader.paginatedreader.Locator
import java.util.Date
-typealias BannerMessage = org.dueattendant149.bookreader.shared.BannerMessage
-typealias UserData = org.dueattendant149.bookreader.shared.UserData
-typealias AppThemeMode = org.dueattendant149.bookreader.shared.AppThemeMode
-typealias AppContrastOption = org.dueattendant149.bookreader.shared.AppContrastOption
-typealias AppFontPreference = org.dueattendant149.bookreader.shared.AppFontPreference
-typealias AppFontPreferenceKind = org.dueattendant149.bookreader.shared.AppFontPreferenceKind
-typealias CustomAppTheme = org.dueattendant149.bookreader.shared.CustomAppTheme
+typealias BannerMessage = com.aryan.reader.shared.BannerMessage
+typealias UserData = com.aryan.reader.shared.UserData
+typealias AppThemeMode = com.aryan.reader.shared.AppThemeMode
+typealias AppContrastOption = com.aryan.reader.shared.AppContrastOption
+typealias AppFontPreference = com.aryan.reader.shared.AppFontPreference
+typealias AppFontPreferenceKind = com.aryan.reader.shared.AppFontPreferenceKind
+typealias CustomAppTheme = com.aryan.reader.shared.CustomAppTheme
data class ImportResult(
val internalUri: Uri,
@@ -43,7 +43,6 @@ data class ReaderScreenState(
val selectedEpubUri: Uri? = null,
val selectedFileType: FileType? = null,
val isLoading: Boolean = false,
- val isTemporaryExternalOpen: Boolean = false,
val errorMessage: String? = null,
val contextualActionItems: Set = emptySet(),
val renderMode: RenderMode = RenderMode.VERTICAL_SCROLL,
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/BookImporter.kt b/app/src/main/java/com/aryan/reader/BookImporter.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/BookImporter.kt
rename to app/src/main/java/com/aryan/reader/BookImporter.kt
index 3c25a9e..5aae54b 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/BookImporter.kt
+++ b/app/src/main/java/com/aryan/reader/BookImporter.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import android.net.Uri
@@ -29,7 +29,7 @@ import java.io.FileOutputStream
import java.io.InputStream
import java.util.UUID
import androidx.core.net.toUri
-import org.dueattendant149.bookreader.shared.SharedFileCapabilities
+import com.aryan.reader.shared.SharedFileCapabilities
private const val BOOKS_DIR = "books"
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/Common.kt b/app/src/main/java/com/aryan/reader/Common.kt
similarity index 98%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/Common.kt
rename to app/src/main/java/com/aryan/reader/Common.kt
index 0f459cd..3f071d5 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/Common.kt
+++ b/app/src/main/java/com/aryan/reader/Common.kt
@@ -1,7 +1,7 @@
// Common.kt
@file:OptIn(ExperimentalMaterial3Api::class) @file:Suppress("KotlinConstantConditions")
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import android.graphics.Bitmap
@@ -138,7 +138,6 @@ import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
@@ -166,23 +165,23 @@ import androidx.compose.ui.window.PopupProperties
import androidx.core.content.edit
import androidx.core.graphics.toColorInt
import androidx.media3.common.util.UnstableApi
-import org.dueattendant149.bookreader.epubreader.PREF_CUSTOM_THEMES
-import org.dueattendant149.bookreader.epubreader.PREF_READER_THEME
-import org.dueattendant149.bookreader.paginatedreader.TtsChunk
-import org.dueattendant149.bookreader.pdf.PdfHighlightColor
-import org.dueattendant149.bookreader.shared.BuiltInReaderThemes
-import org.dueattendant149.bookreader.shared.ReaderTextureFilePrefix
-import org.dueattendant149.bookreader.shared.normalizeReaderTextureExtension
-import org.dueattendant149.bookreader.shared.readerTextureDisplayName as sharedReaderTextureDisplayName
-import org.dueattendant149.bookreader.shared.readerTextureMimeTypeForExtension
-import org.dueattendant149.bookreader.tts.GEMINI_TTS_SPEAKERS
-import org.dueattendant149.bookreader.tts.SpeakerSamplePlayer
-import org.dueattendant149.bookreader.tts.TtsCacheManager
-import org.dueattendant149.bookreader.tts.TtsPlaybackManager
-import org.dueattendant149.bookreader.tts.formatBytes
-import org.dueattendant149.bookreader.tts.loadTtsMode
-import org.dueattendant149.bookreader.tts.rememberTtsController
-import org.dueattendant149.bookreader.tts.splitTextIntoChunks
+import com.aryan.reader.epubreader.PREF_CUSTOM_THEMES
+import com.aryan.reader.epubreader.PREF_READER_THEME
+import com.aryan.reader.paginatedreader.TtsChunk
+import com.aryan.reader.pdf.PdfHighlightColor
+import com.aryan.reader.shared.BuiltInReaderThemes
+import com.aryan.reader.shared.ReaderTextureFilePrefix
+import com.aryan.reader.shared.normalizeReaderTextureExtension
+import com.aryan.reader.shared.readerTextureDisplayName as sharedReaderTextureDisplayName
+import com.aryan.reader.shared.readerTextureMimeTypeForExtension
+import com.aryan.reader.tts.GEMINI_TTS_SPEAKERS
+import com.aryan.reader.tts.SpeakerSamplePlayer
+import com.aryan.reader.tts.TtsCacheManager
+import com.aryan.reader.tts.TtsPlaybackManager
+import com.aryan.reader.tts.formatBytes
+import com.aryan.reader.tts.loadTtsMode
+import com.aryan.reader.tts.rememberTtsController
+import com.aryan.reader.tts.splitTextIntoChunks
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -218,8 +217,8 @@ import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
-typealias ReaderTexture = org.dueattendant149.bookreader.shared.ReaderTexture
-typealias ReaderTheme = org.dueattendant149.bookreader.shared.ReaderTheme
+typealias ReaderTexture = com.aryan.reader.shared.ReaderTexture
+typealias ReaderTheme = com.aryan.reader.shared.ReaderTheme
const val aiServerBasePath = BuildConfig.AI_WORKER_URL
const val summarizeEndpoint = "/summarize"
@@ -474,9 +473,9 @@ data class SearchResult(
val chunkIndex: Int
)
-typealias AiDefinitionResult = org.dueattendant149.bookreader.shared.AiDefinitionResult
+typealias AiDefinitionResult = com.aryan.reader.shared.AiDefinitionResult
-typealias SummarizationResult = org.dueattendant149.bookreader.shared.SummarizationResult
+typealias SummarizationResult = com.aryan.reader.shared.SummarizationResult
data class CachedSummaryItem(
val chapterIndex: Int,
@@ -863,13 +862,6 @@ fun AiDefinitionPopup(
val ttsController = rememberTtsController()
val ttsState by ttsController.ttsState.collectAsState()
val context = LocalContext.current
- val configuration = LocalConfiguration.current
- val maxPopupHeight = readerModalMaxHeightDp(
- screenHeightDp = configuration.screenHeightDp,
- fraction = 0.65f,
- verticalMarginDp = 48,
- preferredMinHeightDp = 180
- ).dp
@Suppress("DEPRECATION") val clipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
@@ -890,7 +882,7 @@ fun AiDefinitionPopup(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 5.dp)
- .heightIn(max = maxPopupHeight),
+ .heightIn(min = 150.dp, max = 400.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)
) {
@@ -2957,14 +2949,7 @@ private fun ThemeGridItem(
Text(text = stringResource(R.string.label_aa_preview), color = textColor, style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Bold)
}
Spacer(modifier = Modifier.height(8.dp))
- Text(
- text = theme.name,
- style = MaterialTheme.typography.labelSmall,
- color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- modifier = Modifier.clickable { onThemeSelected(theme.id) }
- )
+ Text(text = theme.name, style = MaterialTheme.typography.labelSmall, color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis)
if (theme.isCustom && onEdit != null && onDelete != null) {
Spacer(modifier = Modifier.height(6.dp))
@@ -3318,16 +3303,12 @@ fun ThemeColorPickerDialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
- val configuration = LocalConfiguration.current
- val maxDialogHeight = readerModalMaxHeightDp(configuration.screenHeightDp).dp
-
Surface(
shape = RoundedCornerShape(24.dp),
color = Color(0xFF2C2C2C),
modifier = Modifier
.fillMaxWidth(0.9f)
.padding(16.dp)
- .heightIn(max = maxDialogHeight)
) {
Column(
modifier = Modifier
@@ -3507,16 +3488,12 @@ fun HighlightColorPickerDialog(
onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
- val configuration = LocalConfiguration.current
- val maxDialogHeight = readerModalMaxHeightDp(configuration.screenHeightDp).dp
-
Surface(
shape = RoundedCornerShape(24.dp),
color = Color(0xFF2C2C2C),
modifier = Modifier
.fillMaxWidth(0.9f)
.padding(16.dp)
- .heightIn(max = maxDialogHeight)
) {
Column(
modifier = Modifier
@@ -3845,7 +3822,7 @@ fun AiResultContentView(
result: SummarizationResult?,
isLoading: Boolean,
isMainTtsActive: Boolean,
- ttsController: org.dueattendant149.bookreader.tts.TtsController,
+ ttsController: com.aryan.reader.tts.TtsController,
ttsState: TtsPlaybackManager.TtsState,
getAuthToken: suspend () -> String?,
onRegenerate: (() -> Unit)? = null,
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/EmbeddedEbookMetadataExtractor.kt b/app/src/main/java/com/aryan/reader/EmbeddedEbookMetadataExtractor.kt
similarity index 99%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/EmbeddedEbookMetadataExtractor.kt
rename to app/src/main/java/com/aryan/reader/EmbeddedEbookMetadataExtractor.kt
index 70fec74..36c2574 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/EmbeddedEbookMetadataExtractor.kt
+++ b/app/src/main/java/com/aryan/reader/EmbeddedEbookMetadataExtractor.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.util.Xml
import org.xmlpull.v1.XmlPullParser
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/EpubMetadataFileEditor.kt b/app/src/main/java/com/aryan/reader/EpubMetadataFileEditor.kt
similarity index 91%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/EpubMetadataFileEditor.kt
rename to app/src/main/java/com/aryan/reader/EpubMetadataFileEditor.kt
index 9ec9140..f8f1d8e 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/EpubMetadataFileEditor.kt
+++ b/app/src/main/java/com/aryan/reader/EpubMetadataFileEditor.kt
@@ -1,14 +1,14 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import android.net.Uri
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
-import org.dueattendant149.bookreader.data.BookMetadataEdit
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.shared.reader.SharedEpubMetadataEditor
-import org.dueattendant149.bookreader.shared.reader.SharedEpubMetadataSnapshot
-import org.dueattendant149.bookreader.shared.reader.SharedEpubMetadataUpdate
+import com.aryan.reader.data.BookMetadataEdit
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.shared.reader.SharedEpubMetadataEditor
+import com.aryan.reader.shared.reader.SharedEpubMetadataSnapshot
+import com.aryan.reader.shared.reader.SharedEpubMetadataUpdate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/FileHasher.kt b/app/src/main/java/com/aryan/reader/FileHasher.kt
similarity index 94%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/FileHasher.kt
rename to app/src/main/java/com/aryan/reader/FileHasher.kt
index fa76ef3..7d8e436 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/FileHasher.kt
+++ b/app/src/main/java/com/aryan/reader/FileHasher.kt
@@ -17,11 +17,10 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader
+package com.aryan.reader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import timber.log.Timber
import java.io.InputStream
import java.security.MessageDigest
@@ -51,8 +50,9 @@ object FileHasher {
}
hexString.toString()
} catch (e: Exception) {
- Timber.e(e, "Failed to calculate SHA-256 hash")
+ // In a real app, you'd want to log this error
+ e.printStackTrace()
null
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/FileTypeResolver.kt b/app/src/main/java/com/aryan/reader/FileTypeResolver.kt
similarity index 90%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/FileTypeResolver.kt
rename to app/src/main/java/com/aryan/reader/FileTypeResolver.kt
index fecd802..adb94da 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/FileTypeResolver.kt
+++ b/app/src/main/java/com/aryan/reader/FileTypeResolver.kt
@@ -1,6 +1,6 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
-import org.dueattendant149.bookreader.shared.SharedFileCapabilities
+import com.aryan.reader.shared.SharedFileCapabilities
internal fun resolveFileTypeFromName(fileName: String?): FileType? {
return SharedFileCapabilities.resolveFileTypeForName(fileName)
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/FolderSyncWorker.kt b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt
similarity index 90%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/FolderSyncWorker.kt
rename to app/src/main/java/com/aryan/reader/FolderSyncWorker.kt
index e4e67cd..97bdde9 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/FolderSyncWorker.kt
+++ b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt
@@ -18,7 +18,7 @@
* mail: epistemereader@gmail.com
*/
// FolderSyncWorker.kt
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import timber.log.Timber
@@ -29,25 +29,25 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import androidx.work.WorkManager
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.data.RecentFilesRepository
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.data.RecentFilesRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import androidx.core.content.edit
-import org.dueattendant149.bookreader.data.LocalSyncUtils
-import org.dueattendant149.bookreader.data.FolderBookMetadata
-import org.dueattendant149.bookreader.data.toSharedFolderBookMetadata
-import org.dueattendant149.bookreader.shared.BookItem as SharedBookItem
-import org.dueattendant149.bookreader.shared.EpubAnnotationSerializer
-import org.dueattendant149.bookreader.shared.EpubBookmark
-import org.dueattendant149.bookreader.shared.LOCAL_FOLDER_SYNC_DATA_DIR
-import org.dueattendant149.bookreader.shared.LocalFolderSyncEngine
-import org.dueattendant149.bookreader.shared.ReaderLocator
-import org.dueattendant149.bookreader.shared.SharedFolderScannedFile
-import org.dueattendant149.bookreader.shared.SharedReaderScreenState
-import org.dueattendant149.bookreader.shared.reader.ReaderBookmark
+import com.aryan.reader.data.LocalSyncUtils
+import com.aryan.reader.data.FolderBookMetadata
+import com.aryan.reader.data.toSharedFolderBookMetadata
+import com.aryan.reader.shared.BookItem as SharedBookItem
+import com.aryan.reader.shared.EpubAnnotationSerializer
+import com.aryan.reader.shared.EpubBookmark
+import com.aryan.reader.shared.LOCAL_FOLDER_SYNC_DATA_DIR
+import com.aryan.reader.shared.LocalFolderSyncEngine
+import com.aryan.reader.shared.ReaderLocator
+import com.aryan.reader.shared.SharedFolderScannedFile
+import com.aryan.reader.shared.SharedReaderScreenState
+import com.aryan.reader.shared.reader.ReaderBookmark
import java.io.File
import android.provider.DocumentsContract
@@ -72,27 +72,45 @@ class FolderSyncWorker(
val targetFolderUri = inputData.getString(KEY_TARGET_FOLDER_URI)
val prefs = appContext.getSharedPreferences("reader_user_prefs", Context.MODE_PRIVATE)
- val jsonString = prefs.getString(SyncedFolderPrefs.KEY_SYNCED_FOLDERS_JSON, null)
- val folders = SyncedFolderPrefs.decodeSyncedFolders(
- jsonString = jsonString,
- legacyUri = prefs.getString(SyncedFolderPrefs.KEY_LEGACY_SYNCED_FOLDER_URI, null),
- syncableTypes = ANDROID_SYNCABLE_FILE_TYPES
- )
+ val jsonString = prefs.getString("synced_folders_list_json", null)
+ val folders = mutableListOf>>()
+
+ if (jsonString != null) {
+ try {
+ val array = org.json.JSONArray(jsonString)
+ for (i in 0 until array.length()) {
+ val obj = array.getJSONObject(i)
+ val uri = obj.getString("uri")
+ val allowedFileTypes = mutableSetOf()
+ if (obj.has("allowedFileTypes")) {
+ val typesArray = obj.getJSONArray("allowedFileTypes")
+ for (j in 0 until typesArray.length()) {
+ try { allowedFileTypes.add(FileType.valueOf(typesArray.getString(j))) } catch (_: Exception) {}
+ }
+ } else {
+ allowedFileTypes.addAll(ANDROID_SYNCABLE_FILE_TYPES)
+ }
+ folders.add(Pair(uri, allowedFileTypes.filterTo(mutableSetOf()) { it in ANDROID_SYNCABLE_FILE_TYPES }))
+ }
+ } catch (e: Exception) { Timber.e(e) }
+ } else {
+ val single = prefs.getString("synced_folder_uri", null)
+ if (single != null) folders.add(Pair(single, ANDROID_SYNCABLE_FILE_TYPES))
+ }
if (folders.isEmpty()) {
ReaderPerfLog.w("FolderSync worker aborted: no linked folders")
return Result.success()
}
- val enabledFolders = folders.filter { it.localSyncEnabled }
val foldersToProcess = if (targetFolderUri.isNullOrBlank()) {
- enabledFolders
+ folders
} else {
- enabledFolders.filter { it.uriString == targetFolderUri }
+ folders.filter { it.first == targetFolderUri }
}
if (foldersToProcess.isEmpty()) {
- ReaderPerfLog.w("FolderSync worker aborted: target folder not linked or disabled target=$targetFolderUri")
+ ReaderPerfLog.w("FolderSync worker aborted: target folder not linked target=$targetFolderUri")
return Result.success()
}
@@ -105,8 +123,8 @@ class FolderSyncWorker(
syncMutex.withLock {
var allSuccess = true
- for (folderConfig in foldersToProcess) {
- val success = performSyncForFolder(folderConfig, isMetadataOnly)
+ for ((uriString, allowedTypes) in foldersToProcess) {
+ val success = performSyncForFolder(uriString, allowedTypes, isMetadataOnly)
if (!success) allSuccess = false
}
@@ -114,14 +132,13 @@ class FolderSyncWorker(
try {
val array = org.json.JSONArray(jsonString)
val now = System.currentTimeMillis()
- val processedUris = foldersToProcess.mapTo(mutableSetOf()) { it.uriString }
for (i in 0 until array.length()) {
val obj = array.getJSONObject(i)
- if (obj.optString("uri") in processedUris) {
+ if (targetFolderUri.isNullOrBlank() || obj.optString("uri") == targetFolderUri) {
obj.put("lastScanTime", now)
}
}
- prefs.edit { putString(SyncedFolderPrefs.KEY_SYNCED_FOLDERS_JSON, array.toString()) }
+ prefs.edit { putString("synced_folders_list_json", array.toString()) }
} catch (_: Exception) {}
}
@@ -136,9 +153,7 @@ class FolderSyncWorker(
}
}
- private suspend fun performSyncForFolder(folderConfig: SyncedFolder, metadataOnly: Boolean): Boolean {
- val folderUriString = folderConfig.uriString
- val allowedFileTypes = folderConfig.allowedFileTypes
+ private suspend fun performSyncForFolder(folderUriString: String, allowedFileTypes: Set, metadataOnly: Boolean): Boolean {
if (folderUriString.isBlank()) return true
val folderUri = folderUriString.toUri()
val folderStart = ReaderPerfLog.nowNanos()
@@ -220,10 +235,9 @@ class FolderSyncWorker(
val nowMillis = System.currentTimeMillis()
val folder = SyncedFolder(
uriString = folderUriString,
- name = documentTree.name ?: folderConfig.name,
+ name = documentTree.name ?: "Local Folder",
lastScanTime = nowMillis,
- allowedFileTypes = allowedFileTypes,
- localSyncEnabled = true
+ allowedFileTypes = allowedFileTypes
)
val sharedState = SharedReaderScreenState(
rawLibraryBooks = existingFolderBooks.map { it.toFolderSyncSharedBookItem() },
@@ -550,8 +564,7 @@ class FolderSyncWorker(
lastPageIndex = lastPage,
readerPosition = readerPositionOrNull(),
readerBookmarks = parseReaderBookmarks(),
- readerHighlights = EpubAnnotationSerializer.parseHighlightsJson(highlightsJson),
- readingPositionModifiedTimestamp = readingPositionModifiedTimestamp
+ readerHighlights = EpubAnnotationSerializer.parseHighlightsJson(highlightsJson)
)
}
@@ -600,8 +613,8 @@ class FolderSyncWorker(
lastChapterIndex = legacyPosition?.chapterIndex ?: appliedMetadata?.lastChapterIndex ?: existing?.lastChapterIndex,
lastPage = legacyPosition?.pageIndex ?: lastPageIndex ?: appliedMetadata?.lastPage ?: existing?.lastPage,
lastPositionCfi = legacyPosition?.cfi ?: appliedMetadata?.lastPositionCfi ?: existing?.lastPositionCfi,
- locatorBlockIndex = legacyPosition?.blockIndex ?: appliedMetadata?.locatorBlockIndex ?: existing?.locatorBlockIndex,
- locatorCharOffset = legacyPosition?.charOffset ?: appliedMetadata?.locatorCharOffset ?: existing?.locatorCharOffset,
+ locatorBlockIndex = appliedMetadata?.locatorBlockIndex ?: existing?.locatorBlockIndex,
+ locatorCharOffset = appliedMetadata?.locatorCharOffset ?: existing?.locatorCharOffset,
progressPercentage = progressPercentage,
isRecent = isRecent,
isAvailable = true,
@@ -624,7 +637,6 @@ class FolderSyncWorker(
originalDescription = originalDescription,
folderTextMetadataParsed = folderTextMetadataParsed,
folderCoverMetadataParsed = if (contentChanged) false else existing?.folderCoverMetadataParsed ?: false,
- readingPositionModifiedTimestamp = readingPositionModifiedTimestamp,
tags = existing?.tags.orEmpty()
)
}
@@ -708,12 +720,18 @@ class FolderSyncWorker(
private fun isFolderStillLinked(folderUriString: String): Boolean {
val prefs = appContext.getSharedPreferences("reader_user_prefs", Context.MODE_PRIVATE)
- return SyncedFolderPrefs.isLocalSyncEnabled(
- jsonString = prefs.getString(SyncedFolderPrefs.KEY_SYNCED_FOLDERS_JSON, null),
- legacyUri = prefs.getString(SyncedFolderPrefs.KEY_LEGACY_SYNCED_FOLDER_URI, null),
- folderUriString = folderUriString,
- syncableTypes = ANDROID_SYNCABLE_FILE_TYPES
- )
+ val jsonString = prefs.getString("synced_folders_list_json", null)
+ if (jsonString != null) {
+ return try {
+ val array = org.json.JSONArray(jsonString)
+ (0 until array.length()).any { index ->
+ array.getJSONObject(index).optString("uri") == folderUriString
+ }
+ } catch (_: Exception) {
+ false
+ }
+ }
+ return prefs.getString("synced_folder_uri", null) == folderUriString
}
private fun getFileType(name: String, mimeType: String?): FileType? {
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/FontsScreen.kt b/app/src/main/java/com/aryan/reader/FontsScreen.kt
similarity index 56%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/FontsScreen.kt
rename to app/src/main/java/com/aryan/reader/FontsScreen.kt
index c34ffa5..4e86a24 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/FontsScreen.kt
+++ b/app/src/main/java/com/aryan/reader/FontsScreen.kt
@@ -1,13 +1,10 @@
// FontsScreen.kt
@file:Suppress("KotlinConstantConditions")
-package org.dueattendant149.bookreader
+package com.aryan.reader
-import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -30,12 +27,10 @@ import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.CloudDownload
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
@@ -51,7 +46,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -64,21 +58,14 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import org.dueattendant149.bookreader.shared.CustomFontItem
-import org.dueattendant149.bookreader.shared.CustomFontFamilyItem
-import org.dueattendant149.bookreader.shared.CustomFontVariantItem
-import org.dueattendant149.bookreader.shared.fontFaceLabel
-import org.dueattendant149.bookreader.shared.fontFaceSummary
-import org.dueattendant149.bookreader.shared.groupByFamily
-import org.dueattendant149.bookreader.shared.hasVariableWeightFace
-import org.dueattendant149.bookreader.shared.ui.SharedAppFontSelector
-import org.dueattendant149.bookreader.shared.ui.SharedFontSettingsSection
-import org.dueattendant149.bookreader.shared.ui.SharedFontSettingsTabs
-import org.dueattendant149.bookreader.data.CustomFontEntity
+import com.aryan.reader.shared.CustomFontItem
+import com.aryan.reader.shared.ui.SharedAppFontSelector
+import com.aryan.reader.shared.ui.SharedFontSettingsSection
+import com.aryan.reader.shared.ui.SharedFontSettingsTabs
+import com.aryan.reader.data.CustomFontEntity
import java.io.File
@OptIn(ExperimentalMaterial3Api::class)
@@ -93,68 +80,37 @@ fun FontsScreen(
val showGoogleFontsOption = !(BuildConfig.FLAVOR == "oss" && BuildConfig.IS_OFFLINE)
- var fontsPendingDelete by remember { mutableStateOf>(emptyList()) }
+ // Dialog state
+ var showDeleteDialog by remember { mutableStateOf(false) }
+ var fontToDelete by remember { mutableStateOf(null) }
var showGoogleFontsSheet by remember { mutableStateOf(false) }
var selectedSection by remember { mutableStateOf(SharedFontSettingsSection.READER_FONTS) }
- var selectedFontIds by remember { mutableStateOf>(emptySet()) }
- val pickFontLauncher = rememberFilePickerLauncher(viewModel::importFonts)
- val fontMimeTypes = remember { supportedFontMimeTypes() }
- val allFontIds = remember(fonts) { fonts.mapTo(mutableSetOf()) { it.id } }
- val selectedFonts = remember(fonts, selectedFontIds) {
- fonts.filter { it.id in selectedFontIds }
- }
- val fontEntitiesById = remember(fonts) { fonts.associateBy { it.id } }
- val isFontSelectionMode = selectedSection == SharedFontSettingsSection.READER_FONTS && selectedFonts.isNotEmpty()
-
- LaunchedEffect(fonts) {
- selectedFontIds = selectedFontIds.intersect(allFontIds)
+ val pickFontLauncher = rememberFilePickerLauncher { uris ->
+ uris.firstOrNull()?.let { viewModel.importFont(it) }
}
- BackHandler(enabled = isFontSelectionMode) {
- selectedFontIds = emptySet()
- }
+ val fontMimeTypes = arrayOf(
+ "font/ttf", "font/otf", "font/woff2",
+ "application/x-font-ttf", "application/x-font-otf",
+ "application/font-woff2", "application/vnd.ms-opentype",
+ "application/x-font-opentype"
+ )
Scaffold(
modifier = Modifier.statusBarsPadding(),
topBar = {
- if (isFontSelectionMode) {
- ContextualTopAppBar(
- selectedItemCount = selectedFonts.size,
- onNavIconClick = { selectedFontIds = emptySet() },
- onSelectAllClick = {
- selectedFontIds = if (selectedFontIds.containsAll(allFontIds)) {
- emptySet()
- } else {
- allFontIds
- }
- },
- onDeleteClick = {
- if (selectedFonts.isNotEmpty()) {
- fontsPendingDelete = selectedFonts
- }
+ CustomTopAppBar(
+ title = { Text(stringResource(R.string.custom_fonts)) },
+ navigationIcon = {
+ IconButton(onClick = onBackClick) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
}
- )
- } else {
- CustomTopAppBar(
- title = { Text(stringResource(R.string.custom_fonts)) },
- navigationIcon = {
- IconButton(onClick = onBackClick) {
- Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.action_back))
- }
- },
- actions = {
- if (selectedSection == SharedFontSettingsSection.READER_FONTS && fonts.isNotEmpty()) {
- IconButton(onClick = { selectedFontIds = allFontIds }) {
- Icon(Icons.Default.SelectAll, contentDescription = stringResource(R.string.select_all))
- }
- }
- }
- )
- }
+ }
+ )
},
floatingActionButton = {
- if (selectedSection == SharedFontSettingsSection.READER_FONTS && fonts.isNotEmpty() && !isFontSelectionMode) {
+ if (selectedSection == SharedFontSettingsSection.READER_FONTS && fonts.isNotEmpty()) {
Column(
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(16.dp)
@@ -180,14 +136,10 @@ fun FontsScreen(
) { padding ->
Box(modifier = Modifier.fillMaxSize().padding(padding)) {
val sharedFonts = remember(fonts) { fonts.toSharedCustomFontItems() }
- val fontFamilies = remember(sharedFonts) { sharedFonts.groupByFamily() }
Column(modifier = Modifier.fillMaxSize()) {
SharedFontSettingsTabs(
selectedSection = selectedSection,
- onSectionChange = {
- selectedFontIds = emptySet()
- selectedSection = it
- },
+ onSectionChange = { selectedSection = it },
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp)
)
@@ -211,20 +163,12 @@ fun FontsScreen(
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 88.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
- items(fontFamilies, key = { family -> family.variants.joinToString("|") { it.font.id } }) { family ->
- FontFamilyListItem(
- family = family,
- selectedFontIds = selectedFontIds,
- isSelectionMode = isFontSelectionMode,
- fontEntityForId = { id -> fontEntitiesById[id] },
- onVariantSelectionToggle = { id ->
- selectedFontIds = selectedFontIds.toggle(id)
- },
- onFamilySelectionToggle = {
- selectedFontIds = selectedFontIds.toggleAll(family.variants.map { it.font.id })
- },
- onDeleteVariant = { id ->
- fontEntitiesById[id]?.let { fontsPendingDelete = listOf(it) }
+ items(fonts, key = { it.id }) { font ->
+ FontListItem(
+ font = font,
+ onDelete = {
+ fontToDelete = font
+ showDeleteDialog = true
}
)
}
@@ -264,17 +208,17 @@ fun FontsScreen(
}
}
- if (fontsPendingDelete.isNotEmpty()) {
- DeleteFontsConfirmationDialog(
- fonts = fontsPendingDelete,
+ if (showDeleteDialog && fontToDelete != null) {
+ DeleteFontConfirmationDialog(
+ fontName = fontToDelete!!.displayName,
onConfirm = {
- val pendingIds = fontsPendingDelete.map { it.id }
- viewModel.deleteFonts(pendingIds)
- selectedFontIds = selectedFontIds - pendingIds.toSet()
- fontsPendingDelete = emptyList()
+ fontToDelete?.let { viewModel.deleteFont(it.id) }
+ showDeleteDialog = false
+ fontToDelete = null
},
onDismiss = {
- fontsPendingDelete = emptyList()
+ showDeleteDialog = false
+ fontToDelete = null
}
)
}
@@ -444,13 +388,9 @@ fun GoogleFontsBottomSheet(
}
// Existing unchanged components
-@OptIn(ExperimentalFoundationApi::class)
@Composable
fun FontListItem(
font: CustomFontEntity,
- isSelected: Boolean,
- isSelectionMode: Boolean,
- onSelectionToggle: () -> Unit,
onDelete: () -> Unit
) {
val customTypeface = remember(font.path) {
@@ -462,23 +402,8 @@ fun FontListItem(
}
Card(
- modifier = Modifier
- .fillMaxWidth()
- .combinedClickable(
- onClick = {
- if (isSelectionMode) {
- onSelectionToggle()
- }
- },
- onLongClick = onSelectionToggle
- ),
- colors = CardDefaults.cardColors(
- containerColor = if (isSelected) {
- MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.45f)
- } else {
- MaterialTheme.colorScheme.surface
- }
- )
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column(modifier = Modifier.padding(16.dp)) {
Row(
@@ -486,27 +411,17 @@ fun FontListItem(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
- if (isSelectionMode) {
- Checkbox(
- checked = isSelected,
- onCheckedChange = { onSelectionToggle() },
- modifier = Modifier.padding(end = 8.dp)
- )
- }
Text(
text = font.displayName,
style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- modifier = Modifier.weight(1f)
+ fontWeight = FontWeight.Bold
)
- if (!isSelectionMode) {
- IconButton(onClick = onDelete, modifier = Modifier.size(24.dp)) {
- Icon(
- Icons.Default.Delete,
- contentDescription = stringResource(R.string.action_delete),
- tint = MaterialTheme.colorScheme.error
- )
- }
+ IconButton(onClick = onDelete, modifier = Modifier.size(24.dp)) {
+ Icon(
+ Icons.Default.Delete,
+ contentDescription = stringResource(R.string.action_delete),
+ tint = MaterialTheme.colorScheme.error
+ )
}
}
@@ -544,188 +459,6 @@ fun FontListItem(
}
}
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun FontFamilyListItem(
- family: CustomFontFamilyItem,
- selectedFontIds: Set,
- isSelectionMode: Boolean,
- fontEntityForId: (String) -> CustomFontEntity?,
- onVariantSelectionToggle: (String) -> Unit,
- onFamilySelectionToggle: () -> Unit,
- onDeleteVariant: (String) -> Unit
-) {
- val baseFont = remember(family) {
- family.variants.firstOrNull { it.fontFaceLabel() == "Regular" }?.font ?: family.variants.first().font
- }
- val customTypeface = remember(baseFont.path) {
- try {
- FontFamily(Font(File(baseFont.path)))
- } catch (_: Exception) {
- null
- }
- }
- val familyFontIds = remember(family) { family.variants.map { it.font.id }.toSet() }
- val isSelected = familyFontIds.any { it in selectedFontIds }
- val allSelected = familyFontIds.all { it in selectedFontIds }
- val faceSummary = remember(family) {
- buildString {
- append(family.fontFaceSummary())
- if (family.hasVariableWeightFace()) append(" - Variable weight")
- append(" - ${family.variants.size} file")
- if (family.variants.size != 1) append("s")
- }
- }
-
- Card(
- modifier = Modifier
- .fillMaxWidth()
- .combinedClickable(
- onClick = {
- if (isSelectionMode) {
- onFamilySelectionToggle()
- }
- },
- onLongClick = onFamilySelectionToggle
- ),
- colors = CardDefaults.cardColors(
- containerColor = if (isSelected) {
- MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.45f)
- } else {
- MaterialTheme.colorScheme.surface
- }
- )
- ) {
- Column(modifier = Modifier.padding(16.dp)) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (isSelectionMode) {
- Checkbox(
- checked = allSelected,
- onCheckedChange = { onFamilySelectionToggle() },
- modifier = Modifier.padding(end = 8.dp)
- )
- }
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = family.familyName,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- Text(
- text = faceSummary,
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- maxLines = 2,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
-
- HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
-
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), MaterialTheme.shapes.small)
- .padding(12.dp)
- ) {
- if (customTypeface != null) {
- Text(
- text = stringResource(R.string.font_preview_text),
- fontFamily = customTypeface,
- fontSize = 18.sp,
- color = MaterialTheme.colorScheme.onSurface
- )
- } else {
- Text(
- text = stringResource(R.string.font_preview_error),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.error
- )
- }
- }
-
- Spacer(modifier = Modifier.height(8.dp))
-
- family.variants.forEachIndexed { index, variant ->
- FontVariantRow(
- variant = variant,
- entity = fontEntityForId(variant.font.id),
- isSelected = variant.font.id in selectedFontIds,
- isSelectionMode = isSelectionMode,
- onSelectionToggle = { onVariantSelectionToggle(variant.font.id) },
- onDelete = { onDeleteVariant(variant.font.id) }
- )
- if (index != family.variants.lastIndex) {
- HorizontalDivider(modifier = Modifier.padding(start = if (isSelectionMode) 48.dp else 0.dp))
- }
- }
- }
- }
-}
-
-@Composable
-private fun FontVariantRow(
- variant: CustomFontVariantItem,
- entity: CustomFontEntity?,
- isSelected: Boolean,
- isSelectionMode: Boolean,
- onSelectionToggle: () -> Unit,
- onDelete: () -> Unit
-) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clip(RoundedCornerShape(6.dp))
- .clickable(enabled = isSelectionMode) { onSelectionToggle() }
- .padding(vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (isSelectionMode) {
- Checkbox(
- checked = isSelected,
- onCheckedChange = { onSelectionToggle() },
- modifier = Modifier.padding(end = 8.dp)
- )
- }
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = variant.fontFaceLabel(),
- style = MaterialTheme.typography.bodyMedium,
- fontWeight = FontWeight.Medium
- )
- Text(
- text = entity?.fileName ?: variant.font.fileName,
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.outline,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
- Text(
- text = variant.font.fileExtension.uppercase(),
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.outline,
- modifier = Modifier.padding(horizontal = 8.dp)
- )
- if (!isSelectionMode) {
- IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) {
- Icon(
- Icons.Default.Delete,
- contentDescription = stringResource(R.string.action_delete),
- tint = MaterialTheme.colorScheme.error
- )
- }
- }
- }
-}
-
private fun List.toSharedCustomFontItems(): List {
return filterNot { it.isDeleted }
.sortedBy { it.displayName.lowercase() }
@@ -743,32 +476,15 @@ private fun List.toSharedCustomFontItems(): List,
+fun DeleteFontConfirmationDialog(
+ fontName: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
- val isSingleFont = fonts.size == 1
AlertDialog(
onDismissRequest = onDismiss,
- title = {
- Text(
- if (isSingleFont) {
- stringResource(R.string.dialog_delete_font)
- } else {
- stringResource(R.string.dialog_delete_fonts)
- }
- )
- },
- text = {
- Text(
- if (isSingleFont) {
- stringResource(R.string.dialog_delete_font_desc, fonts.first().displayName)
- } else {
- stringResource(R.string.dialog_delete_fonts_desc, fonts.size)
- }
- )
- },
+ title = { Text(stringResource(R.string.dialog_delete_font)) },
+ text = { Text(stringResource(R.string.dialog_delete_font_desc, fontName)) },
confirmButton = {
TextButton(
onClick = onConfirm,
@@ -782,12 +498,3 @@ fun DeleteFontsConfirmationDialog(
}
)
}
-
-private fun Set.toggle(id: String): Set {
- return if (id in this) this - id else this + id
-}
-
-private fun Set.toggleAll(ids: List): Set {
- val idSet = ids.toSet()
- return if (containsAll(idSet)) this - idSet else this + idSet
-}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/HomeScreen.kt b/app/src/main/java/com/aryan/reader/HomeScreen.kt
similarity index 96%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/HomeScreen.kt
rename to app/src/main/java/com/aryan/reader/HomeScreen.kt
index 612c96b..926488a 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/HomeScreen.kt
+++ b/app/src/main/java/com/aryan/reader/HomeScreen.kt
@@ -20,7 +20,7 @@
// HomeScreen
@file:Suppress("DEPRECATION")
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.annotation.SuppressLint
import android.app.Activity
@@ -130,7 +130,6 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
-import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -139,13 +138,12 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.os.LocaleListCompat
-import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.media3.common.util.UnstableApi
import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import coil.request.ImageRequest
-import org.dueattendant149.bookreader.data.RecentFileItem
+import com.aryan.reader.data.RecentFileItem
import kotlinx.coroutines.launch
import timber.log.Timber
import java.text.SimpleDateFormat
@@ -194,7 +192,6 @@ fun HomeScreen(
var showAboutDialog by remember { mutableStateOf(false) }
var showInfoDialog by remember { mutableStateOf(false) }
var itemForInfoDialog by remember { mutableStateOf(null) }
- var pendingSaveOriginalItem by remember { mutableStateOf(null) }
var showBehaviorDialog by remember { mutableStateOf(false) }
var showStrictFilterDialog by remember { mutableStateOf(false) }
var showClearBookCacheDialog by remember { mutableStateOf(false) }
@@ -223,35 +220,6 @@ fun HomeScreen(
}
}
- val saveOriginalLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.CreateDocument("application/octet-stream")
- ) { uri ->
- val item = pendingSaveOriginalItem
- pendingSaveOriginalItem = null
- if (uri != null && item?.uriString != null) {
- viewModel.saveOriginalFile(item.uriString.toUri(), uri)
- }
- }
-
- fun saveOriginalItem(item: RecentFileItem) {
- if (!item.canExportOriginalFile()) return
- pendingSaveOriginalItem = item
- saveOriginalLauncher.launch(item.suggestedOriginalFileName())
- }
-
- fun shareOriginalItem(item: RecentFileItem) {
- val uriString = item.uriString ?: return
- if (!item.canExportOriginalFile()) return
- scope.launch {
- viewModel.shareOriginalFile(
- activityContext = context,
- sourceUri = uriString.toUri(),
- fileType = item.type,
- filename = item.suggestedOriginalFileName()
- )
- }
- }
-
LaunchedEffect(uiState.isRequestingDrivePermission) {
if (uiState.isRequestingDrivePermission) {
val intent = viewModel.getDriveSignInIntent(context)
@@ -421,12 +389,6 @@ fun HomeScreen(
showInfoDialog = true
}
},
- onSaveClick = selectedContextItems.singleOrNull()
- ?.takeIf { it.canExportOriginalFile() }
- ?.let { item -> { saveOriginalItem(item) } },
- onShareClick = selectedContextItems.singleOrNull()
- ?.takeIf { it.canExportOriginalFile() }
- ?.let { item -> { shareOriginalItem(item) } },
onPinClick = { viewModel.togglePinForContextualItems(isHome = true) },
onDeleteClick = { showDeleteConfirmDialog = true },
onSelectAllClick = { viewModel.selectAllRecentFiles() })
@@ -474,7 +436,7 @@ fun HomeScreen(
onRefresh = { viewModel.refreshLibrary() },
isRefreshing = uiState.isRefreshing,
isSyncEnabled = uiState.isSyncEnabled,
- hasSyncedFolder = uiState.syncedFolders.any { it.localSyncEnabled },
+ hasSyncedFolder = uiState.syncedFolders.isNotEmpty(),
usePdfFileNameAsDisplayName = uiState.usePdfFileNameAsDisplayName
)
}
@@ -539,17 +501,28 @@ fun HomeScreen(
)
}
- HydratedFileInfoDialog(
- item = itemForInfoDialog,
- isVisible = showInfoDialog,
- uiState = uiState,
- viewModel = viewModel,
- onDismiss = {
- showInfoDialog = false
- itemForInfoDialog = null
- },
- onOpenTags = { bookId -> viewModel.openTagSelection(setOf(bookId)) }
- )
+ itemForInfoDialog?.let { item ->
+ if (showInfoDialog) {
+ FileInfoDialog(
+ item = item,
+ usePdfFileNameAsDisplayName = uiState.usePdfFileNameAsDisplayName,
+ onDismiss = {
+ showInfoDialog = false
+ itemForInfoDialog = null
+ },
+ onSaveMetadata = { metadata ->
+ viewModel.updateBookMetadata(item.bookId, metadata)
+ },
+ onSaveDisplayName = { name ->
+ viewModel.updateCustomName(item.bookId, name)
+ },
+ onRestoreMetadata = {
+ viewModel.restoreOriginalBookMetadata(item.bookId)
+ },
+ onOpenTags = { viewModel.openTagSelection(setOf(item.bookId)) }
+ )
+ }
+ }
if (showClearReflowCacheDialog) {
DangerousFolderActionDialog(
@@ -794,8 +767,7 @@ private fun RecentFilesGrid(
modifier = Modifier.size(16.dp)
)
}
- },
- modifier = Modifier.testTag("HomeTab_${tab.bookId}")
+ }
)
}
}
@@ -845,7 +817,6 @@ fun RecentFileCard(
androidx.compose.material3.ElevatedCard(
modifier = modifier
- .testTag("HomeRecentFileCard_${item.bookId}")
.graphicsLayer { alpha = if (item.isAvailable) 1.0f else 0.8f }
.then(
if (isSelected) Modifier.border(2.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.large)
@@ -1514,7 +1485,7 @@ private fun AppDrawerContent(
Spacer(modifier = Modifier.weight(1f))
// legal links
- if (uiState.currentUser != null || (isOss && !BuildConfig.IS_OFFLINE)) {
+ if (uiState.currentUser != null && !isOss) {
val uriHandler = LocalUriHandler.current
val baseStyle = MaterialTheme.typography.labelMedium
var scaledTextStyle by remember { mutableStateOf(baseStyle) }
@@ -1812,14 +1783,9 @@ fun ExternalFileBehaviorDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.options_external_file_behavior)) },
text = {
- Column(modifier = Modifier.verticalScroll(androidx.compose.foundation.rememberScrollState())) {
- val options = listOf(
- Triple("ASK", R.string.external_file_behavior_ask, R.string.external_file_behavior_ask_desc),
- Triple("KEEP", R.string.external_file_behavior_keep, R.string.external_file_behavior_keep_desc),
- Triple("DELETE", R.string.external_file_behavior_delete, R.string.external_file_behavior_delete_desc),
- Triple("TEMPORARY", R.string.external_file_behavior_temporary, R.string.external_file_behavior_temporary_desc)
- )
- options.forEach { (value, labelRes, descriptionRes) ->
+ Column {
+ val options = listOf("ASK" to R.string.external_file_behavior_ask, "KEEP" to R.string.external_file_behavior_keep, "DELETE" to R.string.external_file_behavior_delete)
+ options.forEach { (value, labelRes) ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
@@ -1829,14 +1795,7 @@ fun ExternalFileBehaviorDialog(
) {
RadioButton(selected = currentBehavior == value, onClick = null)
Spacer(modifier = Modifier.width(16.dp))
- Column(modifier = Modifier.weight(1f)) {
- Text(stringResource(labelRes))
- Text(
- text = stringResource(descriptionRes),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
+ Text(stringResource(labelRes))
}
}
}
diff --git a/app/src/main/java/com/aryan/reader/LibraryModels.kt b/app/src/main/java/com/aryan/reader/LibraryModels.kt
new file mode 100644
index 0000000..6e5c3f1
--- /dev/null
+++ b/app/src/main/java/com/aryan/reader/LibraryModels.kt
@@ -0,0 +1,41 @@
+package com.aryan.reader
+
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.shared.ReaderFeatureSurface
+import com.aryan.reader.shared.ReaderPlatform
+import com.aryan.reader.shared.SharedFileCapabilities
+
+typealias AddBooksSource = com.aryan.reader.shared.AddBooksSource
+typealias FileType = com.aryan.reader.shared.FileType
+typealias RenderMode = com.aryan.reader.shared.RenderMode
+typealias SortOrder = com.aryan.reader.shared.SortOrder
+typealias ReadStatusFilter = com.aryan.reader.shared.ReadStatusFilter
+typealias LibraryFilters = com.aryan.reader.shared.LibraryFilters
+typealias SyncedFolder = com.aryan.reader.shared.SyncedFolder
+typealias ShelfType = com.aryan.reader.shared.ShelfType
+
+internal val ANDROID_READABLE_FILE_TYPES = SharedFileCapabilities.readableTypesFor(ReaderPlatform.ANDROID)
+internal val ANDROID_SYNCABLE_FILE_TYPES = SharedFileCapabilities.syncableTypesFor(ReaderPlatform.ANDROID)
+internal val PDF_VIEWER_FILE_TYPES = com.aryan.reader.shared.PDF_VIEWER_FILE_TYPES
+internal val EPUB_READER_FILE_TYPES = com.aryan.reader.shared.EPUB_READER_FILE_TYPES
+
+internal fun FileType.readerSurfaceOnAndroid(): ReaderFeatureSurface? {
+ return SharedFileCapabilities.surfaceFor(this, ReaderPlatform.ANDROID)
+}
+
+data class Shelf(
+ val id: String,
+ val name: String,
+ val type: ShelfType,
+ val books: List,
+ val directBooks: List = books,
+ val parentShelfId: String? = null,
+ val childShelfIds: List = emptyList(),
+ val depth: Int = 0,
+ val sortKey: String = name.lowercase()
+) {
+ val bookCount: Int get() = books.size
+ val topBook: RecentFileItem? by lazy(LazyThreadSafetyMode.NONE) { books.maxByOrNull { it.timestamp } }
+ val directBookCount: Int get() = directBooks.size
+ val childShelfCount: Int get() = childShelfIds.size
+}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/LibraryScreen.kt b/app/src/main/java/com/aryan/reader/LibraryScreen.kt
similarity index 92%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/LibraryScreen.kt
rename to app/src/main/java/com/aryan/reader/LibraryScreen.kt
index fa79d21..90370a0 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/LibraryScreen.kt
+++ b/app/src/main/java/com/aryan/reader/LibraryScreen.kt
@@ -20,7 +20,7 @@
// LibraryScreen.kt
@file:Suppress("KotlinConstantConditions")
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.annotation.SuppressLint
import android.net.Uri
@@ -105,7 +105,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -121,7 +120,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
@@ -133,24 +131,19 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.media3.common.util.UnstableApi
import androidx.navigation.NavHostController
-import coil.ImageLoader
import coil.compose.AsyncImage
-import coil.decode.SvgDecoder
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.data.TagEntity
-import org.dueattendant149.bookreader.opds.OpdsAcquisition
-import org.dueattendant149.bookreader.opds.OpdsCatalog
-import org.dueattendant149.bookreader.opds.OpdsDownloadState
-import org.dueattendant149.bookreader.opds.OpdsEntry
-import org.dueattendant149.bookreader.opds.OpdsRepository
-import org.dueattendant149.bookreader.opds.OpdsViewModel
-import org.dueattendant149.bookreader.shared.LOCAL_FOLDER_SYNC_DATA_DIR
-import org.dueattendant149.bookreader.shared.opds.SharedOpdsLocalBookMatcher
+import coil.request.ImageRequest
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.data.TagEntity
+import com.aryan.reader.opds.OpdsAcquisition
+import com.aryan.reader.opds.OpdsCatalog
+import com.aryan.reader.opds.OpdsDownloadState
+import com.aryan.reader.opds.OpdsEntry
+import com.aryan.reader.opds.OpdsViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -272,36 +265,6 @@ fun LibraryScreen(
var showDeleteShelvesDialog by remember { mutableStateOf(false) }
var showInfoDialog by remember { mutableStateOf(false) }
var itemForInfoDialog by remember { mutableStateOf(null) }
- var pendingSaveOriginalItem by remember { mutableStateOf(null) }
-
- val saveOriginalLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.CreateDocument("application/octet-stream")
- ) { uri ->
- val item = pendingSaveOriginalItem
- pendingSaveOriginalItem = null
- if (uri != null && item?.uriString != null) {
- viewModel.saveOriginalFile(item.uriString.toUri(), uri)
- }
- }
-
- fun saveOriginalItem(item: RecentFileItem) {
- if (!item.canExportOriginalFile()) return
- pendingSaveOriginalItem = item
- saveOriginalLauncher.launch(item.suggestedOriginalFileName())
- }
-
- fun shareOriginalItem(item: RecentFileItem) {
- val uriString = item.uriString ?: return
- if (!item.canExportOriginalFile()) return
- scope.launch {
- viewModel.shareOriginalFile(
- activityContext = context,
- sourceUri = uriString.toUri(),
- fileType = item.type,
- filename = item.suggestedOriginalFileName()
- )
- }
- }
BackHandler(enabled = isContextualModeActive) {
viewModel.clearContextualAction()
@@ -348,12 +311,6 @@ fun LibraryScreen(
showInfoDialog = true
}
},
- onSaveClick = selectedItems.singleOrNull()
- ?.takeIf { it.canExportOriginalFile() }
- ?.let { item -> { saveOriginalItem(item) } },
- onShareClick = selectedItems.singleOrNull()
- ?.takeIf { it.canExportOriginalFile() }
- ?.let { item -> { shareOriginalItem(item) } },
onDeleteClick = { showDeleteConfirmDialog = true },
onSelectAllClick = { viewModel.selectAllLibraryFiles() },
onShelfClick = viewModel::onShelfClick,
@@ -368,7 +325,6 @@ fun LibraryScreen(
onEditFolderFiltersClick = { folder, filters -> viewModel.updateFolderFilters(folder, filters) },
syncedFolders = uiState.syncedFolders,
onRemoveFolderClick = { folder -> viewModel.removeSyncedFolder(folder) },
- onFolderLocalSyncChange = viewModel::setFolderLocalSyncEnabled,
onDisconnectSyncFolderClick = viewModel::disconnectAllSyncedFolders,
downloadingBookIds = uiState.downloadingBookIds,
lastFolderScanTime = uiState.lastFolderScanTime,
@@ -434,17 +390,28 @@ fun LibraryScreen(
)
}
- HydratedFileInfoDialog(
- item = itemForInfoDialog,
- isVisible = showInfoDialog,
- uiState = uiState,
- viewModel = viewModel,
- onDismiss = {
- showInfoDialog = false
- itemForInfoDialog = null
- },
- onOpenTags = { bookId -> viewModel.openTagSelection(setOf(bookId)) }
- )
+ itemForInfoDialog?.let { item ->
+ if (showInfoDialog) {
+ FileInfoDialog(
+ item = item,
+ usePdfFileNameAsDisplayName = uiState.usePdfFileNameAsDisplayName,
+ onDismiss = {
+ showInfoDialog = false
+ itemForInfoDialog = null
+ },
+ onSaveMetadata = { metadata ->
+ viewModel.updateBookMetadata(item.bookId, metadata)
+ },
+ onSaveDisplayName = { name ->
+ viewModel.updateCustomName(item.bookId, name)
+ },
+ onRestoreMetadata = {
+ viewModel.restoreOriginalBookMetadata(item.bookId)
+ },
+ onOpenTags = { viewModel.openTagSelection(setOf(item.bookId)) }
+ )
+ }
+ }
CustomTopBanner(bannerMessage = uiState.bannerMessage)
}
}
@@ -462,42 +429,10 @@ fun ShelfScreen(
val sortOrder = uiState.sortOrder
val showRenameDialogFor = uiState.showRenameShelfDialogFor
val showDeleteDialogFor = uiState.showDeleteShelfDialogFor
- val context = LocalContext.current
- val scope = rememberCoroutineScope()
var showRemoveFromShelfDialog by remember { mutableStateOf(false) }
var showInfoDialog by remember { mutableStateOf(false) }
var itemForInfoDialog by remember { mutableStateOf(null) }
- var pendingSaveOriginalItem by remember { mutableStateOf(null) }
-
- val saveOriginalLauncher = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.CreateDocument("application/octet-stream")
- ) { uri ->
- val item = pendingSaveOriginalItem
- pendingSaveOriginalItem = null
- if (uri != null && item?.uriString != null) {
- viewModel.saveOriginalFile(item.uriString.toUri(), uri)
- }
- }
-
- fun saveOriginalItem(item: RecentFileItem) {
- if (!item.canExportOriginalFile()) return
- pendingSaveOriginalItem = item
- saveOriginalLauncher.launch(item.suggestedOriginalFileName())
- }
-
- fun shareOriginalItem(item: RecentFileItem) {
- val uriString = item.uriString ?: return
- if (!item.canExportOriginalFile()) return
- scope.launch {
- viewModel.shareOriginalFile(
- activityContext = context,
- sourceUri = uriString.toUri(),
- fileType = item.type,
- filename = item.suggestedOriginalFileName()
- )
- }
- }
BackHandler(enabled = true) {
when {
@@ -549,12 +484,6 @@ fun ShelfScreen(
showInfoDialog = true
}
},
- onSaveClick = selectedItems.singleOrNull()
- ?.takeIf { it.canExportOriginalFile() }
- ?.let { item -> { saveOriginalItem(item) } },
- onShareClick = selectedItems.singleOrNull()
- ?.takeIf { it.canExportOriginalFile() }
- ?.let { item -> { shareOriginalItem(item) } },
onDeleteClick = { showRemoveFromShelfDialog = true },
onRenameShelf = { viewModel.showRenameShelfDialog(currentShelf.id) },
onDeleteShelf = { viewModel.showDeleteShelfDialog(currentShelf.id) },
@@ -595,14 +524,19 @@ fun ShelfScreen(
)
}
- HydratedFileInfoDialog(
- item = itemForInfoDialog,
- isVisible = showInfoDialog,
- uiState = uiState,
- viewModel = viewModel,
- onDismiss = { showInfoDialog = false; itemForInfoDialog = null },
- onOpenTags = { bookId -> viewModel.openTagSelection(setOf(bookId)) }
- )
+ itemForInfoDialog?.let { item ->
+ if (showInfoDialog) {
+ FileInfoDialog(
+ item = item,
+ usePdfFileNameAsDisplayName = uiState.usePdfFileNameAsDisplayName,
+ onDismiss = { showInfoDialog = false; itemForInfoDialog = null },
+ onSaveMetadata = { metadata -> viewModel.updateBookMetadata(item.bookId, metadata) },
+ onSaveDisplayName = { name -> viewModel.updateCustomName(item.bookId, name) },
+ onRestoreMetadata = { viewModel.restoreOriginalBookMetadata(item.bookId) },
+ onOpenTags = { viewModel.openTagSelection(setOf(item.bookId)) }
+ )
+ }
+ }
CustomTopBanner(bannerMessage = uiState.bannerMessage)
}
}
@@ -637,8 +571,6 @@ fun LibraryScreenContent(
onItemClick: (RecentFileItem) -> Unit,
onItemLongClick: (RecentFileItem) -> Unit,
onInfoClick: () -> Unit,
- onSaveClick: (() -> Unit)?,
- onShareClick: (() -> Unit)?,
onDeleteClick: () -> Unit,
onSelectAllClick: () -> Unit,
onShelfClick: (Shelf) -> Unit,
@@ -658,7 +590,6 @@ fun LibraryScreenContent(
isRefreshing: Boolean,
syncedFolders: List,
onRemoveFolderClick: (SyncedFolder) -> Unit,
- onFolderLocalSyncChange: (SyncedFolder, Boolean, Boolean) -> Unit,
onOpdsBookDownloaded: (Uri, String) -> Unit,
onStreamOpdsBook: (OpdsEntry, OpdsCatalog?) -> Unit,
onDeleteCatalogStreams: (String) -> Unit,
@@ -701,8 +632,6 @@ fun LibraryScreenContent(
onTagClick = onTagClick,
onPinClick = onPinClick,
onInfoClick = onInfoClick,
- onSaveClick = onSaveClick,
- onShareClick = onShareClick,
onDeleteClick = onDeleteClick,
onSelectAllClick = onSelectAllClick
)
@@ -737,8 +666,7 @@ fun LibraryScreenContent(
modifier = Modifier
.weight(1f)
.padding(vertical = 4.dp)
- .focusRequester(searchFocusRequester)
- .testTag("LibrarySearchTextField"),
+ .focusRequester(searchFocusRequester),
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
@@ -765,10 +693,7 @@ fun LibraryScreenContent(
Icon(Icons.Default.FilterList, contentDescription = stringResource(R.string.content_desc_filter))
}
Box {
- TextButton(
- onClick = { showSortMenu = true },
- modifier = Modifier.testTag("LibrarySortButton")
- ) {
+ TextButton(onClick = { showSortMenu = true }) {
Icon(
painter = painterResource(id = R.drawable.sort),
contentDescription = stringResource(R.string.content_desc_sort),
@@ -897,9 +822,7 @@ fun LibraryScreenContent(
text = { Text(stringResource(R.string.fab_new_shelf)) },
icon = { Icon(Icons.Default.Add, contentDescription = stringResource(R.string.fab_new_shelf)) },
onClick = onNewShelfClick,
- modifier = Modifier
- .padding(16.dp)
- .testTag("LibraryNewShelfFab")
+ modifier = Modifier.padding(16.dp)
)
}
}
@@ -965,7 +888,6 @@ fun LibraryScreenContent(
allRecentFiles = rawLibraryFiles,
onAddFolderClick = onSelectSyncFolderClick,
onRemoveFolderClick = onRemoveFolderClick,
- onFolderLocalSyncChange = onFolderLocalSyncChange,
onEditFolderFiltersClick = onEditFolderFiltersClick,
onScanNowClick = onScanNowClick,
onSyncMetadataClick = onSyncMetadataClick,
@@ -1109,8 +1031,6 @@ private fun ShelfDetailScreen(
onClearSelection: () -> Unit,
onTagClick: () -> Unit,
onInfoClick: () -> Unit,
- onSaveClick: (() -> Unit)?,
- onShareClick: (() -> Unit)?,
onDeleteClick: () -> Unit,
onRenameShelf: () -> Unit,
onDeleteShelf: () -> Unit,
@@ -1193,8 +1113,6 @@ private fun ShelfDetailScreen(
onNavIconClick = onClearSelection,
onTagClick = onTagClick,
onInfoClick = onInfoClick,
- onSaveClick = onSaveClick,
- onShareClick = onShareClick,
onDeleteClick = onDeleteClick
)
} else if (isSearchActive) {
@@ -1222,8 +1140,7 @@ private fun ShelfDetailScreen(
modifier = Modifier
.weight(1f)
.padding(vertical = 4.dp)
- .focusRequester(searchFocusRequester)
- .testTag("ShelfSearchTextField"),
+ .focusRequester(searchFocusRequester),
singleLine = true,
colors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
@@ -1276,10 +1193,7 @@ private fun ShelfDetailScreen(
},
actions = {
Box {
- TextButton(
- onClick = { showSortMenu = true },
- modifier = Modifier.testTag("ShelfSortButton")
- ) {
+ TextButton(onClick = { showSortMenu = true }) {
Icon(
painter = painterResource(id = R.drawable.sort),
contentDescription = stringResource(R.string.content_desc_sort),
@@ -1461,10 +1375,7 @@ private fun AddBooksModeScreen(
},
actions = {
Box {
- TextButton(
- onClick = { showSortMenu = true },
- modifier = Modifier.testTag("AddBooksSortButton")
- ) {
+ TextButton(onClick = { showSortMenu = true }) {
Icon(
painter = painterResource(id = R.drawable.sort),
contentDescription = stringResource(R.string.content_desc_sort),
@@ -1670,7 +1581,6 @@ private fun ShelfListItem(
),
modifier = Modifier
.fillMaxWidth()
- .testTag("ShelfItem_${shelf.id}")
.then(
if (isSelected) Modifier.border(2.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.large)
else Modifier
@@ -1749,7 +1659,6 @@ private fun LibraryListItem(
),
modifier = Modifier
.fillMaxWidth()
- .testTag("LibraryBookItem_${item.bookId}")
.graphicsLayer { alpha = if (item.isAvailable) 1.0f else 0.8f }
.then(
if (isSelected) Modifier.border(2.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.large)
@@ -2020,15 +1929,12 @@ private fun FolderSyncScreen(
allRecentFiles: List,
onAddFolderClick: () -> Unit,
onRemoveFolderClick: (SyncedFolder) -> Unit,
- onFolderLocalSyncChange: (SyncedFolder, Boolean, Boolean) -> Unit,
onEditFolderFiltersClick: (SyncedFolder, Set) -> Unit,
onScanNowClick: () -> Unit,
onSyncMetadataClick: () -> Unit,
isLoading: Boolean
) {
var editingFolder by remember { mutableStateOf(null) }
- var disablingFolder by remember { mutableStateOf(null) }
- val hasEnabledSyncFolders = syncedFolders.any { it.localSyncEnabled }
val folderStatsByUri = remember(allRecentFiles) {
allRecentFiles
.asSequence()
@@ -2067,7 +1973,7 @@ private fun FolderSyncScreen(
) {
FilledTonalButton(
onClick = onScanNowClick,
- enabled = !isLoading && hasEnabledSyncFolders,
+ enabled = !isLoading,
modifier = Modifier.weight(1f),
shape = MaterialTheme.shapes.small
) {
@@ -2082,7 +1988,7 @@ private fun FolderSyncScreen(
androidx.compose.material3.OutlinedButton(
onClick = onSyncMetadataClick,
- enabled = !isLoading && hasEnabledSyncFolders,
+ enabled = !isLoading,
modifier = Modifier.weight(1f),
shape = MaterialTheme.shapes.small
) {
@@ -2110,13 +2016,6 @@ private fun FolderSyncScreen(
folder = folder,
stats = folderStatsByUri[folder.uriString] ?: FolderFileStats.Empty,
onRemoveClick = onRemoveFolderClick,
- onLocalSyncToggleClick = { selectedFolder ->
- if (selectedFolder.localSyncEnabled) {
- disablingFolder = selectedFolder
- } else {
- onFolderLocalSyncChange(selectedFolder, true, false)
- }
- },
onEditFiltersClick = { editingFolder = folder }
)
}
@@ -2134,46 +2033,6 @@ private fun FolderSyncScreen(
onDismiss = { editingFolder = null }
)
}
-
- disablingFolder?.let { folder ->
- AlertDialog(
- onDismissRequest = { disablingFolder = null },
- title = { Text(stringResource(R.string.dialog_disable_folder_local_sync_title)) },
- text = {
- Text(
- stringResource(
- R.string.dialog_disable_folder_local_sync_desc,
- LOCAL_FOLDER_SYNC_DATA_DIR
- )
- )
- },
- confirmButton = {
- TextButton(
- onClick = {
- onFolderLocalSyncChange(folder, false, true)
- disablingFolder = null
- }
- ) {
- Text(stringResource(R.string.action_disable_remove_sync_data))
- }
- },
- dismissButton = {
- Row {
- TextButton(onClick = { disablingFolder = null }) {
- Text(stringResource(R.string.action_cancel))
- }
- TextButton(
- onClick = {
- onFolderLocalSyncChange(folder, false, false)
- disablingFolder = null
- }
- ) {
- Text(stringResource(R.string.action_disable_keep_sync_data))
- }
- }
- }
- )
- }
}
private data class FolderFileStats(
@@ -2191,7 +2050,6 @@ private fun FolderCard(
folder: SyncedFolder,
stats: FolderFileStats,
onRemoveClick: (SyncedFolder) -> Unit,
- onLocalSyncToggleClick: (SyncedFolder) -> Unit,
onEditFiltersClick: (SyncedFolder) -> Unit
) {
var showMenu by remember { mutableStateOf(false) }
@@ -2217,22 +2075,13 @@ private fun FolderCard(
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(12.dp))
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = folder.name,
- style = MaterialTheme.typography.titleMedium,
- fontWeight = FontWeight.Bold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- if (!folder.localSyncEnabled) {
- Text(
- text = stringResource(R.string.folder_local_sync_disabled),
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.error
- )
- }
- }
+ Text(
+ text = folder.name,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
}
Box {
@@ -2247,21 +2096,6 @@ private fun FolderCard(
onEditFiltersClick(folder)
}
)
- DropdownMenuItem(
- text = {
- Text(
- if (folder.localSyncEnabled) {
- stringResource(R.string.menu_disable_folder_local_sync)
- } else {
- stringResource(R.string.menu_enable_folder_local_sync)
- }
- )
- },
- onClick = {
- showMenu = false
- onLocalSyncToggleClick(folder)
- }
- )
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_remove_folder)) },
onClick = {
@@ -2543,7 +2377,6 @@ fun OpdsTab(
val uiState by opdsViewModel.uiState.collectAsStateWithLifecycle()
val downloadingState = uiState.downloadingState
val context = LocalContext.current
- val coverImageLoader = rememberOpdsCoverImageLoader(uiState.currentCatalog)
var selectedEntry by remember { mutableStateOf(null) }
var showCatalogDialog by remember { mutableStateOf(false) }
var editingCatalog by remember { mutableStateOf(null) }
@@ -2768,7 +2601,6 @@ fun OpdsTab(
entry = entry,
localLibraryFiles = localLibraryFiles,
downloadState = downloadingState[entry.id],
- coverImageLoader = coverImageLoader,
onDownloadClick = { acquisition ->
opdsViewModel.downloadBook(
entry, acquisition, context
@@ -2817,7 +2649,6 @@ fun OpdsTab(
entry = selectedEntry!!,
localLibraryFiles = localLibraryFiles,
downloadState = downloadingState[selectedEntry!!.id],
- coverImageLoader = coverImageLoader,
onDownloadFormat = { acquisition ->
opdsViewModel.downloadBook(selectedEntry!!, acquisition, context) { downloadedUri ->
onBookDownloaded(downloadedUri, selectedEntry!!.title)
@@ -2945,29 +2776,6 @@ fun OpdsTab(
}
}
-@Composable
-private fun rememberOpdsCoverImageLoader(catalog: OpdsCatalog?): ImageLoader {
- val context = LocalContext.current.applicationContext
- val username = catalog?.username
- val password = catalog?.password
- val imageLoader = remember(context, username, password) {
- ImageLoader.Builder(context)
- .okHttpClient {
- OpdsRepository.sharedHttpClient.newBuilder()
- .authenticator(OpdsRepository.OpdsAuthenticator(username, password))
- .build()
- }
- .components {
- add(SvgDecoder.Factory())
- }
- .build()
- }
- DisposableEffect(imageLoader) {
- onDispose { imageLoader.shutdown() }
- }
- return imageLoader
-}
-
@Composable
fun OpdsCatalogCard(catalog: OpdsCatalog, onClick: () -> Unit, onEdit: (() -> Unit)?, onDelete: (() -> Unit)?) {
Surface(
@@ -3045,20 +2853,13 @@ fun OpdsBookCard(
entry: OpdsEntry,
localLibraryFiles: List,
downloadState: OpdsDownloadState?,
- coverImageLoader: ImageLoader,
onDownloadClick: (OpdsAcquisition) -> Unit,
onReadClick: (RecentFileItem) -> Unit,
onStreamClick: () -> Unit,
onClick: () -> Unit
) {
val libraryItem = remember(entry, localLibraryFiles) {
- SharedOpdsLocalBookMatcher.find(
- entry = entry,
- books = localLibraryFiles,
- title = { it.title },
- displayName = { it.displayName },
- path = { it.uriString }
- )
+ localLibraryFiles.find { it.title.equals(entry.title, ignoreCase = true) || it.displayName.equals(entry.title, ignoreCase = true) }
}
val isDownloading = downloadState?.isDownloading == true
val progress = downloadState?.progress
@@ -3077,7 +2878,6 @@ fun OpdsBookCard(
AsyncImage(
model = entry.coverUrl,
contentDescription = null,
- imageLoader = coverImageLoader,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(width = 70.dp, height = 100.dp)
@@ -3184,7 +2984,6 @@ fun OpdsBookDetailsSheet(
entry: OpdsEntry,
localLibraryFiles: List,
downloadState: OpdsDownloadState?,
- coverImageLoader: ImageLoader,
onDownloadFormat: (OpdsAcquisition) -> Unit,
onReadClick: (RecentFileItem) -> Unit,
onStreamClick: () -> Unit,
@@ -3193,13 +2992,7 @@ fun OpdsBookDetailsSheet(
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val libraryItem = remember(entry, localLibraryFiles) {
- SharedOpdsLocalBookMatcher.find(
- entry = entry,
- books = localLibraryFiles,
- title = { it.title },
- displayName = { it.displayName },
- path = { it.uriString }
- )
+ localLibraryFiles.find { it.title.equals(entry.title, ignoreCase = true) || it.displayName.equals(entry.title, ignoreCase = true) }
}
val isDownloading = downloadState?.isDownloading == true
val progress = downloadState?.progress
@@ -3219,7 +3012,6 @@ fun OpdsBookDetailsSheet(
AsyncImage(
model = entry.coverUrl,
contentDescription = null,
- imageLoader = coverImageLoader,
contentScale = ContentScale.Crop,
modifier = Modifier
.size(width = 110.dp, height = 160.dp)
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/LibraryStateProjector.kt b/app/src/main/java/com/aryan/reader/LibraryStateProjector.kt
similarity index 89%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/LibraryStateProjector.kt
rename to app/src/main/java/com/aryan/reader/LibraryStateProjector.kt
index 61f1df1..586f036 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/LibraryStateProjector.kt
+++ b/app/src/main/java/com/aryan/reader/LibraryStateProjector.kt
@@ -1,14 +1,14 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
-import org.dueattendant149.bookreader.data.BookShelfCrossRef
-import org.dueattendant149.bookreader.data.BookTagCrossRef
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.data.ShelfEntity
-import org.dueattendant149.bookreader.data.TagEntity
-import org.dueattendant149.bookreader.shared.SharedReaderScreenState
-import org.dueattendant149.bookreader.shared.applyLibraryFilters as sharedApplyLibraryFilters
-import org.dueattendant149.bookreader.shared.filterBySearch as sharedFilterBySearch
-import org.dueattendant149.bookreader.shared.sortBooks as sharedSortBooks
+import com.aryan.reader.data.BookShelfCrossRef
+import com.aryan.reader.data.BookTagCrossRef
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.data.ShelfEntity
+import com.aryan.reader.data.TagEntity
+import com.aryan.reader.shared.SharedReaderScreenState
+import com.aryan.reader.shared.applyLibraryFilters as sharedApplyLibraryFilters
+import com.aryan.reader.shared.filterBySearch as sharedFilterBySearch
+import com.aryan.reader.shared.sortBooks as sharedSortBooks
fun interface FolderPathResolver {
fun relativeFolderSegments(item: RecentFileItem): List
@@ -151,7 +151,7 @@ fun sortFiles(files: List, sortOrder: SortOrder): List.mapSharedResults(sharedBooks: List): List {
+private fun List.mapSharedResults(sharedBooks: List): List {
val byId = associateBy { it.bookId }
return sharedBooks.mapNotNull { byId[it.id] }
}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/MainActivity.kt b/app/src/main/java/com/aryan/reader/MainActivity.kt
similarity index 84%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/MainActivity.kt
rename to app/src/main/java/com/aryan/reader/MainActivity.kt
index fd5823e..e5835ed 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/MainActivity.kt
+++ b/app/src/main/java/com/aryan/reader/MainActivity.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Intent
import android.os.Build
@@ -41,8 +41,8 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.rememberNavController
-import org.dueattendant149.bookreader.data.PlatformFeaturesRepository
-import org.dueattendant149.bookreader.ui.theme.AppTheme
+import com.aryan.reader.data.PlatformFeaturesRepository
+import com.aryan.reader.ui.theme.AppTheme
import kotlinx.coroutines.launch
import timber.log.Timber
import androidx.compose.foundation.isSystemInDarkTheme
@@ -50,20 +50,18 @@ import androidx.compose.runtime.getValue
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.media3.common.util.UnstableApi
-import org.dueattendant149.bookreader.tts.ACTION_OPEN_TTS_SESSION
-import org.dueattendant149.bookreader.tts.EXTRA_TTS_BOOK_ID
-import org.dueattendant149.bookreader.tts.EXTRA_TTS_CHAPTER_INDEX
-import org.dueattendant149.bookreader.tts.EXTRA_TTS_PAGE_INDEX
-import org.dueattendant149.bookreader.tts.EXTRA_TTS_SOURCE_CFI
-import org.dueattendant149.bookreader.tts.EXTRA_TTS_START_OFFSET
+import com.aryan.reader.tts.ACTION_OPEN_TTS_SESSION
+import com.aryan.reader.tts.EXTRA_TTS_BOOK_ID
+import com.aryan.reader.tts.EXTRA_TTS_CHAPTER_INDEX
+import com.aryan.reader.tts.EXTRA_TTS_PAGE_INDEX
+import com.aryan.reader.tts.EXTRA_TTS_SOURCE_CFI
+import com.aryan.reader.tts.EXTRA_TTS_START_OFFSET
@UnstableApi
-open class MainActivity : AppCompatActivity() {
+class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()
private lateinit var platformFeaturesRepository: PlatformFeaturesRepository
- private val isTemporaryExternalOpen: Boolean
- get() = intent?.getBooleanExtra(EXTRA_TEMPORARY_EXTERNAL_OPEN, false) == true
private val updateLauncher = registerForActivityResult(
ActivityResultContracts.StartIntentSenderForResult()
@@ -88,14 +86,6 @@ open class MainActivity : AppCompatActivity() {
}
}
- lifecycleScope.launch {
- viewModel.temporaryExternalOpenFinished.collect {
- if (isTemporaryExternalOpen) {
- finishAndRemoveTask()
- }
- }
- }
-
if (savedInstanceState == null) {
handleIntent(intent)
}
@@ -170,12 +160,7 @@ open class MainActivity : AppCompatActivity() {
if (intent?.action == Intent.ACTION_VIEW && intent.data != null) {
Timber.d("Received VIEW intent with URI: ${intent.data}")
val uri = intent.data!!
- viewModel.onFileSelected(
- uri,
- isFromRecent = false,
- isExternalIntent = true,
- isTemporaryExternalIntent = isTemporaryExternalOpen
- )
+ viewModel.onFileSelected(uri, isFromRecent = false, isExternalIntent = true)
}
}
}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/MainPreferenceKeys.kt b/app/src/main/java/com/aryan/reader/MainPreferenceKeys.kt
similarity index 94%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/MainPreferenceKeys.kt
rename to app/src/main/java/com/aryan/reader/MainPreferenceKeys.kt
index 881ba33..e12fd41 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/MainPreferenceKeys.kt
+++ b/app/src/main/java/com/aryan/reader/MainPreferenceKeys.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
internal const val KEY_RENDER_MODE = "render_mode"
internal const val KEY_FOLDER_SYNC_ENABLED = "folder_sync_enabled"
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/MainScreen.kt b/app/src/main/java/com/aryan/reader/MainScreen.kt
similarity index 99%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/MainScreen.kt
rename to app/src/main/java/com/aryan/reader/MainScreen.kt
index fda939f..c0c933d 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/MainScreen.kt
+++ b/app/src/main/java/com/aryan/reader/MainScreen.kt
@@ -18,7 +18,7 @@
* mail: epistemereader@gmail.com
*/
// MainScreen.kt
-package org.dueattendant149.bookreader
+package com.aryan.reader
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/MainViewModel.kt b/app/src/main/java/com/aryan/reader/MainViewModel.kt
similarity index 73%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/MainViewModel.kt
rename to app/src/main/java/com/aryan/reader/MainViewModel.kt
index a1e7069..654ec84 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/MainViewModel.kt
+++ b/app/src/main/java/com/aryan/reader/MainViewModel.kt
@@ -20,7 +20,7 @@
// MainViewModel.kt
@file:Suppress("DEPRECATION", "ANNOTATION_WILL_BE_APPLIED_ALSO_TO_PROPERTY_OR_FIELD")
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.app.Application
import android.content.ClipData
@@ -31,11 +31,11 @@ import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
-import org.dueattendant149.bookreader.tts.TtsController
-import org.dueattendant149.bookreader.tts.TtsPlaybackManager
-import org.dueattendant149.bookreader.paginatedreader.LocatorConverter
+import com.aryan.reader.tts.TtsController
+import com.aryan.reader.tts.TtsPlaybackManager
+import com.aryan.reader.paginatedreader.LocatorConverter
import kotlinx.serialization.protobuf.ProtoBuf
-import org.dueattendant149.bookreader.paginatedreader.semanticBlockModule
+import com.aryan.reader.paginatedreader.semanticBlockModule
import android.provider.DocumentsContract
import android.provider.OpenableColumns
import androidx.annotation.OptIn
@@ -53,72 +53,64 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
-import org.dueattendant149.bookreader.data.BookMetadata
-import org.dueattendant149.bookreader.data.BookMetadataEdit
-import org.dueattendant149.bookreader.data.CloudflareRepository
-import org.dueattendant149.bookreader.data.CustomFontEntity
-import org.dueattendant149.bookreader.data.FeedbackRepository
-import org.dueattendant149.bookreader.data.FirestoreRepository
-import org.dueattendant149.bookreader.data.FontMetadata
-import org.dueattendant149.bookreader.data.FontsRepository
-import org.dueattendant149.bookreader.data.GoogleDriveRepository
-import org.dueattendant149.bookreader.data.LocalSyncUtils
-import org.dueattendant149.bookreader.data.PurchaseEntity
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.data.RecentFilesRepository
-import org.dueattendant149.bookreader.data.RemoteConfigRepository
-import org.dueattendant149.bookreader.data.ShelfMetadata
-import org.dueattendant149.bookreader.data.TagEntity
-import org.dueattendant149.bookreader.data.effectiveAnnotationModifiedTimestamp
-import org.dueattendant149.bookreader.data.effectiveReadingPositionModifiedTimestamp
-import org.dueattendant149.bookreader.data.getUri
-import org.dueattendant149.bookreader.data.toBookMetadata
-import org.dueattendant149.bookreader.data.toRecentFileItem
-import org.dueattendant149.bookreader.epub.CalibreBundleExtractor
-import org.dueattendant149.bookreader.epub.CalibreBundleResult
-import org.dueattendant149.bookreader.epub.EpubBook
-import org.dueattendant149.bookreader.epub.EpubParser
-import org.dueattendant149.bookreader.epub.ImportedFileCache
-import org.dueattendant149.bookreader.epub.MobiParser
-import org.dueattendant149.bookreader.epub.SingleFileImporter
-import org.dueattendant149.bookreader.epub.hasReadableExtractedContent
-import org.dueattendant149.bookreader.ml.ISpeechBubbleDetector
-import org.dueattendant149.bookreader.ml.SpeechBubble
-import org.dueattendant149.bookreader.ml.SpeechBubbleDetector
-import org.dueattendant149.bookreader.paginatedreader.Locator
-import org.dueattendant149.bookreader.paginatedreader.data.BookCacheDatabase
-import org.dueattendant149.bookreader.paginatedreader.data.BookProcessingWorker
-import org.dueattendant149.bookreader.pdf.PdfCoverGenerator
-import org.dueattendant149.bookreader.pdf.PDF_BLANK_PAGE_PERSISTENCE_TAG
-import org.dueattendant149.bookreader.pdf.PdfUserHighlight
-import org.dueattendant149.bookreader.pdf.PdfiumCoreProvider
-import org.dueattendant149.bookreader.pdf.PdfiumEngineProvider
-import org.dueattendant149.bookreader.pdf.PdfiumAnnotationExporter
-import org.dueattendant149.bookreader.pdf.ReflowWorker
-import org.dueattendant149.bookreader.pdf.pdfLayoutDebugSummary
-import org.dueattendant149.bookreader.pdf.remapPdfAnnotationsForLayoutChange
-import org.dueattendant149.bookreader.pdf.remapPdfBookmarksJsonForLayoutChange
-import org.dueattendant149.bookreader.pdf.data.PageLayoutRepository
-import org.dueattendant149.bookreader.pdf.data.PdfAnnotation
-import org.dueattendant149.bookreader.pdf.data.PdfAnnotationRepository
-import org.dueattendant149.bookreader.pdf.data.PdfHighlightRepository
-import org.dueattendant149.bookreader.pdf.data.PdfTextBox
-import org.dueattendant149.bookreader.pdf.data.PdfTextBoxRepository
-import org.dueattendant149.bookreader.pdf.data.PdfTextRepository
-import org.dueattendant149.bookreader.pdf.data.VirtualPage
-import org.dueattendant149.bookreader.pptx.PptxCoverGenerator
-import org.dueattendant149.bookreader.shared.SharedFileCapabilities
-import org.dueattendant149.bookreader.shared.SharedLibraryEditor
-import org.dueattendant149.bookreader.shared.SharedImportOutcomeCounts
-import org.dueattendant149.bookreader.shared.SharedImportPlanner
-import org.dueattendant149.bookreader.shared.pdf.SharedPdfAnnotationSidecarCodec
-import org.dueattendant149.bookreader.shared.shouldApplyRemoteCloudBookMetadataUpdate
-import org.dueattendant149.bookreader.shared.shouldDownloadRemoteCloudBookContent
-import org.dueattendant149.bookreader.shared.shouldUploadLocalCloudBookContent
-import org.dueattendant149.bookreader.shared.shouldUploadLocalCloudBookMetadataUpdate
-import org.dueattendant149.bookreader.shared.sharedCloudBookContentFileName
-import org.dueattendant149.bookreader.shared.AppAction as SharedAppAction
-import org.dueattendant149.bookreader.shared.LibraryAction as SharedLibraryAction
+import com.aryan.reader.data.BookMetadata
+import com.aryan.reader.data.BookMetadataEdit
+import com.aryan.reader.data.CloudflareRepository
+import com.aryan.reader.data.CustomFontEntity
+import com.aryan.reader.data.FeedbackRepository
+import com.aryan.reader.data.FirestoreRepository
+import com.aryan.reader.data.FontMetadata
+import com.aryan.reader.data.FontsRepository
+import com.aryan.reader.data.GoogleDriveRepository
+import com.aryan.reader.data.PurchaseEntity
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.data.RecentFilesRepository
+import com.aryan.reader.data.RemoteConfigRepository
+import com.aryan.reader.data.ShelfMetadata
+import com.aryan.reader.data.TagEntity
+import com.aryan.reader.data.getUri
+import com.aryan.reader.data.toBookMetadata
+import com.aryan.reader.data.toRecentFileItem
+import com.aryan.reader.epub.CalibreBundleExtractor
+import com.aryan.reader.epub.CalibreBundleResult
+import com.aryan.reader.epub.EpubBook
+import com.aryan.reader.epub.EpubParser
+import com.aryan.reader.epub.ImportedFileCache
+import com.aryan.reader.epub.MobiParser
+import com.aryan.reader.epub.SingleFileImporter
+import com.aryan.reader.epub.hasReadableExtractedContent
+import com.aryan.reader.ml.ISpeechBubbleDetector
+import com.aryan.reader.ml.SpeechBubble
+import com.aryan.reader.ml.SpeechBubbleDetector
+import com.aryan.reader.paginatedreader.Locator
+import com.aryan.reader.paginatedreader.data.BookCacheDatabase
+import com.aryan.reader.paginatedreader.data.BookProcessingWorker
+import com.aryan.reader.pdf.PdfCoverGenerator
+import com.aryan.reader.pdf.PDF_BLANK_PAGE_PERSISTENCE_TAG
+import com.aryan.reader.pdf.PdfUserHighlight
+import com.aryan.reader.pdf.PdfiumCoreProvider
+import com.aryan.reader.pdf.PdfiumEngineProvider
+import com.aryan.reader.pdf.PdfiumAnnotationExporter
+import com.aryan.reader.pdf.ReflowWorker
+import com.aryan.reader.pdf.pdfLayoutDebugSummary
+import com.aryan.reader.pdf.remapPdfAnnotationsForLayoutChange
+import com.aryan.reader.pdf.remapPdfBookmarksJsonForLayoutChange
+import com.aryan.reader.pdf.data.PageLayoutRepository
+import com.aryan.reader.pdf.data.PdfAnnotation
+import com.aryan.reader.pdf.data.PdfAnnotationRepository
+import com.aryan.reader.pdf.data.PdfHighlightRepository
+import com.aryan.reader.pdf.data.PdfTextBox
+import com.aryan.reader.pdf.data.PdfTextBoxRepository
+import com.aryan.reader.pdf.data.PdfTextRepository
+import com.aryan.reader.pdf.data.VirtualPage
+import com.aryan.reader.pptx.PptxCoverGenerator
+import com.aryan.reader.shared.SharedFileCapabilities
+import com.aryan.reader.shared.SharedLibraryEditor
+import com.aryan.reader.shared.SharedImportOutcomeCounts
+import com.aryan.reader.shared.SharedImportPlanner
+import com.aryan.reader.shared.pdf.SharedPdfAnnotationSidecarCodec
+import com.aryan.reader.shared.AppAction as SharedAppAction
+import com.aryan.reader.shared.LibraryAction as SharedLibraryAction
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Deferred
@@ -140,6 +132,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -170,14 +163,7 @@ private data class CachedSpeechBubble(
val maskBitmap: Bitmap?
)
-private data class PendingExternalFileRemoval(
- val bookId: String,
- val uriString: String?
-)
-
private const val BANNER_AUTO_DISMISS_MILLIS = 3_000L
-private const val CLOUD_CONTENT_RETRY_DELAY_MILLIS = 10_000L
-private const val CLOUD_METADATA_UPLOAD_DEBOUNCE_MILLIS = 1_500L
@kotlin.OptIn(ExperimentalSerializationApi::class)
@UnstableApi
@@ -190,13 +176,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private val bookCacheDao by lazy { BookCacheDatabase.getDatabase(application).bookCacheDao() }
private val epubParser by lazy { EpubParser(appContext) }
private val mobiParser by lazy { MobiParser(appContext) }
- private val fb2Parser by lazy { org.dueattendant149.bookreader.epub.Fb2Parser(appContext) }
- private val odtParser by lazy { org.dueattendant149.bookreader.epub.OdtParser(appContext) }
+ private val fb2Parser by lazy { com.aryan.reader.epub.Fb2Parser(appContext) }
+ private val odtParser by lazy { com.aryan.reader.epub.OdtParser(appContext) }
private val singleFileImporter by lazy { SingleFileImporter(appContext) }
private val bookImporter by lazy { BookImporter(appContext) }
private val epubMetadataFileEditor by lazy { EpubMetadataFileEditor(appContext) }
private val pageLayoutRepository by lazy { PageLayoutRepository(appContext) }
- private val pdfRichTextRepository by lazy { org.dueattendant149.bookreader.pdf.PdfRichTextRepository(appContext) }
+ private val pdfRichTextRepository by lazy { com.aryan.reader.pdf.PdfRichTextRepository(appContext) }
private val pdfTextBoxRepository by lazy { PdfTextBoxRepository(appContext) }
private val pdfHighlightRepository by lazy { PdfHighlightRepository(appContext) }
private val pdfAnnotationRepository by lazy { PdfAnnotationRepository(appContext) }
@@ -222,17 +208,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private val _navigationEvent = Channel(Channel.BUFFERED)
@Suppress("unused")
val navigationEvent = _navigationEvent.receiveAsFlow()
- private val _temporaryExternalOpenFinished = Channel(Channel.BUFFERED)
- val temporaryExternalOpenFinished = _temporaryExternalOpenFinished.receiveAsFlow()
private var bannerDismissJob: Job? = null
private var bannerDismissGeneration = 0L
private var pendingSwitchDeferred: CompletableDeferred? = null
private var externalOpenedBookId: String? = null
- private var temporaryExternalSessionBookId: String? = null
- private var cloudContentRetryJob: Job? = null
- private val cloudMetadataUploadJobs = ConcurrentHashMap()
- private var panelDetector: org.dueattendant149.bookreader.ml.IPanelDetector? = null
+ private var panelDetector: com.aryan.reader.ml.IPanelDetector? = null
private var speechBubbleDetector: ISpeechBubbleDetector? = null
private val mlDispatcher = newSingleThreadExecutor().asCoroutineDispatcher()
@@ -240,17 +221,17 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private val speechBubbleCache = ConcurrentHashMap>()
private val speechBubbleDetectionJobs = ConcurrentHashMap>>()
- private fun getOrInitDetector(context: Context): org.dueattendant149.bookreader.ml.IPanelDetector? {
+ private fun getOrInitDetector(context: Context): com.aryan.reader.ml.IPanelDetector? {
if (panelDetector == null && BuildConfig.DEBUG) {
val modelFile = File(context.getExternalFilesDir(null), "best_float16.tflite")
if (modelFile.exists()) {
try {
val clazz = Class.forName(
- "org.dueattendant149.bookreader.ml.ComicPanelDetector",
+ "com.aryan.reader.ml.ComicPanelDetector",
false,
context.classLoader
)
- panelDetector = clazz.getConstructor(File::class.java).newInstance(modelFile) as org.dueattendant149.bookreader.ml.IPanelDetector
+ panelDetector = clazz.getConstructor(File::class.java).newInstance(modelFile) as com.aryan.reader.ml.IPanelDetector
} catch (e: Exception) {
Timber.e(e, "Failed to instantiate ComicPanelDetector via reflection")
}
@@ -364,7 +345,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
Timber.d("CBZ copied to cache successfully. Opening archive...")
- val archiveDoc = org.dueattendant149.bookreader.pdf.ArchiveDocumentWrapper(cacheFile)
+ val archiveDoc = com.aryan.reader.pdf.ArchiveDocumentWrapper(cacheFile)
val totalPages = archiveDoc.getPageCount()
Timber.d("Archive opened. Total pages: $totalPages")
@@ -614,16 +595,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val existingItem = recentFilesRepository.getFileByBookId(hash)
if (existingItem != null) {
- val pendingRemoval = pendingExternalFileRemovals()
- .firstOrNull { it.bookId == hash }
- if (pendingRemoval != null) {
- deletePendingExternalFileRemoval(
- pendingRemoval.copy(uriString = pendingRemoval.uriString ?: existingItem.uriString)
- )
- } else {
- Timber.i("Book with ID: $hash already exists. Skipping import.")
- return null
- }
+ Timber.i("Book with ID: $hash already exists. Skipping import.")
+ return null
}
val fileName = displayName ?: ""
@@ -807,10 +780,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
_internalState.update { it.copy(showTagSelectionDialogFor = emptySet()) }
}
- suspend fun getFileInfoItem(bookId: String): RecentFileItem? {
- return recentFilesRepository.getFileByBookId(bookId)
- }
-
fun createAndAssignTag(name: String, bookIds: Set) {
val sanitizedBookIds = SharedLibraryEditor.cleanBookIds(bookIds)
if (sanitizedBookIds.isEmpty()) return
@@ -1222,12 +1191,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- if (_internalState.value.syncedFolders.any { it.localSyncEnabled }) {
+ if (_internalState.value.syncedFolders.isNotEmpty()) {
triggerFolderSyncWorker(metadataOnly = false, showFeedback = false)
}
sweepOrphanedCache()
- cleanupPendingExternalFileRemovals()
restoreReaderSessionIfNeeded()
viewModelScope.launch { billingClientWrapper.initializeConnection() }
@@ -1257,9 +1225,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (_internalState.value.isSyncEnabled) {
viewModelScope.launch {
- logCloudSyncTrace {
- "android.startup.sync_check user=${newUserData.uid} isSyncEnabled=${_internalState.value.isSyncEnabled}"
- }
Timber.tag("AnnotationSync").d(
"Startup: Pro user & Sync enabled. Initiating cloud sync."
)
@@ -1267,7 +1232,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (googleDriveRepository.hasDrivePermissions(appContext)) {
syncWithCloud(showBanner = false)
} else {
- logCloudSyncTrace { "android.startup.sync_skip reason=missing_drive_permissions user=${newUserData.uid}" }
Timber.tag("AnnotationSync").d(
"Startup: Sync skipped. Missing Drive permissions."
)
@@ -1312,119 +1276,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- private fun pendingExternalFileRemovals(): List {
- return prefs.getStringSet(KEY_PENDING_EXTERNAL_FILE_REMOVALS, emptySet())
- .orEmpty()
- .mapNotNull(::decodePendingExternalFileRemoval)
- .distinctBy { it.bookId }
- }
-
- private fun markPendingExternalFileRemoval(bookId: String, uriString: String?) {
- if (bookId.isBlank()) return
- val removalsByBookId = pendingExternalFileRemovals()
- .associateBy { it.bookId }
- .toMutableMap()
- removalsByBookId[bookId] = PendingExternalFileRemoval(bookId, uriString)
- writePendingExternalFileRemovals(removalsByBookId.values)
- }
-
- private fun clearPendingExternalFileRemovals(bookIds: Set) {
- if (bookIds.isEmpty()) return
- val remaining = pendingExternalFileRemovals().filterNot { it.bookId in bookIds }
- writePendingExternalFileRemovals(remaining)
- }
-
- private fun writePendingExternalFileRemovals(removals: Collection) {
- val encoded = removals
- .filter { it.bookId.isNotBlank() }
- .mapTo(mutableSetOf(), ::encodePendingExternalFileRemoval)
- prefs.edit(commit = true) {
- if (encoded.isEmpty()) {
- remove(KEY_PENDING_EXTERNAL_FILE_REMOVALS)
- } else {
- putStringSet(KEY_PENDING_EXTERNAL_FILE_REMOVALS, encoded)
- }
- }
- }
-
- private fun cleanupPendingExternalFileRemovals() {
- val removals = pendingExternalFileRemovals()
- if (removals.isEmpty()) return
-
- val pendingBookIds = removals.mapTo(mutableSetOf()) { it.bookId }
- if (prefs.getString(KEY_LAST_OPEN_BOOK_ID, null) in pendingBookIds) {
- clearPersistedReaderSession()
- }
-
- viewModelScope.launch {
- removals.forEach { removal ->
- deletePendingExternalFileRemoval(removal)
- }
- }
- }
-
- private fun deletePendingExternalFileRemoval(bookId: String, uriString: String?) {
- markPendingExternalFileRemoval(bookId, uriString)
- viewModelScope.launch {
- deletePendingExternalFileRemoval(PendingExternalFileRemoval(bookId, uriString))
- }
- }
-
- private suspend fun deletePendingExternalFileRemoval(removal: PendingExternalFileRemoval) {
- var shouldRetry = false
- runCatching {
- cleanupBookDataLocally(removal.bookId)
- }.onFailure { error ->
- Timber.w(error, "Failed to clear local caches for pending external file ${removal.bookId}")
- }
-
- runCatching {
- recentFilesRepository.deleteFilePermanently(listOf(removal.bookId))
- }.onFailure { error ->
- shouldRetry = true
- Timber.w(error, "Failed to remove pending external file ${removal.bookId} from library")
- }
-
- removal.uriString?.let { uriString ->
- runCatching {
- bookImporter.deleteBookByUriString(uriString)
- }.onFailure { error ->
- shouldRetry = true
- Timber.w(error, "Failed to delete pending external file copy for ${removal.bookId}")
- }
- }
-
- if (!shouldRetry) {
- clearPendingExternalFileRemovals(setOf(removal.bookId))
- }
- }
-
- private fun encodePendingExternalFileRemoval(removal: PendingExternalFileRemoval): String {
- return JSONObject()
- .put("bookId", removal.bookId)
- .apply {
- if (!removal.uriString.isNullOrBlank()) {
- put("uriString", removal.uriString)
- }
- }
- .toString()
- }
-
- private fun decodePendingExternalFileRemoval(value: String): PendingExternalFileRemoval? {
- val trimmed = value.trim()
- if (trimmed.isBlank()) return null
- return if (trimmed.startsWith("{")) {
- runCatching {
- val json = JSONObject(trimmed)
- val bookId = json.optString("bookId").takeIf { it.isNotBlank() }
- val uriString = json.optString("uriString").takeIf { it.isNotBlank() }
- bookId?.let { PendingExternalFileRemoval(it, uriString) }
- }.getOrNull()
- } else {
- PendingExternalFileRemoval(trimmed, null)
- }
- }
-
private fun restoreReaderSessionIfNeeded() {
val currentState = _internalState.value
if (currentState.selectedBookId != null || currentState.selectedPdfUri != null || currentState.selectedEpubUri != null) {
@@ -1435,10 +1286,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
runCatching { FileType.valueOf(typeName) }.getOrNull()
}
val restoreBookId = prefs.getString(KEY_LAST_OPEN_BOOK_ID, null) ?: return
- if (restoreBookId in pendingExternalFileRemovals().map { it.bookId }) {
- clearPersistedReaderSession()
- return
- }
if (persistedType == null) {
clearPersistedReaderSession()
return
@@ -1767,27 +1614,17 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
fun importFont(uri: Uri) {
- importFonts(listOf(uri))
- }
-
- fun importFonts(uris: List) {
- if (uris.isEmpty()) return
viewModelScope.launch {
_internalState.update { it.copy(isLoading = true) }
- try {
- uris.forEach { uri ->
- val result = fontsRepository.importFont(uri)
- result.onSuccess { font ->
- if (uiState.value.isSyncEnabled) {
- uploadNewFont(font)
- }
- }.onFailure {
- showBanner(appContext.getString(R.string.error_import_font, it.message), isError = true)
- }
+ val result = fontsRepository.importFont(uri)
+ result.onSuccess { font ->
+ if (uiState.value.isSyncEnabled) {
+ uploadNewFont(font)
}
- } finally {
- _internalState.update { it.copy(isLoading = false) }
+ }.onFailure {
+ showBanner(appContext.getString(R.string.error_import_font, it.message), isError = true)
}
+ _internalState.update { it.copy(isLoading = false) }
}
}
@@ -1813,17 +1650,9 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
fun deleteFont(fontId: String) {
- deleteFonts(listOf(fontId))
- }
-
- fun deleteFonts(fontIds: Collection) {
- val uniqueFontIds = fontIds.filter { it.isNotBlank() }.toSet()
- if (uniqueFontIds.isEmpty()) return
viewModelScope.launch {
- uniqueFontIds.forEach { fontId ->
- fontsRepository.deleteFont(fontId)
- }
- if (uniqueFontIds.any { _internalState.value.appFontPreference.referencesCustomFont(it) }) {
+ fontsRepository.deleteFont(fontId)
+ if (_internalState.value.appFontPreference.referencesCustomFont(fontId)) {
setAppFontPreference(AppFontPreference.System)
}
}
@@ -1944,24 +1773,24 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
when (val deviceStatus =
firestoreRepository.getDeviceStatus(currentUser.uid, deviceId)) {
- is org.dueattendant149.bookreader.data.DeviceStatus.Active -> {
+ is com.aryan.reader.data.DeviceStatus.Active -> {
Timber.d("Device is active. Updating last seen.")
firestoreRepository.updateDeviceLastSeen(currentUser.uid, deviceId)
}
- is org.dueattendant149.bookreader.data.DeviceStatus.Revoked -> {
+ is com.aryan.reader.data.DeviceStatus.Revoked -> {
Timber.w("Device has been revoked. Signing out.")
firestoreRepository.deleteDevice(currentUser.uid, deviceId) // Clean up
signOut()
showBanner(appContext.getString(R.string.banner_device_removed))
}
- is org.dueattendant149.bookreader.data.DeviceStatus.NotFound -> {
+ is com.aryan.reader.data.DeviceStatus.NotFound -> {
Timber.d("Device not found during verification. Triggering full registration.")
registerOrUpdateDeviceOnSignIn(currentUser.uid)
}
- is org.dueattendant149.bookreader.data.DeviceStatus.Error -> {
+ is com.aryan.reader.data.DeviceStatus.Error -> {
Timber.e(deviceStatus.exception, "Error checking device status.")
_internalState.update {
it.copy(
@@ -2038,7 +1867,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
sourceUri: Uri,
destUri: Uri,
annotations: Map>,
- richTextPageLayouts: List? = null,
+ richTextPageLayouts: List? = null,
textBoxes: List? = null,
highlights: List? = null,
bookId: String
@@ -2096,48 +1925,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- internal fun trackExternalOpenForClose(
- bookId: String,
- importedCopyUriString: String?,
- isTemporaryExternalIntent: Boolean
- ) {
- if (isTemporaryExternalIntent) {
- temporaryExternalSessionBookId = bookId
- if (importedCopyUriString != null) {
- externalOpenedBookId = bookId
- markPendingExternalFileRemoval(bookId, importedCopyUriString)
- }
- return
- }
-
- externalOpenedBookId = bookId
- if (
- importedCopyUriString != null &&
- prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, EXTERNAL_FILE_BEHAVIOR_ASK) == "DELETE"
- ) {
- markPendingExternalFileRemoval(bookId, importedCopyUriString)
- }
- }
-
- fun saveOriginalFile(sourceUri: Uri, destUri: Uri) {
- viewModelScope.launch {
- _internalState.update {
- it.copy(isLoading = true, bannerMessage = BannerMessage(appContext.getString(R.string.banner_saving_original_file)))
- }
- try {
- withContext(Dispatchers.IO) {
- copyUriBytes(sourceUri, destUri)
- }
- showBanner(appContext.getString(R.string.banner_original_file_saved))
- } catch (e: Exception) {
- Timber.e(e, "Failed to save original file")
- showBanner(appContext.getString(R.string.error_saving_file, e.localizedMessage ?: e.message.orEmpty()), isError = true)
- } finally {
- _internalState.update { it.copy(isLoading = false) }
- }
- }
- }
-
fun togglePinForContextualItems(isHome: Boolean) {
if (_internalState.value.contextualActionItems.isEmpty()) return
@@ -2175,7 +1962,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
activityContext: Context,
sourceUri: Uri,
annotations: Map>,
- richTextPageLayouts: List? = null,
+ richTextPageLayouts: List? = null,
textBoxes: List? = null,
highlights: List? = null,
includeAnnotations: Boolean,
@@ -2255,279 +2042,48 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- suspend fun shareOriginalFile(
- activityContext: Context,
- sourceUri: Uri,
- fileType: FileType,
- filename: String
- ) {
- withContext(Dispatchers.IO) {
- try {
- val shareDir = File(appContext.cacheDir, "shared_files")
- if (shareDir.exists()) {
- shareDir.listFiles()?.forEach { file ->
- try {
- file.delete()
- } catch (_: Exception) {
- Timber.w("Failed to delete temp share file: ${file.name}")
- }
- }
- } else {
- shareDir.mkdirs()
- }
-
- val destFile = File(shareDir, filename)
- FileOutputStream(destFile).use { output ->
- appContext.contentResolver.openInputStream(sourceUri)?.use { input ->
- input.copyTo(output)
- } ?: error("Could not open source file.")
- }
-
- val authority = "${appContext.packageName}.provider"
- val contentUri = androidx.core.content.FileProvider.getUriForFile(
- appContext, authority, destFile
- )
- val mimeType = SharedFileCapabilities.mimeTypeFor(fileType) ?: "application/octet-stream"
- val shareIntent = Intent(Intent.ACTION_SEND).apply {
- type = mimeType
- putExtra(Intent.EXTRA_STREAM, contentUri)
- putExtra(Intent.EXTRA_TITLE, filename)
- putExtra(Intent.EXTRA_SUBJECT, appContext.getString(R.string.share_subject, filename))
- clipData = ClipData.newRawUri(filename, contentUri)
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }
- val chooser = Intent.createChooser(shareIntent, appContext.getString(R.string.share_file_chooser_title))
- if (activityContext !is android.app.Activity) {
- chooser.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- }
- withContext(Dispatchers.Main) { activityContext.startActivity(chooser) }
- } catch (e: Exception) {
- Timber.e(e, "Share original file failed")
- showBanner(appContext.getString(R.string.error_share_failed, e.localizedMessage ?: e.message.orEmpty()), isError = true)
- }
- }
- }
-
- private fun copyUriBytes(sourceUri: Uri, destUri: Uri) {
- val contentResolver = appContext.contentResolver
- contentResolver.openInputStream(sourceUri)?.use { input ->
- contentResolver.openOutputStream(destUri)?.use { output ->
- input.copyTo(output)
- } ?: error("Could not open destination file.")
- } ?: error("Could not open source file.")
- }
-
- private fun queueCloudMetadataUpload(bookId: String, reason: String, debounce: Boolean = true) {
- if (!uiState.value.isSyncEnabled) return
- cloudMetadataUploadJobs.remove(bookId)?.cancel()
- val job = viewModelScope.launch {
- if (debounce) delay(CLOUD_METADATA_UPLOAD_DEBOUNCE_MILLIS)
- val latest = recentFilesRepository.getFileByBookId(bookId) ?: run {
- logCloudSyncTrace { "android.upload.queue_skip reason=missing_local book=$bookId trigger=$reason" }
- return@launch
- }
- logCloudSyncTrace {
- "android.upload.queue_fire trigger=$reason debounce=$debounce ${latest.cloudSyncTraceSummary()}"
- }
- uploadSingleBookMetadata(latest)
- }
- cloudMetadataUploadJobs[bookId] = job
- job.invokeOnCompletion {
- if (cloudMetadataUploadJobs[bookId] == job) {
- cloudMetadataUploadJobs.remove(bookId)
- }
- }
- }
-
- fun queuePdfSidecarCloudUpload(bookId: String) {
- queueCloudMetadataUpload(bookId, reason = "pdf_sidecar")
- }
-
private fun uploadSingleBookMetadata(book: RecentFileItem) {
- if (!uiState.value.isSyncEnabled) {
- logCloudSyncTrace { "android.upload.skip reason=sync_disabled ${book.cloudSyncTraceSummary()}" }
- return
- }
+ if (!uiState.value.isSyncEnabled) return
if (book.uriString?.startsWith("opds-pse") == true) {
- logCloudSyncTrace { "android.upload.skip reason=opds_stream ${book.cloudSyncTraceSummary()}" }
Timber.d("Skipping metadata sync for OPDS stream book: ${book.displayName}")
return
}
if (book.sourceFolderUri != null) {
- logCloudSyncTrace { "android.upload.skip reason=folder_book ${book.cloudSyncTraceSummary()}" }
Timber.d("Skipping metadata sync for local folder book: ${book.displayName}")
return
}
if (book.isManualOnlyReaderFile()) {
- logCloudSyncTrace { "android.upload.skip reason=manual_only ${book.cloudSyncTraceSummary()}" }
Timber.d("Skipping metadata sync for manual-only reader file: ${book.displayName}")
return
}
- val currentUser = uiState.value.currentUser ?: run {
- logCloudSyncTrace { "android.upload.skip reason=no_user ${book.cloudSyncTraceSummary()}" }
- return
- }
+ val currentUser = uiState.value.currentUser ?: return
viewModelScope.launch {
try {
val deviceId = getInstallationId()
- var bookForMetadata = book
- var uploadedAnnotationPayload = false
- var uploadedAnnotationModifiedTimestamp = 0L
- var remoteBookLoaded = false
- var remoteBookForUpload: BookMetadata? = null
- var remoteAnnotationDriveTimestampLoaded = false
- var remoteAnnotationDriveTimestamp = 0L
- suspend fun loadRemoteBookForUpload(): BookMetadata? {
- if (!remoteBookLoaded) {
- remoteBookForUpload = firestoreRepository.getBookMetadata(currentUser.uid, book.bookId)
- remoteBookLoaded = true
- }
- return remoteBookForUpload
- }
- suspend fun loadRemoteAnnotationDriveTimestamp(): Long {
- if (!remoteAnnotationDriveTimestampLoaded) {
- val remote = loadRemoteBookForUpload()
- remoteAnnotationDriveTimestamp = if (remote?.hasAnnotations == true) {
- googleDriveRepository.getAccessToken(appContext)?.let { accessToken ->
- googleDriveRepository.getFiles(accessToken)
- ?.files
- .orEmpty()
- .firstOrNull { it.name == cloudPdfAnnotationDriveFileName(book.bookId) }
- ?.modifiedTimeMillis
- } ?: 0L
- } else {
- 0L
- }
- remoteAnnotationDriveTimestampLoaded = true
- }
- return remoteAnnotationDriveTimestamp
- }
- if (book.needsRemoteEpubAnnotationMetadataGuard()) {
- val remote = loadRemoteBookForUpload()
- val merged = bookForMetadata.mergeRemoteEpubAnnotationMetadata(remote)
- if (merged != bookForMetadata) {
- logCloudSyncTrace {
- "android.upload.epub_annotation_preserve book=${book.bookId} " +
- "local=${bookForMetadata.cloudSyncTraceSummary()} " +
- "remote=${remote?.cloudSyncTraceSummary() ?: "null"} " +
- "merged=${merged.cloudSyncTraceSummary()}"
- }
- recentFilesRepository.addRecentFile(merged)
- bookForMetadata = merged
- }
- }
- val remoteForContent = loadRemoteBookForUpload()?.toRecentFileItem()
- if (shouldUploadLocalBookContent(bookForMetadata, remoteForContent)) {
- val accessToken = googleDriveRepository.getAccessToken(appContext) ?: run {
- logCloudSyncTrace { "android.upload.content_guard_skip reason=no_access_token ${bookForMetadata.cloudSyncTraceSummary()}" }
- return@launch
- }
- val source = bookForMetadata.getUri()?.path?.let(::File)
- if (source?.exists() != true) {
- logCloudSyncTrace {
- "android.upload.content_guard_skip reason=file_missing book=${bookForMetadata.bookId} " +
- "path=${(source?.absolutePath).cloudSyncPreview()}"
- }
- return@launch
- }
- logCloudSyncTrace {
- "android.upload.content_guard_start book=${bookForMetadata.bookId} " +
- "localContentTs=${bookForMetadata.fileContentModifiedTimestamp} " +
- "remoteContentTs=${remoteForContent?.fileContentModifiedTimestamp ?: 0L}"
- }
- val uploadedFile = googleDriveRepository.uploadFile(
- accessToken,
- bookForMetadata.bookId,
- source,
- bookForMetadata.type
- )
- if (uploadedFile == null) {
- logCloudSyncTrace { "android.upload.content_guard_failed book=${bookForMetadata.bookId}" }
- return@launch
- }
- val contentTimestamp = bookForMetadata.fileContentModifiedTimestamp.takeIf { it > 0L }
- ?: source.lastModified()
- bookForMetadata = bookForMetadata.copy(
- fileSize = source.length(),
- fileContentModifiedTimestamp = contentTimestamp
- )
- logCloudSyncTrace {
- "android.upload.content_guard_success book=${bookForMetadata.bookId} " +
- "driveId=${uploadedFile.id} contentTs=$contentTimestamp"
- }
- }
- logCloudSyncTrace { "android.upload.start device=$deviceId ${bookForMetadata.cloudSyncTraceSummary()}" }
Timber.tag("AnnotationSync").d("Preparing to sync book: ${book.bookId}")
val inkFile = pdfAnnotationRepository.getAnnotationFileForSync(book.bookId)
- val deletedInkFile = pdfAnnotationRepository.getDeletedAnnotationsFileForSync(book.bookId)
val richTextFile = pdfRichTextRepository.getFileForSync(book.bookId)
val layoutFile = pageLayoutRepository.getLayoutFile(book.bookId)
val textBoxFile = pdfTextBoxRepository.getFileForSync(book.bookId)
val highlightFile = pdfHighlightRepository.getFileForSync(book.bookId)
- val hasInk = inkFile.hasSyncableCloudAnnotationPayload()
- val hasDeletedInk = deletedInkFile.hasSyncableCloudAnnotationPayload()
- val hasRichText = richTextFile.hasSyncableCloudAnnotationPayload()
+ val hasInk = inkFile?.exists() == true
+ val hasRichText = richTextFile.exists()
val hasLayout = layoutFile.exists()
- val hasTextBoxes = textBoxFile.hasSyncableCloudAnnotationPayload()
- val hasHighlights = highlightFile.hasSyncableCloudAnnotationPayload()
- val sidecars = AndroidPdfCloudSidecarState(
- hasInk = hasInk,
- inkTimestamp = inkFile?.lastModified() ?: 0L,
- hasDeletedInk = hasDeletedInk,
- deletedInkTimestamp = deletedInkFile?.lastModified() ?: 0L,
- hasRichText = hasRichText,
- richTextTimestamp = richTextFile.lastModified(),
- hasLayout = hasLayout,
- layoutTimestamp = layoutFile.lastModified(),
- hasTextBoxes = hasTextBoxes,
- textBoxesTimestamp = textBoxFile.lastModified(),
- hasHighlights = hasHighlights,
- highlightsTimestamp = highlightFile.lastModified()
- )
- logCloudSyncTrace {
- "android.upload.sidecars book=${book.bookId} hasInk=$hasInk hasDeletedInk=$hasDeletedInk hasText=$hasRichText " +
- "hasLayout=$hasLayout hasTextBoxes=$hasTextBoxes hasHighlights=$hasHighlights " +
- "payloadTs=${sidecars.annotationPayloadTimestamp} bundleTs=${sidecars.bundleTimestamp}"
- }
- logCloudAnnotationSyncTrace {
- "android.upload.inspect book=${book.bookId} remoteHas=${remoteBookForUpload?.hasAnnotations} " +
- "remoteTs=${remoteBookForUpload?.lastModifiedTimestamp ?: 0L} " +
- "ink{exists=$hasInk bytes=${inkFile?.length() ?: 0L} ts=${sidecars.inkTimestamp}} " +
- "deletedInk{exists=$hasDeletedInk bytes=${deletedInkFile?.length() ?: 0L} ts=${sidecars.deletedInkTimestamp}} " +
- "text{exists=$hasRichText bytes=${richTextFile.length()} ts=${sidecars.richTextTimestamp}} " +
- "layout{exists=$hasLayout bytes=${layoutFile.length()} ts=${sidecars.layoutTimestamp}} " +
- "textBoxes{exists=$hasTextBoxes bytes=${textBoxFile.length()} ts=${sidecars.textBoxesTimestamp}} " +
- "highlights{exists=$hasHighlights bytes=${highlightFile.length()} ts=${sidecars.highlightsTimestamp}} " +
- "payloadTs=${sidecars.annotationPayloadTimestamp} bundleTs=${sidecars.bundleTimestamp} " +
- "hasPayload=${sidecars.hasAnnotationPayload}"
- }
- val remoteAnnotationDriveTimestampForUpload = loadRemoteAnnotationDriveTimestamp()
- val remoteAnnotationTimestampForUpload =
- remoteBookForUpload?.effectiveAnnotationModifiedTimestamp(remoteAnnotationDriveTimestampForUpload) ?: 0L
- val localAnnotationsShouldUpload = shouldUploadLocalPdfCloudAnnotations(
- localSidecars = sidecars,
- remoteHasAnnotations = remoteBookForUpload?.hasAnnotations == true,
- remoteAnnotationModifiedTimestamp = remoteAnnotationTimestampForUpload
- )
- logCloudAnnotationSyncTrace {
- "android.upload.annotation_decision book=${book.bookId} " +
- "localShouldUpload=$localAnnotationsShouldUpload remoteHas=${remoteBookForUpload?.hasAnnotations} " +
- "remoteAnnTs=$remoteAnnotationTimestampForUpload " +
- "remoteDriveAnnTs=$remoteAnnotationDriveTimestampForUpload payloadTs=${sidecars.annotationPayloadTimestamp}"
- }
+ val hasTextBoxes = textBoxFile.exists()
+ val hasHighlights = highlightFile.exists()
+ val hasAnyData = hasInk || hasRichText || hasLayout || hasTextBoxes || hasHighlights
Timber.d(
"android.cloud.export candidates book=${book.bookId} hasRichText=$hasRichText " +
- "richBytes=${if (hasRichText) richTextFile.length() else 0L} " +
- "hasAnnotationPayload=${sidecars.hasAnnotationPayload}"
+ "richBytes=${if (hasRichText) richTextFile.length() else 0L} hasAnyData=$hasAnyData"
)
- if (localAnnotationsShouldUpload) {
+ if (hasAnyData) {
if (googleDriveRepository.hasDrivePermissions(appContext)) {
val accessToken = googleDriveRepository.getAccessToken(appContext)
@@ -2559,7 +2115,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
if (hasInk) putJsonSafe("ink", inkFile)
- if (hasDeletedInk) putJsonSafe(SharedPdfAnnotationSidecarCodec.KEY_PDF_ANNOTATION_DELETIONS, deletedInkFile)
if (hasRichText) putJsonSafe("text", richTextFile)
if (hasLayout) putJsonSafe("layout", layoutFile)
if (hasTextBoxes) putJsonSafe("textBoxes", textBoxFile)
@@ -2568,54 +2123,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val bundleFile =
File(appContext.cacheDir, "sync_bundle_${book.bookId}.json")
val canonicalBundle = SharedPdfAnnotationSidecarCodec.canonicalizeDataJson(bundleJson.toString())
- var uploadBundle = canonicalBundle
- var mergedRemoteIntoUpload = false
- if (remoteBookForUpload?.hasAnnotations == true) {
- val remoteBundleFile = File(appContext.cacheDir, "remote_sync_bundle_${book.bookId}.json")
- try {
- val didDownloadRemote = googleDriveRepository.downloadAnnotationFile(
- accessToken,
- book.bookId,
- remoteBundleFile
- )
- if (didDownloadRemote && remoteBundleFile.isFile) {
- val remoteBundle = remoteBundleFile.readText()
- val mergedBundle = SharedPdfAnnotationSidecarCodec.mergeAnnotationDataJson(
- localDataJson = canonicalBundle,
- remoteDataJson = remoteBundle,
- preferRemoteOnConflict = false
- )
- val localCount = SharedPdfAnnotationSidecarCodec.annotationCountFromDataJson(canonicalBundle)
- val remoteCount = SharedPdfAnnotationSidecarCodec.annotationCountFromDataJson(remoteBundle)
- val mergedCount = SharedPdfAnnotationSidecarCodec.annotationCountFromDataJson(mergedBundle)
- uploadBundle = mergedBundle
- mergedRemoteIntoUpload = mergedBundle != canonicalBundle
- logCloudAnnotationSyncTrace {
- "android.upload.merge_remote book=${book.bookId} didDownload=true " +
- "localCount=$localCount remoteCount=$remoteCount mergedCount=$mergedCount " +
- "changed=$mergedRemoteIntoUpload"
- }
- } else {
- logCloudAnnotationSyncTrace {
- "android.upload.merge_remote_missing book=${book.bookId} " +
- "didDownload=$didDownloadRemote tempExists=${remoteBundleFile.exists()}"
- }
- }
- } catch (e: Exception) {
- logCloudAnnotationSyncError(e) {
- "android.upload.merge_remote_failed book=${book.bookId}"
- }
- } finally {
- remoteBundleFile.delete()
- }
- }
- bundleFile.writeText(uploadBundle)
- logCloudAnnotationSyncTrace {
- "android.upload.bundle_ready book=${book.bookId} " +
- "rawKeys=${bundleJson.keys().asSequence().toList()} " +
- "canonicalBytes=${canonicalBundle.length} uploadBytes=${uploadBundle.length} " +
- "fileBytes=${bundleFile.length()}"
- }
+ bundleFile.writeText(canonicalBundle)
if (hasRichText) {
Timber.d(
"android.cloud.export.bundleReady book=${book.bookId} canonicalLen=${canonicalBundle.length} " +
@@ -2629,36 +2137,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
bundleFile.delete()
if (uploaded != null) {
- uploadedAnnotationPayload = true
- uploadedAnnotationModifiedTimestamp = uploaded.modifiedTimeMillis
- if (mergedRemoteIntoUpload) {
- recentFilesRepository.importAnnotationBundle(
- book.bookId,
- uploadBundle,
- uploadedAnnotationModifiedTimestamp
- )
- logCloudAnnotationSyncTrace {
- "android.upload.local_apply_merged book=${book.bookId} " +
- "driveTs=$uploadedAnnotationModifiedTimestamp bytes=${uploadBundle.length}"
- }
- }
- markPdfCloudAnnotationSidecarsSynced(
- uploadedAnnotationModifiedTimestamp,
- inkFile,
- richTextFile,
- layoutFile,
- textBoxFile,
- highlightFile,
- deletedInkFile
- )
- logCloudAnnotationSyncTrace {
- "android.upload.sidecar_success book=${book.bookId} driveId=${uploaded.id} " +
- "driveTs=$uploadedAnnotationModifiedTimestamp bytes=${uploadBundle.length}"
- }
- logCloudSyncTrace {
- "android.upload.sidecar_success book=${book.bookId} driveId=${uploaded.id} " +
- "driveTs=$uploadedAnnotationModifiedTimestamp bytes=${uploadBundle.length}"
- }
if (hasRichText) {
Timber
.d("android.cloud.export.uploadSuccess book=${book.bookId} driveId=${uploaded.id}")
@@ -2666,10 +2144,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.tag("AnnotationSync")
.d("Bundle upload SUCCESS. ID: ${uploaded.id}")
} else {
- logCloudAnnotationSyncTrace {
- "android.upload.sidecar_failed book=${book.bookId} bytes=${uploadBundle.length}"
- }
- logCloudSyncTrace { "android.upload.sidecar_failed book=${book.bookId}; aborting_metadata_upload" }
if (hasRichText) {
Timber
.e("android.cloud.export.uploadFailed book=${book.bookId}")
@@ -2678,127 +2152,23 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
.e("Bundle upload FAILED. Skipping Firestore sync to prevent data loss.")
return@launch
}
- } else {
- logCloudAnnotationSyncTrace { "android.upload.skip_sidecar reason=no_access_token book=${book.bookId}" }
- logCloudSyncTrace { "android.upload.sidecar_failed book=${book.bookId}; reason=no_access_token; aborting_metadata_upload" }
- return@launch
}
- } else {
- logCloudAnnotationSyncTrace { "android.upload.skip_sidecar reason=missing_drive_permission book=${book.bookId}" }
- logCloudSyncTrace { "android.upload.sidecar_failed book=${book.bookId}; reason=missing_drive_permission; aborting_metadata_upload" }
- return@launch
}
} else {
- logCloudAnnotationSyncTrace {
- "android.upload.skip_sidecar reason=${if (sidecars.hasAnnotationPayload) "remote_annotation_not_older" else "no_annotation_payload"} " +
- "book=${book.bookId} layoutOnly=$hasLayout layoutTs=${sidecars.layoutTimestamp} " +
- "remoteAnnTs=$remoteAnnotationTimestampForUpload payloadTs=${sidecars.annotationPayloadTimestamp}"
- }
- logCloudSyncTrace {
- "android.upload.sidecars_skipped book=${book.bookId} " +
- "reason=${if (sidecars.hasAnnotationPayload) "remote_annotation_not_older" else "no_annotation_payload"}"
- }
- Timber.tag("AnnotationSync").d(
- if (sidecars.hasAnnotationPayload) {
- "Local annotation payload is not newer than remote for ${book.bookId}"
- } else {
- "No local annotation payload (ink/text/text boxes/highlights) to upload for ${book.bookId}"
- }
- )
- }
-
- val latestLocalForMetadata = recentFilesRepository.getFileByBookId(bookForMetadata.bookId)
- val refreshedBookForMetadata = bookForMetadata.withFreshLocalReadingPositionForCloudUpload(
- latestLocalForMetadata
- )
- if (refreshedBookForMetadata != bookForMetadata) {
- logCloudSyncTrace {
- "android.upload.refresh_latest book=${bookForMetadata.bookId} " +
- "before=${bookForMetadata.cloudSyncTraceSummary()} " +
- "latest=${latestLocalForMetadata?.cloudSyncTraceSummary() ?: "null"} " +
- "after=${refreshedBookForMetadata.cloudSyncTraceSummary()}"
- }
- bookForMetadata = refreshedBookForMetadata
- }
- val remoteMetadata = loadRemoteBookForUpload()
- val localReadingTimestamp = bookForMetadata.effectiveReadingPositionModifiedTimestamp()
- val remoteReadingTimestamp = remoteMetadata?.effectiveReadingPositionModifiedTimestamp() ?: 0L
- val remoteAnnotationTimestamp = remoteMetadata?.effectiveAnnotationModifiedTimestamp(
- remoteAnnotationDriveTimestampForUpload
- ) ?: 0L
- val remoteReadingPositionWins = remoteMetadata != null && remoteReadingTimestamp > localReadingTimestamp
- val remoteMetadataWins = remoteMetadata != null &&
- remoteMetadata.lastModifiedTimestamp > bookForMetadata.lastModifiedTimestamp
- val metadataBase = if (remoteMetadataWins && remoteMetadata != null) {
- remoteMetadata.toRecentFileItem().withLocalStorageForCloudMetadata(bookForMetadata)
- } else {
- bookForMetadata
- }
- val metadataBook = when {
- remoteReadingPositionWins && remoteMetadata != null -> metadataBase.withCloudReadingPosition(remoteMetadata)
- metadataBase != bookForMetadata -> metadataBase.withLocalReadingPosition(bookForMetadata)
- else -> bookForMetadata
- }
- val readingPositionTimestamp = if (remoteReadingPositionWins) {
- remoteReadingTimestamp
- } else {
- localReadingTimestamp
- }
- if (remoteReadingPositionWins) {
- logCloudSyncTrace {
- "android.upload.preserve_remote_position book=${bookForMetadata.bookId} " +
- "remoteReadTs=$remoteReadingTimestamp localReadTs=$localReadingTimestamp " +
- "remote=${remoteMetadata?.cloudSyncTraceSummary() ?: "null"}"
- }
- }
- if (remoteMetadataWins) {
- logCloudSyncTrace {
- "android.upload.preserve_remote_metadata book=${bookForMetadata.bookId} " +
- "remoteTs=${remoteMetadata?.lastModifiedTimestamp ?: 0L} localTs=${bookForMetadata.lastModifiedTimestamp} " +
- "remoteReadTs=$remoteReadingTimestamp localReadTs=$localReadingTimestamp " +
- metadataBook.cloudSyncTraceSummary("metadata")
- }
+ Timber.tag("AnnotationSync")
+ .d("No local data (ink/text/layout) to upload for ${book.bookId}")
}
val newTimestamp = System.currentTimeMillis()
- val syncedAnnotationTimestamp = if (uploadedAnnotationPayload) {
- uploadedAnnotationModifiedTimestamp.takeIf { it > 0L }
- ?: maxOf(sidecars.annotationPayloadTimestamp, newTimestamp)
- } else if (remoteMetadata?.hasAnnotations == true) {
- remoteAnnotationTimestamp
- } else {
- 0L
- }
- val syncedHasAnnotations = uploadedAnnotationPayload || remoteMetadata?.hasAnnotations == true
- val metadataToSync = metadataBook.toBookMetadata().copy(
- lastModifiedTimestamp = newTimestamp,
- readingPositionModifiedTimestamp = readingPositionTimestamp,
- annotationModifiedTimestamp = syncedAnnotationTimestamp,
- hasAnnotations = syncedHasAnnotations
+ val metadataToSync = book.toBookMetadata().copy(
+ lastModifiedTimestamp = newTimestamp, hasAnnotations = hasAnyData
)
firestoreRepository.syncBookMetadata(currentUser.uid, metadataToSync, deviceId)
- recentFilesRepository.addRecentFile(
- metadataBook.copy(
- lastModifiedTimestamp = newTimestamp,
- readingPositionModifiedTimestamp = readingPositionTimestamp
- )
- )
- logCloudAnnotationSyncTrace {
- "android.upload.metadata_success book=${bookForMetadata.bookId} newTs=$newTimestamp " +
- "readTs=$readingPositionTimestamp hasAnnotations=$syncedHasAnnotations " +
- "annTs=$syncedAnnotationTimestamp payloadTs=${sidecars.annotationPayloadTimestamp}"
- }
- logCloudSyncTrace {
- "android.upload.metadata_success user=${currentUser.uid} oldTs=${bookForMetadata.lastModifiedTimestamp} " +
- "newTs=$newTimestamp readTs=$readingPositionTimestamp annTs=$syncedAnnotationTimestamp " +
- "remoteReadTs=$remoteReadingTimestamp localReadTs=$localReadingTimestamp " +
- "hasAnnotations=$syncedHasAnnotations ${metadataBook.cloudSyncTraceSummary()}"
- }
+ recentFilesRepository.addRecentFile(book.copy(lastModifiedTimestamp = newTimestamp))
Timber.tag("AnnotationSync")
- .d("Firestore metadata updated for ${book.bookId} (hasAnnotationPayload=${sidecars.hasAnnotationPayload}, syncedHasAnnotations=$syncedHasAnnotations)")
+ .d("Firestore metadata updated for ${book.bookId} (hasData=$hasAnyData)")
} catch (e: Exception) {
- logCloudSyncError(e) { "android.upload.failed ${book.cloudSyncTraceSummary()}" }
Timber.tag("AnnotationSync").e(e, "Failed to sync book data: ${book.bookId}")
}
}
@@ -2872,10 +2242,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val closingBookId = _internalState.value.selectedBookId
val uriString = _internalState.value.selectedPdfUri?.toString()
?: _internalState.value.selectedEpubUri?.toString()
- val isTemporaryExternalSession = closingBookId != null && closingBookId == temporaryExternalSessionBookId
- logCloudSyncTrace {
- "android.reader.close_request book=$closingBookId uri=${uriString.cloudSyncPreview()} sync=${uiState.value.isSyncEnabled}"
- }
val ttsState = ttsController.ttsState.value
val isTtsActive = ttsState.playbackSource == "READER" &&
@@ -2899,7 +2265,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
selectedEpubBook = null,
selectedFileType = null,
isLoading = false,
- isTemporaryExternalOpen = false,
errorMessage = null,
initialLocator = null,
initialPageInBook = null,
@@ -2907,56 +2272,28 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
isOpeningFromTtsNotification = false
)
}
- if (!isTemporaryExternalSession) {
- clearPersistedReaderSession()
- }
+ clearPersistedReaderSession()
- var removesExternalFileOnClose = false
- if (closingBookId != null && (closingBookId == externalOpenedBookId || isTemporaryExternalSession)) {
- val behavior = if (isTemporaryExternalSession) {
- EXTERNAL_FILE_BEHAVIOR_TEMPORARY
- } else {
- prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, EXTERNAL_FILE_BEHAVIOR_ASK) ?: EXTERNAL_FILE_BEHAVIOR_ASK
- }
+ if (closingBookId != null && closingBookId == externalOpenedBookId) {
+ val behavior = prefs.getString(KEY_EXTERNAL_FILE_BEHAVIOR, "ASK") ?: "ASK"
if (behavior == "ASK") {
_internalState.update { it.copy(showExternalFileSavePromptFor = closingBookId) }
} else if (behavior == "DELETE") {
- removesExternalFileOnClose = true
- deletePendingExternalFileRemoval(closingBookId, uriString)
- } else if (behavior == EXTERNAL_FILE_BEHAVIOR_TEMPORARY) {
- removesExternalFileOnClose = true
- val shouldDeleteImportedCopy = closingBookId == externalOpenedBookId
- if (shouldDeleteImportedCopy) {
- viewModelScope.launch {
- deletePendingExternalFileRemoval(PendingExternalFileRemoval(closingBookId, uriString))
- _temporaryExternalOpenFinished.send(Unit)
- }
- } else {
- viewModelScope.launch {
- _temporaryExternalOpenFinished.send(Unit)
- }
- }
- } else {
- clearPendingExternalFileRemovals(setOf(closingBookId))
+ deleteBookPermanently(closingBookId)
}
externalOpenedBookId = null
- temporaryExternalSessionBookId = null
}
- if (uriString != null && !removesExternalFileOnClose) {
+ if (uriString != null) {
viewModelScope.launch {
val freshBook = recentFilesRepository.getFileByUri(uriString)
freshBook?.let {
if (uiState.value.uploadingBookIds.contains(it.bookId)) {
- logCloudSyncTrace { "android.reader.close_upload_skip reason=already_uploading ${it.cloudSyncTraceSummary()}" }
return@launch
}
if (uiState.value.isSyncEnabled) {
- logCloudSyncTrace { "android.reader.close_upload_start ${it.cloudSyncTraceSummary()}" }
Timber.d("Book closed, triggering metadata sync for ${it.bookId}")
uploadSingleBookMetadata(it)
- } else {
- logCloudSyncTrace { "android.reader.close_upload_skip reason=sync_disabled ${it.cloudSyncTraceSummary()}" }
}
if (it.sourceFolderUri != null) {
@@ -3056,32 +2393,72 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
private fun loadSyncedFoldersFromPrefs(): List {
- val jsonString = prefs.getString(SyncedFolderPrefs.KEY_SYNCED_FOLDERS_JSON, null)
- val oldUri = prefs.getString(SyncedFolderPrefs.KEY_LEGACY_SYNCED_FOLDER_URI, null)
- val folders = SyncedFolderPrefs.decodeSyncedFolders(
- jsonString = jsonString,
- legacyUri = oldUri,
- legacyLastScanTime = prefs.getLong(SyncedFolderPrefs.KEY_LEGACY_LAST_FOLDER_SCAN_TIME, 0L),
- legacyNameResolver = { uri -> getDisplayPathFromUri(appContext, uri) }
- )
+ val jsonString = prefs.getString(KEY_SYNCED_FOLDERS_JSON, null)
+ val folders = mutableListOf()
- if (jsonString == null && prefs.contains(SyncedFolderPrefs.KEY_LEGACY_SYNCED_FOLDER_URI) && oldUri != null) {
- saveSyncedFoldersToPrefs(folders)
- prefs.edit {
- remove(SyncedFolderPrefs.KEY_LEGACY_SYNCED_FOLDER_URI)
- remove(SyncedFolderPrefs.KEY_LEGACY_LAST_FOLDER_SCAN_TIME)
+ if (jsonString == null && prefs.contains(KEY_SYNCED_FOLDER_URI)) {
+ val oldUri = prefs.getString(KEY_SYNCED_FOLDER_URI, null)
+ val oldTime = prefs.getLong(KEY_LAST_FOLDER_SCAN_TIME, 0L)
+ if (oldUri != null) {
+ val name = getDisplayPathFromUri(appContext, oldUri)
+ val migrated = SyncedFolder(oldUri, name, oldTime, ANDROID_SYNCABLE_FILE_TYPES)
+ folders.add(migrated)
+ saveSyncedFoldersToPrefs(folders)
+
+ prefs.edit {
+ remove(KEY_SYNCED_FOLDER_URI)
+ remove(KEY_LAST_FOLDER_SCAN_TIME)
+ }
+ }
+ } else if (jsonString != null) {
+ try {
+ val jsonArray = JSONArray(jsonString)
+ for (i in 0 until jsonArray.length()) {
+ val obj = jsonArray.getJSONObject(i)
+
+ val allowedFileTypes = mutableSetOf()
+ if (obj.has("allowedFileTypes")) {
+ val typesArray = obj.getJSONArray("allowedFileTypes")
+ for (j in 0 until typesArray.length()) {
+ try {
+ allowedFileTypes.add(FileType.valueOf(typesArray.getString(j)))
+ } catch (_: Exception) {}
+ }
+ } else {
+ allowedFileTypes.addAll(ANDROID_SYNCABLE_FILE_TYPES)
+ }
+
+ folders.add(
+ SyncedFolder(
+ uriString = obj.getString("uri"),
+ name = obj.getString("name"),
+ lastScanTime = obj.optLong("lastScanTime", 0L),
+ allowedFileTypes = allowedFileTypes.filterTo(mutableSetOf()) { it in ANDROID_SYNCABLE_FILE_TYPES }
+ )
+ )
+ }
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to parse synced folders JSON")
}
}
return folders
}
private fun saveSyncedFoldersToPrefs(folders: List) {
- prefs.edit {
- putString(
- SyncedFolderPrefs.KEY_SYNCED_FOLDERS_JSON,
- SyncedFolderPrefs.encodeSyncedFolders(folders)
- )
+ val jsonArray = JSONArray()
+ folders.forEach { folder ->
+ val obj = JSONObject()
+ obj.put("uri", folder.uriString)
+ obj.put("name", folder.name)
+ obj.put("lastScanTime", folder.lastScanTime)
+ val typesArray = JSONArray()
+ folder.allowedFileTypes
+ .filter { it in ANDROID_SYNCABLE_FILE_TYPES }
+ .forEach { typesArray.put(it.name) }
+ obj.put("allowedFileTypes", typesArray)
+ jsonArray.put(obj)
}
+ prefs.edit { putString(KEY_SYNCED_FOLDERS_JSON, jsonArray.toString()) }
}
fun addSyncedFolder(folderUri: Uri) {
@@ -3105,13 +2482,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
)
val name = getDisplayPathFromUri(appContext, folderUri.toString())
- val newFolder = SyncedFolder(
- uriString = folderUri.toString(),
- name = name,
- lastScanTime = 0L,
- allowedFileTypes = ANDROID_SYNCABLE_FILE_TYPES,
- localSyncEnabled = true
- )
+ val newFolder = SyncedFolder(folderUri.toString(), name, 0L, ANDROID_SYNCABLE_FILE_TYPES)
val newStats = currentFolders + newFolder
saveSyncedFoldersToPrefs(newStats)
@@ -3170,50 +2541,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- fun setFolderLocalSyncEnabled(
- folder: SyncedFolder,
- enabled: Boolean,
- removeSyncDataFolder: Boolean = false
- ) {
- viewModelScope.launch {
- val currentFolders = _internalState.value.syncedFolders.toMutableList()
- val index = currentFolders.indexOfFirst { it.uriString == folder.uriString }
- if (index == -1) return@launch
-
- val updatedFolder = currentFolders[index].copy(localSyncEnabled = enabled)
- currentFolders[index] = updatedFolder
- saveSyncedFoldersToPrefs(currentFolders)
- _internalState.update { it.copy(syncedFolders = currentFolders) }
-
- if (enabled) {
- showBanner(appContext.getString(R.string.banner_folder_local_sync_enabled))
- triggerFolderSyncWorker(
- metadataOnly = false,
- showFeedback = true,
- targetFolderUriString = updatedFolder.uriString
- )
- } else {
- val workManager = WorkManager.getInstance(appContext)
- workManager.cancelUniqueWork(FolderSyncWorker.WORK_NAME_ONETIME)
- workManager.cancelUniqueWork(MetadataExtractionWorker.WORK_NAME)
-
- if (removeSyncDataFolder) {
- val removed = withContext(Dispatchers.IO) {
- LocalSyncUtils.deleteSyncDataFolder(appContext, updatedFolder.uriString.toUri())
- }
- val message = if (removed) {
- appContext.getString(R.string.banner_folder_local_sync_disabled_removed_data)
- } else {
- appContext.getString(R.string.banner_folder_sync_data_remove_failed)
- }
- showBanner(message, isError = !removed)
- } else {
- showBanner(appContext.getString(R.string.banner_folder_local_sync_disabled))
- }
- }
- }
- }
-
fun syncFolderMetadata(showFeedback: Boolean = false) {
triggerFolderSyncWorker(metadataOnly = true, showFeedback = showFeedback)
}
@@ -3227,21 +2554,11 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
showFeedback: Boolean,
targetFolderUriString: String? = null
) {
- val allFolders = _internalState.value.syncedFolders
- val folders = if (targetFolderUriString.isNullOrBlank()) {
- allFolders.filter { it.localSyncEnabled }
- } else {
- allFolders.filter { it.uriString == targetFolderUriString && it.localSyncEnabled }
- }
- if (folders.isEmpty()) {
- if (showFeedback) {
- showBanner(appContext.getString(R.string.error_no_enabled_folder_sync), isError = true)
- }
- return
- }
+ val folders = _internalState.value.syncedFolders
+ if (folders.isEmpty()) return
val targetFolderName = targetFolderUriString
- ?.let { target -> allFolders.firstOrNull { it.uriString == target }?.name ?: target }
+ ?.let { target -> folders.firstOrNull { it.uriString == target }?.name ?: target }
ReaderPerfLog.d(
"FolderSync request folders=${folders.size} target=${targetFolderName ?: "ALL"} " +
"metadataOnly=$metadataOnly feedback=$showFeedback"
@@ -3373,8 +2690,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
prefs.edit {
- remove(SyncedFolderPrefs.KEY_SYNCED_FOLDERS_JSON)
- remove(SyncedFolderPrefs.KEY_LEGACY_SYNCED_FOLDER_URI)
+ remove(KEY_SYNCED_FOLDERS_JSON)
+ remove(KEY_SYNCED_FOLDER_URI)
}
_internalState.update { it.copy(syncedFolders = emptyList()) }
}
@@ -3401,8 +2718,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
it.name
}
- val fileName = sharedCloudBookContentFileName(item.bookId, item.type)
- ?: throw Exception("Unsupported cloud file type: ${item.type}")
+ val fileExtension = item.type.name.lowercase()
+ val fileName = "${item.bookId}.$fileExtension"
val driveFileId = remoteFiles[fileName]?.id
if (driveFileId != null) {
@@ -3412,9 +2729,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
accessToken, driveFileId, destinationFile
)
) {
- if (item.fileContentModifiedTimestamp > 0L) {
- destinationFile.setLastModified(item.fileContentModifiedTimestamp)
- }
addFileToRecent(
destinationFile.toUri(),
item.type,
@@ -3673,38 +2987,25 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
private fun shouldDownloadRemoteBookContent(local: RecentFileItem, remote: RecentFileItem): Boolean {
- val localFile = local.getUri()?.path?.let(::File)
- val localContentTimestamp = local.fileContentModifiedTimestamp.takeIf { it > 0L }
- ?: localFile?.takeIf { it.isFile }?.lastModified()
- ?: 0L
return local.sourceFolderUri == null &&
!local.isDeleted &&
- local.type == remote.type &&
- sharedCloudBookContentFileName(local.bookId, local.type) != null &&
- shouldDownloadRemoteCloudBookContent(
- localFileAvailable = local.isAvailable && localFile?.isFile != false,
- localContentModifiedTimestamp = localContentTimestamp,
- remoteContentModifiedTimestamp = remote.fileContentModifiedTimestamp,
- remoteDeleted = remote.isDeleted
- )
+ local.type == FileType.EPUB &&
+ remote.type == FileType.EPUB &&
+ !remote.isDeleted &&
+ remote.fileContentModifiedTimestamp > 0L &&
+ remote.fileContentModifiedTimestamp > local.fileContentModifiedTimestamp
}
private fun shouldUploadLocalBookContent(local: RecentFileItem, remote: RecentFileItem?): Boolean {
- val localFile = local.getUri()?.path?.let(::File)
- val localContentTimestamp = local.fileContentModifiedTimestamp.takeIf { it > 0L }
- ?: localFile?.takeIf { it.isFile }?.lastModified()
- ?: 0L
return local.sourceFolderUri == null &&
- sharedCloudBookContentFileName(local.bookId, local.type) != null &&
- shouldUploadLocalCloudBookContent(
- localFileAvailable = local.isAvailable && localFile?.isFile == true,
- localContentModifiedTimestamp = localContentTimestamp,
- remoteContentModifiedTimestamp = remote?.fileContentModifiedTimestamp
- )
+ local.type == FileType.EPUB &&
+ local.fileContentModifiedTimestamp > 0L &&
+ local.fileContentModifiedTimestamp > (remote?.fileContentModifiedTimestamp ?: 0L)
}
private suspend fun downloadCloudBookFile(accessToken: String, remote: RecentFileItem): Boolean {
- val fileName = sharedCloudBookContentFileName(remote.bookId, remote.type) ?: return false
+ val fileExtension = remote.type.name.lowercase()
+ val fileName = "${remote.bookId}.$fileExtension"
val driveFileId = googleDriveRepository.getFiles(accessToken)
?.files
.orEmpty()
@@ -3733,17 +3034,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
return true
}
- private fun scheduleCloudContentRetry(bookIds: Set) {
- if (bookIds.isEmpty() || cloudContentRetryJob?.isActive == true) return
- cloudContentRetryJob = viewModelScope.launch {
- delay(CLOUD_CONTENT_RETRY_DELAY_MILLIS)
- if (uiState.value.isSyncEnabled) {
- logCloudSyncTrace { "android.full_sync.content_retry books=${bookIds.joinToString()}" }
- syncWithCloud(showBanner = false).join()
- }
- }
- }
-
fun setFolderSyncEnabled(enabled: Boolean) {
prefs.edit { putBoolean(KEY_FOLDER_SYNC_ENABLED, enabled) }
_internalState.update { it.copy(isFolderSyncEnabled = enabled) }
@@ -3758,20 +3048,12 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val currentUser = _internalState.value.currentUser
if (!hasPermissions || currentUser == null) {
- logCloudSyncTrace {
- "android.full_sync.skip reason=${if (!hasPermissions) "missing_drive_permissions" else "no_user"} " +
- "showBanner=$showBanner"
- }
if (showBanner) _internalState.update {
it.copy(errorMessage = appContext.getString(R.string.error_not_signed_in_sync))
}
return@launch
}
- logCloudSyncTrace {
- "android.full_sync.start user=${currentUser.uid} showBanner=$showBanner " +
- "folderSync=${_internalState.value.isFolderSyncEnabled}"
- }
if (showBanner) {
_internalState.update {
it.copy(bannerMessage = BannerMessage(appContext.getString(R.string.banner_cloud_sync_checking)))
@@ -3779,10 +3061,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
try {
- val accessToken = googleDriveRepository.getAccessToken(appContext) ?: run {
- logCloudSyncTrace { "android.full_sync.skip reason=no_access_token user=${currentUser.uid}" }
- return@launch
- }
+ val accessToken = googleDriveRepository.getAccessToken(appContext) ?: return@launch
val deviceId = getInstallationId()
val remoteBooksDeferred = async(Dispatchers.IO) {
@@ -3807,14 +3086,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val remoteBooks = remoteBooksDeferred.await()
.filterNot { it.isManualOnlyReaderFile() }
val remoteShelves = remoteShelvesDeferred.await()
- val initialDriveFiles = withContext(Dispatchers.IO) {
- googleDriveRepository.getFiles(accessToken)?.files.orEmpty().associateBy { it.name }
- }
- logCloudSyncTrace {
- "android.full_sync.loaded user=${currentUser.uid} device=$deviceId " +
- "localBooks=${localBooks.size} remoteBooks=${remoteBooks.size} " +
- "remoteShelves=${remoteShelves.size} driveFiles=${initialDriveFiles.size}"
- }
val syncableBookIds = (localBooks.map { it.bookId } + remoteBooks.map { it.bookId }).toSet()
val allKnownShelfNames =
(localShelfNames + remoteShelves.map { it.name }).toSet()
@@ -3832,23 +3103,17 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val localBooksMap = localBooks.associateBy { it.bookId }
val remoteBooksMap = remoteBooks.associateBy { it.bookId }
val allBookIds = (localBooksMap.keys + remoteBooksMap.keys).distinct()
- val pendingContentDownloads = mutableSetOf()
allBookIds.forEach { bookId ->
val local = localBooksMap[bookId]
val remote = remoteBooksMap[bookId]
if (local?.sourceFolderUri != null) {
- logCloudSyncTrace { "android.full_sync.book_skip reason=folder_book ${local.cloudSyncTraceSummary()}" }
Timber.d("Skipping cloud book metadata merge for local folder book: ${local.displayName}")
return@forEach
}
if (local != null && remote != null) {
- logCloudSyncTrace {
- "android.full_sync.compare book=$bookId ${local.cloudSyncTraceSummary()} " +
- "${remote.cloudSyncTraceSummary()} "
- }
Timber.tag("AnnotationSync").d(
"Checking $bookId. LocalTS: ${local.lastModifiedTimestamp}, RemoteTS: ${remote.lastModifiedTimestamp}, RemoteHasAnn: ${remote.hasAnnotations}"
)
@@ -3857,182 +3122,42 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
when {
local != null && remote == null -> {
if (local.isDeleted) {
- logCloudSyncTrace { "android.full_sync.decision action=upload_deleted_metadata ${local.cloudSyncTraceSummary()}" }
uploadSingleBookMetadata(local)
} else {
- logCloudSyncTrace { "android.full_sync.decision action=upload_new_book ${local.cloudSyncTraceSummary()}" }
uploadNewBookAndMetadata(local)
}
}
local == null && remote != null -> {
- if (remote.isDeleted) {
- logCloudSyncTrace { "android.full_sync.decision action=skip_deleted_remote_only ${remote.cloudSyncTraceSummary()}" }
- return@forEach
- }
- logCloudSyncTrace { "android.full_sync.decision action=apply_remote_new ${remote.cloudSyncTraceSummary()}" }
recentFilesRepository.addRecentFile(remote.toRecentFileItem())
if (remote.hasAnnotations) {
- val remoteAnnotationDriveTimestamp =
- initialDriveFiles[cloudPdfAnnotationDriveFileName(bookId)]?.modifiedTimeMillis ?: 0L
- val remoteAnnotationTimestamp = remote.effectiveAnnotationModifiedTimestamp(remoteAnnotationDriveTimestamp)
- logCloudAnnotationSyncTrace {
- "android.full_sync.remote_only_download book=$bookId remoteTs=${remote.lastModifiedTimestamp} " +
- "remoteAnnTs=$remoteAnnotationTimestamp remoteDriveAnnTs=$remoteAnnotationDriveTimestamp " +
- "remoteHasAnnotations=${remote.hasAnnotations}"
- }
- downloadAnnotationsForBook(accessToken, bookId, remoteAnnotationTimestamp)
+ downloadAnnotationsForBook(accessToken, bookId)
}
}
local != null && remote != null -> {
val remoteItem = remote.toRecentFileItem()
- val inkFile = pdfAnnotationRepository.getAnnotationFileForSync(bookId)
- val deletedInkFile = pdfAnnotationRepository.getDeletedAnnotationsFileForSync(bookId)
- val richTextFile = pdfRichTextRepository.getFileForSync(bookId)
- val layoutFile = pageLayoutRepository.getLayoutFile(bookId)
- val textBoxFile = pdfTextBoxRepository.getFileForSync(bookId)
- val highlightFile = pdfHighlightRepository.getFileForSync(bookId)
- val localSidecars = AndroidPdfCloudSidecarState(
- hasInk = inkFile.hasSyncableCloudAnnotationPayload(),
- inkTimestamp = inkFile?.lastModified() ?: 0L,
- hasDeletedInk = deletedInkFile.hasSyncableCloudAnnotationPayload(),
- deletedInkTimestamp = deletedInkFile?.lastModified() ?: 0L,
- hasRichText = richTextFile.hasSyncableCloudAnnotationPayload(),
- richTextTimestamp = richTextFile.lastModified(),
- hasLayout = layoutFile.exists(),
- layoutTimestamp = layoutFile.lastModified(),
- hasTextBoxes = textBoxFile.hasSyncableCloudAnnotationPayload(),
- textBoxesTimestamp = textBoxFile.lastModified(),
- hasHighlights = highlightFile.hasSyncableCloudAnnotationPayload(),
- highlightsTimestamp = highlightFile.lastModified()
- )
- val fileLastModified = localSidecars.annotationPayloadTimestamp
- val remoteAnnotationDriveTimestamp =
- initialDriveFiles[cloudPdfAnnotationDriveFileName(bookId)]?.modifiedTimeMillis ?: 0L
- val remoteAnnotationTimestamp = remote.effectiveAnnotationModifiedTimestamp(remoteAnnotationDriveTimestamp)
- val localAnnotationsShouldUpload = shouldUploadLocalPdfCloudAnnotations(
- localSidecars = localSidecars,
- remoteHasAnnotations = remote.hasAnnotations,
- remoteAnnotationModifiedTimestamp = remoteAnnotationTimestamp
- )
- logCloudAnnotationSyncTrace {
- "android.full_sync.inspect book=$bookId remoteHas=${remote.hasAnnotations} " +
- "remoteTs=${remote.lastModifiedTimestamp} remoteAnnTs=$remoteAnnotationTimestamp " +
- "remoteDriveAnnTs=$remoteAnnotationDriveTimestamp " +
- "localTs=${local.lastModifiedTimestamp} " +
- "remoteReadTs=${remote.effectiveReadingPositionModifiedTimestamp()} " +
- "localReadTs=${local.effectiveReadingPositionModifiedTimestamp()} " +
- "localPayload=${localSidecars.hasAnnotationPayload} " +
- "localPayloadTs=${localSidecars.annotationPayloadTimestamp} " +
- "layoutExists=${localSidecars.hasLayout} layoutTs=${localSidecars.layoutTimestamp} " +
- "shouldUploadLocal=$localAnnotationsShouldUpload"
- }
- if (remote.isDeleted) {
- val localWinsDeletedRemote = shouldUploadLocalCloudBookMetadataUpdate(
- localModifiedTimestamp = local.lastModifiedTimestamp,
- remoteModifiedTimestamp = remote.lastModifiedTimestamp
- )
- val remoteDeleteWins = shouldApplyRemoteCloudBookMetadataUpdate(
- localModifiedTimestamp = local.lastModifiedTimestamp,
- remoteModifiedTimestamp = remote.lastModifiedTimestamp
- )
- when {
- localWinsDeletedRemote -> {
- logCloudSyncTrace {
- "android.full_sync.decision action=resurrect_upload_local book=$bookId " +
- "localTs=${local.lastModifiedTimestamp} remoteTs=${remote.lastModifiedTimestamp} payloadSidecarTs=$fileLastModified"
- }
- if (shouldUploadLocalBookContent(local, null)) {
- uploadNewBookAndMetadata(local)
- } else {
- uploadSingleBookMetadata(local)
- }
- }
-
- remoteDeleteWins -> {
- logCloudSyncTrace {
- "android.full_sync.decision action=apply_remote_delete book=$bookId " +
- "localTs=${local.lastModifiedTimestamp} remoteTs=${remote.lastModifiedTimestamp} payloadSidecarTs=$fileLastModified"
- }
- recentFilesRepository.deleteFilePermanently(listOf(bookId))
- }
-
- else -> {
- logCloudSyncTrace {
- "android.full_sync.decision action=skip_equal_delete book=$bookId " +
- "localTs=${local.lastModifiedTimestamp} remoteTs=${remote.lastModifiedTimestamp} payloadSidecarTs=$fileLastModified"
- }
- }
- }
- return@forEach
- }
- val localWithRemoteEpubAnnotations = local.mergeRemoteEpubAnnotationMetadata(remote)
- val effectiveLocal = if (localWithRemoteEpubAnnotations != local) {
- logCloudSyncTrace {
- "android.full_sync.decision action=merge_remote_epub_annotations book=$bookId " +
- "local=${local.cloudSyncTraceSummary()} ${remote.cloudSyncTraceSummary()} " +
- "merged=${localWithRemoteEpubAnnotations.cloudSyncTraceSummary()}"
- }
- recentFilesRepository.addRecentFile(localWithRemoteEpubAnnotations)
- localWithRemoteEpubAnnotations
- } else {
- local
- }
- val localReadingTimestamp = effectiveLocal.effectiveReadingPositionModifiedTimestamp()
- val remoteReadingTimestamp = remote.effectiveReadingPositionModifiedTimestamp()
- val localReadingPositionShouldUpload = localReadingTimestamp > remoteReadingTimestamp
- val shouldDownloadContent = shouldDownloadRemoteBookContent(effectiveLocal, remoteItem)
+ val shouldDownloadContent = shouldDownloadRemoteBookContent(local, remoteItem)
val downloadedRemoteContent = if (shouldDownloadContent) {
- logCloudSyncTrace {
- "android.full_sync.content_download_start book=$bookId " +
- "localContentTs=${effectiveLocal.fileContentModifiedTimestamp} remoteContentTs=${remote.fileContentModifiedTimestamp}"
- }
- downloadCloudBookFile(accessToken, remoteItem.copy(displayName = remoteItem.displayName.ifBlank { effectiveLocal.displayName }))
+ downloadCloudBookFile(accessToken, remoteItem.copy(displayName = remoteItem.displayName.ifBlank { local.displayName }))
} else {
false
}
- logCloudSyncTrace {
- "android.full_sync.content_decision book=$bookId shouldDownload=$shouldDownloadContent " +
- "downloaded=$downloadedRemoteContent localPayloadSidecarTs=$fileLastModified " +
- "localLayoutTs=${localSidecars.layoutTimestamp.takeIf { localSidecars.hasLayout } ?: 0L}"
- }
- if (shouldDownloadContent && !downloadedRemoteContent) {
- pendingContentDownloads += bookId
- }
- if (shouldUploadLocalCloudBookMetadataUpdate(
- localModifiedTimestamp = effectiveLocal.lastModifiedTimestamp,
- remoteModifiedTimestamp = remote.lastModifiedTimestamp
- )
- ) {
- logCloudSyncTrace {
- "android.full_sync.decision action=upload_local book=$bookId " +
- "localTs=${effectiveLocal.lastModifiedTimestamp} remoteTs=${remote.lastModifiedTimestamp} " +
- "localReadTs=$localReadingTimestamp remoteReadTs=$remoteReadingTimestamp payloadSidecarTs=$fileLastModified " +
- "uploadContent=${shouldUploadLocalBookContent(effectiveLocal, remoteItem)}"
- }
- if (shouldUploadLocalBookContent(effectiveLocal, remoteItem)) {
- uploadNewBookAndMetadata(effectiveLocal)
+ if (local.lastModifiedTimestamp > remote.lastModifiedTimestamp) {
+ if (shouldUploadLocalBookContent(local, remoteItem)) {
+ uploadNewBookAndMetadata(local)
} else {
- uploadSingleBookMetadata(effectiveLocal)
+ uploadSingleBookMetadata(local)
}
} else {
val isMetadataNewer =
- shouldApplyRemoteCloudBookMetadataUpdate(
- localModifiedTimestamp = effectiveLocal.lastModifiedTimestamp,
- remoteModifiedTimestamp = remote.lastModifiedTimestamp
- )
+ remote.lastModifiedTimestamp > local.lastModifiedTimestamp
if (isMetadataNewer) {
- logCloudSyncTrace {
- "android.full_sync.decision action=apply_remote_metadata book=$bookId " +
- "localTs=${effectiveLocal.lastModifiedTimestamp} remoteTs=${remote.lastModifiedTimestamp} payloadSidecarTs=$fileLastModified " +
- "downloadedContent=$downloadedRemoteContent"
- }
val remoteForLocalDb = if (shouldDownloadContent && !downloadedRemoteContent) {
remote.toRecentFileItem().copy(
- fileContentModifiedTimestamp = effectiveLocal.fileContentModifiedTimestamp
+ fileContentModifiedTimestamp = local.fileContentModifiedTimestamp
)
} else {
remote.toRecentFileItem()
@@ -4040,76 +3165,31 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
recentFilesRepository.addRecentFile(
remoteForLocalDb
)
- if (localAnnotationsShouldUpload || localReadingPositionShouldUpload) {
- recentFilesRepository.getFileByBookId(bookId)?.let { merged ->
- logCloudAnnotationSyncTrace {
- "android.full_sync.upload_local_annotations book=$bookId reason=remote_metadata_newer " +
- "remoteTs=${remote.lastModifiedTimestamp} localPayloadTs=$fileLastModified " +
- "localReadTs=$localReadingTimestamp remoteReadTs=$remoteReadingTimestamp " +
- "uploadReadingPosition=$localReadingPositionShouldUpload"
- }
- logCloudSyncTrace {
- "android.full_sync.decision action=upload_local_supplement book=$bookId " +
- "remoteMetadataTs=${remote.lastModifiedTimestamp} payloadSidecarTs=$fileLastModified " +
- "uploadAnnotations=$localAnnotationsShouldUpload uploadReadingPosition=$localReadingPositionShouldUpload"
- }
- uploadSingleBookMetadata(merged)
- }
- }
- } else {
- logCloudSyncTrace {
- "android.full_sync.decision action=metadata_noop book=$bookId " +
- "localTs=${effectiveLocal.lastModifiedTimestamp} remoteTs=${remote.lastModifiedTimestamp} " +
- "localReadTs=$localReadingTimestamp remoteReadTs=$remoteReadingTimestamp payloadSidecarTs=$fileLastModified"
- }
- if (localAnnotationsShouldUpload || localReadingPositionShouldUpload) {
- logCloudAnnotationSyncTrace {
- "android.full_sync.upload_local_annotations book=$bookId reason=metadata_noop " +
- "remoteTs=${remote.lastModifiedTimestamp} localPayloadTs=$fileLastModified " +
- "localReadTs=$localReadingTimestamp remoteReadTs=$remoteReadingTimestamp"
- }
- logCloudSyncTrace {
- "android.full_sync.decision action=upload_local_supplement book=$bookId " +
- "metadataEqual=${effectiveLocal.lastModifiedTimestamp == remote.lastModifiedTimestamp} " +
- "uploadAnnotations=$localAnnotationsShouldUpload uploadReadingPosition=$localReadingPositionShouldUpload " +
- "payloadSidecarTs=$fileLastModified"
- }
- uploadSingleBookMetadata(effectiveLocal)
- }
}
- val shouldDownloadRemoteAnnotations = shouldDownloadRemotePdfCloudAnnotations(
- localSidecars = localSidecars,
- localAnnotationsShouldUpload = localAnnotationsShouldUpload,
- remoteHasAnnotations = remote.hasAnnotations,
- remoteAnnotationModifiedTimestamp = remoteAnnotationTimestamp
- )
+ val inkFile = pdfAnnotationRepository.getAnnotationFileForSync(bookId)
+ val richTextFile = pdfRichTextRepository.getFileForSync(bookId)
+ val layoutFile = pageLayoutRepository.getLayoutFile(bookId)
+ val textBoxFile = pdfTextBoxRepository.getFileForSync(bookId)
+ val highlightFile = pdfHighlightRepository.getFileForSync(bookId)
- if (shouldDownloadRemoteAnnotations) {
- logCloudAnnotationSyncTrace {
- "android.full_sync.download_remote_annotations book=$bookId " +
- "metadataNewer=$isMetadataNewer localPayloadMissing=${!localSidecars.hasAnnotationPayload} " +
- "remoteTs=${remote.lastModifiedTimestamp} remoteAnnTs=$remoteAnnotationTimestamp " +
- "localPayloadTs=$fileLastModified " +
- "layoutTs=${localSidecars.layoutTimestamp.takeIf { localSidecars.hasLayout } ?: 0L}"
- }
- logCloudSyncTrace {
- "android.full_sync.sidecar_download_start book=$bookId reason=" +
- "metadataNewer=$isMetadataNewer localPayloadMissing=${!localSidecars.hasAnnotationPayload} " +
- "remoteTs=${remote.lastModifiedTimestamp} remoteAnnTs=$remoteAnnotationTimestamp " +
- "localPayloadSidecarTs=$fileLastModified " +
- "localLayoutTs=${localSidecars.layoutTimestamp.takeIf { localSidecars.hasLayout } ?: 0L}"
- }
+ val anyLocalFileExists =
+ (inkFile?.exists() == true) || richTextFile.exists() || layoutFile.exists() || textBoxFile.exists() || highlightFile.exists()
+ val localFileMissing = !anyLocalFileExists
+
+ val fileLastModified = maxOf(
+ inkFile?.lastModified() ?: 0L,
+ richTextFile.lastModified(),
+ layoutFile.lastModified(),
+ textBoxFile.lastModified(),
+ highlightFile.lastModified()
+ )
+ val isFileStale =
+ remote.hasAnnotations && (remote.lastModifiedTimestamp > fileLastModified)
+
+ if (isMetadataNewer || localFileMissing && remote.hasAnnotations || isFileStale) {
Timber.tag("AnnotationSync").d("Triggering download for $bookId.")
- downloadAnnotationsForBook(accessToken, bookId, remoteAnnotationTimestamp)
- } else {
- logCloudAnnotationSyncTrace {
- "android.full_sync.skip_remote_annotations book=$bookId " +
- "remoteHas=${remote.hasAnnotations} localShouldUpload=$localAnnotationsShouldUpload " +
- "metadataNewer=$isMetadataNewer localPayload=${localSidecars.hasAnnotationPayload} " +
- "remoteTs=${remote.lastModifiedTimestamp} remoteAnnTs=$remoteAnnotationTimestamp " +
- "localPayloadTs=$fileLastModified"
- }
+ downloadAnnotationsForBook(accessToken, bookId)
}
}
}
@@ -4194,80 +3274,41 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
googleDriveRepository.getFiles(accessToken)?.files.orEmpty().associateBy { it.name }
}
+ val downloadJobs = mutableListOf()
+
finalMergedBooks.forEach { book ->
if (book.sourceFolderUri != null) return@forEach
- val fileName = sharedCloudBookContentFileName(book.bookId, book.type) ?: return@forEach
+ val fileExtension = book.type.name.lowercase()
+ val fileName = "${book.bookId}.$fileExtension"
if (book.isDeleted) {
remoteFiles[fileName]?.id?.let { fileId ->
Timber.d("Deleting from Drive: $fileName")
googleDriveRepository.deleteDriveFile(accessToken, fileId)
}
- remoteFiles[cloudPdfAnnotationDriveFileName(book.bookId)]?.id?.let { fileId ->
- Timber.d("Deleting annotation bundle from Drive: ${book.bookId}")
- googleDriveRepository.deleteDriveFile(accessToken, fileId)
- }
recentFilesRepository.deleteFilePermanently(listOf(book.bookId))
} else if (
book.sourceFolderUri == null &&
book.isAvailable &&
!remoteFiles.containsKey(fileName)
) {
- val remoteItem = remoteBooksMap[book.bookId]?.toRecentFileItem()
- if (remoteItem == null || shouldUploadLocalBookContent(book, remoteItem)) {
- book.getUri()?.path?.let { path ->
- val file = File(path)
- if (file.exists()) {
- Timber.d("Uploading book: ${book.displayName}")
- val uploadedFile = googleDriveRepository.uploadFile(
- accessToken, book.bookId, file, book.type
- )
- if (uploadedFile != null) {
- val contentTimestamp = book.fileContentModifiedTimestamp.takeIf { it > 0L }
- ?: file.lastModified()
- uploadSingleBookMetadata(
- book.copy(
- fileSize = file.length(),
- fileContentModifiedTimestamp = contentTimestamp
- )
- )
- }
- }
- }
- } else {
- pendingContentDownloads += book.bookId
- logCloudSyncTrace {
- "android.full_sync.content_wait_missing_remote book=${book.bookId} " +
- "file=$fileName localContentTs=${book.fileContentModifiedTimestamp} " +
- "remoteContentTs=${remoteItem.fileContentModifiedTimestamp}"
+ book.getUri()?.path?.let { path ->
+ val file = File(path)
+ if (file.exists()) {
+ Timber.d("Uploading book: ${book.displayName}")
+ googleDriveRepository.uploadFile(
+ accessToken, book.bookId, file, book.type
+ )
}
}
} else if (!book.isAvailable && remoteFiles.containsKey(fileName)) {
Timber.d("Sync: Triggering auto-download for ${book.displayName}")
- val remoteItem = remoteBooksMap[book.bookId]
- ?.toRecentFileItem()
- ?.copy(displayName = book.displayName)
- ?: book
- val downloaded = downloadCloudBookFile(accessToken, remoteItem)
- if (!downloaded) {
- pendingContentDownloads += book.bookId
- }
- } else if (!book.isAvailable) {
- pendingContentDownloads += book.bookId
+ downloadJobs.add(downloadBook(book))
}
}
- if (pendingContentDownloads.isNotEmpty()) {
- logCloudSyncTrace {
- "android.full_sync.content_pending books=${pendingContentDownloads.joinToString()}"
- }
- scheduleCloudContentRetry(pendingContentDownloads)
- } else {
- cloudContentRetryJob?.cancel()
- cloudContentRetryJob = null
- }
+ downloadJobs.joinAll()
syncFonts(currentUser.uid)
- logCloudSyncTrace { "android.full_sync.complete user=${currentUser.uid}" }
if (showBanner) {
_internalState.update {
it.copy(
@@ -4276,7 +3317,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
} catch (e: Exception) {
- logCloudSyncError(e) { "android.full_sync.failed user=${currentUser.uid}" }
Timber.tag("AnnotationSync").e(e, "Error during cloud sync")
if (showBanner) {
_internalState.update {
@@ -4286,41 +3326,21 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- private suspend fun downloadAnnotationsForBook(
- accessToken: String,
- bookId: String,
- annotationModifiedTimestamp: Long
- ) {
+ private suspend fun downloadAnnotationsForBook(accessToken: String, bookId: String) {
// We download to a temp location first to inspect the content
val tempDownloadFile = File(appContext.cacheDir, "temp_download_${bookId}.json")
- logCloudSyncTrace {
- "android.sidecar_download.start book=$bookId remoteAnnTs=$annotationModifiedTimestamp temp=${tempDownloadFile.name}"
- }
- logCloudAnnotationSyncTrace {
- "android.download.start book=$bookId remoteAnnTs=$annotationModifiedTimestamp temp=${tempDownloadFile.name}"
- }
Timber.tag("AnnotationSync").d("Attempting download of bundle for $bookId.")
val didDownload =
googleDriveRepository.downloadAnnotationFile(accessToken, bookId, tempDownloadFile)
if (didDownload && tempDownloadFile.exists()) {
- logCloudAnnotationSyncTrace {
- "android.download.success book=$bookId remoteAnnTs=$annotationModifiedTimestamp bytes=${tempDownloadFile.length()}"
- }
- logCloudSyncTrace {
- "android.sidecar_download.success book=$bookId remoteAnnTs=$annotationModifiedTimestamp bytes=${tempDownloadFile.length()}"
- }
Timber.tag("AnnotationSync")
.d("Download SUCCESS. Size: ${tempDownloadFile.length()}. Unpacking...")
try {
val jsonString = tempDownloadFile.readText()
- val appliedAnnotationTimestamp =
- annotationModifiedTimestamp.takeIf { it > 0L }
- ?: tempDownloadFile.lastModified().takeIf { it > 0L }
- ?: 0L
Timber.d(
"android.cloud.import.downloaded book=$bookId rawLen=${jsonString.length}"
)
@@ -4338,22 +3358,16 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} catch (_: Exception) {
false
}
- logCloudAnnotationSyncTrace {
- "android.download.inspect book=$bookId isBundle=$isBundle rawBytes=${jsonString.length} " +
- "appliedAnnTs=$appliedAnnotationTimestamp rawPreview=${jsonString.take(80).replace('\n', ' ')}"
- }
val inkFile = pdfAnnotationRepository.getAnnotationFileForSync(bookId) ?: File(
appContext.filesDir, "annotations/annotation_$bookId.json"
)
- val deletedInkFile = File(appContext.filesDir, "annotations/deleted_annotation_$bookId.json")
val richTextFile = pdfRichTextRepository.getFileForSync(bookId)
val layoutFile = pageLayoutRepository.getLayoutFile(bookId)
val textBoxFile = pdfTextBoxRepository.getFileForSync(bookId)
val highlightFile = pdfHighlightRepository.getFileForSync(bookId)
inkFile.parentFile?.mkdirs()
- deletedInkFile.parentFile?.mkdirs()
richTextFile.parentFile?.mkdirs()
layoutFile.parentFile?.mkdirs()
textBoxFile.parentFile?.mkdirs()
@@ -4366,94 +3380,44 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.d(
"android.cloud.import.bundle book=$bookId hasRichText=${bundle.has("text")} keys=${bundle.keys().asSequence().toList()}"
)
- logCloudAnnotationSyncTrace {
- "android.download.bundle_keys book=$bookId keys=${bundle.keys().asSequence().toList()} " +
- "hasInk=${bundle.has("ink")} hasText=${bundle.has("text")} " +
- "hasLayout=${bundle.has("layout")} hasTextBoxes=${bundle.has("textBoxes")} " +
- "hasHighlights=${bundle.has("highlights")} " +
- "hasDeletedInk=${bundle.has(SharedPdfAnnotationSidecarCodec.KEY_PDF_ANNOTATION_DELETIONS)}"
- }
fun writeSafe(key: String, file: File) {
if (bundle.has(key)) {
file.parentFile?.mkdirs()
val content = bundle.get(key).toString()
file.writeText(content)
- appliedAnnotationTimestamp.takeIf { it > 0L }?.let(file::setLastModified)
- logCloudAnnotationSyncTrace {
- "android.download.write key=$key book=$bookId bytes=${content.length} " +
- "path=${file.absolutePath.cloudSyncPreview(140)} ts=${file.lastModified()}"
- }
if (key == "text") {
Timber.d(
"android.cloud.import.writeRichText book=$bookId rawLen=${content.length} file=${file.absolutePath}"
)
}
} else {
- if (key == "layout") {
- logCloudAnnotationSyncTrace {
- "android.download.preserve_missing key=layout book=$bookId " +
- "path=${file.absolutePath.cloudSyncPreview(140)} exists=${file.exists()}"
- }
- Timber.d(
- "android.cloud.import.preserveMissingLayout book=$bookId file=${file.absolutePath}"
- )
- return
- }
if (key == "text" && file.exists()) {
Timber.d(
"android.cloud.import.deleteMissingRichText book=$bookId file=${file.absolutePath}"
)
}
- if (file.exists()) {
- val deleted = file.delete()
- logCloudAnnotationSyncTrace {
- "android.download.delete_missing key=$key book=$bookId deleted=$deleted " +
- "path=${file.absolutePath.cloudSyncPreview(140)}"
- }
- } else {
- logCloudAnnotationSyncTrace {
- "android.download.missing_key key=$key book=$bookId path=${file.absolutePath.cloudSyncPreview(140)}"
- }
- }
+ if (file.exists()) file.delete()
}
}
writeSafe("ink", inkFile)
- writeSafe(SharedPdfAnnotationSidecarCodec.KEY_PDF_ANNOTATION_DELETIONS, deletedInkFile)
writeSafe("text", richTextFile)
writeSafe("layout", layoutFile)
writeSafe("textBoxes", textBoxFile)
writeSafe("highlights", highlightFile)
- logCloudSyncTrace {
- "android.sidecar_download.applied_bundle book=$bookId remoteAnnTs=$annotationModifiedTimestamp " +
- "keys=${bundle.keys().asSequence().toList()}"
- }
Timber.tag("AnnotationSync").d("Unpacked unified bundle.")
} else {
Timber.tag("AnnotationSync").d("Detected legacy format (Ink only).")
inkFile.writeText(jsonString)
- appliedAnnotationTimestamp.takeIf { it > 0L }?.let(inkFile::setLastModified)
- logCloudAnnotationSyncTrace {
- "android.download.write_legacy_ink book=$bookId bytes=${jsonString.length} " +
- "path=${inkFile.absolutePath.cloudSyncPreview(140)} ts=${inkFile.lastModified()}"
- }
- logCloudSyncTrace { "android.sidecar_download.applied_legacy book=$bookId remoteAnnTs=$annotationModifiedTimestamp" }
}
} catch (e: Exception) {
- logCloudAnnotationSyncError(e) { "android.download.apply_failed book=$bookId remoteAnnTs=$annotationModifiedTimestamp" }
- logCloudSyncError(e) { "android.sidecar_download.apply_failed book=$bookId remoteAnnTs=$annotationModifiedTimestamp" }
Timber.e(e, "Error unpacking synced annotation data")
} finally {
tempDownloadFile.delete()
}
} else {
- logCloudAnnotationSyncTrace {
- "android.download.missing book=$bookId remoteAnnTs=$annotationModifiedTimestamp didDownload=$didDownload " +
- "tempExists=${tempDownloadFile.exists()} tempBytes=${tempDownloadFile.length()}"
- }
- logCloudSyncTrace { "android.sidecar_download.missing book=$bookId remoteAnnTs=$annotationModifiedTimestamp" }
Timber.tag("AnnotationSync")
.d("FAILURE: No bundle found on Drive for $bookId (or download failed)")
}
@@ -4647,7 +3611,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
coverPath = recentFilesRepository.saveCoverToCache(coverBitmap, uri)
}
}
- } else if (uri.scheme != "opds-pse" && type in COMIC_ARCHIVE_FILE_TYPES) {
+ } else if (uri.scheme != "opds-pse" && (type == FileType.CBZ || type == FileType.CBR || type == FileType.CB7)) {
if (coverPath == null) {
var cacheFile: File? = null
try {
@@ -4657,7 +3621,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
cacheFile.outputStream().use { output -> input.copyTo(output) }
}
}
- val archiveDoc = org.dueattendant149.bookreader.pdf.ArchiveDocumentWrapper(cacheFile)
+ val archiveDoc = com.aryan.reader.pdf.ArchiveDocumentWrapper(cacheFile)
if (archiveDoc.getPageCount() > 0) {
val page = archiveDoc.openPage(0)
if (page != null) {
@@ -4677,7 +3641,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
archiveDoc.close()
} catch (e: Exception) {
- Timber.e(e, "Error generating comic archive cover")
+ Timber.e(e, "Error generating CBZ cover")
} finally {
try {
if (cacheFile?.exists() == true) {
@@ -4935,12 +3899,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- fun onFileSelected(
- uri: Uri,
- isFromRecent: Boolean = false,
- isExternalIntent: Boolean = false,
- isTemporaryExternalIntent: Boolean = false
- ) {
+ fun onFileSelected(uri: Uri, isFromRecent: Boolean = false, isExternalIntent: Boolean = false) {
if (isFromRecent) {
Timber.i("Opening recent file: $uri")
viewModelScope.launch {
@@ -4953,7 +3912,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
} else {
Timber.i("Importing new file: $uri")
- importExternalFile(uri, isExternalIntent, isTemporaryExternalIntent)
+ importExternalFile(uri, isExternalIntent)
}
}
@@ -5003,22 +3962,9 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- private fun importExternalFile(
- externalUri: Uri,
- isExternalIntent: Boolean = false,
- isTemporaryExternalIntent: Boolean = false
- ) {
- if (isTemporaryExternalIntent) {
- openTemporaryExternalFile(externalUri)
- return
- }
-
+ private fun importExternalFile(externalUri: Uri, isExternalIntent: Boolean = false) {
_internalState.update {
- it.copy(
- isLoading = true,
- errorMessage = null,
- contextualActionItems = emptySet()
- )
+ it.copy(isLoading = true, errorMessage = null, contextualActionItems = emptySet())
}
viewModelScope.launch {
@@ -5028,11 +3974,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (importResult != null) {
val (internalUri, bookId, type) = importResult
if (isExternalIntent) {
- trackExternalOpenForClose(
- bookId = bookId,
- importedCopyUriString = internalUri.toString(),
- isTemporaryExternalIntent = isTemporaryExternalIntent
- )
+ externalOpenedBookId = bookId
}
val displayName = getFileNameFromUri(externalUri, appContext) ?: "Unknown File"
openBook(
@@ -5047,13 +3989,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val existingItem = recentFilesRepository.getFileByBookId(hash)
if (existingItem != null) {
Timber.i("Re-selected an existing book. Opening it.")
- if (isTemporaryExternalIntent) {
- trackExternalOpenForClose(
- bookId = existingItem.bookId,
- importedCopyUriString = null,
- isTemporaryExternalIntent = true
- )
- }
onRecentFileClicked(existingItem)
_internalState.update { it.copy(isLoading = false) }
return@launch
@@ -5087,72 +4022,14 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- private fun openTemporaryExternalFile(externalUri: Uri) {
- _internalState.update {
- it.copy(
- isLoading = true,
- isTemporaryExternalOpen = true,
- errorMessage = null,
- contextualActionItems = emptySet()
- )
- }
- viewModelScope.launch {
- val type = getFileTypeFromUri(externalUri, appContext)
- if (type == null) {
- _internalState.update {
- it.copy(
- isLoading = false,
- isTemporaryExternalOpen = false,
- errorMessage = appContext.getString(R.string.error_unsupported_file_type)
- )
- }
- return@launch
- }
-
- val displayName = getFileNameFromUri(externalUri, appContext) ?: "Temporary File"
- val bookId = "temporary-${UUID.randomUUID()}"
- trackExternalOpenForClose(
- bookId = bookId,
- importedCopyUriString = null,
- isTemporaryExternalIntent = true
- )
- openBook(
- uri = externalUri,
- bookId = bookId,
- type = type,
- originalDisplayName = displayName,
- persistToLibrary = false
- )
- }
- }
-
fun saveHighlights(bookId: String, highlightsJson: String) {
viewModelScope.launch {
val currentBookUri = _internalState.value.selectedPdfUri ?: _internalState.value.selectedEpubUri
if (currentBookUri != null) {
recentFilesRepository.getFileByUri(currentBookUri.toString())?.let { item ->
- if (annotationJsonEquivalentForNoop(item.highlightsJson, highlightsJson)) {
- logCloudSyncTrace {
- "android.reader.highlights_save_noop book=${item.bookId} highlights=${highlightsJson.cloudSyncAnnotationSummary()}"
- }
- return@launch
- }
- logCloudSyncTrace {
- "android.reader.highlights_save book=${item.bookId} highlights=${highlightsJson.cloudSyncAnnotationSummary()}"
- }
recentFilesRepository.updateHighlights(item.bookId, highlightsJson)
}
} else if (bookId.isNotBlank()) {
- val existing = recentFilesRepository.getFileByBookId(bookId)
- if (annotationJsonEquivalentForNoop(existing?.highlightsJson, highlightsJson)) {
- logCloudSyncTrace {
- "android.reader.highlights_save_noop book=$bookId highlights=${highlightsJson.cloudSyncAnnotationSummary()}"
- }
- return@launch
- }
- logCloudSyncTrace {
- "android.reader.highlights_save book=$bookId highlights=${highlightsJson.cloudSyncAnnotationSummary()}"
- }
recentFilesRepository.updateHighlights(bookId, highlightsJson)
}
}
@@ -5352,7 +4229,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- protected open suspend fun cleanupBookDataLocally(bookId: String) {
+ private suspend fun cleanupBookDataLocally(bookId: String) {
pdfTextRepository.clearBookText(bookId)
clearImportedFileCache(bookId)
bookCacheDao.deleteEntireBookCache(bookId)
@@ -5397,8 +4274,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
isInitialPageExplicit: Boolean = false,
initialLocatorOverride: Locator? = null,
initialCfiOverride: String? = null,
- preserveTtsOnOpen: Boolean = false,
- persistToLibrary: Boolean = true
+ preserveTtsOnOpen: Boolean = false
) {
val openBookStartTime = System.currentTimeMillis()
ReaderPerfLog.d("FileOpen start bookId=$bookId type=$type")
@@ -5492,18 +4368,16 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
)
}
ReaderPerfLog.d("FileOpen ready bookId=$bookId type=$type elapsed=${System.currentTimeMillis() - openBookStartTime}ms")
- if (persistToLibrary) {
- persistReaderSession(bookId, type)
- addFileToRecent(
- uri,
- type,
- bookId,
- customDisplayName = originalDisplayName,
- isRecent = true,
- sourceFolderUri = null,
- bundleResult = bundleResult
- )
- }
+ persistReaderSession(bookId, type)
+ addFileToRecent(
+ uri,
+ type,
+ bookId,
+ customDisplayName = originalDisplayName,
+ isRecent = true,
+ sourceFolderUri = null,
+ bundleResult = bundleResult
+ )
if (!suppressNavigation) {
Timber.tag("FileSwitch").d("PDF state updated, emitting navigation event")
@@ -5527,12 +4401,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} else {
null
}
- logCloudSyncTrace {
- "android.reader.open_epub_position book=$bookId " +
- "overrideLocator=$initialLocatorOverride overrideCfi=${initialCfiOverride.cloudSyncPreview()} " +
- (recentItem?.cloudSyncTraceSummary("recent") ?: "recent=null") +
- " chosenLocator=$locator chosenCfi=${(initialCfiOverride ?: recentItem?.lastPositionCfi).cloudSyncPreview()}"
- }
_internalState.update {
it.copy(
@@ -5544,9 +4412,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
)
}
ReaderPerfLog.d("FileOpen ready bookId=$bookId type=$type elapsed=${System.currentTimeMillis() - openBookStartTime}ms")
- if (persistToLibrary) {
- persistReaderSession(bookId, type)
- }
+ persistReaderSession(bookId, type)
if (!suppressNavigation) {
Timber.tag("FileSwitch").d("EPUB state updated, emitting navigation event")
@@ -5555,22 +4421,22 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
when (type) {
FileType.EPUB -> {
- loadEpub(uri, bookId, customDisplayName = originalDisplayName, bundleResult = bundleResult, persistToLibrary = persistToLibrary)
+ loadEpub(uri, bookId, customDisplayName = originalDisplayName, bundleResult = bundleResult)
}
FileType.MOBI -> {
- loadMobi(uri, bookId, customDisplayName = originalDisplayName, bundleResult = bundleResult, persistToLibrary = persistToLibrary)
+ loadMobi(uri, bookId, customDisplayName = originalDisplayName, bundleResult = bundleResult)
}
FileType.FB2 -> {
- loadFb2(uri, bookId, customDisplayName = originalDisplayName, bundleResult = bundleResult, persistToLibrary = persistToLibrary)
+ loadFb2(uri, bookId, customDisplayName = originalDisplayName, bundleResult = bundleResult)
}
FileType.ODT, FileType.FODT -> {
- loadOdt(uri, bookId, type == FileType.FODT, customDisplayName = originalDisplayName, bundleResult = bundleResult, persistToLibrary = persistToLibrary)
+ loadOdt(uri, bookId, type == FileType.FODT, customDisplayName = originalDisplayName, bundleResult = bundleResult)
}
else -> {
loadSingleFile(
- uri, bookId, type, customDisplayName = originalDisplayName, bundleResult = bundleResult, persistToLibrary = persistToLibrary
+ uri, bookId, type, customDisplayName = originalDisplayName, bundleResult = bundleResult
)
}
}
@@ -5593,13 +4459,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- private fun loadFb2(
- uri: Uri,
- bookId: String,
- customDisplayName: String? = null,
- bundleResult: CalibreBundleResult? = null,
- persistToLibrary: Boolean = true
- ) {
+ private fun loadFb2(uri: Uri, bookId: String, customDisplayName: String? = null, bundleResult: CalibreBundleResult? = null) {
val loadStart = System.currentTimeMillis()
Timber.tag("FileOpenPerf").d("[$bookId] loadFb2 START")
viewModelScope.launch {
@@ -5621,11 +4481,9 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.i("FB2 parsing successful. Title: ${fb2Book.title}")
Timber.tag("FileOpenPerf").d("[$bookId] loadFb2 completed | chapters=${fb2Book.chapters.size} | elapsed=${System.currentTimeMillis() - loadStart}ms")
- if (persistToLibrary) {
- addFileToRecent(
- uri, FileType.FB2, bookId, fb2Book, customDisplayName, isRecent = true, sourceFolderUri = null, bundleResult = bundleResult
- )
- }
+ addFileToRecent(
+ uri, FileType.FB2, bookId, fb2Book, customDisplayName, isRecent = true, sourceFolderUri = null, bundleResult = bundleResult
+ )
_internalState.update { it.copy(selectedEpubBook = fb2Book, isLoading = false) }
} catch (e: Exception) {
@@ -5637,14 +4495,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- private fun loadOdt(
- uri: Uri,
- bookId: String,
- isFlat: Boolean,
- customDisplayName: String? = null,
- bundleResult: CalibreBundleResult? = null,
- persistToLibrary: Boolean = true
- ) {
+ private fun loadOdt(uri: Uri, bookId: String, isFlat: Boolean, customDisplayName: String? = null, bundleResult: CalibreBundleResult? = null) {
val loadStart = System.currentTimeMillis()
Timber.tag("FileOpenPerf").d("[$bookId] loadOdt START | isFlat=$isFlat")
viewModelScope.launch {
@@ -5667,11 +4518,9 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.i("ODT parsing successful. Title: ${odtBook.title}")
Timber.tag("FileOpenPerf").d("[$bookId] loadOdt completed | chapters=${odtBook.chapters.size} | elapsed=${System.currentTimeMillis() - loadStart}ms")
- if (persistToLibrary) {
- addFileToRecent(
- uri, if (isFlat) FileType.FODT else FileType.ODT, bookId, odtBook, customDisplayName, isRecent = true, sourceFolderUri = null, bundleResult = bundleResult
- )
- }
+ addFileToRecent(
+ uri, if (isFlat) FileType.FODT else FileType.ODT, bookId, odtBook, customDisplayName, isRecent = true, sourceFolderUri = null, bundleResult = bundleResult
+ )
_internalState.update { it.copy(selectedEpubBook = odtBook, isLoading = false) }
} catch (e: Exception) {
@@ -5688,8 +4537,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
bookId: String,
type: FileType,
customDisplayName: String? = null,
- bundleResult: CalibreBundleResult? = null,
- persistToLibrary: Boolean = true
+ bundleResult: CalibreBundleResult? = null
) {
val loadStart = System.currentTimeMillis()
Timber.tag("FileOpenPerf").d("[$bookId] loadSingleFile START | type=$type")
@@ -5727,18 +4575,16 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.tag("FileOpenPerf")
.d("[$bookId] loadSingleFile: importSingleFile completed | chapters=${epubBook.chapters.size} | elapsed=${System.currentTimeMillis() - loadStart}ms")
Timber.i("Import successful ($type). Title: ${epubBook.title}")
- if (persistToLibrary) {
- addFileToRecent(
- uri,
- type,
- bookId,
- epubBook,
- customDisplayName,
- isRecent = true,
- sourceFolderUri = null,
- bundleResult = bundleResult
- )
- }
+ addFileToRecent(
+ uri,
+ type,
+ bookId,
+ epubBook,
+ customDisplayName,
+ isRecent = true,
+ sourceFolderUri = null,
+ bundleResult = bundleResult
+ )
_internalState.update { it.copy(selectedEpubBook = epubBook, isLoading = false) }
Timber.tag("FileOpenPerf")
@@ -5777,13 +4623,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
return resolveFileTypeFromMetadata(fileName, mimeType)
}
- private fun loadMobi(
- uri: Uri,
- bookId: String,
- customDisplayName: String? = null,
- bundleResult: CalibreBundleResult? = null,
- persistToLibrary: Boolean = true
- ) {
+ private fun loadMobi(uri: Uri, bookId: String, customDisplayName: String? = null, bundleResult: CalibreBundleResult? = null) {
viewModelScope.launch {
if (!_internalState.value.isLoading) {
_internalState.update { it.copy(isLoading = true, errorMessage = null) }
@@ -5805,18 +4645,16 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
if (mobiAsEpubBook != null) {
Timber.i("MOBI parsing successful. Title: ${mobiAsEpubBook.title}")
- if (persistToLibrary) {
- addFileToRecent(
- uri,
- FileType.MOBI,
- bookId,
- mobiAsEpubBook,
- customDisplayName,
- isRecent = true,
- sourceFolderUri = null,
- bundleResult = bundleResult
- )
- }
+ addFileToRecent(
+ uri,
+ FileType.MOBI,
+ bookId,
+ mobiAsEpubBook,
+ customDisplayName,
+ isRecent = true,
+ sourceFolderUri = null,
+ bundleResult = bundleResult
+ )
_internalState.update {
it.copy(selectedEpubBook = mobiAsEpubBook, isLoading = false)
}
@@ -5838,13 +4676,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
- private fun loadEpub(
- uri: Uri,
- bookId: String,
- customDisplayName: String? = null,
- bundleResult: CalibreBundleResult? = null,
- persistToLibrary: Boolean = true
- ) {
+ private fun loadEpub(uri: Uri, bookId: String, customDisplayName: String? = null, bundleResult: CalibreBundleResult? = null) {
val loadStart = System.currentTimeMillis()
Timber.tag("FileOpenPerf").d("[$bookId] loadEpub START")
viewModelScope.launch {
@@ -5870,18 +4702,16 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
Timber.tag("FileOpenPerf")
.d("[$bookId] loadEpub: createEpubBook completed | chapters=${epubBook.chapters.size} | elapsed=${System.currentTimeMillis() - loadStart}ms")
- if (persistToLibrary) {
- addFileToRecent(
- uri,
- FileType.EPUB,
- bookId,
- epubBook,
- customDisplayName,
- isRecent = true,
- sourceFolderUri = null,
- bundleResult = bundleResult
- )
- }
+ addFileToRecent(
+ uri,
+ FileType.EPUB,
+ bookId,
+ epubBook,
+ customDisplayName,
+ isRecent = true,
+ sourceFolderUri = null,
+ bundleResult = bundleResult
+ )
_internalState.update { it.copy(selectedEpubBook = epubBook, isLoading = false) }
Timber.tag("FileOpenPerf")
@@ -5900,24 +4730,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
) {
Timber.d("Saving EPUB position locally: URI=$uri, Locator=$locator")
viewModelScope.launch {
- recentFilesRepository.getFileByUri(uri.toString())?.let { existing ->
- logCloudSyncTrace {
- "android.reader.position_save_start book=${existing.bookId} beforeTs=${existing.lastModifiedTimestamp} " +
- "locator={chapter=${locator.chapterIndex} block=${locator.blockIndex} char=${locator.charOffset}} " +
- "progress=$progress cfi=${cfiForWebView.cloudSyncPreview()}"
- }
+ recentFilesRepository.getFileByUri(uri.toString())?.let { _ ->
recentFilesRepository.updateEpubReadingPosition(
uriString = uri.toString(),
locator = locator,
cfiForWebView = cfiForWebView,
progress = progress
)
- val updated = recentFilesRepository.getFileByBookId(existing.bookId)
- logCloudSyncTrace {
- "android.reader.position_save_done beforeTs=${existing.lastModifiedTimestamp} " +
- (updated?.cloudSyncTraceSummary("after") ?: "after=null")
- }
- queueCloudMetadataUpload(existing.bookId, reason = "epub_position")
}
}
}
@@ -5959,20 +4778,10 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
Timber.tag("PdfPositionDebug").i("ViewModel: Save request triggered | Page: $page | Total: $totalPages | Progress: $progress | URI: ${currentPdfUri.lastPathSegment}")
viewModelScope.launch {
- recentFilesRepository.getFileByUri(currentPdfUri.toString())?.let { existing ->
- logCloudSyncTrace {
- "android.reader.pdf_position_save_start book=${existing.bookId} beforeTs=${existing.lastModifiedTimestamp} " +
- "beforeReadTs=${existing.effectiveReadingPositionModifiedTimestamp()} page=$page progress=$progress"
- }
+ recentFilesRepository.getFileByUri(currentPdfUri.toString())?.let { _ ->
recentFilesRepository.updatePdfReadingPosition(
uriString = currentPdfUri.toString(), page = page, progress = progress
)
- val updated = recentFilesRepository.getFileByBookId(existing.bookId)
- logCloudSyncTrace {
- "android.reader.pdf_position_save_done beforeTs=${existing.lastModifiedTimestamp} " +
- (updated?.cloudSyncTraceSummary("after") ?: "after=null")
- }
- queueCloudMetadataUpload(existing.bookId, reason = "pdf_position")
} ?: run {
Timber.tag("PdfPositionDebug").e("ViewModel: Save aborted. Could not resolve file item from URI in DB.")
}
@@ -6022,7 +4831,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
fun refreshLibrary() {
val syncEnabled = _internalState.value.isSyncEnabled
- val hasFolder = _internalState.value.syncedFolders.any { it.localSyncEnabled }
+ val hasFolder = _internalState.value.syncedFolders.isNotEmpty()
if (!syncEnabled && !hasFolder) {
Timber.d("Refresh skipped: No sync methods active.")
@@ -6115,25 +4924,19 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
private fun uploadNewBookAndMetadata(book: RecentFileItem) {
- if (!uiState.value.isSyncEnabled) {
- logCloudSyncTrace { "android.upload_content.skip reason=sync_disabled ${book.cloudSyncTraceSummary()}" }
- return
- }
+ if (!uiState.value.isSyncEnabled) return
if (book.uriString?.startsWith("opds-pse") == true) {
- logCloudSyncTrace { "android.upload_content.skip reason=opds_stream ${book.cloudSyncTraceSummary()}" }
Timber.d("Skipping book content sync for OPDS stream book: ${book.displayName}")
return
}
if (book.sourceFolderUri != null) {
- logCloudSyncTrace { "android.upload_content.skip reason=folder_book ${book.cloudSyncTraceSummary()}" }
Timber.d("Skipping book content sync for local folder book: ${book.displayName}")
return
}
if (book.isManualOnlyReaderFile()) {
- logCloudSyncTrace { "android.upload_content.skip reason=manual_only ${book.cloudSyncTraceSummary()}" }
Timber.d("Skipping book content sync for manual-only reader file: ${book.displayName}")
return
}
@@ -6141,22 +4944,16 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
viewModelScope.launch {
_internalState.update { it.copy(uploadingBookIds = it.uploadingBookIds + book.bookId) }
try {
- logCloudSyncTrace { "android.upload_content.start ${book.cloudSyncTraceSummary()}" }
- val accessToken = googleDriveRepository.getAccessToken(appContext) ?: run {
- logCloudSyncTrace { "android.upload_content.skip reason=no_access_token ${book.cloudSyncTraceSummary()}" }
- return@launch
- }
+ val accessToken = googleDriveRepository.getAccessToken(appContext) ?: return@launch
book.getUri()?.path?.let { path ->
val file = File(path)
if (file.exists()) {
- logCloudSyncTrace { "android.upload_content.file book=${book.bookId} path=${path.cloudSyncPreview()} bytes=${file.length()}" }
Timber.d("Uploading newly added book content: ${book.displayName}")
val uploadedFile = googleDriveRepository.uploadFile(
accessToken, book.bookId, file, book.type
)
if (uploadedFile != null) {
- logCloudSyncTrace { "android.upload_content.success book=${book.bookId} driveId=${uploadedFile.id}" }
Timber.d("Upload successful, now syncing metadata for ${book.bookId}")
val latestBookState = recentFilesRepository.getFileByBookId(book.bookId)
if (latestBookState != null) {
@@ -6165,16 +4962,13 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
uploadSingleBookMetadata(book)
}
} else {
- logCloudSyncTrace { "android.upload_content.failed_null book=${book.bookId}" }
Timber.e("Google Drive upload returned null for ${book.bookId}")
}
} else {
- logCloudSyncTrace { "android.upload_content.skip reason=file_missing book=${book.bookId} path=${path.cloudSyncPreview()}" }
Timber.w("File for new book upload does not exist at path: $path")
}
}
} catch (e: Exception) {
- logCloudSyncError(e) { "android.upload_content.failed ${book.cloudSyncTraceSummary()}" }
Timber.e(e, "Failed to upload new book content for bookId: ${book.bookId}")
} finally {
_internalState.update {
@@ -6235,10 +5029,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val newBehavior = if (keep) "KEEP" else "DELETE"
setExternalFileBehavior(newBehavior)
}
- if (keep) {
- clearPendingExternalFileRemovals(setOf(bookId))
- } else {
- deletePendingExternalFileRemoval(bookId, null)
+ if (!keep) {
+ deleteBookPermanently(bookId)
}
_internalState.update { it.copy(showExternalFileSavePromptFor = null) }
}
@@ -6430,7 +5222,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val currentUser = uiState.value.currentUser ?: return
viewModelScope.launch(Dispatchers.IO) {
- val db = org.dueattendant149.bookreader.data.AppDatabase.getDatabase(appContext)
+ val db = com.aryan.reader.data.AppDatabase.getDatabase(appContext)
val shelf = db.shelfDao().getShelfById(shelfId) ?: return@launch
val crossRefs = db.shelfDao().getCrossRefsForShelf(shelfId)
val manualOnlyBookIds = recentFilesRepository.getAllFilesForSync()
@@ -6607,15 +5399,10 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
deviceId
)
- sharedCloudBookContentFileName(item.bookId, item.type)
- ?.let { fileName ->
- remoteFiles[fileName]?.id?.let { fileId ->
- Timber.d("Deleting from Drive: $fileName")
- googleDriveRepository.deleteDriveFile(accessToken, fileId)
- }
- }
- remoteFiles[cloudPdfAnnotationDriveFileName(item.bookId)]?.id?.let { fileId ->
- Timber.d("Deleting annotation bundle from Drive: ${item.bookId}")
+ val fileExtension = item.type.name.lowercase()
+ val fileName = "${item.bookId}.$fileExtension"
+ remoteFiles[fileName]?.id?.let { fileId ->
+ Timber.d("Deleting from Drive: $fileName")
googleDriveRepository.deleteDriveFile(accessToken, fileId)
}
@@ -7164,7 +5951,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
val initDuration = System.currentTimeMillis() - initStartTime
Timber.d(">>> [BATCH] Model Initialization: ${initDuration}ms")
- val archiveDoc = org.dueattendant149.bookreader.pdf.ArchiveDocumentWrapper(cacheFile)
+ val archiveDoc = com.aryan.reader.pdf.ArchiveDocumentWrapper(cacheFile)
val totalPages = archiveDoc.getPageCount()
val startIndex = 3
@@ -7310,6 +6097,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private const val KEY_APP_OPEN_COUNT = "app_open_count"
internal const val KEY_SYNCED_FOLDER_URI = "synced_folder_uri"
internal const val KEY_LAST_FOLDER_SCAN_TIME = "last_folder_scan_time"
+ private const val KEY_SYNCED_FOLDERS_JSON = "synced_folders_list_json"
private const val MAX_FOLDER_LIMIT = 10
internal const val KEY_PINNED_HOME = "pinned_home_books"
internal const val KEY_PINNED_LIBRARY = "pinned_library_books"
@@ -7320,9 +6108,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private const val KEY_LAST_OPEN_BOOK_ID = "last_open_book_id"
private const val KEY_LAST_OPEN_FILE_TYPE = "last_open_file_type"
private const val KEY_EXTERNAL_FILE_BEHAVIOR = "external_file_behavior"
- private const val EXTERNAL_FILE_BEHAVIOR_ASK = "ASK"
- private const val EXTERNAL_FILE_BEHAVIOR_TEMPORARY = "TEMPORARY"
- private const val KEY_PENDING_EXTERNAL_FILE_REMOVALS = "pending_external_file_removals"
private const val KEY_USE_STRICT_FILE_FILTER = "use_strict_file_filter"
private const val KEY_USE_PDF_FILE_NAME_AS_DISPLAY_NAME = "use_pdf_file_name_as_display_name"
private const val KEY_SCREEN_CAPTURE_PROTECTION = "screen_capture_protection_enabled"
@@ -7347,61 +6132,3 @@ private fun RecentFileItem.isManualOnlyReaderFile(): Boolean {
private fun BookMetadata.isManualOnlyReaderFile(): Boolean {
return isManualOnlyReaderFileName(displayName)
}
-
-private fun RecentFileItem.withFreshLocalReadingPositionForCloudUpload(
- latestLocal: RecentFileItem?
-): RecentFileItem {
- if (latestLocal == null || latestLocal.bookId != bookId) return this
- val latestReadingTimestamp = latestLocal.effectiveReadingPositionModifiedTimestamp()
- val currentReadingTimestamp = effectiveReadingPositionModifiedTimestamp()
- val shouldRefresh = latestLocal.lastModifiedTimestamp > lastModifiedTimestamp ||
- latestReadingTimestamp > currentReadingTimestamp
- if (!shouldRefresh) return this
-
- return latestLocal.copy(
- fileSize = fileSize.takeIf { it > 0L } ?: latestLocal.fileSize,
- fileContentModifiedTimestamp = maxOf(fileContentModifiedTimestamp, latestLocal.fileContentModifiedTimestamp),
- isAvailable = isAvailable || latestLocal.isAvailable,
- uriString = latestLocal.uriString ?: uriString,
- bookmarksJson = latestLocal.bookmarksJson ?: bookmarksJson,
- highlightsJson = latestLocal.highlightsJson ?: highlightsJson
- )
-}
-
-private fun RecentFileItem.withCloudReadingPosition(remote: BookMetadata): RecentFileItem {
- return copy(
- lastChapterIndex = remote.lastChapterIndex,
- lastPage = remote.lastPage,
- lastPositionCfi = remote.lastPositionCfi,
- locatorBlockIndex = remote.locatorBlockIndex,
- locatorCharOffset = remote.locatorCharOffset,
- progressPercentage = remote.progressPercentage,
- readingPositionModifiedTimestamp = remote.effectiveReadingPositionModifiedTimestamp()
- )
-}
-
-private fun RecentFileItem.withLocalReadingPosition(local: RecentFileItem): RecentFileItem {
- return copy(
- lastChapterIndex = local.lastChapterIndex,
- lastPage = local.lastPage,
- lastPositionCfi = local.lastPositionCfi,
- locatorBlockIndex = local.locatorBlockIndex,
- locatorCharOffset = local.locatorCharOffset,
- progressPercentage = local.progressPercentage,
- readingPositionModifiedTimestamp = local.effectiveReadingPositionModifiedTimestamp()
- )
-}
-
-private fun RecentFileItem.withLocalStorageForCloudMetadata(local: RecentFileItem): RecentFileItem {
- return copy(
- uriString = local.uriString ?: uriString,
- isAvailable = local.isAvailable || isAvailable,
- coverImagePath = local.coverImagePath ?: coverImagePath,
- sourceFolderUri = local.sourceFolderUri ?: sourceFolderUri,
- fileSize = local.fileSize.takeIf { it > 0L } ?: fileSize,
- fileContentModifiedTimestamp = maxOf(local.fileContentModifiedTimestamp, fileContentModifiedTimestamp),
- folderTextMetadataParsed = local.folderTextMetadataParsed || folderTextMetadataParsed,
- folderCoverMetadataParsed = local.folderCoverMetadataParsed || folderCoverMetadataParsed,
- tags = local.tags
- )
-}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/MetadataExtractionWorker.kt b/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt
similarity index 93%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/MetadataExtractionWorker.kt
rename to app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt
index 9e56c19..c3dc3ab 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/MetadataExtractionWorker.kt
+++ b/app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt
@@ -1,5 +1,5 @@
// MetadataExtractionWorker.kt
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import android.provider.OpenableColumns
@@ -10,10 +10,10 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import androidx.work.WorkManager
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.data.RecentFilesRepository
-import org.dueattendant149.bookreader.pdf.PdfiumCoreProvider
-import org.dueattendant149.bookreader.pdf.PdfiumEngineProvider
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.data.RecentFilesRepository
+import com.aryan.reader.pdf.PdfiumCoreProvider
+import com.aryan.reader.pdf.PdfiumEngineProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.xmlpull.v1.XmlPullParser
@@ -50,20 +50,11 @@ class MetadataExtractionWorker(
val sourceFolderUri = inputData.getString(KEY_SOURCE_FOLDER_URI)
val prefs = appContext.getSharedPreferences("reader_user_prefs", Context.MODE_PRIVATE)
- val linkedFolders = SyncedFolderPrefs.decodeSyncedFolders(
- jsonString = prefs.getString(SyncedFolderPrefs.KEY_SYNCED_FOLDERS_JSON, null),
- legacyUri = prefs.getString(SyncedFolderPrefs.KEY_LEGACY_SYNCED_FOLDER_URI, null)
- )
- val enabledFolderUris = linkedFolders
- .filter { it.localSyncEnabled }
- .mapTo(mutableSetOf()) { it.uriString }
+ val hasLegacy = prefs.contains("synced_folder_uri")
+ val hasNew = prefs.contains("synced_folders_list_json")
- if (enabledFolderUris.isEmpty()) {
- ReaderPerfLog.d("MetadataWorker skipped: no linked folders with sync enabled")
- return@withContext Result.success()
- }
- if (!sourceFolderUri.isNullOrBlank() && sourceFolderUri !in enabledFolderUris) {
- ReaderPerfLog.d("MetadataWorker skipped: folder sync disabled folder=$sourceFolderUri")
+ if (!hasLegacy && !hasNew) {
+ ReaderPerfLog.d("MetadataWorker skipped: no linked folders")
return@withContext Result.success()
}
@@ -71,9 +62,7 @@ class MetadataExtractionWorker(
val filesToProcess = recentFilesRepository.getFolderBooksNeedingTextMetadata(
sourceFolderUri = sourceFolderUri,
limit = METADATA_WORKER_BOOK_BATCH_SIZE
- ).filter { item ->
- item.sourceFolderUri != null && item.sourceFolderUri in enabledFolderUris
- }
+ )
if (filesToProcess.isEmpty()) {
ReaderPerfLog.d("MetadataWorker skipped: no metadata pending folder=${sourceFolderUri ?: "ALL"}")
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/MyApplication.kt b/app/src/main/java/com/aryan/reader/MyApplication.kt
similarity index 93%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/MyApplication.kt
rename to app/src/main/java/com/aryan/reader/MyApplication.kt
index 20d419c..1a04905 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/MyApplication.kt
+++ b/app/src/main/java/com/aryan/reader/MyApplication.kt
@@ -17,14 +17,14 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.app.Application
import android.webkit.WebView
import coil.ImageLoader
import coil.ImageLoaderFactory
import coil.decode.SvgDecoder
-import org.dueattendant149.bookreader.paginatedreader.SvgStringFetcher
+import com.aryan.reader.paginatedreader.SvgStringFetcher
import timber.log.Timber // Add this
class MyApplication : Application(), ImageLoaderFactory {
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/ProScreen.kt b/app/src/main/java/com/aryan/reader/ProScreen.kt
similarity index 99%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/ProScreen.kt
rename to app/src/main/java/com/aryan/reader/ProScreen.kt
index fd8d1b5..c92ac8b 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/ProScreen.kt
+++ b/app/src/main/java/com/aryan/reader/ProScreen.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.app.Activity
import timber.log.Timber
@@ -61,7 +61,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
-import org.dueattendant149.bookreader.data.ProductDetailsEntity
+import com.aryan.reader.data.ProductDetailsEntity
import kotlinx.coroutines.launch
import java.text.NumberFormat
import java.util.Currency
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/PurchaseAccountObfuscator.kt b/app/src/main/java/com/aryan/reader/PurchaseAccountObfuscator.kt
similarity index 93%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/PurchaseAccountObfuscator.kt
rename to app/src/main/java/com/aryan/reader/PurchaseAccountObfuscator.kt
index ed91647..9604ae6 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/PurchaseAccountObfuscator.kt
+++ b/app/src/main/java/com/aryan/reader/PurchaseAccountObfuscator.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import java.security.MessageDigest
import java.util.Base64
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderBrightness.kt b/app/src/main/java/com/aryan/reader/ReaderBrightness.kt
similarity index 66%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/ReaderBrightness.kt
rename to app/src/main/java/com/aryan/reader/ReaderBrightness.kt
index 9fd9904..9e9e1d9 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderBrightness.kt
+++ b/app/src/main/java/com/aryan/reader/ReaderBrightness.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import android.view.Window
@@ -10,16 +10,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Remove
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
+import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -33,37 +28,20 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.edit
-import org.dueattendant149.bookreader.shared.ui.ReaderMinimalSlider
import kotlin.math.roundToInt
private const val READER_PREFS_NAME = "reader_prefs"
private const val PREF_READER_BRIGHTNESS_USE_SYSTEM = "reader_brightness_use_system"
private const val PREF_READER_BRIGHTNESS_VALUE = "reader_brightness_value"
private const val DEFAULT_CUSTOM_BRIGHTNESS = 0.75f
-private const val MIN_CUSTOM_BRIGHTNESS_PERCENT = 1
-private const val MAX_CUSTOM_BRIGHTNESS_PERCENT = 100
-private const val CUSTOM_BRIGHTNESS_STEP_PERCENT = 1
-private const val MIN_CUSTOM_BRIGHTNESS = 0.01f
+private const val MIN_CUSTOM_BRIGHTNESS = 0.05f
data class ReaderBrightnessSettings(
val useSystemBrightness: Boolean = true,
val customBrightness: Float = DEFAULT_CUSTOM_BRIGHTNESS
) {
val safeCustomBrightness: Float
- get() = normalizeReaderBrightness(customBrightness)
-}
-
-internal fun normalizeReaderBrightness(brightness: Float): Float {
- val percent = (brightness * 100f).roundToInt()
- .coerceIn(MIN_CUSTOM_BRIGHTNESS_PERCENT, MAX_CUSTOM_BRIGHTNESS_PERCENT)
- return percent / 100f
-}
-
-internal fun stepReaderBrightness(brightness: Float, percentDelta: Int): Float {
- val currentPercent = (normalizeReaderBrightness(brightness) * 100f).roundToInt()
- val nextPercent = (currentPercent + percentDelta)
- .coerceIn(MIN_CUSTOM_BRIGHTNESS_PERCENT, MAX_CUSTOM_BRIGHTNESS_PERCENT)
- return nextPercent / 100f
+ get() = customBrightness.coerceIn(MIN_CUSTOM_BRIGHTNESS, 1f)
}
fun loadReaderBrightnessSettings(context: Context): ReaderBrightnessSettings {
@@ -71,7 +49,7 @@ fun loadReaderBrightnessSettings(context: Context): ReaderBrightnessSettings {
return ReaderBrightnessSettings(
useSystemBrightness = prefs.getBoolean(PREF_READER_BRIGHTNESS_USE_SYSTEM, true),
customBrightness = prefs.getFloat(PREF_READER_BRIGHTNESS_VALUE, DEFAULT_CUSTOM_BRIGHTNESS)
- .let(::normalizeReaderBrightness)
+ .coerceIn(MIN_CUSTOM_BRIGHTNESS, 1f)
)
}
@@ -184,9 +162,17 @@ fun ReaderBrightnessSheet(
color = MaterialTheme.colorScheme.primary
)
}
- ReaderBrightnessControl(
- settings = settings,
- onSettingsChange = onSettingsChange
+ Slider(
+ value = settings.safeCustomBrightness,
+ onValueChange = { brightness ->
+ onSettingsChange(
+ settings.copy(
+ useSystemBrightness = false,
+ customBrightness = brightness.coerceIn(MIN_CUSTOM_BRIGHTNESS, 1f)
+ )
+ )
+ },
+ valueRange = MIN_CUSTOM_BRIGHTNESS..1f
)
Text(
text = stringResource(R.string.reader_brightness_custom_desc),
@@ -200,67 +186,6 @@ fun ReaderBrightnessSheet(
}
}
-@Composable
-private fun ReaderBrightnessControl(
- settings: ReaderBrightnessSettings,
- onSettingsChange: (ReaderBrightnessSettings) -> Unit
-) {
- val brightness = settings.safeCustomBrightness
- val canDecrease = brightness > MIN_CUSTOM_BRIGHTNESS
- val canIncrease = brightness < 1f
-
- fun updateBrightness(value: Float) {
- onSettingsChange(
- settings.copy(
- useSystemBrightness = false,
- customBrightness = normalizeReaderBrightness(value)
- )
- )
- }
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- IconButton(
- onClick = {
- updateBrightness(stepReaderBrightness(brightness, -CUSTOM_BRIGHTNESS_STEP_PERCENT))
- },
- enabled = canDecrease,
- modifier = Modifier.size(36.dp)
- ) {
- Icon(
- imageVector = Icons.Default.Remove,
- contentDescription = stringResource(R.string.content_desc_decrease),
- modifier = Modifier.size(18.dp)
- )
- }
- ReaderMinimalSlider(
- value = brightness,
- onValueChange = ::updateBrightness,
- valueRange = MIN_CUSTOM_BRIGHTNESS..1f,
- activeColor = MaterialTheme.colorScheme.primary,
- inactiveColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
- thumbColor = MaterialTheme.colorScheme.primary,
- modifier = Modifier.weight(1f)
- )
- IconButton(
- onClick = {
- updateBrightness(stepReaderBrightness(brightness, CUSTOM_BRIGHTNESS_STEP_PERCENT))
- },
- enabled = canIncrease,
- modifier = Modifier.size(36.dp)
- ) {
- Icon(
- imageVector = Icons.Default.Add,
- contentDescription = stringResource(R.string.content_desc_increase),
- modifier = Modifier.size(18.dp)
- )
- }
- }
-}
-
private fun Window.setReaderBrightness(brightness: Float) {
attributes = attributes.apply {
screenBrightness = brightness
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderFileInfoDialogs.kt b/app/src/main/java/com/aryan/reader/ReaderFileInfoDialogs.kt
similarity index 54%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/ReaderFileInfoDialogs.kt
rename to app/src/main/java/com/aryan/reader/ReaderFileInfoDialogs.kt
index 75203b6..5de4838 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderFileInfoDialogs.kt
+++ b/app/src/main/java/com/aryan/reader/ReaderFileInfoDialogs.kt
@@ -1,12 +1,9 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import org.dueattendant149.bookreader.data.RecentFileItem
+import com.aryan.reader.data.RecentFileItem
@Composable
private fun rememberReaderFileInfoItem(
@@ -67,17 +64,28 @@ internal fun ReaderFileInfoDialogs(
}
}
- HydratedFileInfoDialog(
- item = item,
- isVisible = isFileInfoVisible,
- uiState = uiState,
- viewModel = viewModel,
- onDismiss = { onFileInfoVisibleChange(false) },
- onOpenTags = { bookId ->
- onFileInfoVisibleChange(false)
- viewModel.openTagSelection(setOf(bookId))
+ item?.let { fileInfoItem ->
+ if (isFileInfoVisible) {
+ FileInfoDialog(
+ item = fileInfoItem,
+ usePdfFileNameAsDisplayName = uiState.usePdfFileNameAsDisplayName,
+ onDismiss = { onFileInfoVisibleChange(false) },
+ onSaveMetadata = { metadata ->
+ viewModel.updateBookMetadata(fileInfoItem.bookId, metadata)
+ },
+ onSaveDisplayName = { name ->
+ viewModel.updateCustomName(fileInfoItem.bookId, name)
+ },
+ onRestoreMetadata = {
+ viewModel.restoreOriginalBookMetadata(fileInfoItem.bookId)
+ },
+ onOpenTags = {
+ onFileInfoVisibleChange(false)
+ viewModel.openTagSelection(setOf(fileInfoItem.bookId))
+ }
+ )
}
- )
+ }
if (uiState.showTagSelectionDialogFor.isNotEmpty()) {
TagSelectionBottomSheet(
@@ -94,49 +102,3 @@ internal fun ReaderFileInfoDialogs(
)
}
}
-
-@Composable
-internal fun HydratedFileInfoDialog(
- item: RecentFileItem?,
- isVisible: Boolean,
- uiState: ReaderScreenState,
- viewModel: MainViewModel,
- onDismiss: () -> Unit,
- onOpenTags: (String) -> Unit
-) {
- var fileInfoItem by remember(item?.bookId) { mutableStateOf(item) }
- var hasResolvedFullItem by remember(item?.bookId) { mutableStateOf(false) }
-
- LaunchedEffect(item) {
- fileInfoItem = item
- hasResolvedFullItem = false
- }
-
- LaunchedEffect(isVisible, item?.bookId) {
- if (isVisible && item != null) {
- fileInfoItem = viewModel.getFileInfoItem(item.bookId)?.copy(tags = item.tags) ?: item
- hasResolvedFullItem = true
- }
- }
-
- val resolvedItem = fileInfoItem
- if (isVisible && resolvedItem != null && hasResolvedFullItem) {
- FileInfoDialog(
- item = resolvedItem,
- usePdfFileNameAsDisplayName = uiState.usePdfFileNameAsDisplayName,
- onDismiss = onDismiss,
- onSaveMetadata = { metadata ->
- viewModel.updateBookMetadata(resolvedItem.bookId, metadata)
- },
- onSaveDisplayName = { name ->
- viewModel.updateCustomName(resolvedItem.bookId, name)
- },
- onRestoreMetadata = {
- viewModel.restoreOriginalBookMetadata(resolvedItem.bookId)
- },
- onOpenTags = {
- onOpenTags(resolvedItem.bookId)
- }
- )
- }
-}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderPaginationPreferences.kt b/app/src/main/java/com/aryan/reader/ReaderPaginationPreferences.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/ReaderPaginationPreferences.kt
rename to app/src/main/java/com/aryan/reader/ReaderPaginationPreferences.kt
index 4d922e0..348153b 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderPaginationPreferences.kt
+++ b/app/src/main/java/com/aryan/reader/ReaderPaginationPreferences.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import androidx.core.content.edit
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderPerfLog.kt b/app/src/main/java/com/aryan/reader/ReaderPerfLog.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/ReaderPerfLog.kt
rename to app/src/main/java/com/aryan/reader/ReaderPerfLog.kt
index dd1749f..da2db2f 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderPerfLog.kt
+++ b/app/src/main/java/com/aryan/reader/ReaderPerfLog.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import timber.log.Timber
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderScreenOrientation.kt b/app/src/main/java/com/aryan/reader/ReaderScreenOrientation.kt
similarity index 99%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/ReaderScreenOrientation.kt
rename to app/src/main/java/com/aryan/reader/ReaderScreenOrientation.kt
index 22ab18b..ccb4f05 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderScreenOrientation.kt
+++ b/app/src/main/java/com/aryan/reader/ReaderScreenOrientation.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.app.Activity
import android.content.Context
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderSliderChromeState.kt b/app/src/main/java/com/aryan/reader/ReaderSliderChromeState.kt
similarity index 92%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/ReaderSliderChromeState.kt
rename to app/src/main/java/com/aryan/reader/ReaderSliderChromeState.kt
index 5b51f8a..9fb9623 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/ReaderSliderChromeState.kt
+++ b/app/src/main/java/com/aryan/reader/ReaderSliderChromeState.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import androidx.compose.ui.graphics.Color
@@ -56,21 +56,6 @@ internal fun shouldRenderReaderSlider(
isSearchActive: Boolean
): Boolean = isToggledOn && isBottomChromeVisible && !isSearchActive
-internal fun readerSliderStepPage(
- currentPage: Int,
- delta: Int,
- minPage: Int,
- maxPage: Int
-): Int {
- val lowerBound = min(minPage, maxPage)
- val upperBound = max(minPage, maxPage)
- val nextPage = currentPage.toLong() + delta.toLong()
-
- return nextPage
- .coerceIn(lowerBound.toLong(), upperBound.toLong())
- .toInt()
-}
-
internal fun readerSliderTogglePreferenceKey(bookId: String): String =
READER_SLIDER_TOGGLE_PREFIX + bookId
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/SafeStringResources.kt b/app/src/main/java/com/aryan/reader/SafeStringResources.kt
similarity index 96%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/SafeStringResources.kt
rename to app/src/main/java/com/aryan/reader/SafeStringResources.kt
index 6f60bc9..4515a31 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/SafeStringResources.kt
+++ b/app/src/main/java/com/aryan/reader/SafeStringResources.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import android.content.res.Configuration
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/SettingsScreen.kt b/app/src/main/java/com/aryan/reader/SettingsScreen.kt
similarity index 89%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/SettingsScreen.kt
rename to app/src/main/java/com/aryan/reader/SettingsScreen.kt
index 8aa395c..f3c2c69 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/SettingsScreen.kt
+++ b/app/src/main/java/com/aryan/reader/SettingsScreen.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.WindowInsets
@@ -27,45 +27,45 @@ import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.media3.common.util.UnstableApi
import androidx.navigation.NavHostController
-import org.dueattendant149.bookreader.data.CustomFontEntity
-import org.dueattendant149.bookreader.epubreader.FormatSettings as AndroidFormatSettings
-import org.dueattendant149.bookreader.epubreader.ReaderFont as AndroidReaderFont
-import org.dueattendant149.bookreader.epubreader.ReaderTextAlign as AndroidReaderTextAlign
-import org.dueattendant149.bookreader.epubreader.loadFormatSettings
-import org.dueattendant149.bookreader.epubreader.loadPageInfoMode
-import org.dueattendant149.bookreader.epubreader.loadPageInfoPosition
-import org.dueattendant149.bookreader.epubreader.loadPullToTurn
-import org.dueattendant149.bookreader.epubreader.loadPullToTurnMultiplier
-import org.dueattendant149.bookreader.epubreader.loadSystemUiMode
-import org.dueattendant149.bookreader.epubreader.savePageInfoMode
-import org.dueattendant149.bookreader.epubreader.savePageInfoPosition
-import org.dueattendant149.bookreader.epubreader.savePullToTurn
-import org.dueattendant149.bookreader.epubreader.savePullToTurnMultiplier
-import org.dueattendant149.bookreader.epubreader.saveReaderSettings
-import org.dueattendant149.bookreader.epubreader.saveSystemUiMode
-import org.dueattendant149.bookreader.pdf.savePdfSystemUiMode
-import org.dueattendant149.bookreader.pdf.savePdfThemeId
-import org.dueattendant149.bookreader.pdf.savePdfVerticalPageGapVisible
-import org.dueattendant149.bookreader.pdf.savePdfPageNumberOverlayVisible
-import org.dueattendant149.bookreader.pdf.loadPdfSystemUiMode
-import org.dueattendant149.bookreader.pdf.loadPdfThemeId
-import org.dueattendant149.bookreader.pdf.loadPdfVerticalPageGapVisible
-import org.dueattendant149.bookreader.pdf.loadPdfPageNumberOverlayVisible
-import org.dueattendant149.bookreader.shared.BuiltInPdfReaderThemes
-import org.dueattendant149.bookreader.shared.CustomFontItem
-import org.dueattendant149.bookreader.shared.SharedSettingsAction
-import org.dueattendant149.bookreader.shared.SharedSettingsDestination
-import org.dueattendant149.bookreader.shared.parentDestination
-import org.dueattendant149.bookreader.shared.toReaderSettingsFontFamily
-import org.dueattendant149.bookreader.shared.toSharedReaderTextAlign
-import org.dueattendant149.bookreader.shared.reader.ReaderReadingMode
-import org.dueattendant149.bookreader.shared.reader.ReaderSettings
-import org.dueattendant149.bookreader.shared.reader.SharedReaderTextAlign
-import org.dueattendant149.bookreader.shared.readerThemeById
-import org.dueattendant149.bookreader.shared.sharedSettingsHubModel
-import org.dueattendant149.bookreader.shared.toReaderSettings
-import org.dueattendant149.bookreader.shared.ui.SharedSettingsHub
-import org.dueattendant149.bookreader.tts.loadTtsMode
+import com.aryan.reader.data.CustomFontEntity
+import com.aryan.reader.epubreader.FormatSettings as AndroidFormatSettings
+import com.aryan.reader.epubreader.ReaderFont as AndroidReaderFont
+import com.aryan.reader.epubreader.ReaderTextAlign as AndroidReaderTextAlign
+import com.aryan.reader.epubreader.loadFormatSettings
+import com.aryan.reader.epubreader.loadPageInfoMode
+import com.aryan.reader.epubreader.loadPageInfoPosition
+import com.aryan.reader.epubreader.loadPullToTurn
+import com.aryan.reader.epubreader.loadPullToTurnMultiplier
+import com.aryan.reader.epubreader.loadSystemUiMode
+import com.aryan.reader.epubreader.savePageInfoMode
+import com.aryan.reader.epubreader.savePageInfoPosition
+import com.aryan.reader.epubreader.savePullToTurn
+import com.aryan.reader.epubreader.savePullToTurnMultiplier
+import com.aryan.reader.epubreader.saveReaderSettings
+import com.aryan.reader.epubreader.saveSystemUiMode
+import com.aryan.reader.pdf.savePdfSystemUiMode
+import com.aryan.reader.pdf.savePdfThemeId
+import com.aryan.reader.pdf.savePdfVerticalPageGapVisible
+import com.aryan.reader.pdf.savePdfPageNumberOverlayVisible
+import com.aryan.reader.pdf.loadPdfSystemUiMode
+import com.aryan.reader.pdf.loadPdfThemeId
+import com.aryan.reader.pdf.loadPdfVerticalPageGapVisible
+import com.aryan.reader.pdf.loadPdfPageNumberOverlayVisible
+import com.aryan.reader.shared.BuiltInPdfReaderThemes
+import com.aryan.reader.shared.CustomFontItem
+import com.aryan.reader.shared.SharedSettingsAction
+import com.aryan.reader.shared.SharedSettingsDestination
+import com.aryan.reader.shared.parentDestination
+import com.aryan.reader.shared.toReaderSettingsFontFamily
+import com.aryan.reader.shared.toSharedReaderTextAlign
+import com.aryan.reader.shared.reader.ReaderReadingMode
+import com.aryan.reader.shared.reader.ReaderSettings
+import com.aryan.reader.shared.reader.SharedReaderTextAlign
+import com.aryan.reader.shared.readerThemeById
+import com.aryan.reader.shared.sharedSettingsHubModel
+import com.aryan.reader.shared.toReaderSettings
+import com.aryan.reader.shared.ui.SharedSettingsHub
+import com.aryan.reader.tts.loadTtsMode
import kotlinx.coroutines.launch
import kotlin.math.max
import kotlin.math.roundToInt
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/SharedComposables.kt b/app/src/main/java/com/aryan/reader/SharedComposables.kt
similarity index 93%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/SharedComposables.kt
rename to app/src/main/java/com/aryan/reader/SharedComposables.kt
index db27184..71e3996 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/SharedComposables.kt
+++ b/app/src/main/java/com/aryan/reader/SharedComposables.kt
@@ -18,7 +18,7 @@
* mail: epistemereader@gmail.com
*/
// SharedComposables.kt
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import android.content.Intent
@@ -43,7 +43,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
-import org.dueattendant149.bookreader.data.TagEntity
+import com.aryan.reader.data.TagEntity
import androidx.compose.material.icons.filled.Search
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
@@ -87,7 +87,6 @@ import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.Restore
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.SelectAll
-import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.FileOpen
import androidx.compose.material.icons.outlined.Gavel
import androidx.compose.material.icons.outlined.Policy
@@ -143,14 +142,10 @@ import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.net.toUri
import androidx.core.text.HtmlCompat
-import org.dueattendant149.bookreader.shared.SharedFileCapabilities
-import org.dueattendant149.bookreader.shared.SharedLegalLinks
-import org.dueattendant149.bookreader.shared.SharedLegalProfile
-import org.dueattendant149.bookreader.data.BookMetadataEdit
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.shared.SharedText
-import org.dueattendant149.bookreader.shared.sharedLegalLinksForProfile
-import org.dueattendant149.bookreader.shared.ui.SharedMarkdownText
+import com.aryan.reader.data.BookMetadataEdit
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.shared.SharedText
+import com.aryan.reader.shared.ui.SharedMarkdownText
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.Date
@@ -159,25 +154,9 @@ import kotlin.math.log10
import kotlin.math.pow
import kotlin.math.roundToInt
-internal fun legalLinksForAndroidFlavor(flavor: String = BuildConfig.FLAVOR): SharedLegalLinks {
- val profile = if (flavor == "oss") SharedLegalProfile.OSS else SharedLegalProfile.STANDARD
- return sharedLegalLinksForProfile(profile)
-}
-
-internal val PRIVACY_POLICY_URL: String get() = legalLinksForAndroidFlavor().privacyPolicyUrl
-internal val TERMS_URL: String get() = legalLinksForAndroidFlavor().termsUrl
-internal val LICENSES_URL: String get() = legalLinksForAndroidFlavor().licensesUrl
-
-fun supportedFontMimeTypes(): Array = arrayOf(
- "font/ttf",
- "font/otf",
- "font/woff2",
- "application/x-font-ttf",
- "application/x-font-otf",
- "application/font-woff2",
- "application/vnd.ms-opentype",
- "application/x-font-opentype"
-)
+internal const val PRIVACY_POLICY_URL = "https://aryan-raj3112.github.io/reader-policy/privacy-policy.html"
+internal const val TERMS_URL = "https://aryan-raj3112.github.io/reader-policy/terms-and-conditions.html"
+internal const val LICENSES_URL = "https://aryan-raj3112.github.io/reader-policy/licenses.html"
class CustomTabUriHandler(private val context: Context) : UriHandler {
override fun openUri(uri: String) {
@@ -276,8 +255,6 @@ fun ContextualTopAppBar(
selectedItemCount: Int,
onNavIconClick: () -> Unit,
onInfoClick: (() -> Unit)? = null,
- onSaveClick: (() -> Unit)? = null,
- onShareClick: (() -> Unit)? = null,
onTagClick: (() -> Unit)? = null,
onSelectAllClick: (() -> Unit)? = null,
onPinClick: (() -> Unit)? = null,
@@ -306,16 +283,6 @@ fun ContextualTopAppBar(
Icon(Icons.Filled.Info, contentDescription = stringResource(R.string.info))
}
}
- if (selectedItemCount == 1 && onSaveClick != null) {
- IconButton(onClick = onSaveClick) {
- Icon(Icons.Filled.Save, contentDescription = stringResource(R.string.action_save_copy_to_device))
- }
- }
- if (selectedItemCount == 1 && onShareClick != null) {
- IconButton(onClick = onShareClick) {
- Icon(Icons.Filled.Share, contentDescription = stringResource(R.string.action_share))
- }
- }
if (onSelectAllClick != null) {
IconButton(onClick = onSelectAllClick) {
Icon(Icons.Filled.SelectAll, contentDescription = stringResource(R.string.select_all))
@@ -1270,55 +1237,53 @@ fun AboutDialog(onDismiss: () -> Unit) {
subtitle = stringResource(R.string.about_github_desc),
onClick = { uriHandler.openUri("https://github.com/Aryan-Raj3112/episteme") }
)
+ } else {
+ AboutInfoRow(
+ icon = {
+ Icon(
+ imageVector = Icons.Outlined.Policy,
+ contentDescription = null,
+ modifier = Modifier.size(22.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ },
+ text = stringResource(R.string.legal_privacy_policy),
+ subtitle = stringResource(R.string.about_privacy_desc),
+ onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) }
+ )
Spacer(modifier = Modifier.height(10.dp))
+
+ AboutInfoRow(
+ icon = {
+ Icon(
+ imageVector = Icons.Outlined.Gavel,
+ contentDescription = null,
+ modifier = Modifier.size(22.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ },
+ text = stringResource(R.string.legal_terms_of_service),
+ subtitle = stringResource(R.string.about_terms_desc),
+ onClick = { uriHandler.openUri(TERMS_URL) }
+ )
+
+ Spacer(modifier = Modifier.height(10.dp))
+
+ AboutInfoRow(
+ icon = {
+ Icon(
+ imageVector = Icons.Outlined.FileOpen,
+ contentDescription = null,
+ modifier = Modifier.size(22.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ },
+ text = stringResource(R.string.legal_licenses),
+ subtitle = stringResource(R.string.about_licenses_desc),
+ onClick = { uriHandler.openUri(LICENSES_URL) }
+ )
}
-
- AboutInfoRow(
- icon = {
- Icon(
- imageVector = Icons.Outlined.Policy,
- contentDescription = null,
- modifier = Modifier.size(22.dp),
- tint = MaterialTheme.colorScheme.primary
- )
- },
- text = stringResource(R.string.legal_privacy_policy),
- subtitle = stringResource(R.string.about_privacy_desc),
- onClick = { uriHandler.openUri(PRIVACY_POLICY_URL) }
- )
-
- Spacer(modifier = Modifier.height(10.dp))
-
- AboutInfoRow(
- icon = {
- Icon(
- imageVector = Icons.Outlined.Gavel,
- contentDescription = null,
- modifier = Modifier.size(22.dp),
- tint = MaterialTheme.colorScheme.primary
- )
- },
- text = stringResource(R.string.legal_terms_of_service),
- subtitle = stringResource(R.string.about_terms_desc),
- onClick = { uriHandler.openUri(TERMS_URL) }
- )
-
- Spacer(modifier = Modifier.height(10.dp))
-
- AboutInfoRow(
- icon = {
- Icon(
- imageVector = Icons.Outlined.FileOpen,
- contentDescription = null,
- modifier = Modifier.size(22.dp),
- tint = MaterialTheme.colorScheme.primary
- )
- },
- text = stringResource(R.string.legal_licenses),
- subtitle = stringResource(R.string.about_licenses_desc),
- onClick = { uriHandler.openUri(LICENSES_URL) }
- )
}
},
confirmButton = {
@@ -1598,31 +1563,6 @@ fun RecentFileItem.isOpdsStream(): Boolean {
return uriString?.startsWith("opds-pse://") == true
}
-fun RecentFileItem.canExportOriginalFile(): Boolean {
- return uriString != null && !isOpdsStream()
-}
-
-fun RecentFileItem.suggestedOriginalFileName(): String {
- val fallbackExtension = SharedFileCapabilities.primaryExtensionFor(type)
- val baseName = displayName
- .takeIf { it.isNotBlank() }
- ?: title?.takeIf { it.isNotBlank() }
- ?: "book"
- val sanitized = baseName
- .replace(Regex("""[\\/:*?"<>|]+"""), "_")
- .trim()
- .take(120)
- .ifBlank { "book" }
- return if (
- fallbackExtension != null &&
- !sanitized.endsWith(".$fallbackExtension", ignoreCase = true)
- ) {
- "$sanitized.$fallbackExtension"
- } else {
- sanitized
- }
-}
-
@Composable
private fun statusBadgeColors(overlay: Boolean): Pair {
val container = if (overlay) {
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/SharedModelMappers.kt b/app/src/main/java/com/aryan/reader/SharedModelMappers.kt
similarity index 52%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/SharedModelMappers.kt
rename to app/src/main/java/com/aryan/reader/SharedModelMappers.kt
index d0c5beb..fa6c19e 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/SharedModelMappers.kt
+++ b/app/src/main/java/com/aryan/reader/SharedModelMappers.kt
@@ -1,22 +1,20 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
-import org.dueattendant149.bookreader.data.BookShelfCrossRef
-import org.dueattendant149.bookreader.data.BookTagCrossRef
-import org.dueattendant149.bookreader.data.RecentFileItem
-import org.dueattendant149.bookreader.data.ShelfEntity
-import org.dueattendant149.bookreader.data.TagEntity
-import org.dueattendant149.bookreader.shared.BookItem as SharedBookItem
-import org.dueattendant149.bookreader.shared.BookShelfRef as SharedBookShelfRef
-import org.dueattendant149.bookreader.shared.EpubAnnotationSerializer
-import org.dueattendant149.bookreader.shared.FileType as SharedFileType
-import org.dueattendant149.bookreader.shared.LibraryFilters as SharedLibraryFilters
-import org.dueattendant149.bookreader.shared.ReaderLocator as SharedReaderLocator
-import org.dueattendant149.bookreader.shared.SharedReaderScreenState
-import org.dueattendant149.bookreader.shared.Shelf as SharedShelf
-import org.dueattendant149.bookreader.shared.ShelfRecord
-import org.dueattendant149.bookreader.shared.SyncedFolder as SharedSyncedFolder
-import org.dueattendant149.bookreader.shared.Tag as SharedTag
-import org.dueattendant149.bookreader.shared.toStablePositionCfi
+import com.aryan.reader.data.BookShelfCrossRef
+import com.aryan.reader.data.BookTagCrossRef
+import com.aryan.reader.data.RecentFileItem
+import com.aryan.reader.data.ShelfEntity
+import com.aryan.reader.data.TagEntity
+import com.aryan.reader.shared.BookItem as SharedBookItem
+import com.aryan.reader.shared.BookShelfRef as SharedBookShelfRef
+import com.aryan.reader.shared.EpubAnnotationSerializer
+import com.aryan.reader.shared.FileType as SharedFileType
+import com.aryan.reader.shared.LibraryFilters as SharedLibraryFilters
+import com.aryan.reader.shared.SharedReaderScreenState
+import com.aryan.reader.shared.Shelf as SharedShelf
+import com.aryan.reader.shared.ShelfRecord
+import com.aryan.reader.shared.SyncedFolder as SharedSyncedFolder
+import com.aryan.reader.shared.Tag as SharedTag
fun FileType.toSharedFileType(): SharedFileType = this
@@ -31,21 +29,11 @@ fun SyncedFolder.toSharedSyncedFolder(): SharedSyncedFolder = this
fun SharedSyncedFolder.toAndroidSyncedFolder(): SyncedFolder = this
fun RecentFileItem.toSharedBookItem(): SharedBookItem {
- return toSharedBookItem(
- displayName = customName ?: displayName,
- includeReaderAnnotations = true
- )
-}
-
-private fun RecentFileItem.toSharedBookItem(
- displayName: String,
- includeReaderAnnotations: Boolean
-): SharedBookItem {
return SharedBookItem(
id = bookId,
path = uriString,
type = type,
- displayName = displayName,
+ displayName = customName ?: displayName,
timestamp = timestamp,
coverImagePath = coverImagePath,
title = title,
@@ -65,22 +53,13 @@ private fun RecentFileItem.toSharedBookItem(
seriesName = seriesName,
seriesIndex = seriesIndex,
lastPageIndex = lastPage,
- readerPosition = toSharedReaderLocatorOrNull(),
tags = tags.map { it.toSharedTag() },
- readerHighlights = if (includeReaderAnnotations) {
- EpubAnnotationSerializer.parseHighlightsJson(highlightsJson)
- } else {
- emptyList()
- },
- readingPositionModifiedTimestamp = readingPositionModifiedTimestamp
+ readerHighlights = EpubAnnotationSerializer.parseHighlightsJson(highlightsJson)
)
}
fun RecentFileItem.toSharedProjectionBookItem(): SharedBookItem {
- return toSharedBookItem(
- displayName = displayName,
- includeReaderAnnotations = false
- )
+ return toSharedBookItem().copy(displayName = displayName)
}
fun SharedBookItem.toRecentFileItem(
@@ -88,49 +67,36 @@ fun SharedBookItem.toRecentFileItem(
tagEntitiesById: Map = emptyMap()
): RecentFileItem {
val resolvedTags = tags.map { tag -> tagEntitiesById[tag.id] ?: tag.toTagEntity(createdAt = 0L) }
- val positionCfi = readerPosition?.toSharedPositionCfi()
- androidBooksById[id]?.let { existing ->
- val mappedLastChapterIndex = readerPosition?.chapterIndex ?: existing.lastChapterIndex
- val mappedLastPositionCfi = positionCfi ?: existing.lastPositionCfi
- val mappedLocatorBlockIndex = readerPosition?.blockIndex ?: existing.locatorBlockIndex
- val mappedLocatorCharOffset = readerPosition?.charOffset ?: existing.locatorCharOffset
-
- if (
- existing.uriString == path &&
- existing.type == type &&
- existing.timestamp == timestamp &&
- existing.coverImagePath == coverImagePath &&
- existing.title == title &&
- existing.author == author &&
- existing.description == description &&
- existing.originalTitle == originalTitle &&
- existing.originalAuthor == originalAuthor &&
- existing.originalSeriesName == originalSeriesName &&
- existing.originalSeriesIndex == originalSeriesIndex &&
- existing.originalDescription == originalDescription &&
- existing.lastPage == lastPageIndex &&
- existing.progressPercentage == progressPercentage &&
- existing.isRecent == isRecent &&
- existing.sourceFolderUri == sourceFolder &&
- existing.fileSize == fileSize &&
- existing.fileContentModifiedTimestamp == fileContentModifiedTimestamp &&
- existing.seriesName == seriesName &&
- existing.seriesIndex == seriesIndex &&
- existing.folderTextMetadataParsed == folderTextMetadataParsed &&
- existing.lastChapterIndex == mappedLastChapterIndex &&
- existing.lastPositionCfi == mappedLastPositionCfi &&
- existing.locatorBlockIndex == mappedLocatorBlockIndex &&
- existing.locatorCharOffset == mappedLocatorCharOffset &&
- existing.readingPositionModifiedTimestamp == readingPositionModifiedTimestamp &&
- existing.tags == resolvedTags
- ) {
- return existing
- }
-
- return existing.copy(
+ return androidBooksById[id]?.copy(tags = resolvedTags)
+ ?.copy(
uriString = path,
type = type,
- displayName = existing.displayName,
+ displayName = androidBooksById[id]?.displayName ?: displayName,
+ timestamp = timestamp,
+ coverImagePath = coverImagePath,
+ title = title,
+ author = author,
+ description = description,
+ originalTitle = originalTitle,
+ originalAuthor = originalAuthor,
+ originalSeriesName = originalSeriesName,
+ originalSeriesIndex = originalSeriesIndex,
+ originalDescription = originalDescription,
+ lastPage = lastPageIndex,
+ progressPercentage = progressPercentage,
+ isRecent = isRecent,
+ sourceFolderUri = sourceFolder,
+ fileSize = fileSize,
+ fileContentModifiedTimestamp = fileContentModifiedTimestamp,
+ seriesName = seriesName,
+ seriesIndex = seriesIndex,
+ folderTextMetadataParsed = folderTextMetadataParsed
+ )
+ ?: RecentFileItem(
+ bookId = id,
+ uriString = path,
+ type = type,
+ displayName = displayName,
timestamp = timestamp,
coverImagePath = coverImagePath,
title = title,
@@ -150,70 +116,8 @@ fun SharedBookItem.toRecentFileItem(
seriesName = seriesName,
seriesIndex = seriesIndex,
folderTextMetadataParsed = folderTextMetadataParsed,
- lastChapterIndex = mappedLastChapterIndex,
- lastPositionCfi = mappedLastPositionCfi,
- locatorBlockIndex = mappedLocatorBlockIndex,
- locatorCharOffset = mappedLocatorCharOffset,
- readingPositionModifiedTimestamp = readingPositionModifiedTimestamp,
tags = resolvedTags
)
- }
-
- return RecentFileItem(
- bookId = id,
- uriString = path,
- type = type,
- displayName = displayName,
- timestamp = timestamp,
- coverImagePath = coverImagePath,
- title = title,
- author = author,
- description = description,
- originalTitle = originalTitle,
- originalAuthor = originalAuthor,
- originalSeriesName = originalSeriesName,
- originalSeriesIndex = originalSeriesIndex,
- originalDescription = originalDescription,
- lastPage = lastPageIndex,
- progressPercentage = progressPercentage,
- isRecent = isRecent,
- sourceFolderUri = sourceFolder,
- fileSize = fileSize,
- fileContentModifiedTimestamp = fileContentModifiedTimestamp,
- seriesName = seriesName,
- seriesIndex = seriesIndex,
- folderTextMetadataParsed = folderTextMetadataParsed,
- lastChapterIndex = readerPosition?.chapterIndex,
- lastPositionCfi = positionCfi,
- locatorBlockIndex = readerPosition?.blockIndex,
- locatorCharOffset = readerPosition?.charOffset,
- readingPositionModifiedTimestamp = readingPositionModifiedTimestamp,
- tags = resolvedTags
- )
-}
-
-private fun RecentFileItem.toSharedReaderLocatorOrNull(): SharedReaderLocator? {
- if (
- lastChapterIndex == null &&
- lastPage == null &&
- lastPositionCfi.isNullOrBlank() &&
- locatorBlockIndex == null &&
- locatorCharOffset == null
- ) {
- return null
- }
- return SharedReaderLocator.fromLegacy(
- chapterIndex = lastChapterIndex,
- cfi = lastPositionCfi,
- pageIndex = lastPage
- ).withFallbacks(
- blockIndex = locatorBlockIndex,
- charOffset = locatorCharOffset
- )
-}
-
-private fun SharedReaderLocator.toSharedPositionCfi(): String? {
- return toStablePositionCfi()
}
fun TagEntity.toSharedTag(): SharedTag {
@@ -263,16 +167,8 @@ fun BookShelfCrossRef.toSharedBookShelfRef(): SharedBookShelfRef {
fun ReaderScreenState.toSharedReaderScreenState(
rawBooks: List = rawLibraryFiles,
- dbTags: List = allTags,
- includeReaderAnnotations: Boolean = true
+ dbTags: List = allTags
): SharedReaderScreenState {
- fun RecentFileItem.toStateSharedBookItem(): SharedBookItem {
- return toSharedBookItem(
- displayName = customName ?: displayName,
- includeReaderAnnotations = includeReaderAnnotations
- )
- }
-
return SharedReaderScreenState(
selectedBookId = selectedBookId,
selectedUriString = selectedPdfUri?.toString() ?: selectedEpubUri?.toString(),
@@ -306,16 +202,16 @@ fun ReaderScreenState.toSharedReaderScreenState(
isSearchActive = isSearchActive,
isRefreshing = isRefreshing,
reflowProgress = reflowProgress,
- recentBooks = recentFiles.map { it.toStateSharedBookItem() },
- libraryBooks = allRecentFiles.map { it.toStateSharedBookItem() },
- rawLibraryBooks = rawBooks.map { it.toStateSharedBookItem() },
+ recentBooks = recentFiles.map { it.toSharedBookItem() },
+ libraryBooks = allRecentFiles.map { it.toSharedBookItem() },
+ rawLibraryBooks = rawBooks.map { it.toSharedBookItem() },
pinnedHomeBookIds = pinnedHomeBookIds,
pinnedLibraryBookIds = pinnedLibraryBookIds,
libraryFilters = libraryFilters,
recentFilesLimit = recentFilesLimit,
isTabsEnabled = isTabsEnabled,
openTabIds = openTabIds,
- openTabs = openTabs.map { it.toStateSharedBookItem() },
+ openTabs = openTabs.map { it.toSharedBookItem() },
activeTabBookId = activeTabBookId,
showExternalFileSavePromptFor = showExternalFileSavePromptFor,
externalFileBehavior = externalFileBehavior,
@@ -341,14 +237,7 @@ fun List.withResolvedTags(
val bookTagsMap = tagRefs.groupBy { it.bookId }.mapValues { entry ->
entry.value.mapNotNull { tagsById[it.tagId] }
}
- return map { item ->
- val resolvedTags = bookTagsMap[item.bookId].orEmpty()
- if (item.tags == resolvedTags) {
- item
- } else {
- item.copy(tags = resolvedTags)
- }
- }
+ return map { item -> item.copy(tags = bookTagsMap[item.bookId].orEmpty()) }
}
fun SharedReaderScreenState.toAndroidReaderScreenState(
@@ -357,11 +246,8 @@ fun SharedReaderScreenState.toAndroidReaderScreenState(
tagEntitiesById: Map = emptyMap()
): ReaderScreenState {
val fallbackBooksById = rawLibraryBooks.associateBy { it.id }
- val mappedBooksById = LinkedHashMap()
fun SharedBookItem.toAndroidBook(): RecentFileItem {
- return mappedBooksById.getOrPut(id) {
- toRecentFileItem(androidBooksById, tagEntitiesById)
- }
+ return toRecentFileItem(androidBooksById, tagEntitiesById)
}
fun bookById(bookId: String): RecentFileItem? {
return androidBooksById[bookId] ?: fallbackBooksById[bookId]?.toAndroidBook()
@@ -374,7 +260,7 @@ fun SharedReaderScreenState.toAndroidReaderScreenState(
isAddingBooksToShelf = isAddingBooksToShelf,
contextualActionShelfIds = selectedShelfIds,
contextualActionItems = selectedBookIds.mapNotNullTo(mutableSetOf()) { bookById(it) },
- shelves = shelves.map { shelf -> shelf.toAndroidShelf { book -> book.toAndroidBook() } },
+ shelves = shelves.map { it.toAndroidShelf(androidBooksById, tagEntitiesById) },
openTabs = openTabs.map { it.toAndroidBook() },
openTabIds = openTabIds,
activeTabBookId = activeTabBookId,
@@ -386,19 +272,13 @@ fun SharedReaderScreenState.toAndroidReaderScreenState(
fun SharedShelf.toAndroidShelf(
androidBooksById: Map = emptyMap(),
tagEntitiesById: Map = emptyMap()
-): Shelf {
- return toAndroidShelf { it.toRecentFileItem(androidBooksById, tagEntitiesById) }
-}
-
-private fun SharedShelf.toAndroidShelf(
- resolveBook: (SharedBookItem) -> RecentFileItem
): Shelf {
return Shelf(
id = id,
name = name,
type = type,
- books = books.map(resolveBook),
- directBooks = directBooks.map(resolveBook),
+ books = books.map { it.toRecentFileItem(androidBooksById, tagEntitiesById) },
+ directBooks = directBooks.map { it.toRecentFileItem(androidBooksById, tagEntitiesById) },
parentShelfId = parentShelfId,
childShelfIds = childShelfIds,
depth = depth,
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/StorageTracker.kt b/app/src/main/java/com/aryan/reader/StorageTracker.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/StorageTracker.kt
rename to app/src/main/java/com/aryan/reader/StorageTracker.kt
index 4c8f990..5ed62bd 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/StorageTracker.kt
+++ b/app/src/main/java/com/aryan/reader/StorageTracker.kt
@@ -1,5 +1,5 @@
// StorageTracker.kt
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import timber.log.Timber
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/ThemedBookCover.kt b/app/src/main/java/com/aryan/reader/ThemedBookCover.kt
similarity index 98%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/ThemedBookCover.kt
rename to app/src/main/java/com/aryan/reader/ThemedBookCover.kt
index d4a73f4..1cfbfcc 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/ThemedBookCover.kt
+++ b/app/src/main/java/com/aryan/reader/ThemedBookCover.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import androidx.compose.foundation.background
import androidx.compose.foundation.border
@@ -31,7 +31,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import coil.request.ImageRequest
-import org.dueattendant149.bookreader.data.RecentFileItem
+import com.aryan.reader.data.RecentFileItem
import java.io.File
import kotlin.math.absoluteValue
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/TtsReplacementStore.kt b/app/src/main/java/com/aryan/reader/TtsReplacementStore.kt
similarity index 79%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/TtsReplacementStore.kt
rename to app/src/main/java/com/aryan/reader/TtsReplacementStore.kt
index bbdff5c..5167ba5 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/TtsReplacementStore.kt
+++ b/app/src/main/java/com/aryan/reader/TtsReplacementStore.kt
@@ -1,11 +1,11 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import android.content.Context
import androidx.core.content.edit
-import org.dueattendant149.bookreader.paginatedreader.TtsChunk
-import org.dueattendant149.bookreader.shared.ReaderTtsReplacementEngine
-import org.dueattendant149.bookreader.shared.ReaderTtsReplacementPreferences
-import org.dueattendant149.bookreader.shared.ReaderTtsReplacementPreferencesJson
+import com.aryan.reader.paginatedreader.TtsChunk
+import com.aryan.reader.shared.ReaderTtsReplacementEngine
+import com.aryan.reader.shared.ReaderTtsReplacementPreferences
+import com.aryan.reader.shared.ReaderTtsReplacementPreferencesJson
private const val READER_PREFS_NAME = "reader_prefs"
private const val TTS_REPLACEMENTS_KEY = "tts_word_replacements_json"
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/TtsWordReplacementsSheet.kt b/app/src/main/java/com/aryan/reader/TtsWordReplacementsSheet.kt
similarity index 98%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/TtsWordReplacementsSheet.kt
rename to app/src/main/java/com/aryan/reader/TtsWordReplacementsSheet.kt
index 47ec692..75d2c7e 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/TtsWordReplacementsSheet.kt
+++ b/app/src/main/java/com/aryan/reader/TtsWordReplacementsSheet.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -56,11 +56,11 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
-import org.dueattendant149.bookreader.shared.ReaderTtsReplacementBookSettings
-import org.dueattendant149.bookreader.shared.ReaderTtsReplacementEngine
-import org.dueattendant149.bookreader.shared.ReaderTtsReplacementPreferences
-import org.dueattendant149.bookreader.shared.ReaderTtsReplacementRule
-import org.dueattendant149.bookreader.shared.ReaderTtsReplacementSuggestions
+import com.aryan.reader.shared.ReaderTtsReplacementBookSettings
+import com.aryan.reader.shared.ReaderTtsReplacementEngine
+import com.aryan.reader.shared.ReaderTtsReplacementPreferences
+import com.aryan.reader.shared.ReaderTtsReplacementRule
+import com.aryan.reader.shared.ReaderTtsReplacementSuggestions
private enum class TtsReplacementScope {
Global,
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/UiLabelResources.kt b/app/src/main/java/com/aryan/reader/UiLabelResources.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/UiLabelResources.kt
rename to app/src/main/java/com/aryan/reader/UiLabelResources.kt
index 9a280eb..5467cd1 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/UiLabelResources.kt
+++ b/app/src/main/java/com/aryan/reader/UiLabelResources.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader
+package com.aryan.reader
import androidx.annotation.StringRes
import java.text.Normalizer
@@ -67,8 +67,7 @@ val supportedAppLanguageOptions = listOf(
"中文",
"简体中文",
)
- ),
- AppLanguageOption("et", R.string.language_estonian, listOf("estonian", "eesti"))
+ )
)
val appLanguageSelectionOptions = listOf(systemAppLanguageOption) + supportedAppLanguageOptions
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/AppDatabase.kt b/app/src/main/java/com/aryan/reader/data/AppDatabase.kt
similarity index 93%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/AppDatabase.kt
rename to app/src/main/java/com/aryan/reader/data/AppDatabase.kt
index 5767976..15a2799 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/AppDatabase.kt
+++ b/app/src/main/java/com/aryan/reader/data/AppDatabase.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import android.content.Context
import androidx.room.Database
@@ -36,7 +36,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
TagEntity::class,
BookTagCrossRef::class
],
- version = 23,
+ version = 22,
exportSchema = false
)
@TypeConverters(FileTypeConverter::class)
@@ -288,25 +288,6 @@ abstract class AppDatabase : RoomDatabase() {
}
}
- val MIGRATION_22_23 = object : Migration(22, 23) {
- override fun migrate(db: SupportSQLiteDatabase) {
- db.execSQL("ALTER TABLE recent_files ADD COLUMN readingPositionModifiedTimestamp INTEGER NOT NULL DEFAULT 0")
- db.execSQL("""
- UPDATE recent_files
- SET readingPositionModifiedTimestamp = lastModifiedTimestamp
- WHERE lastModifiedTimestamp > 0
- AND (
- lastChapterIndex IS NOT NULL OR
- lastPage IS NOT NULL OR
- lastPositionCfi IS NOT NULL OR
- locatorBlockIndex IS NOT NULL OR
- locatorCharOffset IS NOT NULL OR
- COALESCE(progressPercentage, 0) > 0
- )
- """)
- }
- }
-
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
@@ -320,7 +301,7 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12,
MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15, MIGRATION_15_16,
MIGRATION_16_17, MIGRATION_17_18, MIGRATION_18_19, MIGRATION_19_20,
- MIGRATION_20_21, MIGRATION_21_22, MIGRATION_22_23
+ MIGRATION_20_21, MIGRATION_21_22
)
.fallbackToDestructiveMigration(false)
.build()
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/BookMetadataEdit.kt b/app/src/main/java/com/aryan/reader/data/BookMetadataEdit.kt
similarity index 79%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/BookMetadataEdit.kt
rename to app/src/main/java/com/aryan/reader/data/BookMetadataEdit.kt
index 6d37ea6..3e403fe 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/BookMetadataEdit.kt
+++ b/app/src/main/java/com/aryan/reader/data/BookMetadataEdit.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
data class BookMetadataEdit(
val title: String?,
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/CustomFontDao.kt b/app/src/main/java/com/aryan/reader/data/CustomFontDao.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/CustomFontDao.kt
rename to app/src/main/java/com/aryan/reader/data/CustomFontDao.kt
index 4d77445..2ce7dc2 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/CustomFontDao.kt
+++ b/app/src/main/java/com/aryan/reader/data/CustomFontDao.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import androidx.room.Dao
import androidx.room.Insert
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/CustomFontEntity.kt b/app/src/main/java/com/aryan/reader/data/CustomFontEntity.kt
similarity index 96%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/CustomFontEntity.kt
rename to app/src/main/java/com/aryan/reader/data/CustomFontEntity.kt
index 696a26b..6d1e77b 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/CustomFontEntity.kt
+++ b/app/src/main/java/com/aryan/reader/data/CustomFontEntity.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import androidx.room.ColumnInfo
import androidx.room.Entity
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/FileTypeConverter.kt b/app/src/main/java/com/aryan/reader/data/FileTypeConverter.kt
similarity index 92%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/FileTypeConverter.kt
rename to app/src/main/java/com/aryan/reader/data/FileTypeConverter.kt
index 3ce67d3..8819888 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/FileTypeConverter.kt
+++ b/app/src/main/java/com/aryan/reader/data/FileTypeConverter.kt
@@ -17,10 +17,10 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import androidx.room.TypeConverter
-import org.dueattendant149.bookreader.FileType
+import com.aryan.reader.FileType
class FileTypeConverter {
@TypeConverter
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/FolderBookMetadata.kt b/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt
similarity index 96%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/FolderBookMetadata.kt
rename to app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt
index 19b43a7..7d5a7a0 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/FolderBookMetadata.kt
+++ b/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt
@@ -1,8 +1,8 @@
// FolderBookMetadata.kt
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
-import org.dueattendant149.bookreader.FileType
-import org.dueattendant149.bookreader.shared.SharedFolderBookMetadata
+import com.aryan.reader.FileType
+import com.aryan.reader.shared.SharedFolderBookMetadata
data class FolderBookMetadata(
val bookId: String,
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/FontsRepository.kt b/app/src/main/java/com/aryan/reader/data/FontsRepository.kt
similarity index 71%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/FontsRepository.kt
rename to app/src/main/java/com/aryan/reader/data/FontsRepository.kt
index cdee799..8f96108 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/FontsRepository.kt
+++ b/app/src/main/java/com/aryan/reader/data/FontsRepository.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import android.content.Context
import android.net.Uri
@@ -25,15 +25,12 @@ import android.provider.OpenableColumns
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
-import org.dueattendant149.bookreader.ReaderFontDiagnosticsTag
-import org.dueattendant149.bookreader.readerFontDiagnosticSummary
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.util.UUID
private const val FONTS_DIR = "custom_fonts"
-private const val MAX_IMPORTED_FONT_BASENAME_LENGTH = 120
class FontsRepository(private val context: Context) {
private val fontDao = AppDatabase.getDatabase(context).customFontDao()
@@ -76,23 +73,14 @@ class FontsRepository(private val context: Context) {
val contentResolver = context.contentResolver
val originalName = getFileName(uri) ?: "unknown.ttf"
val extension = originalName.substringAfterLast('.', "").lowercase()
- val displayName = originalName.substringBeforeLast('.')
-
- Timber.tag(ReaderFontDiagnosticsTag).i(
- "import.start originalName='$originalName' extension='$extension' " +
- readerFontDiagnosticSummary(displayName)
- )
if (extension !in listOf("ttf", "otf", "woff2")) {
- Timber.tag(ReaderFontDiagnosticsTag).w(
- "import.unsupported originalName='$originalName' extension='$extension'"
- )
return@withContext Result.failure(Exception("Unsupported font format. Please use TTF, OTF, or WOFF2."))
}
val fontId = UUID.randomUUID().toString()
- val destinationFile = uniqueImportedFontFile(displayName, extension, fontId)
- val internalFileName = destinationFile.name
+ val internalFileName = "font_${fontId}.$extension"
+ val destinationFile = File(fontsDir, internalFileName)
contentResolver.openInputStream(uri)?.use { input ->
FileOutputStream(destinationFile).use { output ->
@@ -100,6 +88,8 @@ class FontsRepository(private val context: Context) {
}
}
+ val displayName = originalName.substringBeforeLast('.')
+
val entity = CustomFontEntity(
id = fontId,
displayName = displayName,
@@ -110,16 +100,10 @@ class FontsRepository(private val context: Context) {
)
fontDao.insertFont(entity)
- Timber.tag(ReaderFontDiagnosticsTag).i(
- "import.saved displayName='$displayName' internalFileName='$internalFileName' " +
- "exists=${destinationFile.exists()} bytes=${destinationFile.length()} " +
- "path='${destinationFile.absolutePath}'"
- )
Timber.d("Imported font: $displayName to ${destinationFile.absolutePath}")
Result.success(entity)
} catch (e: Exception) {
- Timber.tag(ReaderFontDiagnosticsTag).e(e, "import.failed uri='$uri'")
Timber.e(e, "Failed to import font")
Result.failure(e)
}
@@ -146,18 +130,6 @@ class FontsRepository(private val context: Context) {
fontDao.deletePermanently(fontId)
}
- private fun uniqueImportedFontFile(displayName: String, extension: String, fontId: String): File {
- val preferredFileName = importedFontFileName(displayName, extension)
- val preferredFile = File(fontsDir, preferredFileName)
- if (!preferredFile.exists()) return preferredFile
-
- val fallbackFileName = importedFontFileName(
- displayName = "${displayName}_${fontId.take(8)}",
- extension = extension
- )
- return File(fontsDir, fallbackFileName)
- }
-
private fun getFileName(uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
@@ -180,18 +152,4 @@ class FontsRepository(private val context: Context) {
}
return result
}
-}
-
-internal fun importedFontFileName(displayName: String, extension: String): String {
- val safeBaseName = displayName
- .replace(Regex("""[\\/:*?"<>|\p{Cntrl}]"""), "_")
- .replace(Regex("""\s+"""), " ")
- .trim(' ', '.')
- .take(MAX_IMPORTED_FONT_BASENAME_LENGTH)
- .ifBlank { "font" }
- val safeExtension = extension
- .lowercase()
- .replace(Regex("""[^a-z0-9]"""), "")
- .ifBlank { "ttf" }
- return "$safeBaseName.$safeExtension"
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/LibraryDaos.kt b/app/src/main/java/com/aryan/reader/data/LibraryDaos.kt
similarity index 98%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/LibraryDaos.kt
rename to app/src/main/java/com/aryan/reader/data/LibraryDaos.kt
index 8bdbefd..8420722 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/LibraryDaos.kt
+++ b/app/src/main/java/com/aryan/reader/data/LibraryDaos.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import androidx.room.Dao
import androidx.room.Insert
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/LibraryEntities.kt b/app/src/main/java/com/aryan/reader/data/LibraryEntities.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/LibraryEntities.kt
rename to app/src/main/java/com/aryan/reader/data/LibraryEntities.kt
index b005dd6..322def1 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/LibraryEntities.kt
+++ b/app/src/main/java/com/aryan/reader/data/LibraryEntities.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import androidx.room.Entity
import androidx.room.ForeignKey
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/LocalSyncUtils.kt b/app/src/main/java/com/aryan/reader/data/LocalSyncUtils.kt
similarity index 95%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/LocalSyncUtils.kt
rename to app/src/main/java/com/aryan/reader/data/LocalSyncUtils.kt
index 90ccae3..889d5a1 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/LocalSyncUtils.kt
+++ b/app/src/main/java/com/aryan/reader/data/LocalSyncUtils.kt
@@ -1,19 +1,18 @@
// LocalSyncUtils.kt
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.provider.DocumentsContract
import androidx.documentfile.provider.DocumentFile
-import org.dueattendant149.bookreader.ReaderPerfLog
-import org.dueattendant149.bookreader.shared.LOCAL_FOLDER_SIDECAR_HASH_PREFIX
-import org.dueattendant149.bookreader.shared.LOCAL_FOLDER_SYNC_DATA_DIR
-import org.dueattendant149.bookreader.shared.localFolderSyncAnnotationFileName
-import org.dueattendant149.bookreader.shared.localFolderSyncAnnotationTempFileName
-import org.dueattendant149.bookreader.shared.localFolderSyncMetadataFileName
-import org.dueattendant149.bookreader.shared.localFolderSyncMetadataTempFileName
-import org.dueattendant149.bookreader.shared.localFolderSyncSidecarStem
+import com.aryan.reader.ReaderPerfLog
+import com.aryan.reader.shared.LOCAL_FOLDER_SIDECAR_HASH_PREFIX
+import com.aryan.reader.shared.localFolderSyncAnnotationFileName
+import com.aryan.reader.shared.localFolderSyncAnnotationTempFileName
+import com.aryan.reader.shared.localFolderSyncMetadataFileName
+import com.aryan.reader.shared.localFolderSyncMetadataTempFileName
+import com.aryan.reader.shared.localFolderSyncSidecarStem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
@@ -22,7 +21,7 @@ import timber.log.Timber
object LocalSyncUtils {
private const val TAG = "FolderSync"
private const val ANNOTATION_SUFFIX = "_annotations"
- private const val SYNC_SUBFOLDER_NAME = LOCAL_FOLDER_SYNC_DATA_DIR
+ private const val SYNC_SUBFOLDER_NAME = "EpistemeSyncData"
private data class SyncFileEntry(
val name: String,
@@ -639,21 +638,6 @@ object LocalSyncUtils {
}
}
- suspend fun deleteSyncDataFolder(
- context: Context,
- sourceFolderUri: Uri
- ): Boolean = withContext(Dispatchers.IO) {
- try {
- val rootTree = DocumentFile.fromTreeUri(context, sourceFolderUri) ?: return@withContext false
- val syncDir = rootTree.findFile(SYNC_SUBFOLDER_NAME) ?: return@withContext true
- if (!syncDir.isDirectory) return@withContext false
- syncDir.delete()
- } catch (e: Exception) {
- Timber.tag(TAG).e(e, "Failed to delete sync data folder")
- false
- }
- }
-
suspend fun getAllFolderMetadata(
context: Context,
sourceFolderUri: Uri
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/PurchaseEntities.kt b/app/src/main/java/com/aryan/reader/data/PurchaseEntities.kt
similarity index 96%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/PurchaseEntities.kt
rename to app/src/main/java/com/aryan/reader/data/PurchaseEntities.kt
index 4f3f49d..a3e41b6 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/PurchaseEntities.kt
+++ b/app/src/main/java/com/aryan/reader/data/PurchaseEntities.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
/**
* Agnostic representation of a purchase to decouple MainViewModel from Billing Library.
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileDao.kt b/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt
similarity index 93%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileDao.kt
rename to app/src/main/java/com/aryan/reader/data/RecentFileDao.kt
index f1c45e7..08cc5fb 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileDao.kt
+++ b/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt
@@ -18,7 +18,7 @@
* mail: epistemereader@gmail.com
*/
// RecentFileDao.kt
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import androidx.room.Dao
import androidx.room.Query
@@ -33,7 +33,7 @@ interface RecentFileDao {
@Upsert
suspend fun insertOrUpdateFiles(files: List)
- @Query("SELECT bookId, uriString, type, displayName, timestamp, coverImagePath, title, author, lastChapterIndex, lastPage, lastPositionCfi, progressPercentage, isRecent, isAvailable, lastModifiedTimestamp, isDeleted, locatorBlockIndex, locatorCharOffset, sourceFolderUri, isReflowPreferred, customName, fileSize, fileContentModifiedTimestamp, seriesName, seriesIndex, substr(description, 1, 4096) AS description, originalTitle, originalAuthor, originalSeriesName, originalSeriesIndex, substr(originalDescription, 1, 4096) AS originalDescription, readingPositionModifiedTimestamp FROM recent_files WHERE isDeleted = 0 ORDER BY timestamp DESC")
+ @Query("SELECT bookId, uriString, type, displayName, timestamp, coverImagePath, title, author, lastChapterIndex, lastPage, lastPositionCfi, progressPercentage, isRecent, isAvailable, lastModifiedTimestamp, isDeleted, locatorBlockIndex, locatorCharOffset, sourceFolderUri, isReflowPreferred, customName, fileSize, fileContentModifiedTimestamp, seriesName, seriesIndex, description, originalTitle, originalAuthor, originalSeriesName, originalSeriesIndex, originalDescription FROM recent_files WHERE isDeleted = 0 ORDER BY timestamp DESC")
fun getRecentFiles(): Flow>
@Query("SELECT * FROM recent_files WHERE sourceFolderUri = :sourceFolderUri AND isDeleted = 0")
@@ -45,7 +45,7 @@ interface RecentFileDao {
@Query("UPDATE recent_files SET isReflowPreferred = :isPreferred WHERE bookId = :bookId")
suspend fun updateReflowPreference(bookId: String, isPreferred: Boolean)
- @Query("SELECT bookId, uriString, type, displayName, timestamp, coverImagePath, title, author, lastChapterIndex, lastPage, lastPositionCfi, progressPercentage, isRecent, isAvailable, lastModifiedTimestamp, isDeleted, locatorBlockIndex, locatorCharOffset, sourceFolderUri, isReflowPreferred, customName, fileSize, fileContentModifiedTimestamp, seriesName, seriesIndex, substr(description, 1, 4096) AS description, originalTitle, originalAuthor, originalSeriesName, originalSeriesIndex, substr(originalDescription, 1, 4096) AS originalDescription, readingPositionModifiedTimestamp FROM recent_files WHERE isDeleted = 0 ORDER BY timestamp DESC LIMIT :limit")
+ @Query("SELECT bookId, uriString, type, displayName, timestamp, coverImagePath, title, author, lastChapterIndex, lastPage, lastPositionCfi, progressPercentage, isRecent, isAvailable, lastModifiedTimestamp, isDeleted, locatorBlockIndex, locatorCharOffset, sourceFolderUri, isReflowPreferred, customName, fileSize, fileContentModifiedTimestamp, seriesName, seriesIndex, description, originalTitle, originalAuthor, originalSeriesName, originalSeriesIndex, originalDescription FROM recent_files WHERE isDeleted = 0 ORDER BY timestamp DESC LIMIT :limit")
fun getRecentFilesList(limit: Int): List
@Query("DELETE FROM recent_files WHERE bookId IN (:bookIds)")
@@ -75,10 +75,10 @@ interface RecentFileDao {
@Query("DELETE FROM recent_files")
suspend fun clearAll()
- @Query("UPDATE recent_files SET lastPositionCfi = :cfi, lastChapterIndex = :chapterIndex, locatorBlockIndex = :blockIndex, locatorCharOffset = :charOffset, progressPercentage = :progress, timestamp = :timestamp, lastModifiedTimestamp = :timestamp, readingPositionModifiedTimestamp = :timestamp WHERE bookId = :bookId")
+ @Query("UPDATE recent_files SET lastPositionCfi = :cfi, lastChapterIndex = :chapterIndex, locatorBlockIndex = :blockIndex, locatorCharOffset = :charOffset, progressPercentage = :progress, timestamp = :timestamp, lastModifiedTimestamp = :timestamp WHERE bookId = :bookId")
suspend fun updateEpubReadingPosition(bookId: String, cfi: String?, chapterIndex: Int, blockIndex: Int, charOffset: Int, progress: Float, timestamp: Long)
- @Query("UPDATE recent_files SET lastPage = :page, progressPercentage = :progress, timestamp = :timestamp, lastModifiedTimestamp = :timestamp, readingPositionModifiedTimestamp = :timestamp WHERE bookId = :bookId")
+ @Query("UPDATE recent_files SET lastPage = :page, progressPercentage = :progress, timestamp = :timestamp, lastModifiedTimestamp = :timestamp WHERE bookId = :bookId")
suspend fun updatePdfReadingPosition(bookId: String, page: Int, progress: Float, timestamp: Long)
@Query("UPDATE recent_files SET bookmarks = :bookmarksJson, lastModifiedTimestamp = :timestamp WHERE bookId = :bookId")
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileEntity.kt b/app/src/main/java/com/aryan/reader/data/RecentFileEntity.kt
similarity index 94%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileEntity.kt
rename to app/src/main/java/com/aryan/reader/data/RecentFileEntity.kt
index 500398a..26ace97 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileEntity.kt
+++ b/app/src/main/java/com/aryan/reader/data/RecentFileEntity.kt
@@ -17,13 +17,13 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
-import org.dueattendant149.bookreader.FileType
+import com.aryan.reader.FileType
@Entity(tableName = "recent_files")
@TypeConverters(FileTypeConverter::class)
@@ -62,8 +62,7 @@ data class RecentFileEntity(
@ColumnInfo(defaultValue = "NULL") val originalAuthor: String? = null,
@ColumnInfo(defaultValue = "NULL") val originalSeriesName: String? = null,
@ColumnInfo(defaultValue = "NULL") val originalSeriesIndex: Double? = null,
- @ColumnInfo(defaultValue = "NULL") val originalDescription: String? = null,
- @ColumnInfo(defaultValue = "0") val readingPositionModifiedTimestamp: Long = 0L
+ @ColumnInfo(defaultValue = "NULL") val originalDescription: String? = null
)
data class RecentFileSummary(
@@ -97,6 +96,5 @@ data class RecentFileSummary(
@ColumnInfo(defaultValue = "NULL") val originalAuthor: String? = null,
@ColumnInfo(defaultValue = "NULL") val originalSeriesName: String? = null,
@ColumnInfo(defaultValue = "NULL") val originalSeriesIndex: Double? = null,
- @ColumnInfo(defaultValue = "NULL") val originalDescription: String? = null,
- @ColumnInfo(defaultValue = "0") val readingPositionModifiedTimestamp: Long = 0L
+ @ColumnInfo(defaultValue = "NULL") val originalDescription: String? = null
)
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileItem.kt b/app/src/main/java/com/aryan/reader/data/RecentFileItem.kt
similarity index 83%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileItem.kt
rename to app/src/main/java/com/aryan/reader/data/RecentFileItem.kt
index 3700fb1..6276f79 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileItem.kt
+++ b/app/src/main/java/com/aryan/reader/data/RecentFileItem.kt
@@ -17,9 +17,9 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
-import org.dueattendant149.bookreader.FileType
+import com.aryan.reader.FileType
data class RecentFileItem(
val bookId: String,
@@ -57,25 +57,9 @@ data class RecentFileItem(
val originalDescription: String? = null,
val folderTextMetadataParsed: Boolean = false,
val folderCoverMetadataParsed: Boolean = false,
- val readingPositionModifiedTimestamp: Long = 0L,
val tags: List = emptyList()
)
-fun RecentFileItem.hasReadingPositionForSync(): Boolean {
- return lastChapterIndex != null ||
- lastPage != null ||
- !lastPositionCfi.isNullOrBlank() ||
- locatorBlockIndex != null ||
- locatorCharOffset != null ||
- (progressPercentage ?: 0f) > 0f
-}
-
-fun RecentFileItem.effectiveReadingPositionModifiedTimestamp(): Long {
- return readingPositionModifiedTimestamp.takeIf { it > 0L }
- ?: lastModifiedTimestamp.takeIf { hasReadingPositionForSync() }
- ?: 0L
-}
-
fun RecentFileEntity.toRecentFileItem(): RecentFileItem {
return RecentFileItem(
bookId = this.bookId,
@@ -112,8 +96,7 @@ fun RecentFileEntity.toRecentFileItem(): RecentFileItem {
originalSeriesIndex = this.originalSeriesIndex,
originalDescription = this.originalDescription,
folderTextMetadataParsed = this.folderTextMetadataParsed,
- folderCoverMetadataParsed = this.folderCoverMetadataParsed,
- readingPositionModifiedTimestamp = this.readingPositionModifiedTimestamp
+ folderCoverMetadataParsed = this.folderCoverMetadataParsed
)
}
@@ -153,8 +136,7 @@ fun RecentFileItem.toRecentFileEntity(): RecentFileEntity {
originalSeriesIndex = this.originalSeriesIndex ?: this.seriesIndex,
originalDescription = this.originalDescription ?: this.description,
folderTextMetadataParsed = this.folderTextMetadataParsed,
- folderCoverMetadataParsed = this.folderCoverMetadataParsed,
- readingPositionModifiedTimestamp = this.effectiveReadingPositionModifiedTimestamp()
+ folderCoverMetadataParsed = this.folderCoverMetadataParsed
)
}
@@ -174,7 +156,6 @@ fun RecentFileItem.toBookMetadata(): BookMetadata {
isRecent = this.isRecent,
isDeleted = this.isDeleted,
lastModifiedTimestamp = this.lastModifiedTimestamp,
- readingPositionModifiedTimestamp = this.effectiveReadingPositionModifiedTimestamp(),
bookmarksJson = this.bookmarksJson,
hasAnnotations = false,
customName = this.customName,
@@ -191,27 +172,6 @@ fun RecentFileItem.toBookMetadata(): BookMetadata {
)
}
-fun BookMetadata.hasReadingPositionForSync(): Boolean {
- return lastChapterIndex != null ||
- lastPage != null ||
- !lastPositionCfi.isNullOrBlank() ||
- locatorBlockIndex != null ||
- locatorCharOffset != null ||
- (progressPercentage ?: 0f) > 0f
-}
-
-fun BookMetadata.effectiveReadingPositionModifiedTimestamp(): Long {
- return readingPositionModifiedTimestamp.takeIf { it > 0L }
- ?: lastModifiedTimestamp.takeIf { hasReadingPositionForSync() }
- ?: 0L
-}
-
-fun BookMetadata.effectiveAnnotationModifiedTimestamp(sidecarModifiedTimestamp: Long = 0L): Long {
- return sidecarModifiedTimestamp.takeIf { it > 0L }
- ?: annotationModifiedTimestamp.takeIf { it > 0L }
- ?: 0L
-}
-
fun BookMetadata.toRecentFileItem(): RecentFileItem {
return RecentFileItem(
bookId = this.bookId,
@@ -243,8 +203,7 @@ fun BookMetadata.toRecentFileItem(): RecentFileItem {
originalAuthor = this.originalAuthor,
originalSeriesName = this.originalSeriesName,
originalSeriesIndex = this.originalSeriesIndex,
- originalDescription = this.originalDescription,
- readingPositionModifiedTimestamp = this.effectiveReadingPositionModifiedTimestamp()
+ originalDescription = this.originalDescription
)
}
@@ -282,7 +241,6 @@ fun RecentFileSummary.toRecentFileItem(): RecentFileItem {
originalAuthor = this.originalAuthor,
originalSeriesName = this.originalSeriesName,
originalSeriesIndex = this.originalSeriesIndex,
- originalDescription = this.originalDescription,
- readingPositionModifiedTimestamp = this.readingPositionModifiedTimestamp
+ originalDescription = this.originalDescription
)
}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileItemAndroid.kt b/app/src/main/java/com/aryan/reader/data/RecentFileItemAndroid.kt
similarity index 71%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileItemAndroid.kt
rename to app/src/main/java/com/aryan/reader/data/RecentFileItemAndroid.kt
index d8b87c5..76d5aae 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFileItemAndroid.kt
+++ b/app/src/main/java/com/aryan/reader/data/RecentFileItemAndroid.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import android.net.Uri
import androidx.core.net.toUri
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFilesRepository.kt b/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt
similarity index 86%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFilesRepository.kt
rename to app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt
index dcbd7de..673485e 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/data/RecentFilesRepository.kt
+++ b/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt
@@ -18,27 +18,21 @@
* mail: epistemereader@gmail.com
*/
// RecentFilesRepository.kt
-package org.dueattendant149.bookreader.data
+package com.aryan.reader.data
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.net.toUri
-import org.dueattendant149.bookreader.FileType
-import org.dueattendant149.bookreader.ReaderPerfLog
-import org.dueattendant149.bookreader.SyncedFolderPrefs
-import org.dueattendant149.bookreader.cloudSyncPreview
-import org.dueattendant149.bookreader.cloudSyncTraceSummary
-import org.dueattendant149.bookreader.logCloudAnnotationSyncTrace
-import org.dueattendant149.bookreader.logCloudSyncTrace
-import org.dueattendant149.bookreader.scaledToCanvasLimit
+import com.aryan.reader.FileType
+import com.aryan.reader.ReaderPerfLog
+import com.aryan.reader.scaledToCanvasLimit
import timber.log.Timber
-import org.dueattendant149.bookreader.BookImporter
-import org.dueattendant149.bookreader.cloudSyncAnnotationSummary
-import org.dueattendant149.bookreader.paginatedreader.Locator
-import org.dueattendant149.bookreader.pdf.PdfRichTextRepository
-import org.dueattendant149.bookreader.epub.ImportedFileCache
+import com.aryan.reader.BookImporter
+import com.aryan.reader.paginatedreader.Locator
+import com.aryan.reader.pdf.PdfRichTextRepository
+import com.aryan.reader.epub.ImportedFileCache
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@@ -46,10 +40,10 @@ import kotlinx.coroutines.withContext
import androidx.room.withTransaction
import java.io.File
import java.io.FileOutputStream
-import org.dueattendant149.bookreader.pdf.data.PdfAnnotationRepository
-import org.dueattendant149.bookreader.pdf.data.PageLayoutRepository
-import org.dueattendant149.bookreader.pdf.data.PdfTextBoxRepository
-import org.dueattendant149.bookreader.shared.pdf.SharedPdfAnnotationSidecarCodec
+import com.aryan.reader.pdf.data.PdfAnnotationRepository
+import com.aryan.reader.pdf.data.PageLayoutRepository
+import com.aryan.reader.pdf.data.PdfTextBoxRepository
+import com.aryan.reader.shared.pdf.SharedPdfAnnotationSidecarCodec
import org.json.JSONObject
import org.json.JSONArray
import java.util.UUID
@@ -71,7 +65,7 @@ class RecentFilesRepository(private val context: Context) {
private val pdfRichTextRepository = PdfRichTextRepository(context)
private val pageLayoutRepository = PageLayoutRepository(context)
private val pdfTextBoxRepository = PdfTextBoxRepository(context)
- private val pdfHighlightRepository = org.dueattendant149.bookreader.pdf.data.PdfHighlightRepository(context)
+ private val pdfHighlightRepository = com.aryan.reader.pdf.data.PdfHighlightRepository(context)
val activeShelvesFlow = database.shelfDao().getAllActiveShelves()
val shelfCrossRefsFlow = database.shelfDao().getAllBookShelfCrossRefs()
@@ -195,20 +189,7 @@ class RecentFilesRepository(private val context: Context) {
!embeddedMetadataFileChanged &&
existingItem.hasEmbeddedMetadataChanges()
- val incomingEntity = item.toRecentFileEntity()
- val incomingReadingTimestamp = item.effectiveReadingPositionModifiedTimestamp()
- val existingReadingTimestamp = existingItem.readingPositionModifiedTimestamp.takeIf { it > 0L }
- ?: existingItem.lastModifiedTimestamp.takeIf {
- existingItem.lastChapterIndex != null ||
- existingItem.lastPage != null ||
- !existingItem.lastPositionCfi.isNullOrBlank() ||
- existingItem.locatorBlockIndex != null ||
- existingItem.locatorCharOffset != null ||
- (existingItem.progressPercentage ?: 0f) > 0f
- }
- ?: 0L
- val incomingReadingWins = incomingReadingTimestamp >= existingReadingTimestamp
- incomingEntity.copy(
+ item.toRecentFileEntity().copy(
uriString = existingItem.uriString ?: item.uriString,
isAvailable = existingItem.isAvailable || item.isAvailable,
coverImagePath = if (folderFileChanged) {
@@ -230,13 +211,13 @@ class RecentFilesRepository(private val context: Context) {
} else {
item.author ?: existingItem.author
},
- lastChapterIndex = if (incomingReadingWins) item.lastChapterIndex ?: existingItem.lastChapterIndex else existingItem.lastChapterIndex,
- lastPage = if (incomingReadingWins) item.lastPage ?: existingItem.lastPage else existingItem.lastPage,
- lastPositionCfi = if (incomingReadingWins) item.lastPositionCfi ?: existingItem.lastPositionCfi else existingItem.lastPositionCfi,
- locatorBlockIndex = if (incomingReadingWins) item.locatorBlockIndex ?: existingItem.locatorBlockIndex else existingItem.locatorBlockIndex,
- locatorCharOffset = if (incomingReadingWins) item.locatorCharOffset ?: existingItem.locatorCharOffset else existingItem.locatorCharOffset,
+ lastChapterIndex = item.lastChapterIndex ?: existingItem.lastChapterIndex,
+ lastPage = item.lastPage ?: existingItem.lastPage,
+ lastPositionCfi = item.lastPositionCfi ?: existingItem.lastPositionCfi,
+ locatorBlockIndex = item.locatorBlockIndex ?: existingItem.locatorBlockIndex,
+ locatorCharOffset = item.locatorCharOffset ?: existingItem.locatorCharOffset,
bookmarks = item.bookmarksJson ?: existingItem.bookmarks,
- progressPercentage = if (incomingReadingWins) item.progressPercentage ?: existingItem.progressPercentage else existingItem.progressPercentage,
+ progressPercentage = item.progressPercentage ?: existingItem.progressPercentage,
isRecent = item.isRecent,
isDeleted = item.isDeleted,
sourceFolderUri = item.sourceFolderUri ?: existingItem.sourceFolderUri,
@@ -278,26 +259,13 @@ class RecentFilesRepository(private val context: Context) {
item.folderCoverMetadataParsed
} else {
item.folderCoverMetadataParsed || existingItem.folderCoverMetadataParsed
- },
- readingPositionModifiedTimestamp = maxOf(incomingReadingTimestamp, existingReadingTimestamp)
+ }
)
} else {
item.toRecentFileEntity()
}
Timber.d("SyncDebug: -> Final entity to insert: uri='${entityToInsert.uriString}', isAvailable=${entityToInsert.isAvailable}, isDeleted=${entityToInsert.isDeleted}, isRecent=${entityToInsert.isRecent}")
- logCloudSyncTrace {
- "android.db.upsert book=${item.bookId} ${item.cloudSyncTraceSummary("incoming")} " +
- "existingTs=${existingItem?.lastModifiedTimestamp} existingPage=${existingItem?.lastPage} " +
- "existingReadTs=${existingItem?.readingPositionModifiedTimestamp} " +
- "existingChapter=${existingItem?.lastChapterIndex} finalTs=${entityToInsert.lastModifiedTimestamp} " +
- "finalReadTs=${entityToInsert.readingPositionModifiedTimestamp} " +
- "finalPage=${entityToInsert.lastPage} finalChapter=${entityToInsert.lastChapterIndex} " +
- "finalBlock=${entityToInsert.locatorBlockIndex} finalChar=${entityToInsert.locatorCharOffset} " +
- "finalProgress=${entityToInsert.progressPercentage} finalCfi=${entityToInsert.lastPositionCfi.cloudSyncPreview()} " +
- "finalBookmarks=${entityToInsert.bookmarks.cloudSyncAnnotationSummary()} " +
- "finalHighlights=${entityToInsert.highlights.cloudSyncAnnotationSummary()}"
- }
recentFileDao.insertOrUpdateFile(entityToInsert)
Timber.d("Added/Updated recent file in DB: ${item.displayName}")
}
@@ -360,11 +328,6 @@ class RecentFilesRepository(private val context: Context) {
val folderUriString = entity.sourceFolderUri
if (folderUriString != null) {
- if (!isLocalFolderSyncEnabled(folderUriString)) {
- Timber.d("SyncDebug: Folder sync disabled for $folderUriString. Skipping metadata sidecar.")
- return@withContext
- }
-
val hasProgress = (entity.progressPercentage != null && entity.progressPercentage > 0f)
val hasBookmarks = !entity.bookmarks.isNullOrEmpty() && entity.bookmarks != "[]"
val hasHighlights = !entity.highlights.isNullOrEmpty() && entity.highlights != "[]"
@@ -422,10 +385,6 @@ class RecentFilesRepository(private val context: Context) {
Timber.tag("FolderAnnotationSync").w("sourceFolderUri is null for bookId: $bookId")
return@withContext
}
- if (!isLocalFolderSyncEnabled(folderUriString)) {
- Timber.tag("FolderAnnotationSync").d("Folder sync disabled for $folderUriString. Skipping annotation sidecar.")
- return@withContext
- }
val inkFile = pdfAnnotationRepository.getAnnotationFileForSync(bookId)
val richTextFile = pdfRichTextRepository.getFileForSync(bookId)
@@ -507,20 +466,12 @@ class RecentFilesRepository(private val context: Context) {
)
}
- suspend fun importAnnotationBundle(
- bookId: String,
- jsonString: String,
- lastModifiedTimestamp: Long? = null
- ) = withContext(Dispatchers.IO) {
+ suspend fun importAnnotationBundle(bookId: String, jsonString: String) = withContext(Dispatchers.IO) {
Timber.tag("FolderAnnotationSync").d("importAnnotationBundle: Processing bundle for $bookId")
try {
val bundle = JSONObject(
SharedPdfAnnotationSidecarCodec.legacyAndroidDataJsonFromCanonical(jsonString)
)
- logCloudAnnotationSyncTrace {
- "android.repository.import_bundle book=$bookId remoteTs=${lastModifiedTimestamp ?: 0L} " +
- "rawBytes=${jsonString.length} keys=${bundle.keys().asSequence().toList()}"
- }
Timber.d(
"android.folder.import.bundle book=$bookId rawLen=${jsonString.length} " +
"hasRichText=${bundle.has("text")} keys=${bundle.keys().asSequence().toList()}"
@@ -531,22 +482,12 @@ class RecentFilesRepository(private val context: Context) {
file.parentFile?.mkdirs()
val contentStr = bundle.get(key).toString()
file.writeText(contentStr)
- lastModifiedTimestamp?.takeIf { it > 0L }?.let(file::setLastModified)
- logCloudAnnotationSyncTrace {
- "android.repository.import_write key=$key book=$bookId bytes=${contentStr.length} " +
- "path=${file.absolutePath.cloudSyncPreview(140)} ts=${file.lastModified()}"
- }
if (key == "text") {
Timber.d(
"android.folder.import.writeRichText book=$bookId rawLen=${contentStr.length} file=${file.absolutePath}"
)
}
Timber.tag("FolderAnnotationSync").v(" -> Updated $key file (${contentStr.length} chars)")
- } else if (file != null) {
- logCloudAnnotationSyncTrace {
- "android.repository.import_missing_key key=$key book=$bookId " +
- "path=${file.absolutePath.cloudSyncPreview(140)} exists=${file.exists()}"
- }
}
}
@@ -555,10 +496,6 @@ class RecentFilesRepository(private val context: Context) {
context.filesDir, "annotations/annotation_$bookId.json"
)
writeSafe("ink", inkFile)
- writeSafe(
- SharedPdfAnnotationSidecarCodec.KEY_PDF_ANNOTATION_DELETIONS,
- File(context.filesDir, "annotations/deleted_annotation_$bookId.json")
- )
// 2. Text
writeSafe("text", pdfRichTextRepository.getFileForSync(bookId))
@@ -669,15 +606,6 @@ class RecentFilesRepository(private val context: Context) {
Timber.d("Detached all folder books. They are now standard local files.")
}
- private fun isLocalFolderSyncEnabled(folderUriString: String): Boolean {
- val prefs = context.getSharedPreferences("reader_user_prefs", Context.MODE_PRIVATE)
- return SyncedFolderPrefs.isLocalSyncEnabled(
- jsonString = prefs.getString(SyncedFolderPrefs.KEY_SYNCED_FOLDERS_JSON, null),
- legacyUri = prefs.getString(SyncedFolderPrefs.KEY_LEGACY_SYNCED_FOLDER_URI, null),
- folderUriString = folderUriString
- )
- }
-
suspend fun updateBookmarks(bookId: String, bookmarksJson: String) = withContext(Dispatchers.IO) {
val currentTime = System.currentTimeMillis()
recentFileDao.updateBookmarks(bookId, bookmarksJson, currentTime)
@@ -690,9 +618,6 @@ class RecentFilesRepository(private val context: Context) {
val currentTime = System.currentTimeMillis()
recentFileDao.updatePdfReadingPosition(item.bookId, page, progress, currentTime)
Timber.tag("PdfPositionDebug").i("Repository: Executed DB update for ${item.bookId} to Page $page, Progress $progress% at TS: $currentTime")
- logCloudSyncTrace {
- "android.repository.pdf_position_update book=${item.bookId} page=$page progress=$progress ts=$currentTime"
- }
} else {
Timber.tag("PdfPositionDebug").e("Repository: DB Update Failed! No recent file found matching URI: $uriString")
}
diff --git a/app/src/main/java/com/aryan/reader/data/SmartCollectionEngine.kt b/app/src/main/java/com/aryan/reader/data/SmartCollectionEngine.kt
new file mode 100644
index 0000000..41d0b80
--- /dev/null
+++ b/app/src/main/java/com/aryan/reader/data/SmartCollectionEngine.kt
@@ -0,0 +1,20 @@
+package com.aryan.reader.data
+
+import com.aryan.reader.toSharedBookItem
+import com.aryan.reader.shared.SmartCollectionEngine as SharedSmartCollectionEngine
+
+typealias SmartField = com.aryan.reader.shared.SmartField
+typealias SmartOperator = com.aryan.reader.shared.SmartOperator
+typealias SmartRule = com.aryan.reader.shared.SmartRule
+typealias SmartCollectionDefinition = com.aryan.reader.shared.SmartCollectionDefinition
+
+object SmartCollectionEngine {
+ fun toJson(definition: SmartCollectionDefinition): String =
+ SharedSmartCollectionEngine.toJson(definition)
+
+ fun fromJson(json: String?): SmartCollectionDefinition? =
+ SharedSmartCollectionEngine.fromJson(json)
+
+ fun evaluate(book: RecentFileItem, definition: SmartCollectionDefinition): Boolean =
+ SharedSmartCollectionEngine.evaluate(book.toSharedBookItem(), definition)
+}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/BitmapSerializer.kt b/app/src/main/java/com/aryan/reader/epub/BitmapSerializer.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/BitmapSerializer.kt
rename to app/src/main/java/com/aryan/reader/epub/BitmapSerializer.kt
index 3a5e736..a94dd51 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/BitmapSerializer.kt
+++ b/app/src/main/java/com/aryan/reader/epub/BitmapSerializer.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import android.graphics.Bitmap
import android.graphics.BitmapFactory
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/CalibreBundleExtractor.kt b/app/src/main/java/com/aryan/reader/epub/CalibreBundleExtractor.kt
similarity index 86%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/CalibreBundleExtractor.kt
rename to app/src/main/java/com/aryan/reader/epub/CalibreBundleExtractor.kt
index 5387a2e..12a06e4 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/CalibreBundleExtractor.kt
+++ b/app/src/main/java/com/aryan/reader/epub/CalibreBundleExtractor.kt
@@ -1,13 +1,13 @@
// CalibreBundleExtractor.kt
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.net.toUri
-import org.dueattendant149.bookreader.BookImporter
-import org.dueattendant149.bookreader.FileType
-import org.dueattendant149.bookreader.data.RecentFilesRepository
+import com.aryan.reader.BookImporter
+import com.aryan.reader.FileType
+import com.aryan.reader.data.RecentFilesRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.w3c.dom.Element
@@ -15,8 +15,8 @@ import timber.log.Timber
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
-import java.io.IOException
import java.util.zip.ZipInputStream
+import javax.xml.parsers.DocumentBuilderFactory
data class CalibreBundleResult(
val internalBookUri: Uri,
@@ -89,7 +89,7 @@ object CalibreBundleExtractor {
if (tempBookFile != null && opfData != null && extractedType != null) {
val finalBookFile = bookImporter.createBookFile("$bookId.$ext")
- moveExtractedBook(tempBookFile!!, finalBookFile)
+ tempBookFile!!.renameTo(finalBookFile)
var coverPath: String? = null
if (coverBytes != null) {
@@ -106,7 +106,7 @@ object CalibreBundleExtractor {
var seriesIndex: Double? = null
try {
- val factory = secureDocumentBuilderFactory()
+ val factory = DocumentBuilderFactory.newInstance()
val builder = factory.newDocumentBuilder()
val document = builder.parse(ByteArrayInputStream(opfData!!.toByteArray(Charsets.UTF_8)))
val metadataNodes = document.getElementsByTagName("metadata")
@@ -159,25 +159,8 @@ object CalibreBundleExtractor {
} catch (e: Exception) {
Timber.e(e, "Failed to process zip bundle")
} finally {
- tempBookFile?.takeIf { it.exists() }?.delete()
+ tempBookFile?.delete() // Cleanup if parsing failed midway
}
return@withContext null
}
-
- private fun moveExtractedBook(tempBookFile: File, finalBookFile: File) {
- finalBookFile.parentFile?.mkdirs()
- if (finalBookFile.exists() && !finalBookFile.delete()) {
- throw IOException("Could not replace existing book file: ${finalBookFile.absolutePath}")
- }
- if (tempBookFile.renameTo(finalBookFile)) return
-
- tempBookFile.inputStream().use { input ->
- FileOutputStream(finalBookFile).use { output ->
- input.copyTo(output)
- }
- }
- if (!finalBookFile.isFile) {
- throw IOException("Could not move extracted book to: ${finalBookFile.absolutePath}")
- }
- }
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubBook.kt b/app/src/main/java/com/aryan/reader/epub/EpubBook.kt
similarity index 95%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubBook.kt
rename to app/src/main/java/com/aryan/reader/epub/EpubBook.kt
index 1f2fe85..ad07f81 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubBook.kt
+++ b/app/src/main/java/com/aryan/reader/epub/EpubBook.kt
@@ -17,10 +17,10 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import android.graphics.Bitmap
-import org.dueattendant149.bookreader.epub.EpubParser.EpubPageTarget
+import com.aryan.reader.epub.EpubParser.EpubPageTarget
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import java.io.File
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubChapter.kt b/app/src/main/java/com/aryan/reader/epub/EpubChapter.kt
similarity index 83%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubChapter.kt
rename to app/src/main/java/com/aryan/reader/epub/EpubChapter.kt
index 8cf739c..ad80edf 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubChapter.kt
+++ b/app/src/main/java/com/aryan/reader/epub/EpubChapter.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
@@ -33,10 +33,5 @@ data class EpubChapter @OptIn(ExperimentalSerializationApi::class) constructor(
@ProtoNumber(5) val plainTextContent: String,
@ProtoNumber(6) val htmlContent: String,
@ProtoNumber(7) val depth: Int = 0,
- @ProtoNumber(8) val isInToc: Boolean = true,
- @ProtoNumber(9) val plainTextLength: Int = plainTextContent.length
-)
-
-fun EpubChapter.plainTextCharacterCount(): Int {
- return maxOf(plainTextLength, plainTextContent.length)
-}
+ @ProtoNumber(8) val isInToc: Boolean = true
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubImage.kt b/app/src/main/java/com/aryan/reader/epub/EpubImage.kt
similarity index 96%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubImage.kt
rename to app/src/main/java/com/aryan/reader/epub/EpubImage.kt
index bbe9990..2fd2802 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubImage.kt
+++ b/app/src/main/java/com/aryan/reader/epub/EpubImage.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubParser.kt b/app/src/main/java/com/aryan/reader/epub/EpubParser.kt
similarity index 96%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubParser.kt
rename to app/src/main/java/com/aryan/reader/epub/EpubParser.kt
index 44ffc27..716fba2 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubParser.kt
+++ b/app/src/main/java/com/aryan/reader/epub/EpubParser.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import android.content.Context
import android.graphics.Bitmap
@@ -457,17 +457,19 @@ class EpubParser(private val context: Context) {
var pageTargets: List = emptyList()
val ncxMetadataMap = mutableMapOf()
val extractionRoot = File(extractionBasePath)
- val tocFileItem = if (shouldUseToc) resolveTocFileItem(document.spine, manifestItems) else null
- val tocDocumentNode = tocFileItem?.let { item ->
- val ncxData = filesContentMap[item.absPath]?.data?.takeIf { it.isNotEmpty() }
- ?: File(extractionRoot, item.absPath).takeIf { it.exists() }?.readBytes()
- ncxData?.let { parseXMLFile(it) }
- }
- val ncxParentDir = tocFileItem?.let { File(it.absPath).parentFile ?: File("") }
if (shouldUseToc) {
Timber.d("shouldUseToc is true. Attempting to parse NCX.")
- if (tocFileItem != null && ncxParentDir != null) {
+ val tocFileItem = manifestItems.values.firstOrNull {
+ it.absPath.endsWith(".ncx", ignoreCase = true)
+ }
+ if (tocFileItem != null) {
+ val ncxParentDir = File(tocFileItem.absPath).parentFile ?: File("")
+ val ncxData = filesContentMap[tocFileItem.absPath]?.data?.takeIf { it.isNotEmpty() }
+ ?: File(extractionRoot, tocFileItem.absPath).takeIf { it.exists() }?.readBytes()
+
+ val tocDocumentNode = ncxData?.let { parseXMLFile(it) }
+
if (tocDocumentNode != null) {
Timber.d("Successfully parsed NCX file: ${tocFileItem.absPath}")
val pageListElement = tocDocumentNode.selectFirstTag("pageList") as Element?
@@ -493,8 +495,15 @@ class EpubParser(private val context: Context) {
Timber.d("Parsing chapters based on OPF spine for rendering order. NCX titles/depth will be used if available.")
val tableOfContents = if (shouldUseToc) {
- if (tocDocumentNode != null && ncxParentDir != null) {
- val navMapElement = tocDocumentNode.selectFirstTag("navMap") as Element?
+ val tocFileItem = manifestItems.values.firstOrNull {
+ it.absPath.endsWith(".ncx", ignoreCase = true)
+ }
+ if (tocFileItem != null) {
+ val ncxParentDir = File(tocFileItem.absPath).parentFile ?: File("")
+ val ncxData = filesContentMap[tocFileItem.absPath]?.data?.takeIf { it.isNotEmpty() }
+ ?: File(extractionRoot, tocFileItem.absPath).takeIf { it.exists() }?.readBytes()
+ val tocDocumentNode = ncxData?.let { parseXMLFile(it) }
+ val navMapElement = tocDocumentNode?.selectFirstTag("navMap") as Element?
if (navMapElement != null) {
parseTableOfContents(navMapElement, ncxParentDir)
@@ -583,21 +592,6 @@ class EpubParser(private val context: Context) {
return result
}
- private fun resolveTocFileItem(
- spine: Node,
- manifestItems: Map
- ): EpubManifestItem? {
- spine.getAttributeValue("toc")
- ?.takeIf { it.isNotBlank() }
- ?.let { tocId -> manifestItems[tocId] }
- ?.let { return it }
-
- return manifestItems.values.firstOrNull {
- it.mediaType.equals("application/x-dtbncx+xml", ignoreCase = true)
- } ?: manifestItems.values.firstOrNull {
- it.absPath.endsWith(".ncx", ignoreCase = true)
- }
- }
@Throws(EpubParserException::class)
private fun createEpubDocument(files: Map): EpubDocument {
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubParserException.kt b/app/src/main/java/com/aryan/reader/epub/EpubParserException.kt
similarity index 95%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubParserException.kt
rename to app/src/main/java/com/aryan/reader/epub/EpubParserException.kt
index 9ff63f3..b9b0d96 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubParserException.kt
+++ b/app/src/main/java/com/aryan/reader/epub/EpubParserException.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
/**
diff --git a/app/src/main/java/com/aryan/reader/epub/EpubUtils.kt b/app/src/main/java/com/aryan/reader/epub/EpubUtils.kt
new file mode 100644
index 0000000..9cffed3
--- /dev/null
+++ b/app/src/main/java/com/aryan/reader/epub/EpubUtils.kt
@@ -0,0 +1,44 @@
+/*
+ * Episteme Reader - A native Android document reader.
+ * Copyright (C) 2026 Episteme
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * mail: epistemereader@gmail.com
+ */
+package com.aryan.reader.epub
+
+import org.w3c.dom.Document
+import org.w3c.dom.Element
+import org.w3c.dom.Node
+import org.w3c.dom.NodeList
+import java.io.InputStream
+import javax.xml.parsers.DocumentBuilderFactory
+
+fun parseXMLFile(inputSteam: InputStream): Document? =
+ DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSteam)
+
+fun parseXMLFile(byteArray: ByteArray): Document? = parseXMLFile(byteArray.inputStream())
+
+fun String.asFileName(): String = this.replace("/", "_")
+
+fun Document.selectFirstTag(tag: String): Node? = getElementsByTagName(tag).item(0)
+fun Node.selectFirstChildTag(tag: String) = childElements.find { it.tagName == tag }
+fun Node.selectChildTag(tag: String) = childElements.filter { it.tagName == tag }
+fun Node.getAttributeValue(attribute: String): String? =
+ attributes?.getNamedItem(attribute)?.textContent
+
+val NodeList.elements get() = (0..length).asSequence().mapNotNull { item(it) as? Element }
+val Node.childElements get() = childNodes.elements
+
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubXMLFileParser.kt b/app/src/main/java/com/aryan/reader/epub/EpubXMLFileParser.kt
similarity index 98%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubXMLFileParser.kt
rename to app/src/main/java/com/aryan/reader/epub/EpubXMLFileParser.kt
index 532ad77..0fb9764 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/EpubXMLFileParser.kt
+++ b/app/src/main/java/com/aryan/reader/epub/EpubXMLFileParser.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/Fb2Parser.kt b/app/src/main/java/com/aryan/reader/epub/Fb2Parser.kt
similarity index 85%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/Fb2Parser.kt
rename to app/src/main/java/com/aryan/reader/epub/Fb2Parser.kt
index dfc2003..458b37f 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/Fb2Parser.kt
+++ b/app/src/main/java/com/aryan/reader/epub/Fb2Parser.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import android.content.Context
import android.graphics.BitmapFactory
@@ -12,7 +12,6 @@ import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
-import java.security.MessageDigest
import java.util.zip.ZipInputStream
class Fb2Parser(private val context: Context) {
@@ -211,12 +210,7 @@ class Fb2Parser(private val context: Context) {
if (!inBody) {
if (coverImageId == null) coverImageId = id
} else {
- val safeImageName = safeResourceFileName(id)
- if (safeImageName != null) {
- currentChapterHtml.append("
")
- } else {
- Timber.w("Skipping unsafe FB2 image reference: $id")
- }
+ currentChapterHtml.append("
")
}
}
}
@@ -226,23 +220,12 @@ class Fb2Parser(private val context: Context) {
val base64Data = parser.nextText()
try {
val bytes = Base64.decode(base64Data, Base64.DEFAULT)
- val safeId = safeResourceFileName(id)
if (parseContent) {
- if (safeId != null) {
- val imgFile = safeFileInRoot(extractionDir, safeId)
- if (imgFile != null) {
- FileOutputStream(imgFile).use { it.write(bytes) }
- } else {
- Timber.w("Skipping unsafe FB2 binary path: $id")
- }
- } else {
- Timber.w("Skipping unsafe FB2 binary id: $id")
- }
+ val imgFile = File(extractionDir, id)
+ FileOutputStream(imgFile).use { it.write(bytes) }
}
- if (safeId != null) {
- images.add(EpubImage(absPath = safeId))
- }
+ images.add(EpubImage(absPath = id))
if (id == coverImageId || (coverImageId == null && id.contains("cover", ignoreCase = true))) {
coverBytes = bytes
@@ -343,30 +326,4 @@ class Fb2Parser(private val context: Context) {
}
}
}
-
- private fun safeResourceFileName(id: String): String? {
- val rawName = id.substringAfterLast('/').substringAfterLast('\\').trim()
- if (rawName.isBlank() || rawName == "." || rawName == "..") return null
-
- val extension = rawName.substringAfterLast('.', missingDelimiterValue = "")
- .takeIf { it.isNotBlank() && it.length <= 12 }
- ?.replace(Regex("[^A-Za-z0-9]"), "")
- .orEmpty()
- val baseName = rawName.substringBeforeLast('.', rawName)
- .replace(Regex("[^A-Za-z0-9._-]+"), "_")
- .trim('.', '_', '-')
- .ifBlank { "image" }
- .take(48)
- val suffix = sha256Hex(id).take(12)
- return if (extension.isBlank()) {
- "${baseName}_$suffix"
- } else {
- "${baseName}_$suffix.$extension"
- }
- }
-
- private fun sha256Hex(value: String): String {
- val digest = MessageDigest.getInstance("SHA-256").digest(value.toByteArray(Charsets.UTF_8))
- return digest.joinToString("") { "%02x".format(it) }
- }
}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/ImportedFileCache.kt b/app/src/main/java/com/aryan/reader/epub/ImportedFileCache.kt
similarity index 98%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/ImportedFileCache.kt
rename to app/src/main/java/com/aryan/reader/epub/ImportedFileCache.kt
index a79104f..5199091 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/ImportedFileCache.kt
+++ b/app/src/main/java/com/aryan/reader/epub/ImportedFileCache.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import android.content.Context
import java.io.File
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/MobiParser.kt b/app/src/main/java/com/aryan/reader/epub/MobiParser.kt
similarity index 99%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/MobiParser.kt
rename to app/src/main/java/com/aryan/reader/epub/MobiParser.kt
index 870d882..35fb60f 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/MobiParser.kt
+++ b/app/src/main/java/com/aryan/reader/epub/MobiParser.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import android.content.Context
import android.graphics.BitmapFactory
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/OdtParser.kt b/app/src/main/java/com/aryan/reader/epub/OdtParser.kt
similarity index 89%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/OdtParser.kt
rename to app/src/main/java/com/aryan/reader/epub/OdtParser.kt
index 2dbb1d0..4451f12 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/OdtParser.kt
+++ b/app/src/main/java/com/aryan/reader/epub/OdtParser.kt
@@ -1,4 +1,4 @@
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import android.content.Context
import android.graphics.BitmapFactory
@@ -41,7 +41,7 @@ class OdtParser(private val context: Context) {
val mathJaxFileName = "tex-mml-chtml.js"
val mathJaxFile = File(extractionDir, mathJaxFileName)
- if (parseContent && !mathJaxFile.exists()) {
+ if (!mathJaxFile.exists()) {
try {
context.assets.open("mathjax/$mathJaxFileName").use { input ->
FileOutputStream(mathJaxFile).use { output ->
@@ -170,33 +170,28 @@ class OdtParser(private val context: Context) {
try {
if (!isFlat) {
+ val zis = ZipInputStream(inputStream)
+ var entry = zis.nextEntry
var contentXmlBytes: ByteArray? = null
var stylesXmlBytes: ByteArray? = null
val ignoredFiles = setOf("meta.xml", "settings.xml", "META-INF/manifest.xml")
- ZipInputStream(inputStream).use { zis ->
- var entry = zis.nextEntry
- while (entry != null) {
- if (!entry.isDirectory) {
- when (entry.name) {
- "content.xml" -> contentXmlBytes = zis.readBytes()
- "styles.xml" -> stylesXmlBytes = zis.readBytes()
- "Thumbnails/thumbnail.png" -> coverBytes = zis.readBytes()
- else -> {
- if (entry.name !in ignoredFiles) {
- val extractedFile = safeFileInRoot(extractionDir, entry.name)
- if (extractedFile != null) {
- extractedFile.parentFile?.mkdirs()
- FileOutputStream(extractedFile).use { out -> zis.copyTo(out) }
- } else {
- Timber.w("Skipping unsafe ODT entry outside extraction root: ${entry.name}")
- }
- }
+ while (entry != null) {
+ if (!entry.isDirectory) {
+ when (entry.name) {
+ "content.xml" -> contentXmlBytes = zis.readBytes()
+ "styles.xml" -> stylesXmlBytes = zis.readBytes()
+ "Thumbnails/thumbnail.png" -> coverBytes = zis.readBytes()
+ else -> {
+ if (entry.name !in ignoredFiles) {
+ val extractedFile = File(extractionDir, entry.name)
+ extractedFile.parentFile?.mkdirs()
+ FileOutputStream(extractedFile).use { out -> zis.copyTo(out) }
}
}
}
- entry = zis.nextEntry
}
+ entry = zis.nextEntry
}
// Pre-parse styles if available
@@ -376,18 +371,10 @@ class OdtParser(private val context: Context) {
if (isFlat) {
try {
val bytes = Base64.decode(base64Builder.toString(), Base64.DEFAULT)
- val imgName = currentImageHref
- ?.substringAfterLast("/")
- ?.substringAfterLast("\\")
- ?.takeIf { it.isNotBlank() }
- ?: "${UUID.randomUUID()}.png"
- val imgFile = safeFileInRoot(extractionDir, imgName)
- if (imgFile != null) {
- FileOutputStream(imgFile).use { it.write(bytes) }
- currentChapterHtml.append("
")
- } else {
- Timber.w("Skipping unsafe FODT image path: $imgName")
- }
+ val imgName = currentImageHref?.substringAfterLast("/") ?: "${UUID.randomUUID()}.png"
+ val imgFile = File(extractionDir, imgName)
+ FileOutputStream(imgFile).use { it.write(bytes) }
+ currentChapterHtml.append("
")
} catch (e: Exception) {
Timber.e(e, "Failed to decode FODT image")
}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/SingleFileImporter.kt b/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt
similarity index 90%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epub/SingleFileImporter.kt
rename to app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt
index ba109c9..0d6c679 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epub/SingleFileImporter.kt
+++ b/app/src/main/java/com/aryan/reader/epub/SingleFileImporter.kt
@@ -17,10 +17,10 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epub
+package com.aryan.reader.epub
import android.content.Context
-import org.dueattendant149.bookreader.FileType
+import com.aryan.reader.FileType
import com.vladsch.flexmark.ext.autolink.AutolinkExtension
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
@@ -56,8 +56,6 @@ class SingleFileImporter(private val context: Context) {
private const val MAX_HTML_BUFFERED_LINE_CHARS = 128_000
private const val MAX_HTML_HEAD_SCAN_CHARS = 256_000
private const val MAX_HTML_INLINE_CSS_CHARS = 256_000
- private const val MAX_SINGLE_FILE_METADATA_BYTES = 2L * 1024L * 1024L
- private const val BOOK_METADATA_FILE = "book_metadata.json"
private const val PAGE_BREAK_MARKER = ""
}
@@ -70,62 +68,6 @@ class SingleFileImporter(private val context: Context) {
private val htmlOutputSettings = Document.OutputSettings().prettyPrint(false)
- private fun metadataFile(extractionDir: File): File = File(extractionDir, BOOK_METADATA_FILE)
-
- private fun EpubBook.lightweightSingleFileCache(): EpubBook {
- val cacheChapters = chapters.map { chapter ->
- chapter.copy(
- plainTextContent = "",
- htmlContent = ""
- )
- }
- return copy(
- coverImage = null,
- chapters = cacheChapters,
- chaptersForPagination = cacheChapters
- )
- }
-
- private fun readCachedSingleFileBook(metadataFile: File, extractionDir: File, tag: String): EpubBook? {
- if (!metadataFile.exists()) return null
- if (metadataFile.length() > MAX_SINGLE_FILE_METADATA_BYTES) {
- Timber.w(
- "Ignoring oversized $tag metadata cache (${metadataFile.length()} bytes). " +
- "The file will be reparsed with lightweight metadata."
- )
- runCatching { metadataFile.delete() }
- return null
- }
-
- return try {
- val decodedBook = jsonSerializer.decodeFromString(metadataFile.readText())
- val cacheChapters = decodedBook.chapters.map { it.copy(htmlContent = "") }
- decodedBook.copy(
- chapters = cacheChapters,
- chaptersForPagination = cacheChapters,
- extractionBasePath = extractionDir.absolutePath
- ).takeIf { it.hasReadableExtractedContent() }
- } catch (e: OutOfMemoryError) {
- Timber.e(e, "Failed to load cached $tag metadata without exhausting memory")
- runCatching { metadataFile.delete() }
- null
- } catch (e: Exception) {
- Timber.e(e, "Failed to load cached $tag, parsing again")
- null
- }
- }
-
- private fun writeSingleFileMetadata(metadataFile: File, book: EpubBook, tag: String) {
- try {
- metadataFile.writeText(jsonSerializer.encodeToString(book.lightweightSingleFileCache()))
- } catch (e: OutOfMemoryError) {
- Timber.e(e, "Failed to cache lightweight $tag metadata without exhausting memory")
- runCatching { metadataFile.delete() }
- } catch (e: Exception) {
- Timber.e(e, "Failed to cache $tag metadata")
- }
- }
-
suspend fun importSingleFile(
inputStream: InputStream,
type: FileType,
@@ -137,7 +79,7 @@ class SingleFileImporter(private val context: Context) {
val lowerHint = originalBookNameHint.lowercase()
val isCsv = lowerHint.endsWith(".csv") || lowerHint.endsWith(".tsv") ||
lowerHint.endsWith(".csv.txt") || lowerHint.endsWith(".tsv.txt")
- val isCodeOrData = org.dueattendant149.bookreader.isCodeOrDataFileName(originalBookNameHint)
+ val isCodeOrData = com.aryan.reader.isCodeOrDataFileName(originalBookNameHint)
if (type == FileType.HTML && (isCsv || isCodeOrData)) {
return parseDynamicContentToHtml(inputStream, originalBookNameHint, bookId, parseContent, isCsv)
@@ -253,11 +195,17 @@ class SingleFileImporter(private val context: Context) {
}
val extractionDir = ImportedFileCache.ensureActiveBookDir(context, bookId)
- val metadataFile = metadataFile(extractionDir)
+ val metadataFile = File(extractionDir, "book_metadata.json")
- readCachedSingleFileBook(metadataFile, extractionDir, "MD")?.let { cachedBook ->
- Timber.tag("FileOpenPerf").d("[MD] Loaded from cache instantly | bookId=$bookId")
- return@withContext cachedBook
+ if (metadataFile.exists()) {
+ try {
+ val cachedBook = jsonSerializer.decodeFromString(metadataFile.readText())
+ .copy(extractionBasePath = extractionDir.absolutePath)
+ Timber.tag("FileOpenPerf").d("[MD] Loaded from cache instantly | bookId=$bookId")
+ return@withContext cachedBook
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to load cached MD, parsing again")
+ }
}
ImportedFileCache.resetActiveBookDir(context, bookId)
@@ -351,7 +299,11 @@ class SingleFileImporter(private val context: Context) {
css = emptyMap()
)
- writeSingleFileMetadata(metadataFile, book, "MD")
+ try {
+ metadataFile.writeText(jsonSerializer.encodeToString(book))
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to cache MD metadata")
+ }
return@withContext book
}
@@ -379,11 +331,17 @@ class SingleFileImporter(private val context: Context) {
}
val extractionDir = ImportedFileCache.ensureActiveBookDir(context, bookId)
- val metadataFile = metadataFile(extractionDir)
+ val metadataFile = File(extractionDir, "book_metadata.json")
- readCachedSingleFileBook(metadataFile, extractionDir, "TXT")?.let { cachedBook ->
- Timber.tag("FileOpenPerf").d("[TXT] Loaded from cache instantly | bookId=$bookId")
- return@withContext cachedBook
+ if (metadataFile.exists()) {
+ try {
+ val cachedBook = jsonSerializer.decodeFromString(metadataFile.readText())
+ .copy(extractionBasePath = extractionDir.absolutePath)
+ Timber.tag("FileOpenPerf").d("[TXT] Loaded from cache instantly | bookId=$bookId")
+ return@withContext cachedBook
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to load cached TXT, parsing again")
+ }
}
ImportedFileCache.resetActiveBookDir(context, bookId)
@@ -504,7 +462,11 @@ class SingleFileImporter(private val context: Context) {
css = emptyMap()
)
- writeSingleFileMetadata(metadataFile, book, "TXT")
+ try {
+ metadataFile.writeText(jsonSerializer.encodeToString(book))
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to cache TXT metadata")
+ }
return@withContext book
}
@@ -532,11 +494,17 @@ class SingleFileImporter(private val context: Context) {
}
val extractionDir = ImportedFileCache.ensureActiveBookDir(context, bookId)
- val metadataFile = metadataFile(extractionDir)
+ val metadataFile = File(extractionDir, "book_metadata.json")
- readCachedSingleFileBook(metadataFile, extractionDir, "HTML")?.let { cachedBook ->
- Timber.tag("FileOpenPerf").d("[HTML] Loaded from cache instantly | bookId=$bookId")
- return@withContext cachedBook
+ if (metadataFile.exists()) {
+ try {
+ val cachedBook = jsonSerializer.decodeFromString(metadataFile.readText())
+ .copy(extractionBasePath = extractionDir.absolutePath)
+ Timber.tag("FileOpenPerf").d("[HTML] Loaded from cache instantly | bookId=$bookId")
+ return@withContext cachedBook
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to load cached HTML, parsing again")
+ }
}
ImportedFileCache.resetActiveBookDir(context, bookId)
@@ -731,7 +699,11 @@ class SingleFileImporter(private val context: Context) {
css = emptyMap()
)
- writeSingleFileMetadata(metadataFile, book, "HTML")
+ try {
+ metadataFile.writeText(jsonSerializer.encodeToString(book))
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to cache HTML metadata")
+ }
return@withContext book
}
@@ -747,7 +719,7 @@ class SingleFileImporter(private val context: Context) {
if (!originalBookNameHint.endsWith(".txt", ignoreCase = true)) return originalBookNameHint
val innerName = originalBookNameHint.dropLast(4)
- return if (innerName.contains('.') && org.dueattendant149.bookreader.isCodeOrDataFileName(innerName)) {
+ return if (innerName.contains('.') && com.aryan.reader.isCodeOrDataFileName(innerName)) {
innerName
} else {
originalBookNameHint
@@ -811,11 +783,17 @@ class SingleFileImporter(private val context: Context) {
}
val extractionDir = ImportedFileCache.ensureActiveBookDir(context, bookId)
- val metadataFile = metadataFile(extractionDir)
+ val metadataFile = File(extractionDir, "book_metadata.json")
- readCachedSingleFileBook(metadataFile, extractionDir, "DOCX")?.let { cachedBook ->
- Timber.tag("FileOpenPerf").d("[DOCX] Loaded from cache instantly | bookId=$bookId")
- return@withContext cachedBook
+ if (metadataFile.exists()) {
+ try {
+ val cachedBook = jsonSerializer.decodeFromString(metadataFile.readText())
+ .copy(extractionBasePath = extractionDir.absolutePath)
+ Timber.tag("FileOpenPerf").d("[DOCX] Loaded from cache instantly | bookId=$bookId")
+ return@withContext cachedBook
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to load cached DOCX, parsing again")
+ }
}
ImportedFileCache.resetActiveBookDir(context, bookId)
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/ChapterWebView.kt b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt
similarity index 90%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/ChapterWebView.kt
rename to app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt
index 4f20714..34001a3 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/ChapterWebView.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/ChapterWebView.kt
@@ -18,10 +18,12 @@
* mail: epistemereader@gmail.com
*/
// ChapterWebView.kt
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
+import android.content.ClipData
+import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Color
@@ -36,6 +38,8 @@ import android.widget.Toast
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -48,10 +52,8 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
@@ -74,7 +76,6 @@ import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
@@ -84,29 +85,21 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.core.net.toUri
-import org.dueattendant149.bookreader.R
-import org.dueattendant149.bookreader.ReaderFontDiagnosticsTag
-import org.dueattendant149.bookreader.copyPlainTextToClipboard
-import org.dueattendant149.bookreader.getReaderTextureDataUri
-import org.dueattendant149.bookreader.readerFontDiagnosticSummary
-import org.dueattendant149.bookreader.shared.detectFontVariant
-import org.dueattendant149.bookreader.shared.familyFilenameSignature
-import org.dueattendant149.bookreader.shared.fontWeightCssDescriptor
-import org.dueattendant149.bookreader.shared.ui.SharedSelectionMenuRect
-import org.dueattendant149.bookreader.shared.ui.SharedSelectionMenuSize
-import org.dueattendant149.bookreader.shared.ui.SharedSelectionMenuViewport
-import org.dueattendant149.bookreader.shared.ui.sharedSelectionMenuPlacement
+import com.aryan.reader.R
+import com.aryan.reader.getReaderTextureDataUri
+import com.aryan.reader.shared.ui.SharedSelectionMenuRect
+import com.aryan.reader.shared.ui.SharedSelectionMenuSize
+import com.aryan.reader.shared.ui.SharedSelectionMenuViewport
+import com.aryan.reader.shared.ui.sharedSelectionMenuPlacement
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.json.JSONObject
import timber.log.Timber
import java.io.BufferedReader
-import java.io.File
import java.io.InputStreamReader
private const val TAG_LINK_NAV = "LINK_NAV"
private const val TAG_VERTICAL_JITTER = "EpubVerticalJitter"
-private const val TAG_ANDROID_HIGHLIGHT_RENDER_DIAG = "AndroidHighlightRenderDiag"
private val READER_WEB_VIEW_JS_INTERFACES = arrayOf(
"PageInfoReporter",
"ProgressReporter",
@@ -208,49 +201,6 @@ private fun getFontCssInjection(): String {
""".trimIndent()
}
-private fun buildCustomFontCssForWebView(customFontPath: String?, phase: String): String {
- if (customFontPath == null) return ""
-
- val fontFile = File(customFontPath)
- val signature = fontFile.nameWithoutExtension.familyFilenameSignature()
- val siblings = fontFile.parentFile?.listFiles()?.filter {
- it.isFile && it.extension.lowercase() in setOf("ttf", "otf", "woff", "woff2") &&
- it.nameWithoutExtension.familyFilenameSignature() == signature
- } ?: listOf(fontFile)
-
- Timber.tag(ReaderFontDiagnosticsTag).i(
- "webview.$phase.customCss.start basePath='${fontFile.absolutePath}' " +
- "exists=${fontFile.exists()} bytes=${fontFile.length()} " +
- readerFontDiagnosticSummary(fontFile.nameWithoutExtension) +
- " siblings=${siblings.joinToString { it.name }}"
- )
-
- val css = siblings.mapNotNull { sibling ->
- val variant = sibling.nameWithoutExtension.detectFontVariant()
- if (variant == null) {
- Timber.tag(ReaderFontDiagnosticsTag).w(
- "webview.$phase.customCss.skipNoVariant file='${sibling.name}' " +
- readerFontDiagnosticSummary(sibling.nameWithoutExtension)
- )
- return@mapNotNull null
- }
-
- val weight = sibling.nameWithoutExtension.fontWeightCssDescriptor(variant.weight)
- val style = if (variant.style == FontStyle.Italic) "italic" else "normal"
- Timber.tag(ReaderFontDiagnosticsTag).i(
- "webview.$phase.customCss.face file='${sibling.name}' fontWeight='$weight' fontStyle='$style' " +
- readerFontDiagnosticSummary(sibling.nameWithoutExtension)
- )
- "@font-face { font-family: 'CustomFont'; src: url('file://${sibling.absolutePath}'); font-weight: $weight; font-style: $style; }"
- }.joinToString(" ")
-
- val faceCount = Regex("@font-face").findAll(css).count()
- Timber.tag(ReaderFontDiagnosticsTag).i(
- "webview.$phase.customCss.done faceCount=$faceCount cssLength=${css.length}"
- )
- return css
-}
-
private fun getJsToInject(context: Context): String {
return try {
context.assets.open("epub_reader.js").use { inputStream ->
@@ -436,36 +386,6 @@ private data class CustomMenuState(
val selectedColor: HighlightColor? = null
)
-internal fun highlightsJsonForWebView(userHighlights: List): String {
- val jsonArray = org.json.JSONArray()
- userHighlights.forEach { highlight ->
- val obj = JSONObject()
- obj.put("id", highlight.id)
- obj.put("cfi", highlight.cfi)
- obj.put("text", highlight.text)
- obj.put("cssClass", highlight.color.cssClass)
- obj.put("colorId", highlight.color.id)
- obj.put("chapterIndex", highlight.chapterIndex)
- obj.put(
- "locator",
- JSONObject().apply {
- highlight.locator.chapterIndex?.let { put("chapterIndex", it) }
- highlight.locator.chapterId?.let { put("chapterId", it) }
- highlight.locator.href?.let { put("href", it) }
- highlight.locator.pageIndex?.let { put("pageIndex", it) }
- highlight.locator.startOffset?.let { put("startOffset", it) }
- highlight.locator.endOffset?.let { put("endOffset", it) }
- highlight.locator.blockIndex?.let { put("blockIndex", it) }
- highlight.locator.charOffset?.let { put("charOffset", it) }
- highlight.locator.textQuote?.let { put("textQuote", it) }
- highlight.locator.cfi?.let { put("cfi", it) }
- }
- )
- jsonArray.put(obj)
- }
- return jsonArray.toString()
-}
-
@Suppress("unused")
class AiJsBridge(
private val scope: CoroutineScope, private val onContentReady: suspend (String) -> Unit
@@ -563,7 +483,6 @@ fun ChapterWebView(
onFootnoteRequested: (String) -> Unit,
currentFontFamily: ReaderFont,
customFontPath: String? = null,
- epubFontFaceCss: String = "",
currentTextAlign: ReaderTextAlign,
onHighlightClicked: () -> Unit,
onAutoScrollChapterEnd: () -> Unit = {},
@@ -607,7 +526,17 @@ fun ChapterWebView(
)
}
- val highlightsJson = remember(userHighlights) { highlightsJsonForWebView(userHighlights) }
+ val highlightsJson = remember(userHighlights) {
+ val jsonArray = org.json.JSONArray()
+ userHighlights.forEach { h ->
+ val obj = JSONObject()
+ obj.put("cfi", h.cfi)
+ obj.put("text", h.text)
+ obj.put("cssClass", h.color.cssClass)
+ jsonArray.put(obj)
+ }
+ jsonArray.toString()
+ }
if (showExternalLinkDialog != null) {
val urlToShow = showExternalLinkDialog!!
@@ -628,14 +557,10 @@ fun ChapterWebView(
showExternalLinkDialog = null
}) { Text(stringResource(R.string.action_open)) }
TextButton(onClick = {
- val copied = copyPlainTextToClipboard(
- context = context,
- label = context.getString(R.string.clip_label_copied_link),
- text = urlToShow
- )
- if (!copied) {
- Toast.makeText(context, context.getString(R.string.error_copy_to_clipboard), Toast.LENGTH_SHORT).show()
- }
+ val clipboard =
+ context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_link), urlToShow)
+ clipboard.setPrimaryClip(clip)
showExternalLinkDialog = null
}) { Text(stringResource(R.string.action_copy)) }
}
@@ -794,11 +719,6 @@ fun ChapterWebView(
)
}
- message.startsWith("$TAG_ANDROID_HIGHLIGHT_RENDER_DIAG:") -> {
- Timber.tag(TAG_ANDROID_HIGHLIGHT_RENDER_DIAG)
- .d("JS -> ${message.substringAfter("$TAG_ANDROID_HIGHLIGHT_RENDER_DIAG: ")}")
- }
-
message.startsWith("ReaderFontDiagnosis") -> {
Timber.d(
"JS -> ${message.substringAfter("ReaderFontDiagnosis: ")}"
@@ -981,14 +901,13 @@ fun ChapterWebView(
)
val fontCss = getFontCssInjection().replace("\n", " ")
- val customFontCss = buildCustomFontCssForWebView(customFontPath, "initial")
- val combinedCss = listOf(fontCss, customFontCss, epubFontFaceCss)
- .filter { it.isNotBlank() }
- .joinToString(separator = " ")
- val escapedCombinedCss = escapeJsString(combinedCss)
+ val customFontCss = if (customFontPath != null) {
+ "@font-face { font-family: 'CustomFont'; src: url('file://$customFontPath'); }"
+ } else ""
+ val combinedCss = "$fontCss $customFontCss"
val injectFontJs =
- "var style = document.createElement('style'); style.id='injectedFonts'; style.innerHTML = \"$escapedCombinedCss\"; document.head.appendChild(style);"
+ "var style = document.createElement('style'); style.id='injectedFonts'; style.innerHTML = \"$combinedCss\"; document.head.appendChild(style);"
view?.evaluateJavascript("javascript:$injectFontJs") {
Timber.d("CSS Injection result: $it")
}
@@ -1180,10 +1099,10 @@ fun ChapterWebView(
runtimeApplierState.logPending(chapterTitle)
} else {
val fontCss = getFontCssInjection().replace("\n", " ")
- val customFontCss = buildCustomFontCssForWebView(customFontPath, "runtime")
- val combinedCss = listOf(fontCss, customFontCss, epubFontFaceCss)
- .filter { it.isNotBlank() }
- .joinToString(separator = " ")
+ val customFontCss = if (customFontPath != null) {
+ "@font-face { font-family: 'CustomFont'; src: url('file://$customFontPath'); }"
+ } else ""
+ val combinedCss = "$fontCss $customFontCss"
val fontNameForJs = if (customFontPath != null) {
"CustomFont"
} else if (currentFontFamily == ReaderFont.ORIGINAL) {
@@ -1221,9 +1140,8 @@ fun ChapterWebView(
if (fontCssChanged) {
runtimeApplierState.fontCss = combinedCss
- val escapedCombinedCss = escapeJsString(combinedCss)
val injectFontJs =
- "var style = document.getElementById('injectedFonts'); if(!style) { style = document.createElement('style'); style.id='injectedFonts'; document.head.appendChild(style); } style.innerHTML = \"$escapedCombinedCss\";"
+ "var style = document.getElementById('injectedFonts'); if(!style) { style = document.createElement('style'); style.id='injectedFonts'; document.head.appendChild(style); } style.innerHTML = \"$combinedCss\";"
webView.evaluateJavascript("javascript:$injectFontJs", null)
}
@@ -1360,14 +1278,10 @@ fun ChapterWebView(
) {
PaginatedTextSelectionMenu(
onCopy = {
- val copied = copyPlainTextToClipboard(
- context = context,
- label = context.getString(R.string.clip_label_copied_text),
- text = state.selectedText
- )
- if (!copied) {
- Toast.makeText(context, context.getString(R.string.error_copy_to_clipboard), Toast.LENGTH_SHORT).show()
- }
+ val clipboard =
+ context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText(context.getString(R.string.clip_label_copied_text), state.selectedText)
+ clipboard.setPrimaryClip(clip)
state.finishActionModeCallback()
localWebViewRef?.clearFocus()
localWebViewRef?.evaluateJavascript(
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/DictionarySettingsDialog.kt b/app/src/main/java/com/aryan/reader/epubreader/DictionarySettingsDialog.kt
similarity index 92%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/DictionarySettingsDialog.kt
rename to app/src/main/java/com/aryan/reader/epubreader/DictionarySettingsDialog.kt
index 6122f80..0aab63d 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/DictionarySettingsDialog.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/DictionarySettingsDialog.kt
@@ -1,13 +1,10 @@
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
@@ -23,14 +20,12 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
-import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
-import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -40,15 +35,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
-import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
import androidx.core.graphics.drawable.toBitmap
-import org.dueattendant149.bookreader.R
-import org.dueattendant149.bookreader.areReaderAiFeaturesEnabled
-import org.dueattendant149.bookreader.readerModalMaxHeightDp
+import com.aryan.reader.R
+import com.aryan.reader.areReaderAiFeaturesEnabled
@Suppress("KotlinConstantConditions")
@OptIn(ExperimentalMaterial3Api::class)
@@ -71,28 +65,25 @@ fun DictionarySettingsDialog(
val context = LocalContext.current
var dictionaryApps by remember { mutableStateOf>(emptyList()) }
var searchApps by remember { mutableStateOf>(emptyList()) }
- val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
- val configuration = LocalConfiguration.current
- val maxSheetHeight = readerModalMaxHeightDp(configuration.screenHeightDp).dp
LaunchedEffect(Unit) {
dictionaryApps = ExternalDictionaryHelper.getAvailableDictionaries(context)
searchApps = ExternalDictionaryHelper.getAvailableSearchApps(context)
}
- ModalBottomSheet(
- onDismissRequest = onDismiss,
- sheetState = sheetState,
- containerColor = MaterialTheme.colorScheme.surface,
- contentWindowInsets = { WindowInsets.navigationBars }
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = maxSheetHeight)
- .verticalScroll(rememberScrollState())
- .padding(24.dp)
+ Dialog(onDismissRequest = onDismiss) {
+ Surface(
+ shape = RoundedCornerShape(24.dp),
+ color = MaterialTheme.colorScheme.surface,
+ tonalElevation = 6.dp,
+ modifier = Modifier.fillMaxWidth()
) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())
+ .padding(24.dp)
+ ) {
Text(
text = stringResource(R.string.dict_lookup_settings),
style = MaterialTheme.typography.headlineSmall,
@@ -221,6 +212,7 @@ fun DictionarySettingsDialog(
onSelect = onSelectSearchPackage,
placeholder = stringResource(R.string.dict_select_app)
)
+ }
}
}
}
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderAi.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAi.kt
similarity index 95%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderAi.kt
rename to app/src/main/java/com/aryan/reader/epubreader/EpubReaderAi.kt
index ea332c6..176176e 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderAi.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAi.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import android.content.Context
import androidx.compose.foundation.layout.fillMaxWidth
@@ -31,20 +31,20 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
-import org.dueattendant149.bookreader.AiDefinitionPopup
-import org.dueattendant149.bookreader.AiFeature
-import org.dueattendant149.bookreader.AiDefinitionResult
-import org.dueattendant149.bookreader.AiHubBottomSheet
-import org.dueattendant149.bookreader.BuildConfig
-import org.dueattendant149.bookreader.R
-import org.dueattendant149.bookreader.SummarizationResult
-import org.dueattendant149.bookreader.SummaryCacheManager
-import org.dueattendant149.bookreader.callByokTextAi
-import org.dueattendant149.bookreader.epub.EpubBook
-import org.dueattendant149.bookreader.epub.contentFilePath
-import org.dueattendant149.bookreader.fetchRecap
-import org.dueattendant149.bookreader.paginatedreader.IPaginator
-import org.dueattendant149.bookreader.summarizationUrl
+import com.aryan.reader.AiDefinitionPopup
+import com.aryan.reader.AiFeature
+import com.aryan.reader.AiDefinitionResult
+import com.aryan.reader.AiHubBottomSheet
+import com.aryan.reader.BuildConfig
+import com.aryan.reader.R
+import com.aryan.reader.SummarizationResult
+import com.aryan.reader.SummaryCacheManager
+import com.aryan.reader.callByokTextAi
+import com.aryan.reader.epub.EpubBook
+import com.aryan.reader.epub.contentFilePath
+import com.aryan.reader.fetchRecap
+import com.aryan.reader.paginatedreader.IPaginator
+import com.aryan.reader.summarizationUrl
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderAnnotations.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderAnnotations.kt
rename to app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt
index 41fac2f..56fead4 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderAnnotations.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import android.content.Context
import android.widget.TextView
@@ -61,7 +61,6 @@ import androidx.compose.runtime.*
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -69,19 +68,18 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.edit
import androidx.core.text.HtmlCompat
-import org.dueattendant149.bookreader.R
-import org.dueattendant149.bookreader.epub.EpubChapter
-import org.dueattendant149.bookreader.shared.EpubAnnotationSerializer
-import org.dueattendant149.bookreader.shared.ReaderLocator
+import com.aryan.reader.R
+import com.aryan.reader.epub.EpubChapter
+import com.aryan.reader.shared.EpubAnnotationSerializer
private const val BOOKMARK_PREFS_NAME = "epub_reader_bookmarks"
-typealias Bookmark = org.dueattendant149.bookreader.shared.EpubBookmark
-typealias HighlightColor = org.dueattendant149.bookreader.shared.HighlightColor
-typealias UserHighlight = org.dueattendant149.bookreader.shared.UserHighlight
+typealias Bookmark = com.aryan.reader.shared.EpubBookmark
+typealias HighlightColor = com.aryan.reader.shared.HighlightColor
+typealias UserHighlight = com.aryan.reader.shared.UserHighlight
fun escapeJsString(value: String): String {
- return org.dueattendant149.bookreader.shared.escapeJsString(value)
+ return com.aryan.reader.shared.escapeJsString(value)
}
fun saveHighlightPalette(context: Context, palette: List) {
@@ -157,20 +155,14 @@ fun processAndAddHighlight(
newText: String,
newColor: HighlightColor,
chapterIndex: Int,
- currentList: MutableList,
- locator: ReaderLocator = ReaderLocator.fromLegacy(
- chapterIndex = chapterIndex,
- cfi = newCfi,
- textQuote = newText
- )
+ currentList: MutableList
): String {
return EpubAnnotationSerializer.processAndAddHighlight(
newCfi = newCfi,
newText = newText,
newColor = newColor,
chapterIndex = chapterIndex,
- currentList = currentList,
- locator = locator
+ currentList = currentList
)
}
@@ -601,7 +593,6 @@ fun HighlightColorRow(
modifier = Modifier
.padding(horizontal = 4.dp)
.size(28.dp)
- .testTag("HighlightColor_${colorEnum.id}")
.clip(CircleShape) // 1. Clip shape for ripple
.background(colorEnum.color) // 2. Apply background
.clickable {
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderContent.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderContent.kt
similarity index 88%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderContent.kt
rename to app/src/main/java/com/aryan/reader/epubreader/EpubReaderContent.kt
index 871bf32..0e9d8c6 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderContent.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderContent.kt
@@ -17,21 +17,19 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import android.content.Context
-import org.dueattendant149.bookreader.R
-import org.dueattendant149.bookreader.applyBookReplacementsToHtmlDocument
-import org.dueattendant149.bookreader.epub.EpubBook
-import org.dueattendant149.bookreader.epub.contentFilePath
-import org.dueattendant149.bookreader.paginatedreader.LocatorConverter
-import org.dueattendant149.bookreader.shared.ReaderBookReplacementPreferences
+import com.aryan.reader.R
+import timber.log.Timber
+import com.aryan.reader.epub.EpubBook
+import com.aryan.reader.epub.contentFilePath
+import com.aryan.reader.paginatedreader.LocatorConverter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
-import timber.log.Timber
import java.io.File
data class ChapterLoadingResult(
@@ -88,9 +86,7 @@ suspend fun loadChapterContent(
chunkTargetOverride: Int?,
isInitialCfiLoad: Boolean,
cfiToLoad: String?,
- locatorConverter: LocatorConverter,
- bookReplacementPreferences: ReaderBookReplacementPreferences = ReaderBookReplacementPreferences(),
- bookReplacementFileId: String? = null,
+ locatorConverter: LocatorConverter
): ChapterLoadingResult = withContext(Dispatchers.IO) {
val chapter =
epubBook.chapters.getOrNull(chapterIndex) ?: return@withContext ChapterLoadingResult(
@@ -104,11 +100,6 @@ suspend fun loadChapterContent(
val doc = Jsoup.parse(htmlFile, "UTF-8")
val head = doc.head().html()
doc.select("script").remove()
- applyBookReplacementsToHtmlDocument(
- document = doc,
- preferences = bookReplacementPreferences,
- fileId = bookReplacementFileId,
- )
val bodyNodes = doc.body().childNodes().toList()
val htmlChunks = splitBodyNodesIntoReaderChunks(bodyNodes)
if (htmlChunks.isEmpty()) {
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderControls.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt
similarity index 88%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderControls.kt
rename to app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt
index 3bb0ae3..c3799da 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderControls.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt
@@ -1,6 +1,30 @@
-package org.dueattendant149.bookreader.epubreader
+/*
+ * Episteme Reader - A native Android document reader.
+ * Copyright (C) 2026 Episteme
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ * mail: epistemereader@gmail.com
+ */
+// EpubReaderControls.kt
+package com.aryan.reader.epubreader
+import android.annotation.SuppressLint
+import android.graphics.Bitmap
+import android.graphics.Canvas
import android.os.Build
+import android.webkit.WebView
import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.compose.foundation.lazy.LazyListState
@@ -14,12 +38,14 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
@@ -30,7 +56,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.only
@@ -47,8 +72,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowForward
-import androidx.compose.material.icons.automirrored.filled.NavigateBefore
-import androidx.compose.material.icons.automirrored.filled.NavigateNext
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.filled.ArrowUpward
@@ -59,10 +82,6 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.GraphicEq
import androidx.compose.material.icons.filled.Info
-import androidx.compose.material.icons.filled.KeyboardArrowDown
-import androidx.compose.material.icons.filled.KeyboardArrowLeft
-import androidx.compose.material.icons.filled.KeyboardArrowRight
-import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Pause
@@ -102,7 +121,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -115,21 +136,24 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.zIndex
+import androidx.core.graphics.createBitmap
import androidx.media3.common.util.UnstableApi
-import org.dueattendant149.bookreader.BuildConfig
-import org.dueattendant149.bookreader.R
-import org.dueattendant149.bookreader.RenderMode
-import org.dueattendant149.bookreader.SearchState
-import org.dueattendant149.bookreader.SearchTopBar
-import org.dueattendant149.bookreader.TooltipIconButton
-import org.dueattendant149.bookreader.areReaderAiFeaturesEnabled
-import org.dueattendant149.bookreader.loadNativeVoice
-import org.dueattendant149.bookreader.readerSliderStepPage
-import org.dueattendant149.bookreader.shared.ui.ReaderMinimalSlider
-import org.dueattendant149.bookreader.tts.GEMINI_TTS_SPEAKERS
-import org.dueattendant149.bookreader.tts.ReaderTtsOverlaySize
-import org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsState
-import org.dueattendant149.bookreader.tts.formatReaderTtsChunkLabel
+import com.aryan.reader.BuildConfig
+import com.aryan.reader.R
+import com.aryan.reader.RenderMode
+import com.aryan.reader.SearchState
+import com.aryan.reader.SearchTopBar
+import com.aryan.reader.TooltipIconButton
+import com.aryan.reader.areReaderAiFeaturesEnabled
+import com.aryan.reader.epub.EpubChapter
+import com.aryan.reader.loadNativeVoice
+import com.aryan.reader.paginatedreader.BookPaginator
+import com.aryan.reader.paginatedreader.IPaginator
+import com.aryan.reader.tts.GEMINI_TTS_SPEAKERS
+import com.aryan.reader.tts.TtsPlaybackManager.TtsState
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import timber.log.Timber
import kotlin.math.roundToInt
enum class ReaderTool(@StringRes val titleRes: Int, val category: String) {
@@ -153,8 +177,7 @@ enum class ReaderTool(@StringRes val titleRes: Int, val category: String) {
SCREEN_ORIENTATION(R.string.menu_screen_orientation, "Top Bar"),
AUTO_SCROLL(R.string.menu_auto_scroll, "Overflow Menu"),
TTS_SETTINGS(R.string.menu_tts_settings, "Overflow Menu"),
- TTS_REPLACEMENTS(R.string.menu_tts_word_replacements, "Overflow Menu"),
- BOOK_REPLACEMENTS(R.string.menu_book_word_replacements, "Overflow Menu")
+ TTS_REPLACEMENTS(R.string.menu_tts_word_replacements, "Overflow Menu")
}
enum class FlatItemType { SECTION_HEADER, TOOL, EMPTY_PLACEHOLDER, MORE_HEADER, MORE_TOOL }
@@ -263,7 +286,6 @@ internal enum class EpubOverflowMenuSection {
KEEP_SCREEN_ON,
VISUAL_OPTIONS,
AUTO_SCROLL,
- BOOK_REPLACEMENTS,
TTS_SETTINGS,
FILE_INFO
}
@@ -287,7 +309,6 @@ internal fun epubOverflowMenuSections(
if (!hiddenTools.contains(ReaderTool.KEEP_SCREEN_ON.name)) add(EpubOverflowMenuSection.KEEP_SCREEN_ON)
if (!hiddenTools.contains(ReaderTool.VISUAL_OPTIONS.name)) add(EpubOverflowMenuSection.VISUAL_OPTIONS)
if (!hiddenTools.contains(ReaderTool.AUTO_SCROLL.name)) add(EpubOverflowMenuSection.AUTO_SCROLL)
- if (!hiddenTools.contains(ReaderTool.BOOK_REPLACEMENTS.name)) add(EpubOverflowMenuSection.BOOK_REPLACEMENTS)
if (
!hiddenTools.contains(ReaderTool.TTS_SETTINGS.name) ||
!hiddenTools.contains(ReaderTool.TTS_REPLACEMENTS.name)
@@ -360,13 +381,11 @@ fun EpubReaderTopBar(
volumeScrollEnabled: Boolean,
isPageTurnAnimationEnabled: Boolean,
isRightToLeftPagination: Boolean,
- useNativeVerticalRenderer: Boolean,
onNavigateBack: () -> Unit,
isKeepScreenOn: Boolean,
onToggleKeepScreenOn: (Boolean) -> Unit,
onCloseSearch: () -> Unit,
onChangeRenderMode: (RenderMode) -> Unit,
- onUseNativeVerticalRendererChange: (Boolean) -> Unit,
onToggleBookmark: () -> Unit,
onToggleTapToNavigate: (Boolean) -> Unit,
onToggleVolumeScroll: (Boolean) -> Unit,
@@ -375,7 +394,6 @@ fun EpubReaderTopBar(
onStartAutoScroll: () -> Unit,
onOpenTtsSettings: () -> Unit,
onOpenTtsReplacements: () -> Unit,
- onOpenBookReplacements: () -> Unit,
onOpenDictionarySettings: () -> Unit,
onOpenThemeSettings: () -> Unit,
onOpenBrightness: () -> Unit,
@@ -683,29 +701,14 @@ fun EpubReaderTopBar(
)
if (showReadingModeExpanded) {
DropdownMenuItem(
- text = { Text(stringResource(R.string.menu_reading_mode_vertical_webview)) },
+ text = { Text(stringResource(R.string.menu_reading_mode_vertical)) },
enabled = !isTtsActive,
onClick = {
- onUseNativeVerticalRendererChange(false)
showMoreMenu = false
onChangeRenderMode(RenderMode.VERTICAL_SCROLL)
},
trailingIcon = {
- if (currentRenderMode == RenderMode.VERTICAL_SCROLL && !useNativeVerticalRenderer) Icon(
- Icons.Default.Check,
- contentDescription = stringResource(R.string.content_desc_selected)
- )
- })
- DropdownMenuItem(
- text = { Text(stringResource(R.string.menu_reading_mode_vertical_native)) },
- enabled = !isTtsActive,
- onClick = {
- onUseNativeVerticalRendererChange(true)
- showMoreMenu = false
- onChangeRenderMode(RenderMode.VERTICAL_SCROLL)
- },
- trailingIcon = {
- if (currentRenderMode == RenderMode.VERTICAL_SCROLL && useNativeVerticalRenderer) Icon(
+ if (currentRenderMode == RenderMode.VERTICAL_SCROLL) Icon(
Icons.Default.Check,
contentDescription = stringResource(R.string.content_desc_selected)
)
@@ -846,22 +849,6 @@ fun EpubReaderTopBar(
onStartAutoScroll()
})
}
- EpubOverflowMenuSection.BOOK_REPLACEMENTS -> {
- DropdownMenuItem(
- text = { Text(stringResource(R.string.menu_book_word_replacements)) },
- onClick = {
- showMoreMenu = false
- onOpenBookReplacements()
- },
- leadingIcon = {
- Icon(
- painter = painterResource(id = R.drawable.text_fields),
- contentDescription = null,
- modifier = Modifier.size(20.dp)
- )
- }
- )
- }
EpubOverflowMenuSection.TTS_SETTINGS -> {
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_tts_settings)) },
@@ -1018,7 +1005,7 @@ fun EpubReaderBottomBar(
isTtsSessionActive: Boolean,
ttsState: TtsState,
isProUser: Boolean,
- currentTtsMode: org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode,
+ currentTtsMode: com.aryan.reader.tts.TtsPlaybackManager.TtsMode,
isSliderActive: Boolean,
onOpenSlider: () -> Unit,
onOpenDrawer: () -> Unit,
@@ -1171,18 +1158,26 @@ fun EpubReaderBottomBar(
}
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+@SuppressLint("UnusedBoxWithConstraintsScope")
+@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EpubReaderPageSlider(
isVisible: Boolean,
+ currentRenderMode: RenderMode,
totalPages: Int,
sliderCurrentPage: Float,
sliderStartPage: Int,
+ startPageThumbnail: Bitmap?,
+ paginator: IPaginator?,
+ chapters: List,
onScrub: (Float) -> Unit,
onJumpToPage: (Int) -> Unit,
modifier: Modifier = Modifier,
activeColor: Color = Color.Unspecified,
inactiveColor: Color = Color.Unspecified,
- contentColor: Color = Color.Unspecified
+ contentColor: Color = Color.Unspecified,
+ thumbnailSurfaceColor: Color = Color.Unspecified,
+ thumbnailContentColor: Color = Color.Unspecified
) {
val effectiveActiveColor = if (activeColor == Color.Unspecified) {
MaterialTheme.colorScheme.primary
@@ -1199,8 +1194,16 @@ fun EpubReaderPageSlider(
} else {
contentColor
}
- val maxPage = totalPages.coerceAtLeast(1)
- val currentPage = sliderCurrentPage.roundToInt().coerceIn(1, maxPage)
+ val effectiveThumbnailSurfaceColor = if (thumbnailSurfaceColor == Color.Unspecified) {
+ MaterialTheme.colorScheme.surfaceVariant
+ } else {
+ thumbnailSurfaceColor
+ }
+ val effectiveThumbnailContentColor = if (thumbnailContentColor == Color.Unspecified) {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ } else {
+ thumbnailContentColor
+ }
AnimatedVisibility(
visible = isVisible,
@@ -1208,73 +1211,128 @@ fun EpubReaderPageSlider(
exit = slideOutVertically(animationSpec = tween(200)) { fullHeight -> fullHeight } + fadeOut(animationSpec = tween(200)),
modifier = modifier
) {
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) {},
- ) {
- Row(
+ Column(modifier = Modifier.fillMaxWidth()) {
+ Spacer(Modifier.height(72.dp))
+ Box(
modifier = Modifier
.fillMaxWidth()
- .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal))
- .padding(horizontal = 12.dp, vertical = 6.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
+ .clickable(indication = null, interactionSource = remember { MutableInteractionSource() }) {},
) {
- IconButton(
- onClick = {
- onJumpToPage(
- readerSliderStepPage(
- currentPage = currentPage,
- delta = -1,
- minPage = 1,
- maxPage = maxPage
- )
- )
- },
- enabled = currentPage > 1,
- modifier = Modifier.size(40.dp)
- ) {
- Icon(
- Icons.AutoMirrored.Filled.NavigateBefore,
- contentDescription = stringResource(R.string.desktop_previous_page),
- tint = effectiveContentColor.copy(alpha = if (currentPage > 1) 0.9f else 0.32f)
- )
- }
-
- ReaderMinimalSlider(
- value = sliderCurrentPage.coerceIn(1f, maxPage.toFloat()),
- onValueChange = onScrub,
- valueRange = 1f..maxPage.toFloat(),
- enabled = maxPage > 1,
- activeColor = effectiveActiveColor,
- inactiveColor = effectiveInactiveColor,
- thumbColor = effectiveActiveColor,
- markerValue = sliderStartPage.toFloat(),
- markerColor = effectiveActiveColor,
+ Row(
modifier = Modifier
- .weight(1f)
- .height(32.dp)
- )
-
- IconButton(
- onClick = {
- onJumpToPage(
- readerSliderStepPage(
- currentPage = currentPage,
- delta = 1,
- minPage = 1,
- maxPage = maxPage
- )
- )
- },
- enabled = currentPage < maxPage,
- modifier = Modifier.size(40.dp)
+ .fillMaxWidth()
+ .windowInsetsPadding(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal))
+ .padding(horizontal = 32.dp, vertical = 14.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
- Icon(
- Icons.AutoMirrored.Filled.NavigateNext,
- contentDescription = stringResource(R.string.desktop_next_page),
- tint = effectiveContentColor.copy(alpha = if (currentPage < maxPage) 0.9f else 0.32f)
+ BoxWithConstraints(
+ modifier = Modifier.weight(1f),
+ contentAlignment = Alignment.Center
+ ) {
+ Slider(
+ value = sliderCurrentPage,
+ onValueChange = onScrub,
+ valueRange = 1f..(totalPages.toFloat().coerceAtLeast(1f)),
+ steps = if (totalPages > 2) totalPages - 2 else 0,
+ modifier = Modifier.fillMaxWidth(),
+ thumb = {
+ Surface(
+ modifier = Modifier.size(20.dp),
+ shape = CircleShape,
+ color = effectiveActiveColor,
+ tonalElevation = 0.dp,
+ shadowElevation = 0.dp
+ ) {}
+ },
+ track = { sliderState ->
+ val trackHeight = 2.dp
+ val trackShape = RoundedCornerShape(trackHeight)
+ val range = sliderState.valueRange.endInclusive - sliderState.valueRange.start
+ val fraction = if (range == 0f) 0f else {
+ ((sliderState.value - sliderState.valueRange.start) / range).coerceIn(0f, 1f)
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(trackHeight)
+ .background(
+ color = effectiveInactiveColor,
+ shape = trackShape
+ )
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth(fraction)
+ .fillMaxHeight()
+ .background(
+ color = effectiveActiveColor,
+ shape = trackShape
+ )
+ )
+ }
+ }
+ )
+
+ // Thumbnail Indicator
+ val startPageOffsetFraction = if (totalPages > 1) {
+ (sliderStartPage - 1).toFloat() / (totalPages - 1)
+ } else {
+ 0f
+ }
+ val thumbWidth = 20.dp
+ val trackWidth = maxWidth - thumbWidth
+ val startPagePixelPosition = (trackWidth * startPageOffsetFraction) + (thumbWidth / 2)
+ val thumbnailModifier = Modifier
+ .graphicsLayer { clip = false }
+ .align(Alignment.TopStart)
+ .offset(
+ x = startPagePixelPosition - (45.dp / 2),
+ y = (-72).dp
+ )
+
+ if (currentRenderMode == RenderMode.VERTICAL_SCROLL) {
+ startPageThumbnail?.let { thumbnail ->
+ ThumbnailWithIndicator(
+ modifier = thumbnailModifier,
+ borderColor = effectiveActiveColor,
+ onClick = { onJumpToPage(sliderStartPage) }
+ ) {
+ Image(
+ bitmap = thumbnail.asImageBitmap(),
+ contentDescription = stringResource(R.string.content_desc_start_page_thumbnail),
+ contentScale = ContentScale.FillBounds,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+ } else {
+ val startPageChapterIndex = remember(sliderStartPage, paginator) {
+ (paginator as? BookPaginator)?.findChapterIndexForPage(sliderStartPage - 1)
+ }
+ val startPageChapterTitle = remember(startPageChapterIndex) {
+ startPageChapterIndex?.let { chapters.getOrNull(it)?.title }
+ }
+ ThumbnailWithIndicator(
+ modifier = thumbnailModifier,
+ borderColor = effectiveActiveColor,
+ onClick = { onJumpToPage(sliderStartPage) }
+ ) {
+ PaginatedThumbnailContent(
+ pageNumber = sliderStartPage,
+ chapterTitle = startPageChapterTitle,
+ surfaceColor = effectiveThumbnailSurfaceColor,
+ contentColor = effectiveThumbnailContentColor
+ )
+ }
+ }
+ }
+
+ Text(
+ text = "${sliderCurrentPage.roundToInt()} / $totalPages",
+ style = MaterialTheme.typography.bodyLarge,
+ color = effectiveContentColor,
+ fontSize = 18.sp
)
}
}
@@ -1317,6 +1375,109 @@ fun PageScrubbingAnimation(currentPage: Int, totalPages: Int) {
}
}
+@Composable
+internal fun ThumbnailWithIndicator(
+ modifier: Modifier = Modifier,
+ borderColor: Color = Color.Unspecified,
+ onClick: () -> Unit,
+ content: @Composable () -> Unit
+) {
+ val effectiveBorderColor = if (borderColor == Color.Unspecified) {
+ MaterialTheme.colorScheme.primary
+ } else {
+ borderColor
+ }
+ Column(
+ modifier = modifier,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Surface(
+ modifier = Modifier
+ .width(45.dp)
+ .height(64.dp)
+ .clickable(onClick = onClick),
+ shape = RoundedCornerShape(4.dp),
+ border = BorderStroke(2.dp, effectiveBorderColor)
+ ) {
+ content()
+ }
+ Box(
+ modifier = Modifier
+ .offset(y = (-4).dp)
+ .size(8.dp)
+ .rotate(45f)
+ .background(effectiveBorderColor)
+ )
+ }
+}
+
+@Composable
+private fun PaginatedThumbnailContent(
+ pageNumber: Int,
+ chapterTitle: String?,
+ surfaceColor: Color = Color.Unspecified,
+ contentColor: Color = Color.Unspecified
+) {
+ val effectiveSurfaceColor = if (surfaceColor == Color.Unspecified) {
+ MaterialTheme.colorScheme.surfaceVariant
+ } else {
+ surfaceColor
+ }
+ val effectiveContentColor = if (contentColor == Color.Unspecified) {
+ MaterialTheme.colorScheme.onSurfaceVariant
+ } else {
+ contentColor
+ }
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = effectiveSurfaceColor,
+ contentColor = effectiveContentColor
+ ) {
+ Column(
+ modifier = Modifier.padding(4.dp),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ if (chapterTitle != null) {
+ Text(
+ text = chapterTitle,
+ style = MaterialTheme.typography.labelSmall,
+ maxLines = 3,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ lineHeight = 10.sp
+ )
+ Spacer(modifier = Modifier.height(4.dp))
+ }
+ Text(
+ text = "$pageNumber",
+ style = MaterialTheme.typography.bodyLarge,
+ fontWeight = FontWeight.Bold
+ )
+ }
+ }
+}
+
+suspend fun captureWebViewVisibleArea(webView: WebView): Bitmap? {
+ return withContext(Dispatchers.Main) {
+ if (webView.width <= 0 || webView.height <= 0) return@withContext null
+ try {
+ val thumbnailWidth = 180
+ val thumbnailHeight = 256
+ val bitmap = createBitmap(thumbnailWidth, thumbnailHeight)
+ val canvas = Canvas(bitmap)
+ val scale = thumbnailWidth.toFloat() / webView.width.toFloat()
+ canvas.scale(scale, scale)
+ canvas.translate(-webView.scrollX.toFloat(), -webView.scrollY.toFloat())
+ webView.draw(canvas)
+ bitmap
+ } catch (e: Exception) {
+ Timber.e(e, "Failed to capture webview content")
+ null
+ }
+ }
+}
+
@Composable
fun SpeedDropdown(
label: String,
@@ -2043,7 +2204,6 @@ private fun ToolPreviewIcon(tool: ReaderTool, isSliderActive: Boolean = false) {
ReaderTool.SEARCH -> Icon(Icons.Default.Search, contentDescription = title, modifier = Modifier.size(20.dp))
ReaderTool.AI_FEATURES -> Icon(painterResource(id = R.drawable.ai), contentDescription = title, modifier = Modifier.size(20.dp))
ReaderTool.TTS_CONTROLS -> Icon(painterResource(id = R.drawable.text_to_speech), contentDescription = title, modifier = Modifier.size(20.dp))
- ReaderTool.BOOK_REPLACEMENTS -> Icon(painterResource(id = R.drawable.text_fields), contentDescription = title, modifier = Modifier.size(20.dp))
ReaderTool.FILE_INFO -> Icon(Icons.Default.Info, contentDescription = title, modifier = Modifier.size(20.dp))
ReaderTool.SCREEN_ORIENTATION -> Icon(Icons.Default.ScreenRotation, contentDescription = title, modifier = Modifier.size(20.dp))
else -> Icon(Icons.Default.MoreVert, contentDescription = title, modifier = Modifier.size(20.dp))
@@ -2097,11 +2257,11 @@ private fun HiddenEpubToolMenuItem(
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TtsOverlayControls(
- ttsController: org.dueattendant149.bookreader.tts.TtsController,
+ ttsController: com.aryan.reader.tts.TtsController,
ttsState: TtsState,
- currentTtsMode: org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode,
- overlaySize: ReaderTtsOverlaySize,
- onOverlaySizeChange: (ReaderTtsOverlaySize) -> Unit,
+ currentTtsMode: com.aryan.reader.tts.TtsPlaybackManager.TtsMode,
+ isCollapsed: Boolean,
+ onCollapseChange: (Boolean) -> Unit,
onLocateCurrentChunk: () -> Unit,
onOpenTtsSettings: () -> Unit,
onClose: () -> Unit,
@@ -2114,7 +2274,7 @@ fun TtsOverlayControls(
var isDraggingRate by remember { mutableStateOf(false) }
var isDraggingPitch by remember { mutableStateOf(false) }
- val activeMode = try { org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode.valueOf(ttsState.ttsMode) } catch(_: Exception) { org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode.CLOUD }
+ val activeMode = try { com.aryan.reader.tts.TtsPlaybackManager.TtsMode.valueOf(ttsState.ttsMode) } catch(_: Exception) { com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD }
val progressPercent = ttsState.bookProgressPercent
val cleanChapterTitle = remember(ttsState.chapterTitle) {
ttsState.chapterTitle
@@ -2141,17 +2301,11 @@ fun TtsOverlayControls(
}
}
val chunkLabel = remember(ttsState.currentChunkIndex, ttsState.totalChunks) {
- formatReaderTtsChunkLabel(ttsState.currentChunkIndex, ttsState.totalChunks)
- }
- val miniBarTitle = ttsState.bookTitle
- ?.takeIf { it.isNotBlank() }
- ?: stringResource(R.string.action_read_aloud)
- val miniBarSubtitle = remember(chapterLabel, chunkLabel, progressPercent, miniBarTitle) {
- listOfNotNull(
- chunkLabel,
- progressPercent?.let { "$it%" },
- chapterLabel?.takeIf { it != miniBarTitle }
- ).joinToString(" - ")
+ if (ttsState.currentChunkIndex >= 0 && ttsState.totalChunks > 0) {
+ "Chunk ${ttsState.currentChunkIndex + 1}/${ttsState.totalChunks}"
+ } else {
+ null
+ }
}
val canSkipPreviousChunk = !ttsState.isLoading &&
ttsState.currentChunkIndex > 0 &&
@@ -2163,7 +2317,7 @@ fun TtsOverlayControls(
val saveAndApply = {
saveTtsSpeechRate(context, rate)
saveTtsPitch(context, pitch)
- if (activeMode == org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
+ if (activeMode == com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
ttsController.setPlaybackParameters(rate, pitch)
} else {
ttsController.sliceAndRetainPosition()
@@ -2176,40 +2330,24 @@ fun TtsOverlayControls(
tonalElevation = 0.dp,
shadowElevation = 0.dp,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.8f)),
- modifier = modifier
- .widthIn(max = if (overlaySize == ReaderTtsOverlaySize.MEDIUM) 560.dp else 400.dp)
- .animateContentSize()
+ modifier = modifier.widthIn(max = 400.dp).animateContentSize()
) {
AnimatedContent(
- targetState = overlaySize,
+ targetState = isCollapsed,
transitionSpec = { fadeIn(tween(200)) togetherWith fadeOut(tween(200)) },
label = "TtsOverlayUnified"
- ) { size ->
- if (size == ReaderTtsOverlaySize.SMALL) {
+ ) { collapsed ->
+ if (collapsed) {
Row(
modifier = Modifier.padding(horizontal = 6.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
IconButton(
- onClick = { onOverlaySizeChange(ReaderTtsOverlaySize.LARGE) },
+ onClick = { onCollapseChange(false) },
modifier = Modifier.size(36.dp)
) {
- Icon(
- Icons.Default.KeyboardArrowUp,
- stringResource(R.string.content_desc_expand),
- tint = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- IconButton(
- onClick = { onOverlaySizeChange(ReaderTtsOverlaySize.MEDIUM) },
- modifier = Modifier.size(36.dp)
- ) {
- Icon(
- Icons.Default.KeyboardArrowLeft,
- stringResource(R.string.content_desc_expand),
- tint = MaterialTheme.colorScheme.onSurfaceVariant
- )
+ Icon(Icons.Default.ChevronLeft, "Expand", tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
Box(modifier = Modifier.size(36.dp), contentAlignment = Alignment.Center) {
FilledIconButton(
@@ -2233,114 +2371,6 @@ fun TtsOverlayControls(
)
}
}
- } else if (size == ReaderTtsOverlaySize.MEDIUM) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(min = 64.dp)
- .padding(horizontal = 8.dp, vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Column(
- modifier = Modifier
- .weight(1f)
- .clip(RoundedCornerShape(16.dp))
- .clickable(onClick = onLocateCurrentChunk)
- .padding(horizontal = 10.dp, vertical = 6.dp),
- verticalArrangement = Arrangement.Center
- ) {
- Text(
- text = miniBarTitle,
- style = MaterialTheme.typography.labelLarge,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- if (miniBarSubtitle.isNotBlank()) {
- Text(
- text = miniBarSubtitle,
- style = MaterialTheme.typography.labelSmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- }
- }
-
- Spacer(Modifier.width(4.dp))
-
- IconButton(
- enabled = canSkipPreviousChunk,
- onClick = { ttsController.skipToPreviousChunk() },
- modifier = Modifier.size(40.dp)
- ) {
- Icon(
- imageVector = Icons.Default.SkipPrevious,
- contentDescription = stringResource(R.string.content_desc_tts_previous_chunk),
- modifier = Modifier.size(24.dp)
- )
- }
-
- Box(modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center) {
- FilledIconButton(
- onClick = { if (ttsState.isPlaying) ttsController.pause() else ttsController.resume() },
- modifier = Modifier.size(44.dp),
- colors = IconButtonDefaults.filledIconButtonColors(
- containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
- contentColor = MaterialTheme.colorScheme.primary
- )
- ) {
- Icon(
- painterResource(if (ttsState.isPlaying) R.drawable.pause else R.drawable.play),
- stringResource(R.string.content_desc_play_pause),
- modifier = Modifier.size(22.dp)
- )
- }
- if (ttsState.isLoading) {
- CircularProgressIndicator(
- modifier = Modifier.size(48.dp),
- color = MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),
- strokeWidth = 2.dp
- )
- }
- }
-
- IconButton(
- enabled = canSkipNextChunk,
- onClick = { ttsController.skipToNextChunk() },
- modifier = Modifier.size(40.dp)
- ) {
- Icon(
- imageVector = Icons.Default.SkipNext,
- contentDescription = stringResource(R.string.content_desc_tts_next_chunk),
- modifier = Modifier.size(24.dp)
- )
- }
-
- Row(horizontalArrangement = Arrangement.spacedBy(0.dp)) {
- IconButton(
- onClick = { onOverlaySizeChange(ReaderTtsOverlaySize.LARGE) },
- modifier = Modifier.size(34.dp)
- ) {
- Icon(
- imageVector = Icons.Default.KeyboardArrowUp,
- contentDescription = stringResource(R.string.content_desc_expand),
- modifier = Modifier.size(22.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- IconButton(
- onClick = { onOverlaySizeChange(ReaderTtsOverlaySize.SMALL) },
- modifier = Modifier.size(34.dp)
- ) {
- Icon(
- imageVector = Icons.Default.KeyboardArrowRight,
- contentDescription = stringResource(R.string.content_desc_collapse),
- modifier = Modifier.size(22.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- }
- }
} else {
Column(modifier = Modifier.padding(16.dp)) {
Row(
@@ -2348,16 +2378,13 @@ fun TtsOverlayControls(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
- Row(
- modifier = Modifier.weight(1f),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
shape = RoundedCornerShape(8.dp)
) {
Text(
- if (activeMode == org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
+ if (activeMode == com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
stringResource(R.string.tts_mode_cloud_ai)
} else {
stringResource(R.string.tts_mode_device_native)
@@ -2372,7 +2399,7 @@ fun TtsOverlayControls(
color = MaterialTheme.colorScheme.secondaryContainer,
shape = RoundedCornerShape(8.dp)
) {
- val voiceName = if (activeMode == org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
+ val voiceName = if (activeMode == com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
GEMINI_TTS_SPEAKERS.find { it.id == ttsState.speakerId }?.name ?: ttsState.speakerId
} else loadNativeVoice(context)?.split("-")?.lastOrNull() ?: stringResource(R.string.label_default)
@@ -2386,7 +2413,7 @@ fun TtsOverlayControls(
)
}
- if (BuildConfig.FLAVOR != "oss" && activeMode == org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
+ if (BuildConfig.FLAVOR != "oss" && activeMode == com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
Surface(
color = MaterialTheme.colorScheme.tertiaryContainer,
shape = RoundedCornerShape(8.dp)
@@ -2401,8 +2428,6 @@ fun TtsOverlayControls(
}
}
- Spacer(Modifier.width(8.dp))
-
Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {
IconButton(onClick = onLocateCurrentChunk, modifier = Modifier.size(32.dp)) {
Icon(
@@ -2412,27 +2437,8 @@ fun TtsOverlayControls(
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
- IconButton(
- onClick = { onOverlaySizeChange(ReaderTtsOverlaySize.MEDIUM) },
- modifier = Modifier.size(32.dp)
- ) {
- Icon(
- Icons.Default.KeyboardArrowDown,
- stringResource(R.string.content_desc_collapse),
- modifier = Modifier.size(18.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant
- )
- }
- IconButton(
- onClick = { onOverlaySizeChange(ReaderTtsOverlaySize.SMALL) },
- modifier = Modifier.size(32.dp)
- ) {
- Icon(
- Icons.Default.KeyboardArrowRight,
- stringResource(R.string.content_desc_collapse),
- modifier = Modifier.size(18.dp),
- tint = MaterialTheme.colorScheme.onSurfaceVariant
- )
+ IconButton(onClick = { onCollapseChange(true) }, modifier = Modifier.size(32.dp)) {
+ Icon(Icons.Default.ChevronRight, stringResource(R.string.content_desc_collapse), modifier = Modifier.size(18.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
}
IconButton(onClick = onClose, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Close, stringResource(R.string.content_desc_stop_tts), tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp))
@@ -2554,7 +2560,7 @@ fun TtsOverlayControls(
Slider(
value = rate,
onValueChange = {
- rate = it; if (!isDraggingRate && activeMode != org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
+ rate = it; if (!isDraggingRate && activeMode != com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
isDraggingRate = true; ttsController.pause()
}
},
@@ -2627,7 +2633,7 @@ fun TtsOverlayControls(
Slider(
value = pitch,
onValueChange = {
- pitch = it; if (!isDraggingPitch && activeMode != org.dueattendant149.bookreader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
+ pitch = it; if (!isDraggingPitch && activeMode != com.aryan.reader.tts.TtsPlaybackManager.TtsMode.CLOUD) {
isDraggingPitch = true; ttsController.pause()
}
},
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderDrawer.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt
similarity index 99%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderDrawer.kt
rename to app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt
index 6efef74..9cc33de 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderDrawer.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderDrawer.kt
@@ -17,7 +17,7 @@
*
* mail: epistemereader@gmail.com
*/
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import android.graphics.BitmapFactory
import androidx.compose.animation.animateColorAsState
@@ -97,10 +97,10 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastSumBy
-import org.dueattendant149.bookreader.R
-import org.dueattendant149.bookreader.RenderMode
-import org.dueattendant149.bookreader.epub.EpubChapter
-import org.dueattendant149.bookreader.epub.EpubTocEntry
+import com.aryan.reader.R
+import com.aryan.reader.RenderMode
+import com.aryan.reader.epub.EpubChapter
+import com.aryan.reader.epub.EpubTocEntry
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderImages.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderImages.kt
similarity index 97%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderImages.kt
rename to app/src/main/java/com/aryan/reader/epubreader/EpubReaderImages.kt
index d179398..cab8d29 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderImages.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderImages.kt
@@ -1,8 +1,8 @@
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
-import org.dueattendant149.bookreader.epub.EpubBook
-import org.dueattendant149.bookreader.epub.EpubChapter
-import org.dueattendant149.bookreader.paginatedreader.AndroidHtmlResourceResolver
+import com.aryan.reader.epub.EpubBook
+import com.aryan.reader.epub.EpubChapter
+import com.aryan.reader.paginatedreader.AndroidHtmlResourceResolver
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.io.File
diff --git a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderScreen.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt
similarity index 80%
rename from app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderScreen.kt
rename to app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt
index cfbfd58..3f8996e 100644
--- a/app/src/main/java/com/dueattendant149/bookreader/reader/epubreader/EpubReaderScreen.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt
@@ -22,13 +22,16 @@
"UnusedVariable", "Unused", "SimplifyBooleanWithConstants", "KotlinConstantConditions"
)
-package org.dueattendant149.bookreader.epubreader
+package com.aryan.reader.epubreader
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
+import android.content.ClipData
+import android.content.ClipboardManager
import android.content.Context
import android.content.pm.PackageManager
+import android.graphics.Bitmap
import android.media.AudioManager
import android.net.Uri
import android.os.Build
@@ -138,7 +141,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
@@ -151,90 +153,73 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.media3.common.util.UnstableApi
-import org.dueattendant149.bookreader.AiDefinitionResult
-import org.dueattendant149.bookreader.BuildConfig
-import org.dueattendant149.bookreader.copyPlainTextToClipboard
-import org.dueattendant149.bookreader.BookWordReplacementsSheet
-import org.dueattendant149.bookreader.BuiltInThemes
-import org.dueattendant149.bookreader.MainViewModel
-import org.dueattendant149.bookreader.R
-import org.dueattendant149.bookreader.ReaderBrightnessEffect
-import org.dueattendant149.bookreader.ReaderFileInfoDialogs
-import org.dueattendant149.bookreader.ReaderBrightnessSheet
-import org.dueattendant149.bookreader.ReaderScreenOrientationEffect
-import org.dueattendant149.bookreader.ReaderScreenOrientationSheet
-import org.dueattendant149.bookreader.ReaderThemePanel
-import org.dueattendant149.bookreader.RenderMode
-import org.dueattendant149.bookreader.SearchResult
-import org.dueattendant149.bookreader.SummarizationResult
-import org.dueattendant149.bookreader.SummaryCacheManager
-import org.dueattendant149.bookreader.TtsSettingsSheet
-import org.dueattendant149.bookreader.TtsWordReplacementsSheet
-import org.dueattendant149.bookreader.areReaderAiFeaturesEnabled
-import org.dueattendant149.bookreader.countWords
-import org.dueattendant149.bookreader.isByokCloudTtsAvailable
-import org.dueattendant149.bookreader.data.CustomFontEntity
-import org.dueattendant149.bookreader.epub.EpubBook
-import org.dueattendant149.bookreader.epub.hasReadableExtractedContent
-import org.dueattendant149.bookreader.epub.plainTextCharacterCount
-import org.dueattendant149.bookreader.fetchAiDefinition
-import org.dueattendant149.bookreader.loadCustomThemes
-import org.dueattendant149.bookreader.loadGlobalTextureTransparency
-import org.dueattendant149.bookreader.loadBookReplacementPreferences
-import org.dueattendant149.bookreader.loadReaderBrightnessSettings
-import org.dueattendant149.bookreader.loadReaderScreenOrientationMode
-import org.dueattendant149.bookreader.loadEpubRightToLeftPagination
-import org.dueattendant149.bookreader.loadReaderThemeId
-import org.dueattendant149.bookreader.loadReaderSliderToggled
-import org.dueattendant149.bookreader.loadReaderTextureBitmap
-import org.dueattendant149.bookreader.loadTtsReplacementPreferences
-import org.dueattendant149.bookreader.readerSliderBookmarkPosition
-import org.dueattendant149.bookreader.readerSliderChromeColors
-import org.dueattendant149.bookreader.readerSliderToggleState
-import org.dueattendant149.bookreader.paginatedreader.CssParser
-import org.dueattendant149.bookreader.paginatedreader.BookPaginator
-import org.dueattendant149.bookreader.paginatedreader.HeaderBlock
-import org.dueattendant149.bookreader.paginatedreader.IPaginator
-import org.dueattendant149.bookreader.paginatedreader.ListItemBlock
-import org.dueattendant149.bookreader.paginatedreader.Locator
-import org.dueattendant149.bookreader.paginatedreader.LocatorConverter
-import org.dueattendant149.bookreader.paginatedreader.NativeVerticalLocation
-import org.dueattendant149.bookreader.paginatedreader.NativeVerticalReaderScreen
-import org.dueattendant149.bookreader.paginatedreader.PaginatedReaderScreen
-import org.dueattendant149.bookreader.paginatedreader.ParagraphBlock
-import org.dueattendant149.bookreader.paginatedreader.QuoteBlock
-import org.dueattendant149.bookreader.paginatedreader.TextContentBlock
-import org.dueattendant149.bookreader.paginatedreader.TtsChunk
-import org.dueattendant149.bookreader.paginatedreader.buildEpubFontFaceCss
-import org.dueattendant149.bookreader.paginatedreader.data.BookCacheDatabase
-import org.dueattendant149.bookreader.paginatedreader.locatorForPersistence
-import org.dueattendant149.bookreader.paginatedreader.nativeVerticalChapterPageInfo
-import org.dueattendant149.bookreader.paginatedreader.nativeVerticalProgressForCompatPage
-import org.dueattendant149.bookreader.paginatedreader.semanticBlockModule
-import org.dueattendant149.bookreader.rememberSearchState
-import org.dueattendant149.bookreader.saveCustomThemes
-import org.dueattendant149.bookreader.saveGlobalTextureTransparency
-import org.dueattendant149.bookreader.saveBookReplacementPreferences
-import org.dueattendant149.bookreader.saveReaderBrightnessSettings
-import org.dueattendant149.bookreader.saveReaderScreenOrientationMode
-import org.dueattendant149.bookreader.saveEpubRightToLeftPagination
-import org.dueattendant149.bookreader.saveReaderThemeId
-import org.dueattendant149.bookreader.saveReaderSliderToggled
-import org.dueattendant149.bookreader.saveTtsReplacementPreferences
-import org.dueattendant149.bookreader.shouldRenderReaderSlider
-import org.dueattendant149.bookreader.shared.ReaderBookReplacementPreferences
-import org.dueattendant149.bookreader.shared.ReaderTtsReplacementPreferences
-import org.dueattendant149.bookreader.shared.ReaderLocator as SharedReaderLocator
-import org.dueattendant149.bookreader.tts.ReaderTtsOverlaySize
-import org.dueattendant149.bookreader.tts.SpeakerSamplePlayer
-import org.dueattendant149.bookreader.tts.TtsPlaybackManager
-import org.dueattendant149.bookreader.tts.loadTtsMode
-import org.dueattendant149.bookreader.tts.loadReaderTtsOverlaySize
-import org.dueattendant149.bookreader.tts.readerTtsOverlayAlignmentBias
-import org.dueattendant149.bookreader.tts.saveReaderTtsOverlaySize
-import org.dueattendant149.bookreader.tts.splitTextIntoChunks
-import org.dueattendant149.bookreader.withTtsReplacements
-import org.dueattendant149.bookreader.shared.reader.ReaderJumpHistory
+import com.aryan.reader.AiDefinitionResult
+import com.aryan.reader.BuildConfig
+import com.aryan.reader.BuiltInThemes
+import com.aryan.reader.MainViewModel
+import com.aryan.reader.R
+import com.aryan.reader.ReaderBrightnessEffect
+import com.aryan.reader.ReaderFileInfoDialogs
+import com.aryan.reader.ReaderBrightnessSheet
+import com.aryan.reader.ReaderScreenOrientationEffect
+import com.aryan.reader.ReaderScreenOrientationSheet
+import com.aryan.reader.ReaderThemePanel
+import com.aryan.reader.RenderMode
+import com.aryan.reader.SearchResult
+import com.aryan.reader.SummarizationResult
+import com.aryan.reader.SummaryCacheManager
+import com.aryan.reader.TtsSettingsSheet
+import com.aryan.reader.TtsWordReplacementsSheet
+import com.aryan.reader.areReaderAiFeaturesEnabled
+import com.aryan.reader.countWords
+import com.aryan.reader.isByokCloudTtsAvailable
+import com.aryan.reader.data.CustomFontEntity
+import com.aryan.reader.epub.EpubBook
+import com.aryan.reader.epub.hasReadableExtractedContent
+import com.aryan.reader.fetchAiDefinition
+import com.aryan.reader.loadCustomThemes
+import com.aryan.reader.loadGlobalTextureTransparency
+import com.aryan.reader.loadReaderBrightnessSettings
+import com.aryan.reader.loadReaderScreenOrientationMode
+import com.aryan.reader.loadEpubRightToLeftPagination
+import com.aryan.reader.loadReaderThemeId
+import com.aryan.reader.loadReaderSliderToggled
+import com.aryan.reader.loadReaderTextureBitmap
+import com.aryan.reader.loadTtsReplacementPreferences
+import com.aryan.reader.readerSliderBookmarkPosition
+import com.aryan.reader.readerSliderChromeColors
+import com.aryan.reader.readerSliderToggleState
+import com.aryan.reader.paginatedreader.BookPaginator
+import com.aryan.reader.paginatedreader.HeaderBlock
+import com.aryan.reader.paginatedreader.IPaginator
+import com.aryan.reader.paginatedreader.ListItemBlock
+import com.aryan.reader.paginatedreader.Locator
+import com.aryan.reader.paginatedreader.LocatorConverter
+import com.aryan.reader.paginatedreader.PaginatedReaderScreen
+import com.aryan.reader.paginatedreader.ParagraphBlock
+import com.aryan.reader.paginatedreader.QuoteBlock
+import com.aryan.reader.paginatedreader.TextContentBlock
+import com.aryan.reader.paginatedreader.TtsChunk
+import com.aryan.reader.paginatedreader.data.BookCacheDatabase
+import com.aryan.reader.paginatedreader.semanticBlockModule
+import com.aryan.reader.rememberSearchState
+import com.aryan.reader.saveCustomThemes
+import com.aryan.reader.saveGlobalTextureTransparency
+import com.aryan.reader.saveReaderBrightnessSettings
+import com.aryan.reader.saveReaderScreenOrientationMode
+import com.aryan.reader.saveEpubRightToLeftPagination
+import com.aryan.reader.saveReaderThemeId
+import com.aryan.reader.saveReaderSliderToggled
+import com.aryan.reader.saveTtsReplacementPreferences
+import com.aryan.reader.shouldRenderReaderSlider
+import com.aryan.reader.shared.ReaderTtsReplacementPreferences
+import com.aryan.reader.shared.ReaderLocator as SharedReaderLocator
+import com.aryan.reader.tts.SpeakerSamplePlayer
+import com.aryan.reader.tts.TtsPlaybackManager
+import com.aryan.reader.tts.loadTtsMode
+import com.aryan.reader.tts.splitTextIntoChunks
+import com.aryan.reader.withTtsReplacements
+import com.aryan.reader.shared.reader.ReaderJumpHistory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -247,14 +232,12 @@ import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.protobuf.ProtoBuf
-import org.jsoup.Jsoup
import org.json.JSONArray
import org.json.JSONObject
import timber.log.Timber
import java.io.File
import kotlin.math.ceil
import kotlin.math.floor
-import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@@ -570,7 +553,7 @@ fun EpubReaderScreen(
coverImagePath: String?,
onRenderModeChange: (RenderMode) -> Unit,
customFonts: List,
- onImportFonts: (List) -> Unit,
+ onImportFont: (Uri) -> Unit,
viewModel: MainViewModel
) {
val uiState by viewModel.uiState.collectAsState()
@@ -652,7 +635,7 @@ fun EpubReaderScreen(
coverImagePath = coverImagePath,
onRenderModeChange = onRenderModeChange,
customFonts = customFonts,
- onImportFonts = onImportFonts,
+ onImportFont = onImportFont,
onToggleReflow = onOpenOriginal,
onDeleteReflow = if (isReflowFile) {
{
@@ -691,7 +674,7 @@ fun EpubReaderHost(
coverImagePath: String?,
onRenderModeChange: (RenderMode) -> Unit,
customFonts: List,
- onImportFonts: (List) -> Unit,
+ onImportFont: (Uri) -> Unit,
onToggleReflow: ((Int) -> Unit)? = null,
onDeleteReflow: (() -> Unit)? = null,
stableBookId: String? = null,
@@ -707,7 +690,7 @@ fun EpubReaderHost(
var showBrightnessSheet by remember { mutableStateOf(false) }
ReaderBrightnessEffect(window, readerBrightnessSettings)
- val updateReaderBrightness: (org.dueattendant149.bookreader.ReaderBrightnessSettings) -> Unit = { settings ->
+ val updateReaderBrightness: (com.aryan.reader.ReaderBrightnessSettings) -> Unit = { settings ->
readerBrightnessSettings = settings
saveReaderBrightnessSettings(context, settings)
}
@@ -735,6 +718,7 @@ fun EpubReaderHost(
val scrubDebounceJob = remember { mutableStateOf(null) }
val volumeScrollFocusDebounceJob = remember { mutableStateOf(null) }
var sliderStartPage by remember { mutableIntStateOf(0) }
+ var startPageThumbnail by remember { mutableStateOf(null) }
var pendingNoteForNewHighlight by remember { mutableStateOf(false) }
var highlightToNoteCfi by remember { mutableStateOf(null) }
@@ -816,7 +800,7 @@ fun EpubReaderHost(
}
var isAutoScrollCollapsed by remember { mutableStateOf(false) }
- var ttsOverlaySize by remember(context) { mutableStateOf(loadReaderTtsOverlaySize(context)) }
+ var isTtsCollapsed by remember { mutableStateOf(false) }
var isAutoScrollLocal by remember { mutableStateOf(loadAutoScrollLocalMode(context, bookId)) }
@@ -1010,18 +994,12 @@ fun EpubReaderHost(
var showRecapPopup by remember { mutableStateOf(false) }
var currentRenderMode by remember(renderMode) { mutableStateOf(renderMode) }
- var useNativeVerticalRenderer by remember { mutableStateOf(loadNativeVerticalRenderer(context)) }
- val isNativeVerticalMode = currentRenderMode == RenderMode.VERTICAL_SCROLL && useNativeVerticalRenderer
var epubJumpHistory by remember(readerCacheBookId) { mutableStateOf(ReaderJumpHistory()) }
var chapterToLoadOnSwitch by remember { mutableStateOf(null) }
var lastKnownLocator by remember(initialLocator) { mutableStateOf(initialLocator) }
var paginatedReconfigurationAnchor by remember { mutableStateOf(null) }
var isPaginatedReconfigurationRestoring by remember { mutableStateOf(false) }
- LaunchedEffect(useNativeVerticalRenderer) {
- saveNativeVerticalRenderer(context, useNativeVerticalRenderer)
- }
-
val bottomPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val roundedCornerBottomPadding = rememberBottomRoundedCornerPadding(view)
val pageInfoCornerBottomPadding = roundedCornerBottomPadding.coerceAtMost(8.dp)
@@ -1121,7 +1099,7 @@ fun EpubReaderHost(
val ttsState by ttsController.ttsState.collectAsState()
val totalBookLengthChars = remember(chapters) {
- chapters.sumOf { it.plainTextCharacterCount().toLong() }
+ chapters.sumOf { it.plainTextContent.length.toLong() }
}
var topVisibleChunkIndex by remember { mutableIntStateOf(0) }
@@ -1132,20 +1110,6 @@ fun EpubReaderHost(
var chapterChunkElementStartIndices by remember(currentChapterIndex) { mutableStateOf>(emptyList()) }
var chapterChunkElementCounts by remember(currentChapterIndex) { mutableStateOf>(emptyList()) }
var chapterHead by remember(currentChapterIndex) { mutableStateOf("") }
- val epubFontFaceCss = remember(epubBook.css, epubBook.extractionBasePath) {
- val fontFaces = epubBook.css.flatMap { (path, content) ->
- CssParser.parse(
- cssContent = content,
- cssPath = path,
- baseFontSizeSp = 16f,
- density = 1f,
- constraints = Constraints(maxWidth = 1, maxHeight = 1),
- isDarkTheme = false,
- adaptThemeColors = false
- ).fontFaces
- }
- buildEpubFontFaceCss(fontFaces, epubBook.extractionBasePath)
- }
var isChapterParsing by remember(currentChapterIndex) { mutableStateOf(true) }
var cfiToLoad by remember { mutableStateOf(initialCfi) }
@@ -1153,7 +1117,6 @@ fun EpubReaderHost(
var imageToLoad by remember { mutableStateOf(null) }
var isInitialCfiLoad by remember(initialLocator) { mutableStateOf(initialLocator != null) }
var bookmarkPageMap by remember { mutableStateOf