]> git.otsuka.systems Git - alexandria/commitdiff
remove non-eink themes
authorCameron Otsuka <cameron@otsuka.haus>
Tue, 11 Aug 2026 16:54:18 +0000 (11:54 -0500)
committerCameron Otsuka <cameron@otsuka.haus>
Tue, 11 Aug 2026 16:54:18 +0000 (11:54 -0500)
14 files changed:
README.md
app/src/main/AndroidManifest.xml
app/src/main/java/com/alexandria/reader/EInkPalette.kt [new file with mode: 0644]
app/src/main/java/com/alexandria/reader/EpubParser.kt
app/src/main/java/com/alexandria/reader/LibraryScreen.kt
app/src/main/java/com/alexandria/reader/MainActivity.kt
app/src/main/java/com/alexandria/reader/Models.kt
app/src/main/java/com/alexandria/reader/ReaderScreen.kt
app/src/main/java/com/alexandria/reader/ReaderWebView.kt
app/src/main/res/drawable/ic_launcher.xml
app/src/main/res/values/colors.xml [new file with mode: 0644]
app/src/main/res/values/styles.xml
scripts/test.ps1
scripts/verify-fixed-page-tools.ps1

index 1306213d243cbdf0f719c1030cc016f3ba1145e2..3cc41c0aac5e2921e1e1119456c44616e70365d3 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # Alexandria
 
-Alexandria is an Android 11 EPUB library and reader written in Kotlin. Its design follows Plato's library and reader workflows.
+Alexandria is an Android 11 EPUB library and reader written in Kotlin. Its design follows Plato's library and reader workflows and is optimized for Carta 1000 e-ink displays.
 
 The app requests no network or shared-storage permission. It imports each selected book through Android's Storage Access Framework and keeps a private copy.
 
@@ -23,7 +23,7 @@ The app requests no network or shared-storage permission. It imports each select
 - Text selection, highlights, notes, annotation editing, and annotation export
 - System dictionary and text-processing integration
 - Typeface, text size, line height, margin, alignment, publisher-style, and hyphenation controls
-- Light, sepia, and dark themes; contrast and screen-brightness controls
+- One high-contrast e-ink appearance with colors aligned to the display's 16 grayscale levels, plus contrast and screen-brightness controls
 - Fixed-page fit-to-page, fit-to-width page streams with line-preserving cuts, and 50–400% custom zoom and bounded pan
 - Shared or separate even/odd crop margins with a visual crop editor
 - Plato-compatible contrast exponent and gray-point controls, plus 16-level and black-and-white spatial dithering
index c3ee8194194077ac51958c7fcaf1654db67b7946..3012bf875635188ae7b88c76d9759a4b187828e3 100644 (file)
@@ -7,7 +7,7 @@
         </intent>
     </queries>
     <application
-        android:theme="@style/AppTheme"
+        android:theme="@style/EInkTheme"
         android:label="Alexandria"
         android:icon="@drawable/ic_launcher"
         android:roundIcon="@drawable/ic_launcher"
diff --git a/app/src/main/java/com/alexandria/reader/EInkPalette.kt b/app/src/main/java/com/alexandria/reader/EInkPalette.kt
new file mode 100644 (file)
index 0000000..6cab51c
--- /dev/null
@@ -0,0 +1,27 @@
+package com.alexandria.reader
+
+import android.graphics.Color
+
+/**
+ * Colors for a Carta 1000 panel. Each RGB channel is one of the panel's
+ * 16 native grayscale levels: 0x00, 0x11, …, 0xff.
+ */
+internal object EInkPalette {
+    const val LEVEL_COUNT = 16
+
+    val INK = gray(0)
+    val ACTION = gray(1)
+    val SECONDARY_INK = gray(4)
+    val MUTED_INK = gray(6)
+    val DIVIDER = gray(11)
+    val PAGE_PREVIEW = gray(13)
+    val SURFACE = gray(14)
+    val PAPER = gray(15)
+    val TRANSPARENT = Color.TRANSPARENT
+
+    private fun gray(level: Int): Int {
+        require(level in 0 until LEVEL_COUNT)
+        val channel = level * 0x11
+        return Color.rgb(channel, channel, channel)
+    }
+}
index c275f0ce9975fa8c13653e37e8d1ca2ac7ce19f1..b9023726eddd52c4cb41a3c5f877c19a7a79eaeb 100644 (file)
@@ -594,7 +594,7 @@ $publisherCss
 @font-face { font-family:'Alexandria Sans'; src:url('file:///android_asset/fonts/NotoSans-Bold.ttf'); font-style:normal; font-weight:700; }
 @font-face { font-family:'Atkinson Hyperlegible'; src:url('file:///android_asset/fonts/Atkinson.otf'); }
 @font-face { font-family:'Alexandria Mono'; src:url('file:///android_asset/fonts/SourceCode-Regular.otf'); }
