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:
134
README.md
Normal file
134
README.md
Normal 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.
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
40
app/src/main/java/com/bm/pagescribe/util/EdgeToEdge.kt
Normal file
40
app/src/main/java/com/bm/pagescribe/util/EdgeToEdge.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user