UI: fix status-bar/action-bar overlap under enforced edge-to-edge

targetSdk 36 enforces edge-to-edge and the built-in AppCompat action bar
is no longer inset correctly: the dump showed the action bar at
[0,136..283] while android:id/content started at y=0, so the first list
row sat under the bar and taps on it never reached the row.

Replace the theme action bar with a MaterialToolbar in an AppBarLayout on
each screen (NoActionBar theme) and apply the system-bar insets explicitly
via util/EdgeToEdge: the status-bar inset pads the app bar (whose
background then paints the strip behind the bar) and the navigation-bar
inset pads the content below it. enableEdgeToEdge with a dark status-bar
style keeps the icons light against the always-dark toolbar.

Verified on a Pixel 7: content now starts at y=283, the first book row is
tappable and opens the camera, the capture screen binds CameraX and gets
its first frame, and BookActivity lays out with a working up arrow.

Add README.
This commit is contained in:
2026-10-15 10:20:38 -07:00
parent 051022bb4b
commit fc486d87e9
9 changed files with 489 additions and 198 deletions

134
README.md Normal file
View File

@@ -0,0 +1,134 @@
# PageScribe
Photograph the open spread of a book, page by page. PageScribe reads the text
off each photo in the background, puts the pages back in order by their printed
page numbers, and writes them into a single `.txt` file named after the book.
The photos themselves are shrunk down and kept alongside the text.
You never have to wait for recognition: shoot the next spread while the previous
one is still being read.
## What it does
- **One photo = one two-page spread.** The camera screen shows the spread and a
shutter; each shot is dropped straight into the book's queue.
- **Background OCR.** A WorkManager chain drains the queue, so you can keep
shooting. Progress is shown on the shelf, in the book, and on the camera
screen, and as a notification.
- **A text file per book.** Pages are ordered by the folio (printed page
number) they carry, not by the order you happened to photograph them. The
finished file is `<book title>.txt`.
- **Figures and captions are dropped.** Isolated short lines that sit under
picture content (with their one or two credit lines) are filtered out, while a
short line over blank paper is kept.
- **Small archived photos.** Each source JPEG is scaled so the smallest text
line on the page is about 20 px tall, converted to greyscale, and encoded at
the lowest quality on a ladder that still keeps the text region close to the
original. The smaller of JPEG and WebP wins. A ~350 KB source becomes a
~35 KB archive.
Everything lives in app-private storage (`files/books/<title>/`); the **Export
text** button hands the assembled `.txt` to the system share sheet, from where
it can go to Drive, email, or the Files app.
## Screens
- **Shelf** (`MainActivity`) — one row per book, plus a `+` to create one.
- **Capture** (`CaptureActivity`) — CameraX preview, flash toggle, shutter, and
a jump to the book.
- **Book** (`BookActivity`) — the pages read so far, the log, **Export text**,
**Capture**, and **Delete book**.
## How the pipeline works
Each queued photo goes through `SpreadProcessor`:
1. Decode the JPEG (EXIF rotation honoured), longest edge `OCR_MAX_EDGE`.
2. Find the **gutter** between the two pages as a tilted line, so a slightly
skewed shot still splits cleanly. Falls back to the image centre.
3. Run ML Kit text recognition over the whole spread (bundled Latin model, so
it works offline).
4. Split the recognised lines by the gutter and read each half's **folio** — the
bare page number near the outer edge, top or bottom.
5. Drop **captions**: short, narrow lines isolated below picture content.
6. **Reflow** the body into paragraphs, gluing hyphenated line breaks and
trimming running heads.
7. Write `pages/%04d.txt` (folio = sort key) and archive the image.
At the end, `assembleTextFile` sorts the page files by folio, joins each page to
the next when a sentence runs across the spread, and writes `<title>.txt`.
### Storage layout
```
files/books/<sanitized title>/
meta.properties book title, created-at
queue/ photos waiting to be read (claimed by the worker)
work/ in-flight photos
pages/ %04d.txt, one per recognised page
images/ the archived (shrunk) photos, same numbering
failed/ photos given up on after MAX_JOB_ATTEMPTS
log.txt human-readable progress log
failures.properties why each failed photo failed
<title>.txt the assembled book
```
The queue is crash-safe: `PageScribeApp.onCreate` reclaims photos orphaned in
`work/` older than two minutes and re-arms the worker, and failed photos are
retried up to `MAX_JOB_ATTEMPTS` before being parked in `failed/`.
## Building
```bash
./gradlew :app:assembleDebug # APK at app/build/outputs/apk/debug/
./gradlew :app:testDebugUnitTest # 83 unit tests (JVM + Robolectric)
```
Install on a connected device:
```bash
adb install -r app/build/outputs/apk/debug/app-debug.apk
```
Release builds read signing credentials from `keystore.properties` at the repo
root (git-ignored); without it a release APK is produced unsigned.
## Tech
- Kotlin, Material Components, `minSdk 26`, `targetSdk 36`.
- CameraX `1.6.2` for preview and capture.
- ML Kit `text-recognition` `16.0.1` (bundled Latin model — no Play services,
no network).
- WorkManager `2.12.0` for the durable OCR queue.
Note: from `targetSdk 35` on, Android enforces edge-to-edge and the built-in
AppCompat action bar is no longer inset correctly, so each screen uses a
`MaterialToolbar` in an `AppBarLayout` and applies the system-bar insets itself
(`util/EdgeToEdge.kt`).
## Tests
`app/src/test/` holds 83 JVM/Robolectric tests covering the parts that do not
need a camera:
| Suite | What it covers |
| --- | --- |
| `GutterDetectorTest` | tilted-gutter fit and centre fallback |
| `PageNumberDetectorTest` | folio geometry and margin isolation |
| `PageNumberResolverTest` | choosing the right folio per half |
| `CaptionFilterTest` | dropping captions under figures, keeping orphans |
| `ParagraphReflowTest` | hyphen gluing, running heads, across-page join |
| `BitmapsTest` | EXIF-aware downsample and scaling |
| `ImageArchiverTest` | archive scale and JPEG/WebP quality ladder |
| `BookRepositoryTest` | create/list/delete and navigation |
| `BookTest` | queue claim/fail/complete and text assembly |
## Known limitations
- The archive size is driven by the *smallest* text on the page, so a page with
a tiny folio or caption archives a little larger than a body-text floor would
suggest.
- A figure that interrupts a paragraph produces a paragraph break (treated as a
section break).
- Two-page spreads whose text runs past the gutter (an illustration, or a very
wide table) can only be split at the fitted gutter line.

