Fix crash when changing seasons & improve error page (#94)

Related to #64 

If an error occurred while querying for a season's episodes, the app
would crash instead of display the error message.

This PR fixes the app crash, but not the underlying query issue. It also
changes the error page to show a button for sending logs to the server
immediately.
This commit is contained in:
damontecres 2025-10-28 17:04:15 -04:00 committed by GitHub
parent 50b0b0a508
commit 3ab9eafc7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 88 additions and 42 deletions

View file

@ -1,14 +1,39 @@
package com.github.damontecres.wholphin.ui.components package com.github.damontecres.wholphin.ui.components
import android.content.Context
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.tv.material3.Button
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo
import javax.inject.Inject
@HiltViewModel
class ErrorViewModel
@Inject
constructor(
@param:ApplicationContext private val context: Context,
private val api: ApiClient,
private val clientInfo: ClientInfo,
private val deviceInfo: DeviceInfo,
) : ViewModel() {
fun sendLogs() {
sendAppLogs(context, api, clientInfo, deviceInfo)
}
}
/** /**
* Displays an error message and/or exception * Displays an error message and/or exception
@ -18,40 +43,48 @@ fun ErrorMessage(
message: String?, message: String?,
exception: Throwable?, exception: Throwable?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: ErrorViewModel = hiltViewModel(),
) { ) {
LazyColumn( Column(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier, modifier = modifier.padding(16.dp),
) { ) {
Text(
text = "An error occurred! Press the button to send logs to your server.",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.titleMedium,
)
Button(
onClick = {
viewModel.sendLogs()
},
) {
Text(
text = "Send Logs",
)
}
message?.let { message?.let {
item { Text(
Text( text = it,
text = it, color = MaterialTheme.colorScheme.error,
color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.titleLarge,
style = MaterialTheme.typography.titleLarge, )
)
}
} }
exception?.localizedMessage?.let { exception?.localizedMessage?.let {
item { Text(
Text( text = it,
text = it, color = MaterialTheme.colorScheme.error,
color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.titleMedium, )
)
}
} }
var cause = exception?.cause var cause = exception?.cause
while (cause != null) { while (cause != null) {
cause.localizedMessage?.let { cause.localizedMessage?.let {
item { Text(
Text( text = "Caused by: $it",
text = "Caused by: $it", color = MaterialTheme.colorScheme.error,
color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.titleSmall,
style = MaterialTheme.typography.titleSmall, )
)
}
} }
cause = cause.cause cause = cause.cause
} }

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.detail package com.github.damontecres.wholphin.ui.detail
import android.content.Context
import android.util.Log import android.util.Log
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
@ -34,11 +35,16 @@ import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.ItemPlayback import com.github.damontecres.wholphin.data.model.ItemPlayback
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.clientLogApi
import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import javax.inject.Inject import javax.inject.Inject
@ -110,6 +116,26 @@ class DebugViewModel
} }
return logLines return logLines
} }
fun ViewModel.sendAppLogs(
context: Context,
api: ApiClient,
clientInfo: ClientInfo?,
deviceInfo: DeviceInfo?,
) {
viewModelScope.launchIO(ExceptionHandler(true)) {
val logcat = getLogCatLines().joinToString("\n") { it.text }
val body =
"""
Send App Logs
clientInfo=$clientInfo
deviceInfo=$deviceInfo
""".trimIndent() + logcat
val response by api.clientLogApi.logFile(body)
showToast(context, "Sent! Filename=${response.fileName}")
}
}
} }
} }

View file

@ -215,7 +215,7 @@ class SeriesViewModel
) )
val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope) val pager = ApiRequestPager(api, request, GetEpisodesRequestHandler, viewModelScope)
pager.init() pager.init()
Timber.Forest.v("Loaded ${pager.size} episodes for season $season") Timber.v("Loaded ${pager.size} episodes for season $season")
return EpisodeList.Success(convertPager(pager)) return EpisodeList.Success(convertPager(pager))
} }

View file

@ -12,18 +12,16 @@ import com.github.damontecres.wholphin.data.isPinned
import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem import com.github.damontecres.wholphin.data.model.NavDrawerPinnedItem
import com.github.damontecres.wholphin.data.model.NavPinType import com.github.damontecres.wholphin.data.model.NavPinType
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.detail.DebugViewModel import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
import com.github.damontecres.wholphin.ui.nav.NavigationManager import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
import com.github.damontecres.wholphin.util.RememberTabManager import com.github.damontecres.wholphin.util.RememberTabManager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.clientLogApi
import org.jellyfin.sdk.model.ClientInfo import org.jellyfin.sdk.model.ClientInfo
import org.jellyfin.sdk.model.DeviceInfo import org.jellyfin.sdk.model.DeviceInfo
import javax.inject.Inject import javax.inject.Inject
@ -91,17 +89,6 @@ class PreferencesViewModel
} }
fun sendAppLogs() { fun sendAppLogs() {
viewModelScope.launchIO(ExceptionHandler(true)) { sendAppLogs(context, api, clientInfo, deviceInfo)
val logcat = DebugViewModel.getLogCatLines().joinToString("\n") { it.text }
val body =
"""
Send App Logs
$clientInfo
$deviceInfo
""".trimIndent() + logcat
val response by api.clientLogApi.logFile(body)
showToast(context, "Sent! Filename=${response.fileName}")
}
} }
} }