-:root { --alex-side:32px; --alex-v:22px; --alex-font-size:20px; --alex-line-height:1.32; --alex-font:'Alexandria Serif'; --alex-paper:#fff; --alex-ink:#171717; --alex-link:#202020; }
+:root { --alex-side:32px; --alex-v:22px; --alex-font-size:20px; --alex-line-height:1.32; --alex-font:'Alexandria Serif'; --alex-paper:#fff; --alex-ink:#000; --alex-link:#111; }
 html { margin:0 !important; padding:0 !important; width:100%; height:100%; overflow:hidden; background:var(--alex-paper); color:var(--alex-ink); }
 body { margin:0 !important; width:100%; color:var(--alex-ink); background:var(--alex-paper); font-family:var(--alex-font), serif; font-size:var(--alex-font-size); line-height:var(--alex-line-height); text-rendering:optimizeLegibility; -webkit-font-smoothing:antialiased; }
 body.alex-paged { box-sizing:border-box; height:100vh; padding:var(--alex-v) var(--alex-side) !important; column-width:calc(100vw - (2 * var(--alex-side))); column-gap:calc(2 * var(--alex-side)); column-fill:auto; overflow:visible !important; }
@@ -616,7 +616,7 @@ body.alex-paged .x-ebookmaker-cover { height:calc(100vh - (2 * var(--alex-v))) !
 svg[height="100%"], .x-ebookmaker-cover svg { display:block; height:calc(100vh - (2 * var(--alex-v))) !important; max-height:calc(100vh - (2 * var(--alex-v))) !important; }
 table { max-width:100%; border-collapse:collapse; }
 pre, code { font-family:'Alexandria Mono', monospace; white-space:pre-wrap; overflow-wrap:anywhere; }
-a { color:var(--alex-link); }
+a { color:var(--alex-link); text-decoration:underline; }
 body.alex-hyphenate { -webkit-hyphens:auto; hyphens:auto; }
 body.alex-no-hyphenate { -webkit-hyphens:none !important; hyphens:none !important; }
 body.alex-font-override, body.alex-font-override p, body.alex-font-override div, body.alex-font-override h1, body.alex-font-override h2, body.alex-font-override h3, body.alex-font-override h4, body.alex-font-override h5, body.alex-font-override h6 { font-family:var(--alex-font), serif !important; }
@@ -624,12 +624,9 @@ body.alex-align-left .alex-chapter, body.alex-align-left p { text-align:left !im
 body.alex-align-right .alex-chapter, body.alex-align-right p { text-align:right !important; }
 body.alex-align-center .alex-chapter, body.alex-align-center p { text-align:center !important; }
 body.alex-align-justify .alex-chapter, body.alex-align-justify p { text-align:justify !important; }
-body.alex-theme-sepia { --alex-paper:#f3ead7; --alex-ink:#2d261c; --alex-link:#2d261c; }
-body.alex-theme-dark { --alex-paper:#171717; --alex-ink:#ededed; --alex-link:#ededed; }
-body:not(.alex-fixed).alex-theme-sepia .alex-chapter, body:not(.alex-fixed).alex-theme-dark .alex-chapter, body:not(.alex-fixed).alex-theme-sepia .alex-chapter *, body:not(.alex-fixed).alex-theme-dark .alex-chapter * { color:var(--alex-ink) !important; background-color:transparent !important; border-color:currentColor !important; }
-mark.alex-search { color:inherit !important; background:#bdbdbd !important; border-bottom:2px solid #111; }
-mark.alex-search-current { background:#7d7d7d !important; color:#fff !important; }
-mark.alex-annotation { color:inherit !important; background:rgba(125,125,125,.34) !important; border-bottom:2px solid currentColor; cursor:pointer; }
+mark.alex-search { color:inherit !important; background:#ccc !important; border-bottom:2px solid #111; }
+mark.alex-search-current { background:#666 !important; color:#fff !important; }
+mark.alex-annotation { color:inherit !important; background:#ddd !important; border-bottom:2px solid #111; cursor:pointer; }
 </style>
 </head>
 <body class="${if (publication.fixedLayout) "alex-fixed" else "alex-paged"} alex-hyphenate" data-direction="${publication.readingDirection}">
@@ -751,7 +748,6 @@ ${if (publication.fixedLayout) fixedReaderScript() else readerScript()}
     body.classList.toggle('alex-hyphenate', settings.hyphenation !== false); body.classList.toggle('alex-no-hyphenate', settings.hyphenation === false);
     body.classList.toggle('alex-font-override', family !== 'Publisher');
     ['left','right','center','justify'].forEach(function (name) { body.classList.toggle('alex-align-' + name, settings.textAlign === name); });
-    body.classList.remove('alex-theme-light','alex-theme-sepia','alex-theme-dark'); body.classList.add('alex-theme-' + (settings.theme || 'light'));
     document.getElementById('alex-publisher-styles').disabled = settings.publisherStyles === false;
     applyToneFilter(settings);
     setTimeout(function () { recalculate(); if (keep) goToLocator(keep); }, 80);
@@ -1088,8 +1084,6 @@ ${if (publication.fixedLayout) fixedReaderScript() else readerScript()}
   }
   function applySettings(settings,keep) {
     state.settings=settings||{};
-    var body=document.body,theme=state.settings.theme||'light';
-    body.classList.remove('alex-theme-light','alex-theme-sepia','alex-theme-dark');body.classList.add('alex-theme-'+theme);
     // Fixed-layout coordinates depend on the publisher stylesheet.
     document.getElementById('alex-publisher-styles').disabled=false;
     if(keep){state.page=clamp(Math.round(number(keep.chapter,keep.page||0)),0,state.total-1);state.offsetX=number(keep.viewportX,NaN);state.offsetY=number(keep.viewportY,NaN);}
index a35a0447ad9eeb3e6ef55869b96c6d4d7c5d25bf..29876fdd2c56cc618d0144a320d8a1ba571b7ca7 100644 (file)
@@ -3,7 +3,6 @@ package com.alexandria.reader
 import android.app.Activity
 import android.app.AlertDialog
 import android.graphics.BitmapFactory
-import android.graphics.Color
 import android.graphics.Typeface
 import android.graphics.drawable.ColorDrawable
 import android.text.Editable
@@ -39,7 +38,7 @@ class LibraryScreen(
     private var sort = repository.sortOrder()
 
     init {
-        setBackgroundColor(Color.rgb(250, 250, 248))
+        setBackgroundColor(EInkPalette.PAPER)
         build()
         refresh()
     }
@@ -49,8 +48,7 @@ class LibraryScreen(
         toolbar.orientation = LinearLayout.HORIZONTAL
         toolbar.gravity = Gravity.CENTER_VERTICAL
         toolbar.setPadding(dp(18), 0, dp(8), 0)
-        toolbar.setBackgroundColor(Color.rgb(250, 250, 248))
-        toolbar.elevation = dp(2).toFloat()
+        toolbar.setBackgroundColor(EInkPalette.PAPER)
         addView(toolbar, LayoutParams(LayoutParams.MATCH_PARENT, toolbarHeight, Gravity.TOP))
 
         toolbar.addView(TextView(activity).apply {
@@ -58,7 +56,7 @@ class LibraryScreen(
             textSize = 19f
             typeface = Typeface.create("sans-serif", Typeface.BOLD)
             letterSpacing = .11f
-            setTextColor(Color.rgb(20, 20, 20))
+            setTextColor(EInkPalette.INK)
             contentDescription = "Alexandria library"
         }, LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1f).apply { gravity = Gravity.CENTER_VERTICAL })
         toolbar.addView(button("⌕", "Search library") { _ -> toggleSearch() })
@@ -66,9 +64,9 @@ class LibraryScreen(
         toolbar.addView(button("+", "Import EPUB") { _ -> importBooks() })
 
         list.apply {
-            divider = ColorDrawable(Color.rgb(190, 190, 187))
+            divider = ColorDrawable(EInkPalette.DIVIDER)
             dividerHeight = 1
-            setBackgroundColor(Color.rgb(250, 250, 248))
+            setBackgroundColor(EInkPalette.PAPER)
             this.adapter = this@LibraryScreen.adapter
             emptyView = buildEmptyView()
             setOnItemClickListener { _, _, position, _ -> openBook(this@LibraryScreen.adapter.value(position)) }
@@ -83,27 +81,27 @@ class LibraryScreen(
             orientation = LinearLayout.VERTICAL
             gravity = Gravity.CENTER
             setPadding(dp(32), dp(32), dp(32), dp(32))
-            setBackgroundColor(Color.rgb(250, 250, 248))
+            setBackgroundColor(EInkPalette.PAPER)
             addView(TextView(activity).apply {
                 text = "Your library is empty"
                 textSize = 28f
                 typeface = Typeface.create("serif", Typeface.ITALIC)
                 gravity = Gravity.CENTER
-                setTextColor(Color.rgb(25, 25, 25))
+                setTextColor(EInkPalette.INK)
             })
             addView(TextView(activity).apply {
                 text = "Import an EPUB to start reading. Alexandria keeps its own private copy and requests no storage or network permission."
                 textSize = 16f
                 gravity = Gravity.CENTER
-                setTextColor(Color.DKGRAY)
+                setTextColor(EInkPalette.MUTED_INK)
                 setPadding(0, dp(16), 0, dp(24))
             })
             addView(TextView(activity).apply {
                 text = "Open EPUB"
                 textSize = 17f
                 gravity = Gravity.CENTER
-                setTextColor(Color.WHITE)
-                setBackgroundColor(Color.rgb(35, 35, 35))
+                setTextColor(EInkPalette.PAPER)
+                setBackgroundColor(EInkPalette.ACTION)
                 setPadding(dp(30), dp(13), dp(30), dp(13))
                 contentDescription = "Open EPUB file picker"
                 setOnClickListener { importBooks() }
@@ -117,7 +115,7 @@ class LibraryScreen(
         text = label
         textSize = 23f
         gravity = Gravity.CENTER
-        setTextColor(Color.rgb(25, 25, 25))
+        setTextColor(EInkPalette.INK)
         contentDescription = description
         isClickable = true
         isFocusable = true
@@ -155,7 +153,7 @@ class LibraryScreen(
 
     private fun titleView(): TextView = TextView(activity).apply {
         text = "ALEXANDRIA"; textSize = 19f; typeface = Typeface.DEFAULT_BOLD; letterSpacing = .11f
-        gravity = Gravity.CENTER_VERTICAL; setTextColor(Color.rgb(20, 20, 20))
+        gravity = Gravity.CENTER_VERTICAL; setTextColor(EInkPalette.INK)
     }
 
     private fun showSortMenu(anchor: View) {
@@ -255,16 +253,15 @@ class LibraryScreen(
             setPadding(dp(18), dp(10), dp(18), dp(10))
             minimumHeight = dp(112)
             cover.scaleType = ImageView.ScaleType.CENTER_CROP
-            cover.setBackgroundColor(Color.rgb(226, 226, 222))
+            cover.setBackgroundColor(EInkPalette.SURFACE)
             addView(cover, LinearLayout.LayoutParams(dp(62), dp(88)).apply { marginEnd = dp(18) })
             val text = LinearLayout(activity).apply { orientation = VERTICAL; gravity = Gravity.CENTER_VERTICAL }
-            title.textSize = 20f; title.typeface = Typeface.create("serif", Typeface.ITALIC); title.maxLines = 2; title.setTextColor(Color.BLACK)
-            author.textSize = 15f; author.maxLines = 1; author.setTextColor(Color.rgb(45, 45, 45))
-            details.textSize = 12f; details.maxLines = 1; details.setTextColor(Color.GRAY)
+            title.textSize = 20f; title.typeface = Typeface.create("serif", Typeface.ITALIC); title.maxLines = 2; title.setTextColor(EInkPalette.INK)
+            author.textSize = 15f; author.maxLines = 1; author.setTextColor(EInkPalette.SECONDARY_INK)
+            details.textSize = 12f; details.maxLines = 1; details.setTextColor(EInkPalette.MUTED_INK)
             text.addView(title); text.addView(author); text.addView(details)
             addView(text, LinearLayout.LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f))
-            progress.textSize = 14f; progress.gravity = Gravity.CENTER; progress.setTextColor(Color.DKGRAY
-            )
+            progress.textSize = 14f; progress.gravity = Gravity.CENTER; progress.setTextColor(EInkPalette.MUTED_INK)
             addView(progress, LinearLayout.LayoutParams(dp(92), LayoutParams.MATCH_PARENT))
         }
 
index 543d0f8bc3808a606e2e2eb55de6670ba5bc5aec..6262c82e6c25ccee668d237e77f0a19ae6740c52 100644 (file)
@@ -3,7 +3,6 @@ package com.alexandria.reader
 import android.app.Activity
 import android.app.AlertDialog
 import android.content.Intent
-import android.graphics.Color
 import android.net.Uri
 import android.os.Bundle
 import android.view.Gravity
@@ -21,8 +20,8 @@ class MainActivity : Activity() {
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
-        window.statusBarColor = Color.rgb(250, 250, 248)
-        window.navigationBarColor = Color.rgb(250, 250, 248)
+        window.statusBarColor = EInkPalette.PAPER
+        window.navigationBarColor = EInkPalette.PAPER
         window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
         repository = LibraryRepository(this)
         if (!handleIntent(intent)) showLibrary()
@@ -143,8 +142,8 @@ class MainActivity : Activity() {
             text = message
             textSize = 20f
             gravity = Gravity.CENTER
-            setTextColor(Color.rgb(35, 35, 35))
-            setBackgroundColor(Color.WHITE)
+            setTextColor(EInkPalette.INK)
+            setBackgroundColor(EInkPalette.PAPER)
             contentDescription = message
         })
     }
index a8e693f52582241688dfee1027370a905f32040c..eb97ee53245392aaef466efc8fdc5f1e9d3d5cc6 100644 (file)
@@ -147,7 +147,6 @@ data class ReaderSettings(
     val lineHeight: Float = 1.32f,
     val margin: Int = 32,
     val textAlign: String = "publisher",
-    val theme: String = "light",
     val publisherStyles: Boolean = true,
     val hyphenation: Boolean = true,
     val mode: String = "paged",
@@ -170,7 +169,6 @@ data class ReaderSettings(
         put("lineHeight", lineHeight.toDouble())
         put("margin", margin)
         put("textAlign", textAlign)
-        put("theme", theme)
         put("publisherStyles", publisherStyles)
         put("hyphenation", hyphenation)
         put("mode", mode)
@@ -196,8 +194,6 @@ data class ReaderSettings(
             margin = value.optInt("margin", 32).coerceIn(0, 96),
             textAlign = value.optString("textAlign", "publisher")
                 .takeIf { it in setOf("publisher", "left", "right", "center", "justify") } ?: "publisher",
-            theme = value.optString("theme", "light")
-                .takeIf { it in setOf("light", "sepia", "dark") } ?: "light",
             publisherStyles = value.optBoolean("publisherStyles", true),
             hyphenation = value.optBoolean("hyphenation", true),
             mode = value.optString("mode", "paged").takeIf { it in setOf("paged", "continuous") } ?: "paged",
index f305124bed282f18a72c9a7eead567dc9f854e0f..cf81da34a3f5771c2d48a3fa325639f3e1864968 100644 (file)
@@ -8,7 +8,6 @@ import android.content.Context
 import android.content.Intent
 import android.content.pm.ActivityInfo
 import android.graphics.Canvas
-import android.graphics.Color
 import android.graphics.Paint
 import android.graphics.RectF
 import android.graphics.Typeface
@@ -76,14 +75,14 @@ class ReaderScreen(
     private var searchDirection = ReaderWebView.SearchDirection.FORWARD
 
     init {
-        setBackgroundColor(Color.WHITE)
+        setBackgroundColor(EInkPalette.PAPER)
         isFocusableInTouchMode = true
         buildReader()
         webView.listener = this
         webView.configurePageTools(publication.fixedLayout, settings)
         webView.load(repository.readerFile(publication))
         repository.markOpened(publication.book.id)
-        applyNativeTheme()
+        applyEInkAppearance()
     }
 
     private fun buildReader() {
@@ -96,14 +95,13 @@ class ReaderScreen(
         topBar.orientation = LinearLayout.HORIZONTAL
         topBar.gravity = Gravity.CENTER_VERTICAL
         topBar.setPadding(dp(4), 0, dp(4), 0)
-        topBar.elevation = dp(2).toFloat()
         addView(topBar, LayoutParams(LayoutParams.MATCH_PARENT, barHeight, Gravity.TOP))
 
         topBar.addView(actionButton("‹", "Back to library") { _ -> flush(); closeReader() }, lp(dp(52)))
         titleLabel.apply {
             text = publication.book.title
             textSize = 15f
-            setTextColor(Color.BLACK)
+            setTextColor(EInkPalette.INK)
             maxLines = 2
             gravity = Gravity.CENTER_VERTICAL
             ellipsize = android.text.TextUtils.TruncateAt.END
@@ -128,7 +126,6 @@ class ReaderScreen(
         bottomBar.orientation = LinearLayout.HORIZONTAL
         bottomBar.gravity = Gravity.CENTER_VERTICAL
         bottomBar.setPadding(dp(4), 0, dp(4), 0)
-        bottomBar.elevation = dp(2).toFloat()
         addView(bottomBar, LayoutParams(LayoutParams.MATCH_PARENT, barHeight, Gravity.BOTTOM))
         bottomBar.addView(actionButton("|‹", "Previous chapter") { _ -> previousChapter() }, lp(dp(52)))
         bottomBar.addView(actionButton("‹", "Previous page") { _ -> previousPage() }, lp(dp(48)))
@@ -136,7 +133,7 @@ class ReaderScreen(
             text = "Page 1 of 1"
             textSize = 14f
             gravity = Gravity.CENTER
-            setTextColor(Color.DKGRAY)
+            setTextColor(EInkPalette.MUTED_INK)
             contentDescription = "Reading progress. Tap to go to a page"
             setOnClickListener { showGoToPage() }
             setOnLongClickListener { showGoToPercent(); true }
@@ -149,8 +146,8 @@ class ReaderScreen(
             text = "Laying out ${publication.book.title}…"
             textSize = 18f
             gravity = Gravity.CENTER
-            setTextColor(Color.DKGRAY)
-            setBackgroundColor(Color.WHITE)
+            setTextColor(EInkPalette.MUTED_INK)
+            setBackgroundColor(EInkPalette.PAPER)
             isClickable = true
             contentDescription = "Opening EPUB"
         }
@@ -169,8 +166,8 @@ class ReaderScreen(
     private fun styleAction(view: TextView) {
         view.textSize = 20f
         view.gravity = Gravity.CENTER
-        view.setTextColor(Color.rgb(25, 25, 25))
-        view.setBackgroundColor(Color.TRANSPARENT)
+        view.setTextColor(EInkPalette.INK)
+        view.setBackgroundColor(EInkPalette.TRANSPARENT)
         view.isClickable = true
         view.isFocusable = true
         view.setPadding(dp(4), 0, dp(4), 0)
@@ -517,8 +514,7 @@ class ReaderScreen(
             orientation = LinearLayout.HORIZONTAL
             gravity = Gravity.CENTER_VERTICAL
             setPadding(dp(8), dp(4), dp(4), dp(4))
-            setBackgroundColor(Color.WHITE)
-            elevation = dp(3).toFloat()
+            setBackgroundColor(EInkPalette.PAPER)
         }
         val input = EditText(activity).apply {
             tag = "query"
@@ -550,7 +546,7 @@ class ReaderScreen(
         updateSearchDirectionButton(directionButton, input)
         row.addView(directionButton, lp(dp(42)))
         row.addView(input, LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1f))
-        searchStatus = TextView(activity).apply { text = ""; textSize = 12f; gravity = Gravity.CENTER; setTextColor(Color.DKGRAY) }
+        searchStatus = TextView(activity).apply { text = ""; textSize = 12f; gravity = Gravity.CENTER; setTextColor(EInkPalette.MUTED_INK) }
         row.addView(searchStatus, lp(dp(74)))
         row.addView(actionButton("‹", "Previous search result") { _ -> if (searchCount > 0) { webView.moveSearch(-1); searchIndex = (searchIndex - 1 + searchCount) % searchCount; updateSearchStatus() } }, lp(dp(42)))
         row.addView(actionButton("›", "Next search result") { _ -> if (searchCount > 0) { webView.moveSearch(1); searchIndex = (searchIndex + 1) % searchCount; updateSearchStatus() } }, lp(dp(42)))
@@ -611,7 +607,7 @@ class ReaderScreen(
     private fun showAppearance() {
         val content = LinearLayout(activity).apply { orientation = LinearLayout.VERTICAL; setPadding(dp(24), dp(8), dp(24), dp(16)) }
         fun heading(value: String) = TextView(activity).apply {
-            text = value; textSize = 13f; setTextColor(Color.DKGRAY); setPadding(0, dp(14), 0, dp(3))
+            text = value; textSize = 13f; setTextColor(EInkPalette.MUTED_INK); setPadding(0, dp(14), 0, dp(3))
         }
         fun spinner(label: String, choices: List<String>, selected: Int, change: (Int) -> Unit) {
             content.addView(heading(label))
@@ -642,10 +638,6 @@ class ReaderScreen(
                 })
             })
         }
-        val themeNames = listOf("Light", "Sepia", "Dark")
-        spinner("Theme", themeNames, listOf("light", "sepia", "dark").indexOf(settings.theme)) {
-            updateSettings(settings.copy(theme = listOf("light", "sepia", "dark")[it]))
-        }
         val families = listOf("Publisher", "Alexandria Serif", "Alexandria Sans", "Atkinson Hyperlegible", "Alexandria Mono")
         spinner("Typeface", families, families.indexOf(settings.fontFamily).coerceAtLeast(1)) { updateSettings(settings.copy(fontFamily = families[it])) }
         slider("Text size", 26, settings.fontSize - 12, { "${it + 12} sp" }) { updateSettings(settings.copy(fontSize = it + 12)) }
@@ -689,7 +681,7 @@ class ReaderScreen(
             setPadding(dp(24), dp(8), dp(24), dp(20))
         }
         fun heading(value: String) = TextView(activity).apply {
-            text = value; textSize = 13f; setTextColor(Color.DKGRAY); setPadding(0, dp(14), 0, dp(3))
+            text = value; textSize = 13f; setTextColor(EInkPalette.MUTED_INK); setPadding(0, dp(14), 0, dp(3))
         }
         fun spinner(label: String, choices: List<String>, selected: Int, change: (Int) -> Unit) {
             content.addView(heading(label))
@@ -725,8 +717,8 @@ class ReaderScreen(
                 textSize = 16f
                 gravity = Gravity.CENTER_VERTICAL
                 setPadding(dp(14), dp(12), dp(14), dp(12))
-                setTextColor(Color.rgb(25, 25, 25))
-                setBackgroundColor(Color.rgb(232, 232, 229))
+                setTextColor(EInkPalette.INK)
+                setBackgroundColor(EInkPalette.SURFACE)
                 setOnClickListener { showCropEditor(label, margins(), save) }
             }, LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply { topMargin = dp(7) })
         }
@@ -734,7 +726,7 @@ class ReaderScreen(
         if (!publication.fixedLayout) {
             content.addView(TextView(activity).apply {
                 text = "This EPUB is reflowable. Crop, zoom, pan, and page-cut controls become active for a pre-paginated EPUB. Tone and dithering controls remain available."
-                textSize = 14f; setTextColor(Color.DKGRAY); setPadding(0, dp(8), 0, dp(8))
+                textSize = 14f; setTextColor(EInkPalette.MUTED_INK); setPadding(0, dp(8), 0, dp(8))
             })
         } else {
             val zoomValues = listOf("fit-page", "fit-width", "custom")
@@ -791,7 +783,7 @@ class ReaderScreen(
         fun edge(label: String, initialValue: Float, update: (Float) -> CropMargins) {
             fun format(value: Float) = String.format(java.util.Locale.getDefault(), "%.1f%%", value)
             val caption = TextView(activity).apply {
-                text = "$label: ${format(initialValue)}"; textSize = 13f; setTextColor(Color.DKGRAY); setPadding(0, dp(8), 0, 0)
+                text = "$label: ${format(initialValue)}"; textSize = 13f; setTextColor(EInkPalette.MUTED_INK); setPadding(0, dp(8), 0, 0)
             }
             content.addView(caption)
             content.addView(SeekBar(activity).apply {
@@ -825,7 +817,7 @@ class ReaderScreen(
             webView.captureLocation { location ->
                 settings = value
                 repository.saveSettings(publication.book.id, settings)
-                applyNativeTheme()
+                applyEInkAppearance()
                 webView.configurePageTools(publication.fixedLayout, settings)
                 webView.applySettings(settings, location)
             }
@@ -833,24 +825,19 @@ class ReaderScreen(
             settings = value
             repository.saveSettings(publication.book.id, settings)
             webView.configurePageTools(publication.fixedLayout, settings)
-            applyNativeTheme()
+            applyEInkAppearance()
         }
     }
 
-    private fun applyNativeTheme() {
-        val (paper, ink) = when (settings.theme) {
-            "sepia" -> Color.rgb(243, 234, 215) to Color.rgb(45, 38, 28)
-            "dark" -> Color.rgb(23, 23, 23) to Color.rgb(237, 237, 237)
-            else -> Color.WHITE to Color.rgb(25, 25, 25)
-        }
-        setBackgroundColor(paper)
-        topBar.setBackgroundColor(paper); bottomBar.setBackgroundColor(paper)
-        titleLabel.setTextColor(ink); progressLabel.setTextColor(ink)
+    private fun applyEInkAppearance() {
+        setBackgroundColor(EInkPalette.PAPER)
+        topBar.setBackgroundColor(EInkPalette.PAPER); bottomBar.setBackgroundColor(EInkPalette.PAPER)
+        titleLabel.setTextColor(EInkPalette.INK); progressLabel.setTextColor(EInkPalette.INK)
         sequenceOf(topBar, bottomBar).flatMap { bar -> (0 until bar.childCount).asSequence().map { bar.getChildAt(it) } }
-            .filterIsInstance<TextView>().forEach { it.setTextColor(ink) }
-        activity.window.statusBarColor = paper
-        activity.window.navigationBarColor = paper
-        activity.window.decorView.systemUiVisibility = if (settings.theme == "dark") 0 else
+            .filterIsInstance<TextView>().forEach { it.setTextColor(EInkPalette.INK) }
+        activity.window.statusBarColor = EInkPalette.PAPER
+        activity.window.navigationBarColor = EInkPalette.PAPER
+        activity.window.decorView.systemUiVisibility =
             View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
         activity.window.attributes = activity.window.attributes.apply {
             screenBrightness = if (settings.brightness >= 0) settings.brightness / 100f
@@ -896,7 +883,7 @@ class ReaderScreen(
 
     override fun onRenderError(message: String) {
         loading.text = "Unable to render this EPUB\n\n$message"
-        loading.setTextColor(Color.rgb(120, 20, 20))
+        loading.setTextColor(EInkPalette.INK)
     }
 
     fun handleKey(keyCode: Int): Boolean {
@@ -963,7 +950,7 @@ class ReaderScreen(
             val pageHeight = pageWidth / pageRatio
             val page = RectF((width - pageWidth) / 2f, (height - pageHeight) / 2f,
                 (width + pageWidth) / 2f, (height + pageHeight) / 2f)
-            paint.style = Paint.Style.FILL; paint.color = Color.rgb(216, 216, 213); canvas.drawRect(page, paint)
+            paint.style = Paint.Style.FILL; paint.color = EInkPalette.PAGE_PREVIEW; canvas.drawRect(page, paint)
             val horizontalScale = if (margins.left + margins.right > 95f) 95f / (margins.left + margins.right) else 1f
             val verticalScale = if (margins.top + margins.bottom > 95f) 95f / (margins.top + margins.bottom) else 1f
             val crop = RectF(
@@ -972,9 +959,9 @@ class ReaderScreen(
                 page.right - page.width() * margins.right * horizontalScale / 100f,
                 page.bottom - page.height() * margins.bottom * verticalScale / 100f,
             )
-            paint.color = Color.WHITE; canvas.drawRect(crop, paint)
+            paint.color = EInkPalette.PAPER; canvas.drawRect(crop, paint)
             paint.style = Paint.Style.STROKE; paint.strokeWidth = resources.displayMetrics.density * 2f
-            paint.color = Color.rgb(25, 25, 25); canvas.drawRect(crop, paint)
+            paint.color = EInkPalette.INK; canvas.drawRect(crop, paint)
         }
     }
 
index 3424b88f3af5fdafd9bdc6b24573a02466160bd7..26ed88c917585203b8c6fb33d9b4dd65d5ec4f57 100644 (file)
@@ -2,7 +2,6 @@ package com.alexandria.reader
 
 import android.annotation.SuppressLint
 import android.content.Context
-import android.graphics.Color
 import android.os.Handler
 import android.os.Looper
 import android.util.Log
@@ -122,7 +121,7 @@ class ReaderWebView(context: Context) : WebView(context) {
     })
 
     init {
-        setBackgroundColor(Color.WHITE)
+        setBackgroundColor(EInkPalette.PAPER)
         isVerticalScrollBarEnabled = false
         isHorizontalScrollBarEnabled = false
         overScrollMode = OVER_SCROLL_NEVER
index beac9256d4a2485af90f9c5dc409235309a9394e..7f203f1a95fd3d134691d21a8117bf4ceb0f7e98 100644 (file)
@@ -1,6 +1,6 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="48dp" android:height="48dp" android:viewportWidth="48" android:viewportHeight="48">
-    <path android:fillColor="#FAFAF8" android:pathData="M0,0h48v48h-48z"/>
-    <path android:fillColor="#202020" android:pathData="M9,7h25c2.8,0 5,2.2 5,5v27h-26c-2.2,0 -4,-1.8 -4,-4z"/>
-    <path android:fillColor="#FAFAF8" android:pathData="M14,11h18c1.7,0 3,1.3 3,3v20h-21z"/>
-    <path android:fillColor="#202020" android:pathData="M18,17h13v2h-13zM18,23h13v2h-13zM18,29h9v2h-9z"/>
+    <path android:fillColor="@color/eink_gray_15" android:pathData="M0,0h48v48h-48z"/>
+    <path android:fillColor="@color/eink_gray_01" android:pathData="M9,7h25c2.8,0 5,2.2 5,5v27h-26c-2.2,0 -4,-1.8 -4,-4z"/>
+    <path android:fillColor="@color/eink_gray_15" android:pathData="M14,11h18c1.7,0 3,1.3 3,3v20h-21z"/>
+    <path android:fillColor="@color/eink_gray_01" android:pathData="M18,17h13v2h-13zM18,23h13v2h-13zM18,29h9v2h-9z"/>
 </vector>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644 (file)
index 0000000..df0f0f7
--- /dev/null
@@ -0,0 +1,19 @@
+<resources>
+    <!-- Carta 1000 native 16-level grayscale palette. -->
+    <color name="eink_gray_00">#000000</color>
+    <color name="eink_gray_01">#111111</color>
+    <color name="eink_gray_02">#222222</color>
+    <color name="eink_gray_03">#333333</color>
+    <color name="eink_gray_04">#444444</color>
+    <color name="eink_gray_05">#555555</color>
+    <color name="eink_gray_06">#666666</color>
+    <color name="eink_gray_07">#777777</color>
+    <color name="eink_gray_08">#888888</color>
+    <color name="eink_gray_09">#999999</color>
+    <color name="eink_gray_10">#aaaaaa</color>
+    <color name="eink_gray_11">#bbbbbb</color>
+    <color name="eink_gray_12">#cccccc</color>
+    <color name="eink_gray_13">#dddddd</color>
+    <color name="eink_gray_14">#eeeeee</color>
+    <color name="eink_gray_15">#ffffff</color>
+</resources>
index 98a9b3e3d5d6a13497ee0817c23340792751b5a4..8c61d033fc08979f814a704876ad49b181762498 100644 (file)
@@ -1,11 +1,20 @@
 <resources>
-    <style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
+    <style name="EInkTheme" parent="android:style/Theme.Material.Light.NoActionBar">
         <item name="android:fontFamily">@font/atkinson</item>
+        <item name="android:colorAccent">@color/eink_gray_02</item>
+        <item name="android:colorControlNormal">@color/eink_gray_01</item>
+        <item name="android:colorControlActivated">@color/eink_gray_00</item>
+        <item name="android:colorControlHighlight">@color/eink_gray_13</item>
+        <item name="android:textColorPrimary">@color/eink_gray_00</item>
+        <item name="android:textColorSecondary">@color/eink_gray_05</item>
+        <item name="android:navigationBarColor">@color/eink_gray_15</item>
+        <item name="android:statusBarColor">@color/eink_gray_15</item>
+        <item name="android:windowBackground">@color/eink_gray_15</item>
+        <item name="android:forceDarkAllowed">false</item>
         <item name="android:windowActionModeOverlay">true</item>
-        <item name="android:colorAccent">#303030</item>
-        <item name="android:navigationBarColor">#fafaf8</item>
-        <item name="android:statusBarColor">#fafaf8</item>
-        <item name="android:windowBackground">#fafaf8</item>
+        <item name="android:windowActivityTransitions">false</item>
+        <item name="android:windowContentTransitions">false</item>
+        <item name="android:windowAnimationStyle">@null</item>
         <item name="android:windowDisablePreview">true</item>
         <item name="android:windowLightStatusBar">true</item>
         <item name="android:windowLightNavigationBar">true</item>
index 47d5196269272dcd5a44b4d6b016dfa88db1ed7f..9041728bc3e0a445684d7eba294c4a8b74510ca8 100644 (file)
@@ -45,7 +45,7 @@ try {
     $deadline = (Get-Date).AddMinutes(3)
     do {
         $deviceLine = & $adb devices | Select-String "^emulator-[0-9]+\s+device$" | Select-Object -First 1
-        if ($deviceLine) { $serial = $deviceLine.ToString().Split("\t")[0] }
+        if ($deviceLine) { $serial = $deviceLine.ToString().Split("`t")[0] }
         if (-not $serial) { Start-Sleep -Seconds 2 }
     } while (-not $serial -and (Get-Date) -lt $deadline)
     if (-not $serial) { throw "Timed out waiting for the emulator." }
index 2aeef526b05c040693f447b890aeb8bf0db26937..ce34741ec3fdac0b69a0c2ee3fe871ab162c6c39 100644 (file)
@@ -63,7 +63,7 @@ function New-Settings(
     [double]$Contrast = 1.0, [int]$GrayPoint = 255, [string]$Dithering = "off"
 ) {
     [ordered]@{
-        fontFamily="Publisher"; fontSize=20; lineHeight=1.32; margin=32; textAlign="publisher"; theme="light"
+        fontFamily="Publisher"; fontSize=20; lineHeight=1.32; margin=32; textAlign="publisher"
         publisherStyles=$true; hyphenation=$true; mode="paged"; brightness=-1; volumeKeys=$true
         zoomMode=$ZoomMode; customZoom=$CustomZoom; scrollMode=$ScrollMode; cropScheme=$CropScheme
         cropAny=@{}; cropEven=$CropEven; cropOdd=$CropOdd