View File

@@ -2,6 +2,7 @@ package com.bm.pagescribe
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.os.Handler
@@ -11,6 +12,8 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
@@ -18,6 +21,8 @@ import androidx.recyclerview.widget.RecyclerView
import com.bm.pagescribe.book.Book
import com.bm.pagescribe.book.BookRepository
import com.bm.pagescribe.ocr.WorkScheduler
import com.bm.pagescribe.util.EdgeToEdge
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File
@@ -51,8 +56,16 @@ class BookActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT))
setContentView(R.layout.activity_book)
val toolbar = findViewById<MaterialToolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
EdgeToEdge.padTop(findViewById(R.id.appBar))
EdgeToEdge.padBottom(findViewById(R.id.contentRoot))
bookKey = intent.getStringExtra(EXTRA_KEY).orEmpty()
repository = BookRepository(this)

View File

@@ -4,6 +4,7 @@ import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -11,6 +12,8 @@ import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
@@ -23,6 +26,8 @@ import androidx.core.content.ContextCompat
import com.bm.pagescribe.book.Book
import com.bm.pagescribe.book.BookRepository
import com.bm.pagescribe.ocr.WorkScheduler
import com.bm.pagescribe.util.EdgeToEdge
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton
import java.io.File
@@ -65,8 +70,16 @@ class CaptureActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT))
setContentView(R.layout.activity_capture)
val toolbar = findViewById<MaterialToolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
EdgeToEdge.padTop(findViewById(R.id.appBar))
EdgeToEdge.padBottom(findViewById(R.id.contentRoot))
repository = BookRepository(this)
previewView = findViewById(R.id.previewView)
statusText = findViewById(R.id.captureStatus)

View File

