# 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.
- 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
</intent>
</queries>
<application
- android:theme="@style/AppTheme"
+ android:theme="@style/EInkTheme"
android:label="Alexandria"
android:icon="@drawable/ic_launcher"
android:roundIcon="@drawable/ic_launcher"
--- /dev/null
+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)
+ }
+}
@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; }
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; }
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}">
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);
}
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);}
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
private var sort = repository.sortOrder()
init {
- setBackgroundColor(Color.rgb(250, 250, 248))
+ setBackgroundColor(EInkPalette.PAPER)
build()
refresh()
}
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 {
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() })
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)) }
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() }
text = label
textSize = 23f
gravity = Gravity.CENTER
- setTextColor(Color.rgb(25, 25, 25))
+ setTextColor(EInkPalette.INK)
contentDescription = description
isClickable = true
isFocusable = true
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) {
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))
}
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
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()
text = message
textSize = 20f
gravity = Gravity.CENTER
- setTextColor(Color.rgb(35, 35, 35))
- setBackgroundColor(Color.WHITE)
+ setTextColor(EInkPalette.INK)
+ setBackgroundColor(EInkPalette.PAPER)
contentDescription = message
})
}
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",
put("lineHeight", lineHeight.toDouble())
put("margin", margin)
put("textAlign", textAlign)
- put("theme", theme)
put("publisherStyles", publisherStyles)
put("hyphenation", hyphenation)
put("mode", mode)
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",
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
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() {
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
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)))
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 }
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"
}
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)
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"
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)))
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))
})
})
}
- 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)) }
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))
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) })
}
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")
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 {
webView.captureLocation { location ->
settings = value
repository.saveSettings(publication.book.id, settings)
- applyNativeTheme()
+ applyEInkAppearance()
webView.configurePageTools(publication.fixedLayout, settings)
webView.applySettings(settings, location)
}
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
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 {
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(
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)
}
}
import android.annotation.SuppressLint
import android.content.Context
-import android.graphics.Color
import android.os.Handler
import android.os.Looper
import android.util.Log
})
init {
- setBackgroundColor(Color.WHITE)
+ setBackgroundColor(EInkPalette.PAPER)
isVerticalScrollBarEnabled = false
isHorizontalScrollBarEnabled = false
overScrollMode = OVER_SCROLL_NEVER
<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>
--- /dev/null
+<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>
<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>
$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." }
[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