- ui/Failure.kt: Throwable.shownAs(fallback) and FALLBACK_ERROR. The
core's own message, else a fallback, which Share, Tags and Sync each
defined privately and Board and Update wrote inline.
- The five view-model factories use lifecycle's viewModelFactory { initializer }
instead of an unchecked-cast object each.
- BoardViewModel.beginSitting: the editingSession bump the four editor
openings each spelled out.
The fallback line stays an English constant, as it was: view models hold no
Context to read strings.xml. Formatted with ktlint in the CI image.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.fabledsword.inkwell.core.Inkwell
|
||||
import com.fabledsword.inkwell.core.Label
|
||||
import com.fabledsword.inkwell.core.Note
|
||||
@@ -178,7 +180,7 @@ class BoardViewModel(
|
||||
// Broad by intent: the board must render something for any
|
||||
// failure, and the core reports problems as one error type
|
||||
// carrying a message meant to be shown.
|
||||
state.copy(loading = false, error = e.message ?: FALLBACK_ERROR)
|
||||
state.copy(loading = false, error = e.shownAs())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,7 +270,7 @@ class BoardViewModel(
|
||||
val hits = withContext(Dispatchers.IO) { load(destination, filters, text) }
|
||||
state.copy(notes = hits, loading = false, error = null)
|
||||
} catch (e: Exception) {
|
||||
state.copy(loading = false, error = e.message ?: FALLBACK_ERROR)
|
||||
state.copy(loading = false, error = e.shownAs())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,11 +287,20 @@ class BoardViewModel(
|
||||
fun openNoteById(id: String) {
|
||||
viewModelScope.launch {
|
||||
runCatching { withContext(Dispatchers.IO) { core.getNote(id) } }
|
||||
.onSuccess { state = state.copy(editing = it, editingSession = state.editingSession + 1) }
|
||||
.onSuccess { beginSitting(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun openNote(note: Note) {
|
||||
beginSitting(note)
|
||||
}
|
||||
|
||||
/**
|
||||
* A new sitting in the editor, on [note]: bumps [BoardState.editingSession] so
|
||||
* the editor's field is re-keyed onto what it now shows. Saving within a
|
||||
* sitting never comes through here.
|
||||
*/
|
||||
private fun beginSitting(note: Note? = state.editing) {
|
||||
state = state.copy(editing = note, editingSession = state.editingSession + 1)
|
||||
}
|
||||
|
||||
@@ -307,7 +318,7 @@ class BoardViewModel(
|
||||
*/
|
||||
fun compose() {
|
||||
draftDismissed = false
|
||||
state = state.copy(editing = blankDraft(), editingSession = state.editingSession + 1)
|
||||
beginSitting(blankDraft())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,7 +346,7 @@ class BoardViewModel(
|
||||
// Draft` deliberately does not bump this — it is written for the autosave
|
||||
// case, where re-keying mid-typing would be the bug.
|
||||
draftDismissed = false
|
||||
state = state.copy(editingSession = state.editingSession + 1)
|
||||
beginSitting()
|
||||
// A shared photo arrives with no words, and the note it makes is still a
|
||||
// note: the picture is its content.
|
||||
createFromDraft(content, allowEmpty = files.isNotEmpty()) { created ->
|
||||
@@ -423,7 +434,7 @@ class BoardViewModel(
|
||||
error = null,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
state.copy(saving = false, error = e.message ?: FALLBACK_ERROR)
|
||||
state.copy(saving = false, error = e.shownAs())
|
||||
}
|
||||
if (!draftDismissed) state.editing?.let(then)
|
||||
}
|
||||
@@ -607,7 +618,7 @@ class BoardViewModel(
|
||||
// Broad by intent, as elsewhere: the core reports every failure
|
||||
// as one error type carrying a message meant to be shown, and a
|
||||
// half-applied edit must still leave a usable screen.
|
||||
state.copy(saving = false, error = e.message ?: FALLBACK_ERROR)
|
||||
state.copy(saving = false, error = e.shownAs())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,7 +656,6 @@ class BoardViewModel(
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FALLBACK_ERROR = "Something went wrong."
|
||||
private const val SEARCH_DEBOUNCE_MS = 180L
|
||||
|
||||
// The core's board vocabulary. "archived", not "archive" — it matches on
|
||||
@@ -659,11 +669,7 @@ class BoardViewModel(
|
||||
readFile: (Uri) -> PickedFile,
|
||||
onRemindersChanged: () -> Unit,
|
||||
): ViewModelProvider.Factory =
|
||||
object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
BoardViewModel(core, onRemindersChanged, readFile) as T
|
||||
}
|
||||
viewModelFactory { initializer { BoardViewModel(core, onRemindersChanged, readFile) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.fabledsword.inkwell.ui
|
||||
|
||||
/** What a failure says when the core gave it no message of its own. */
|
||||
internal const val FALLBACK_ERROR = "Something went wrong."
|
||||
|
||||
/**
|
||||
* The message to show for a failure.
|
||||
*
|
||||
* The core writes its errors for people to read ("notes.example.com responded, but
|
||||
* not with Inkwell's configuration"), so they are shown as they are rather than
|
||||
* replaced with a generic line that would throw away the only useful part.
|
||||
* [fallback] is for the failure that carries no message at all.
|
||||
*/
|
||||
internal fun Throwable.shownAs(fallback: String = FALLBACK_ERROR): String = message ?: fallback
|
||||
@@ -6,6 +6,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.fabledsword.inkwell.core.CoreException
|
||||
import com.fabledsword.inkwell.core.Inkwell
|
||||
import com.fabledsword.inkwell.core.Member
|
||||
@@ -99,7 +101,7 @@ class ShareViewModel(
|
||||
)
|
||||
}
|
||||
} catch (e: CoreException) {
|
||||
state.copy(loading = false, loadError = e.describeShareFailure())
|
||||
state.copy(loading = false, loadError = e.shownAs())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -126,7 +128,7 @@ class ShareViewModel(
|
||||
onStoreChanged()
|
||||
state.copy(busy = false, shares = shares)
|
||||
} catch (e: CoreException) {
|
||||
state.copy(busy = false, error = e.describeShareFailure())
|
||||
state.copy(busy = false, error = e.shownAs())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,13 +137,6 @@ class ShareViewModel(
|
||||
fun factory(
|
||||
core: Inkwell,
|
||||
onStoreChanged: () -> Unit,
|
||||
): ViewModelProvider.Factory =
|
||||
object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = ShareViewModel(core, onStoreChanged) as T
|
||||
}
|
||||
): ViewModelProvider.Factory = viewModelFactory { initializer { ShareViewModel(core, onStoreChanged) } }
|
||||
}
|
||||
}
|
||||
|
||||
/** The core's message is written to be shown ("The server doesn't have this note yet…"). */
|
||||
private fun Throwable.describeShareFailure(): String = message ?: "Something went wrong."
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.fabledsword.inkwell.core.Compatibility
|
||||
import com.fabledsword.inkwell.core.Inkwell
|
||||
import com.fabledsword.inkwell.core.ProbeResult
|
||||
@@ -143,7 +145,7 @@ class SyncViewModel(
|
||||
// A store that won't answer is a real fault, but the screen
|
||||
// still has to render — showing the unlinked state is honest,
|
||||
// since without a readable link there is effectively none.
|
||||
state.copy(loading = false, status = null, syncError = e.describe())
|
||||
state.copy(loading = false, status = null, syncError = e.shownAs())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +165,7 @@ class SyncViewModel(
|
||||
try {
|
||||
state.copy(probe = core.probe(url), probing = false)
|
||||
} catch (e: Exception) {
|
||||
state.copy(probing = false, probeError = e.describe())
|
||||
state.copy(probing = false, probeError = e.shownAs())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,7 +219,7 @@ class SyncViewModel(
|
||||
status = withContext(Dispatchers.IO) { core.syncStatus() },
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
state.copy(linking = false, linkError = e.describe())
|
||||
state.copy(linking = false, linkError = e.shownAs())
|
||||
}
|
||||
if (state.linked) syncNow()
|
||||
}
|
||||
@@ -259,7 +261,7 @@ class SyncViewModel(
|
||||
} catch (e: Exception) {
|
||||
state.copy(
|
||||
syncing = false,
|
||||
syncError = if (announce) e.describe() else state.syncError,
|
||||
syncError = if (announce) e.shownAs() else state.syncError,
|
||||
)
|
||||
}
|
||||
// Only when something actually arrived: a no-op sync must not make the
|
||||
@@ -293,7 +295,7 @@ class SyncViewModel(
|
||||
pending = false,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
state.copy(syncing = false, syncError = e.describe())
|
||||
state.copy(syncing = false, syncError = e.shownAs())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,11 +320,7 @@ class SyncViewModel(
|
||||
fun factory(
|
||||
core: Inkwell,
|
||||
onStoreChanged: () -> Unit,
|
||||
): ViewModelProvider.Factory =
|
||||
object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = SyncViewModel(core, onStoreChanged) as T
|
||||
}
|
||||
): ViewModelProvider.Factory = viewModelFactory { initializer { SyncViewModel(core, onStoreChanged) } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,12 +337,3 @@ private fun SyncOutcome.changedTheStore(): Boolean =
|
||||
pull.labelsApplied > 0uL ||
|
||||
pull.labelsDeleted > 0uL ||
|
||||
pull.blobsDownloaded > 0uL
|
||||
|
||||
/**
|
||||
* The message to show for a failure.
|
||||
*
|
||||
* The core writes these for people to read — "notes.example.com responded, but not
|
||||
* with Inkwell's configuration" — so they are shown as-is rather than
|
||||
* replaced with a generic string that would throw away the only useful part.
|
||||
*/
|
||||
private fun Exception.describe(): String = message ?: "Something went wrong."
|
||||
|
||||
@@ -6,6 +6,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.fabledsword.inkwell.core.Inkwell
|
||||
import com.fabledsword.inkwell.core.Label
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -86,7 +88,7 @@ class TagsViewModel(
|
||||
// the only thing on the display, and an empty list here
|
||||
// would read as "you have no tags" rather than "I couldn't
|
||||
// look".
|
||||
onFailure = { state.copy(loading = false, error = it.describeTagFailure()) },
|
||||
onFailure = { state.copy(loading = false, error = it.shownAs()) },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -154,7 +156,7 @@ class TagsViewModel(
|
||||
state.copy(
|
||||
tags = tags,
|
||||
busy = false,
|
||||
error = failure?.describeTagFailure(),
|
||||
error = failure?.shownAs(),
|
||||
)
|
||||
// Even a FAILED write can have changed the store — a merge that threw
|
||||
// partway still moved rows — so the board is told either way.
|
||||
@@ -166,16 +168,6 @@ class TagsViewModel(
|
||||
fun factory(
|
||||
core: Inkwell,
|
||||
onStoreChanged: () -> Unit,
|
||||
): ViewModelProvider.Factory =
|
||||
object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = TagsViewModel(core, onStoreChanged) as T
|
||||
}
|
||||
): ViewModelProvider.Factory = viewModelFactory { initializer { TagsViewModel(core, onStoreChanged) } }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The core reports problems as one error type carrying a message meant to be
|
||||
* shown, so the message is used when there is one.
|
||||
*/
|
||||
private fun Throwable.describeTagFailure(): String = message ?: "Something went wrong."
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.fabledsword.inkwell.AppUpdate
|
||||
import com.fabledsword.inkwell.UpdateOutcome
|
||||
import com.fabledsword.inkwell.core.ClientUpdate
|
||||
@@ -143,7 +145,7 @@ class UpdateViewModel(
|
||||
// Broad by intent, as everywhere the core is called: it reports
|
||||
// every failure as one error type carrying a message written to
|
||||
// be read, and a failed check must not take the screen down.
|
||||
state.copy(checking = false, error = e.message ?: FALLBACK)
|
||||
state.copy(checking = false, error = e.shownAs(FALLBACK))
|
||||
}
|
||||
// Fetched before anything is said, so the banner is a one-tap install
|
||||
// rather than the start of a wait. Off wifi this simply does not happen
|
||||
@@ -162,7 +164,7 @@ class UpdateViewModel(
|
||||
core.downloadClientUpdate(AppUpdate.downloadTarget(context).absolutePath)
|
||||
state.copy(downloading = false, ready = true)
|
||||
} catch (e: Exception) {
|
||||
state.copy(downloading = false, error = e.message ?: FALLBACK_DOWNLOAD)
|
||||
state.copy(downloading = false, error = e.shownAs(FALLBACK_DOWNLOAD))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +192,7 @@ class UpdateViewModel(
|
||||
// Off the main thread: this streams ~55 MiB into the session.
|
||||
withContext(Dispatchers.IO) { AppUpdate.install(context, target) }
|
||||
} catch (e: Exception) {
|
||||
e.message ?: FALLBACK
|
||||
e.shownAs(FALLBACK)
|
||||
}
|
||||
// `working` stays TRUE on success: the install is still in flight, and
|
||||
// on a silent update this process is about to be replaced. Clearing it
|
||||
@@ -220,11 +222,7 @@ class UpdateViewModel(
|
||||
core: Inkwell,
|
||||
context: Context,
|
||||
): ViewModelProvider.Factory =
|
||||
object : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T =
|
||||
UpdateViewModel(core, context.applicationContext) as T
|
||||
}
|
||||
viewModelFactory { initializer { UpdateViewModel(core, context.applicationContext) } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user