@@ -3,6 +3,7 @@ package com.bm.pagescribe
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Handler
@@ -14,6 +15,8 @@ import android.view.ViewGroup
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
@@ -23,6 +26,8 @@ import com.bm.pagescribe.book.Book
import com.bm.pagescribe.book.BookExistsException
import com.bm.pagescribe.book.BookRepository
import com.bm.pagescribe.ocr.WorkScheduler
import com.bm.pagescribe.util.EdgeToEdge
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
@@ -47,8 +52,15 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// The toolbar is always the dark brand blue, so the status-bar icons are
// always light regardless of the DayNight mode.
enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT))
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById<MaterialToolbar>(R.id.toolbar))
EdgeToEdge.padTop(findViewById(R.id.appBar))
EdgeToEdge.padBottom(findViewById(R.id.contentRoot))
repository = BookRepository(this)
statusText = findViewById(R.id.ocrStatus)
emptyText = findViewById(R.id.emptyText)

View File

@@ -0,0 +1,40 @@
package com.bm.pagescribe.util
import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
/**
* targetSdk 36 enforces edge-to-edge: the window is laid out behind the status
* and navigation bars and there is no opt-out. These helpers push each screen's
* chrome back inside the bars.
*
* The bars are applied in two halves so a status-bar inset is never counted
* twice — [padTop] on the app bar (whose background then also paints the strip
* behind the status bar) and [padBottom] on the content below it. Because the
* two views are siblings, each receives the horizontal insets exactly once.
*/
object EdgeToEdge {
private val BAR_TYPES =
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
/** Inset the top and sides: for the app bar that sits under the status bar. */
fun padTop(view: View) {
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val bars = insets.getInsets(BAR_TYPES)
v.updatePadding(left = bars.left, top = bars.top, right = bars.right)
insets
}
}
/** Inset the bottom and sides: for the content that reaches the nav bar. */
fun padBottom(view: View) {
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
val bars = insets.getInsets(BAR_TYPES)
v.updatePadding(left = bars.left, right = bars.right, bottom = bars.bottom)
insets
}
}
}

View File

@@ -3,83 +3,108 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
android:background="@color/paper"
android:orientation="vertical">
<TextView
android:id="@+id/bookMeta"
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="13sp" />
android:background="@color/primary"
android:elevation="4dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/primary"
app:navigationIconTint="@android:color/white"
app:titleTextColor="@android:color/white" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCapture"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:icon="@drawable/ic_camera"
android:text="@string/book_capture" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnExport"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:icon="@drawable/ic_share"
android:text="@string/book_export" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDelete"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:icon="@drawable/ic_delete"
android:text="@string/book_delete" />
<TextView
android:id="@+id/emptyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center"
android:text="@string/book_no_pages"
android:textSize="15sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/pageList"
android:id="@+id/contentRoot"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/book_log"
android:textSize="12sp"
android:textStyle="bold" />
<ScrollView
android:id="@+id/logScroll"
android:layout_width="match_parent"
android:layout_height="96dp"
android:fadeScrollbars="false">
android:layout_weight="1"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/logText"
android:id="@+id/bookMeta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textSize="12sp" />
</ScrollView>
android:textSize="13sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCapture"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:icon="@drawable/ic_camera"
android:text="@string/book_capture" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnExport"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_weight="1"
android:icon="@drawable/ic_share"
android:text="@string/book_export" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDelete"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:icon="@drawable/ic_delete"
android:text="@string/book_delete" />
<TextView
android:id="@+id/emptyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="center"
android:text="@string/book_no_pages"
android:textSize="15sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/pageList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/book_log"
android:textSize="12sp"
android:textStyle="bold" />
<ScrollView
android:id="@+id/logScroll"
android:layout_width="match_parent"
android:layout_height="96dp"
android:fadeScrollbars="false">
<TextView
android:id="@+id/logText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:textSize="12sp" />
</ScrollView>
</LinearLayout>
</LinearLayout>

View File

