Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
## LightBooks v1.19 — Your book first, colour on black, and the mode you left in

**The shelf puts what you're reading first.** Opening a book moves it (or its series stack) to the
front. Long-press a book for **PIN TO TOP** to keep it there no matter what else you open.

**Three books per row.** Settings → Appearance → "Books per row". Two is still the default. Three
fits more of the shelf on the LP3's short screen at the cost of smaller covers.

**Colour accents in the Light Phone theme.** Settings → Appearance → "Colour accents", shown when the
theme is Light Phone. The surfaces stay true black and the ORP letter and chapter titles keep the
colours you picked, for phones that show colour (Luma, for one). Want grey? Set saturation to 0. On a
stock LP3 with the `WRITE_SECURE_SETTINGS` grant, the reader lifts the system greyscale so the colour
is visible. Without the grant it stays grey, same as before.

**Open in last mode.** Settings → Opening a book. Off by default. When it's on, a book opens in
whichever reader you last switched to, FastRead or the page view. Back from FastRead then leaves the
book instead of stopping at the pages first.

Thanks to the Discord feedback that asked for all four.

---

## LightBooks v1.18 — Volume down turns forward

**The volume keys turn the other way now.** Down is forward, up is back. v1.17 matched them to the
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/lightfastread/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import com.lightfastread.hw.VolumeBus
import com.lightfastread.hw.volumePageStep
import com.lightfastread.calibre.ProgressSync
import com.lightfastread.data.BookKind
import com.lightfastread.data.BookRepository
import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.remember
import com.lightfastread.ui.comic.ComicReader
import android.net.Uri
import android.content.Intent
Expand Down Expand Up @@ -210,13 +213,17 @@ class MainActivity : ComponentActivity() {
@androidx.compose.runtime.Composable
private fun AppNav() {
val navController = rememberNavController()
val appContext = LocalContext.current.applicationContext
// Opening a book stamps it, so the shelf puts what you are reading first.
val bookRepo = remember { BookRepository.get(appContext) }
NavHost(navController = navController, startDestination = "home") {
composable("home") {
HomeScreen(
// Which reader opens is a property of the book, decided here rather than inside
// ReaderScreen: a comic has no words to load, so the text reader should never be
// built for one at all.
onOpenBook = { book ->
bookRepo.markOpened(book.id)
val route = if (book.kind == BookKind.Comic) "comic" else "reader"
navController.navigate("$route/${book.id}")
},
Expand All @@ -238,6 +245,7 @@ private fun AppNav() {
SeriesShelf(
seriesKey = Uri.decode(backStackEntry.arguments?.getString("seriesKey").orEmpty()),
onOpenBook = { book ->
bookRepo.markOpened(book.id)
val route = if (book.kind == BookKind.Comic) "comic" else "reader"
navController.navigate("$route/${book.id}")
},
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/com/lightfastread/data/Book.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ data class Book(
val totalWords: Int,
val currentWordIndex: Int = 0,
val addedAtMs: Long = System.currentTimeMillis(),
/** When the book was last opened; 0 if never. The shelf sorts on this so what you're reading is first. */
val lastOpenedAtMs: Long = 0L,
/** Pinned books sit at the front of the shelf ahead of everything else, newest-read first. */
val pinned: Boolean = false,
val chapters: List<Chapter> = emptyList(),
val bookmarks: List<Int> = emptyList(),
/**
Expand Down
15 changes: 15 additions & 0 deletions app/src/main/java/com/lightfastread/data/BookRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,21 @@ class BookRepository private constructor(private val appContext: Context) {

fun getBook(id: String): Book? = books.firstOrNull { it.id == id }

/** Stamp a book as just opened, which moves it (or its stack) to the front of the shelf. */
fun markOpened(id: String) {
val idx = books.indexOfFirst { it.id == id }
if (idx < 0) return
books[idx] = books[idx].copy(lastOpenedAtMs = System.currentTimeMillis())
persist()
}

fun setPinned(id: String, pinned: Boolean) {
val idx = books.indexOfFirst { it.id == id }
if (idx < 0 || books[idx].pinned == pinned) return
books[idx] = books[idx].copy(pinned = pinned)
persist()
}

fun updateProgress(id: String, wordIndex: Int) {
val idx = books.indexOfFirst { it.id == id }
if (idx < 0) return
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/com/lightfastread/data/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,22 @@ data class Settings(
* phone for someone who did not, so it is opt-in. Volume down goes forward, as on any e-reader.
*/
val volumeKeysTurnPages: Boolean = false,
/**
* Keep the title and ORP accents in the colours picked, even in the Light Phone theme.
*
* Off by default: on a stock LP3 the panel is held greyscale, and the default red ORP arrives
* as a grey dark enough to vanish, which is why the theme flattens accents with a lifted floor.
* On a phone already running in colour (Luma, or the adb grant below) that flattening throws
* away exactly what was asked for — so this keeps the true-black surfaces and the hues together.
* Want grey anyway? Set saturation to 0.
*/
val lightPhoneColorAccents: Boolean = false,
/** Covers per shelf row: 2 (the default, art worth looking at) or 3 (more of the shelf on screen). */
val shelfColumns: Int = 2,
/**
* Open a book in whichever reader was last used — FastRead or the page view — instead of
* always the page view. Off by default; [lastModeFastRead] is what it remembers.
*/
val openInLastMode: Boolean = false,
val lastModeFastRead: Boolean = false,
)
12 changes: 12 additions & 0 deletions app/src/main/java/com/lightfastread/data/Shelf.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ object Shelf {
return out
}

/**
* The order the shelf is drawn in: pinned books first, then by whichever is later — when the
* book was last opened or when it was added. Feed the result to [entries], which keeps the
* order it is given, so a stack sits wherever its most recent volume does.
*
* Stable, so books with equal timestamps keep the order they were stored in.
*/
fun ordered(books: List<Book>): List<Book> = books.sortedWith(
compareByDescending<Book> { it.pinned }
.thenByDescending { maxOf(it.lastOpenedAtMs, it.addedAtMs) }
)

/** The volumes of one stack, for the series screen. Empty when nothing matches any more. */
fun volumesOf(books: List<Book>, seriesKey: String): List<Book> =
(entries(books).firstOrNull {
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/java/com/lightfastread/ui/home/BookActions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ fun BookActions(
onFlipDirection: () -> Unit,
onDelete: () -> Unit,
onDismiss: () -> Unit,
/** Null where pinning means nothing (inside a series); the shelf passes it. */
onTogglePin: (() -> Unit)? = null,
) {
val colors = LightThemeTokens.colors
var confirmingDelete by remember { mutableStateOf(false) }
Expand Down Expand Up @@ -84,13 +86,19 @@ fun BookActions(
onClick = onFlipDirection,
)
ActionRow(text = "FIX NAME", onClick = onRename)
if (onTogglePin != null) {
ActionRow(text = if (book.pinned) "UNPIN" else "PIN TO TOP", onClick = onTogglePin)
}
ActionRow(text = "DELETE", onClick = { confirmingDelete = true })
ActionRow(text = "CANCEL", lighten = true, onClick = onDismiss)
} else {
ActionRow(text = "FIND COVER", onClick = onFindCover)
// Directly under FIND COVER, because it is the thing to try when that keeps
// failing: a lookup is only as good as the title it is given.
ActionRow(text = "FIX NAME", onClick = onRename)
if (onTogglePin != null) {
ActionRow(text = if (book.pinned) "UNPIN" else "PIN TO TOP", onClick = onTogglePin)
}
ActionRow(text = "DELETE", onClick = { confirmingDelete = true })
ActionRow(text = "CANCEL", lighten = true, onClick = onDismiss)
}
Expand Down
8 changes: 6 additions & 2 deletions app/src/main/java/com/lightfastread/ui/home/HomeScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ fun HomeScreen(

// Recomputed whenever the shelf changes, which is cheap: a parse of each title and a group-by,
// over a list that is tens of items long, not thousands.
val entries = remember(books.toList()) { Shelf.entries(books.toList()) }
val entries = remember(books.toList()) { Shelf.entries(Shelf.ordered(books.toList())) }
var importing by remember { mutableStateOf(false) }
var importStep by remember { mutableStateOf<String?>(null) }
var status by remember { mutableStateOf<String?>(null) }
Expand Down Expand Up @@ -221,7 +221,7 @@ fun HomeScreen(
}
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMNS),
columns = GridCells.Fixed(shelfColumns(settings)),
state = gridState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
Expand Down Expand Up @@ -337,6 +337,10 @@ fun HomeScreen(
"“${book.title}” now reads right to left"
}
},
onTogglePin = {
actionsFor = null
repo.setPinned(book.id, !book.pinned)
},
onDelete = {
actionsFor = null
repo.deleteBook(book)
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/com/lightfastread/ui/home/SeriesShelf.kt
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ fun SeriesShelf(
)
} else {
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMNS),
columns = GridCells.Fixed(shelfColumns(settings)),
state = gridState,
modifier = Modifier.fillMaxSize().navigationBarsPadding(),
contentPadding = PaddingValues(
Expand Down
9 changes: 7 additions & 2 deletions app/src/main/java/com/lightfastread/ui/home/ShelfCells.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.lightfastread.data.Book
import com.lightfastread.data.Settings
import com.lightfastread.data.ShelfEntry
import com.lightfastread.ui.light.LightText
import com.lightfastread.ui.light.LightTextVariant
Expand All @@ -33,8 +34,12 @@ import com.lightfastread.ui.light.lightCombinedClickable
* file — a series should look exactly like the shelf it came from, because it *is* one.
*/

/** Two to a row, as on the main shelf. Anything more and the art stops being worth showing. */
internal const val COLUMNS = 2
/**
* Covers per row. Two by default — the art is worth showing — or three, which gets a second full
* row onto the LP3's short screen at the cost of smaller covers. Clamped so a hand-edited or
* future value can't produce a shelf of slivers.
*/
internal fun shelfColumns(settings: Settings): Int = settings.shelfColumns.coerceIn(2, 3)

/** Book covers are 2:3, near enough to every trade paperback ever printed. */
internal const val COVER_ASPECT = 2f / 3f
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ fun EreaderScreen(
onProgress: (wordIndex: Int) -> Unit,
/** Hand the reader over to the RSVP screen, at the page currently showing. */
onFastRead: (wordIndex: Int) -> Unit,
/** The book laid out to zero pages; hand over to RSVP. Not a user choice, so kept separate. */
onCannotPaginate: (wordIndex: Int) -> Unit = onFastRead,
/** Leave the book entirely, at the page currently showing. */
onExit: (wordIndex: Int) -> Unit,
) {
Expand Down Expand Up @@ -396,7 +398,7 @@ fun EreaderScreen(
// Nothing to page through. Falling back to the word reader is better
// than throwing the reader out to the shelf: there is still text, it
// just could not be laid out into pages at this size.
onFastRead(anchor)
onCannotPaginate(anchor)
return@LaunchedEffect
}
PageCache.put(cacheKey, built)
Expand Down
49 changes: 43 additions & 6 deletions app/src/main/java/com/lightfastread/ui/reader/ReaderScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import com.lightfastread.data.SwipeMode
import com.lightfastread.ui.light.LightText
import com.lightfastread.ui.light.LightTextVariant
import com.lightfastread.ui.theme.LocalIsLightPhone
import com.lightfastread.ui.light.ColourEffect
import com.lightfastread.ui.theme.toLightPhoneGrey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
Expand Down Expand Up @@ -86,13 +87,21 @@ fun ReaderScreen(
// Flatten them here instead of at the draw site, so the emulator preview
// matches the device exactly and low-luma picks (the default red ORP) don't
// vanish against the dimmed context lines.
//
// Unless the user has asked for colour accents: then the surfaces stay true black and the
// hues come through as picked, for phones that show colour (Luma, or the adb grant).
val isLightPhone = LocalIsLightPhone.current
val flattenAccents = isLightPhone && !settings.lightPhoneColorAccents
val titleColor = Color(settings.titleColorArgb).let {
if (isLightPhone) it.toLightPhoneGrey() else it
if (flattenAccents) it.toLightPhoneGrey() else it
}
val orpColor = Color(settings.orpColorArgb).let {
if (isLightPhone) it.toLightPhoneGrey(floor = 0.70f) else it
if (flattenAccents) it.toLightPhoneGrey(floor = 0.70f) else it
}
// On a stock LP3 with the WRITE_SECURE_SETTINGS grant, lift the system greyscale while
// reading so the accents are actually visible. A no-op on a phone that is already in colour,
// and without the grant.
ColourEffect(enabled = isLightPhone && settings.lightPhoneColorAccents)

if (book == null) {
LaunchedEffect(Unit) { onBack() }
Expand All @@ -114,7 +123,27 @@ fun ReaderScreen(
// A book opens to its pages, not to the word reader. FastRead is a button in the page
// view's top bar, which is itself hidden until you tap the screen — so the app you get
// when you open a book is an e-reader, and RSVP is a mode you ask for.
var showEreader by rememberSaveable { mutableStateOf(true) }
//
// Unless "open in last mode" is on: then it opens in whichever the user last switched to.
var showEreader by rememberSaveable {
mutableStateOf(!(settings.openInLastMode && settings.lastModeFastRead))
}
// Only explicit switches are remembered — not the fallback to RSVP when a book can't be
// paginated, which is about that book rather than a preference.
val rememberMode: (fastRead: Boolean) -> Unit = { fastRead ->
if (settings.openInLastMode && settings.lastModeFastRead != fastRead) {
settingsRepo.update { it.copy(lastModeFastRead = fastRead) }
}
}
// With last-mode on, RSVP is the reader's home rather than a detour from the pages, so back
// leaves the book instead of stepping through the page view (which would also be a switch).
val backFromRsvp: () -> Unit = {
if (settings.openInLastMode) {
onBack()
} else {
showEreader = true
}
}
val historyBack = rememberSaveable(bookId, saver = IntListSaver) { mutableStateListOf<Int>() }
val historyForward = rememberSaveable(bookId, saver = IntListSaver) { mutableStateListOf<Int>() }
val bookmarks = bookRepo.books.firstOrNull { it.id == bookId }?.bookmarks ?: emptyList()
Expand All @@ -123,7 +152,7 @@ fun ReaderScreen(

// The system gesture agrees with the top bar's own arrow. While the page view is up it owns
// back itself (it is a Dialog, with its own window), so this only ever fires in RSVP.
BackHandler(enabled = !showEreader) { showEreader = true }
BackHandler(enabled = !showEreader) { backFromRsvp() }

val jumpTo: (Int) -> Unit = { target ->
val safe = target.coerceIn(0, (words.size - 1).coerceAtLeast(0))
Expand Down Expand Up @@ -402,7 +431,10 @@ fun ReaderScreen(
}
} != null
if (releasedInTime) {
if (!moved) showEreader = true
if (!moved) {
rememberMode(false)
showEreader = true
}
return@awaitEachGesture
}
// Still held past the tap window: fall through to
Expand Down Expand Up @@ -573,7 +605,7 @@ fun ReaderScreen(
// Back out of the word reader is back to the *pages*, not out of the
// book: the page view is the book's home screen now, and leaving from
// there is one more press.
onBack = { showEreader = true },
onBack = backFromRsvp,
onQuickSettings = { showQuickSettings = true },
)
}
Expand Down Expand Up @@ -656,6 +688,11 @@ fun ReaderScreen(
bookRepo.updateProgress(bookId, currentIndex)
},
onFastRead = { newIndex ->
rememberMode(true)
showEreader = false
jumpTo(newIndex)
},
onCannotPaginate = { newIndex ->
showEreader = false
jumpTo(newIndex)
},
Expand Down
34 changes: 34 additions & 0 deletions app/src/main/java/com/lightfastread/ui/settings/SettingsScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,15 @@ fun SettingsScreen(onBack: () -> Unit) {
underline = s.titleStyle != TitleStyle.Color,
)

SectionTitle("Opening a book")
ToggleRow(
label = "Open in last mode",
caption = "Books open in FastRead or the page view, whichever you switched to " +
"last. Back from FastRead then leaves the book.",
checked = s.openInLastMode,
onToggle = { v -> repo.update { it.copy(openInLastMode = v) } },
)

SectionTitle("Appearance")
RowLabel("Theme")
Caption(
Expand All @@ -409,6 +418,31 @@ fun SettingsScreen(onBack: () -> Unit) {
onClick = { repo.update { it.copy(themeMode = mode) } },
)
}
if (s.themeMode == ThemeMode.LightPhone) {
Spacer(Modifier.height(12f.designVerticalPxToDp()))
ToggleRow(
label = "Colour accents",
caption = "Keep true black but show the ORP and title colours as picked. " +
"For phones that display colour. Set saturation to 0 for grey.",
checked = s.lightPhoneColorAccents,
onToggle = { v -> repo.update { it.copy(lightPhoneColorAccents = v) } },
) {
if (s.lightPhoneColorAccents && !ColorMode.granted(context) &&
!ColorMode.phoneIsColour(context)
) {
Caption("This phone is greyscale. Grant the permission below, or the colours show as grey.")
}
}
}
Spacer(Modifier.height(12f.designVerticalPxToDp()))
RowLabel("Books per row")
listOf(2, 3).forEach { n ->
ChoiceRow(
label = "$n",
selected = s.shelfColumns.coerceIn(2, 3) == n,
onClick = { repo.update { it.copy(shelfColumns = n) } },
)
}
Spacer(Modifier.height(12f.designVerticalPxToDp()))
ToggleRow(
label = "Colour covers",
Expand Down
Loading
Loading