@@ -1,103 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
android:background="@android:color/black"
android:orientation="vertical">
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="#66000000"
android:orientation="vertical"
android:padding="12dp">
android:background="@color/primary"
android:elevation="4dp">
<TextView
android:id="@+id/captureStatus"
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold" />
android:layout_height="?attr/actionBarSize"
android:background="@color/primary"
app:navigationIconTint="@android:color/white"
app:titleTextColor="@android:color/white" />
</com.google.android.material.appbar.AppBarLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/capture_hint"
android:textColor="#DDFFFFFF"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
<FrameLayout
android:id="@+id/contentRoot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#66000000"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="20dp">
android:layout_height="0dp"
android:layout_weight="1">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnFlash"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_flash"
android:text="@string/capture_flash_off"
android:textColor="@android:color/white"
app:strokeColor="@android:color/white" />
<androidx.camera.view.PreviewView
android:id="@+id/previewView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageButton
android:id="@+id/btnShutter"
android:layout_width="84dp"
android:layout_height="84dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:background="@drawable/shutter_bg"
android:contentDescription="@string/capture_shutter"
android:src="@drawable/ic_camera"
app:tint="@color/ink" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPages"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_pages"
android:text="@string/capture_pages"
android:textColor="@android:color/white"
app:strokeColor="@android:color/white" />
</LinearLayout>
<LinearLayout
android:id="@+id/permissionPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#CC000000"
android:orientation="vertical"
android:padding="24dp"
android:visibility="gone">
<TextView
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/capture_permission"
android:textColor="@android:color/white" />
android:layout_gravity="top"
android:background="#66000000"
android:orientation="vertical"
android:padding="12dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPermission"
android:layout_width="wrap_content"
<TextView
android:id="@+id/captureStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/capture_hint"
android:textColor="#DDFFFFFF"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
android:text="@string/capture_grant" />
</LinearLayout>
</FrameLayout>
android:layout_gravity="bottom"
android:background="#66000000"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="20dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btnFlash"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_flash"
android:text="@string/capture_flash_off"
android:textColor="@android:color/white"
app:strokeColor="@android:color/white" />
<ImageButton
android:id="@+id/btnShutter"
android:layout_width="84dp"
android:layout_height="84dp"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:background="@drawable/shutter_bg"
android:contentDescription="@string/capture_shutter"
android:src="@drawable/ic_camera"
app:tint="@color/ink" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPages"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:icon="@drawable/ic_pages"
android:text="@string/capture_pages"
android:textColor="@android:color/white"
app:strokeColor="@android:color/white" />
</LinearLayout>
<LinearLayout
android:id="@+id/permissionPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="#CC000000"
android:orientation="vertical"
android:padding="24dp"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/capture_permission"
android:textColor="@android:color/white" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPermission"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="12dp"
android:text="@string/capture_grant" />
</LinearLayout>
</FrameLayout>
</LinearLayout>

View File

@@ -1,49 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:background="@color/paper"
android:orientation="vertical">
<LinearLayout
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/ocrStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/ocr_idle"
android:textSize="13sp" />
<TextView
android:id="@+id/emptyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="center"
android:text="@string/books_empty"
android:textSize="16sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bookList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1"
android:clipToPadding="false"
android:paddingBottom="88dp" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnNewBook"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="20dp"
android:contentDescription="@string/books_new"
android:src="@drawable/ic_add"
app:tint="@android:color/white" />
</FrameLayout>
android:background="@color/primary"
android:elevation="4dp">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/primary"
app:title="@string/app_name"
app:titleTextColor="@android:color/white" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/contentRoot"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/ocrStatus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/ocr_idle"
android:textSize="13sp" />
<TextView
android:id="@+id/emptyText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:gravity="center"
android:text="@string/books_empty"
android:textSize="16sp"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/bookList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_weight="1"
android:clipToPadding="false"
android:paddingBottom="88dp" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btnNewBook"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="20dp"
android:contentDescription="@string/books_new"
android:src="@drawable/ic_add"
app:tint="@android:color/white" />
</FrameLayout>
</LinearLayout>

View File

@@ -1,5 +1,10 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.PageScribe" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!--
NoActionBar, plus an explicit MaterialToolbar per screen: the built-in
AppCompat action bar is not inset correctly under the edge-to-edge layout
that targetSdk 36 enforces, so it would overlap the first row of content.
-->
<style name="Theme.PageScribe" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryDark">@color/primary_dark</item>
<item name="colorSecondary">@color/accent</item>