mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
UI Updates & Fixes (#114)
Fixes #113 - Ensures all audio and subtitle options are shown during playback - Fix home rows not being at top when going back - Show error message when background home page refresh fails Also shows more details for login failures and adds a link to the debug page (with log info) after multiple login failure attempts. Hopefully this can help diagnose #112 and #18
This commit is contained in:
parent
b1275ae210
commit
3f7c2ac730
11 changed files with 128 additions and 53 deletions
|
|
@ -227,7 +227,7 @@ dependencies {
|
|||
ksp(libs.hilt.android.compiler)
|
||||
|
||||
implementation(libs.timber)
|
||||
implementation(libs.slf4j.simple)
|
||||
implementation(libs.slf4j2.timber)
|
||||
implementation(libs.aboutlibraries.core)
|
||||
implementation(libs.aboutlibraries.compose.m3)
|
||||
implementation(libs.multiplatform.markdown.renderer)
|
||||
|
|
|
|||
|
|
@ -150,8 +150,8 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(server) {
|
||||
serverEventListener.init()
|
||||
LaunchedEffect(server, user) {
|
||||
serverEventListener.init(server, user)
|
||||
}
|
||||
ApplicationContent(
|
||||
user = user,
|
||||
|
|
|
|||
|
|
@ -66,10 +66,10 @@ class DebugViewModel
|
|||
withContext(Dispatchers.Main) {
|
||||
itemPlaybacks.value = results
|
||||
}
|
||||
val logcat = getLogCatLines()
|
||||
withContext(Dispatchers.Main) {
|
||||
this@DebugViewModel.logcat.value = logcat
|
||||
}
|
||||
}
|
||||
val logcat = getLogCatLines()
|
||||
withContext(Dispatchers.Main) {
|
||||
this@DebugViewModel.logcat.value = logcat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ class DebugViewModel
|
|||
while (count < lineCount) {
|
||||
val line = reader.readLine()
|
||||
if (line != null) {
|
||||
val level = line.split(" ").getOrNull(4)
|
||||
val level = line.split(Regex("\\s+")).getOrNull(4)
|
||||
val logLevel =
|
||||
when (level?.uppercase()) {
|
||||
"V" -> Log.VERBOSE
|
||||
|
|
|
|||
|
|
@ -234,7 +234,9 @@ class SeriesViewModel
|
|||
}.coerceAtLeast(0)
|
||||
} else {
|
||||
// Force the first page to to be fetched
|
||||
pager.getBlocking(0)
|
||||
if (pager.isNotEmpty()) {
|
||||
pager.getBlocking(0)
|
||||
}
|
||||
0
|
||||
}
|
||||
Timber.v("Loaded ${pager.size} episodes for season $seasonId, initialIndex=$initialIndex")
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.github.damontecres.wholphin.ui.main
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
|
|
@ -20,6 +21,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
|
|
@ -32,6 +34,7 @@ import androidx.compose.ui.focus.onFocusChanged
|
|||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
|
@ -62,6 +65,7 @@ import com.github.damontecres.wholphin.ui.tryRequestFocus
|
|||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
import com.github.damontecres.wholphin.util.formatDateTime
|
||||
import com.github.damontecres.wholphin.util.seasonEpisode
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.extensions.ticks
|
||||
import kotlin.time.Duration
|
||||
|
|
@ -78,6 +82,7 @@ fun HomePage(
|
|||
modifier: Modifier = Modifier,
|
||||
viewModel: HomeViewModel = hiltViewModel(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var firstLoad by rememberSaveable { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init(preferences).join()
|
||||
|
|
@ -85,6 +90,20 @@ fun HomePage(
|
|||
}
|
||||
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
|
||||
val homeRows by viewModel.homeRows.observeAsState(listOf())
|
||||
LaunchedEffect(loading) {
|
||||
val state = loading
|
||||
if (!firstLoad && state is LoadingState.Error) {
|
||||
// After the first load, refreshes occur in the background and an ErrorMessage won't show
|
||||
// So send a Toast on errors instead
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
"Home refresh error: ${state.localizedMessage}",
|
||||
Toast.LENGTH_LONG,
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
when (val state = if (firstLoad) loading else LoadingState.Success) {
|
||||
is LoadingState.Error -> ErrorMessage(state)
|
||||
|
||||
|
|
@ -148,6 +167,7 @@ fun HomePageContent(
|
|||
onFocusPosition: ((RowColumn) -> Unit)? = null,
|
||||
loadingState: LoadingState? = null,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
var position by rememberSaveable(stateSaver = RowColumnSaver) {
|
||||
mutableStateOf(RowColumn(0, 0))
|
||||
}
|
||||
|
|
@ -158,6 +178,9 @@ fun HomePageContent(
|
|||
val positionFocusRequester = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
positionFocusRequester.tryRequestFocus()
|
||||
// Hacky, but mostly works
|
||||
delay(50)
|
||||
listState.animateScrollToItem(position.row)
|
||||
}
|
||||
LaunchedEffect(position) {
|
||||
listState.animateScrollToItem(position.row)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import androidx.compose.foundation.layout.padding
|
|||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
|
|
@ -638,7 +640,7 @@ private fun BottomDialog(
|
|||
.padding(8.dp)
|
||||
.background(Color.DarkGray, shape = RoundedCornerShape(16.dp)),
|
||||
) {
|
||||
Column(
|
||||
LazyColumn(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -647,7 +649,7 @@ private fun BottomDialog(
|
|||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
choices.forEachIndexed { index, choice ->
|
||||
itemsIndexed(choices) { index, choice ->
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val focused = interactionSource.collectIsFocusedAsState().value
|
||||
val color =
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
|
|||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.tv.material3.Button
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
|
|
@ -61,6 +62,7 @@ fun SwitchUserContent(
|
|||
var showAddUser by remember { mutableStateOf(false) }
|
||||
|
||||
val userState by viewModel.switchUserState.observeAsState(LoadingState.Pending)
|
||||
val loginAttempts by viewModel.loginAttempts.observeAsState(0)
|
||||
LaunchedEffect(userState) {
|
||||
if (!showAddUser) {
|
||||
when (val s = userState) {
|
||||
|
|
@ -129,6 +131,7 @@ fun SwitchUserContent(
|
|||
var useQuickConnect by remember { mutableStateOf(quickConnectEnabled) }
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.clearSwitchUserState()
|
||||
viewModel.resetAttempts()
|
||||
if (useQuickConnect) {
|
||||
viewModel.initiateQuickConnect(server)
|
||||
}
|
||||
|
|
@ -138,6 +141,10 @@ fun SwitchUserContent(
|
|||
viewModel.cancelQuickConnect()
|
||||
showAddUser = false
|
||||
},
|
||||
properties =
|
||||
DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
|
@ -145,7 +152,7 @@ fun SwitchUserContent(
|
|||
Modifier
|
||||
.focusGroup()
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(),
|
||||
.fillMaxWidth(.66f),
|
||||
) {
|
||||
if (useQuickConnect) {
|
||||
if (quickConnect == null && userState !is LoadingState.Error) {
|
||||
|
|
@ -206,7 +213,7 @@ fun SwitchUserContent(
|
|||
UserStateError(userState)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.focusGroup(),
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
Text(
|
||||
text = "Username",
|
||||
|
|
@ -231,7 +238,7 @@ fun SwitchUserContent(
|
|||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
Text(
|
||||
text = "Password",
|
||||
|
|
@ -265,6 +272,20 @@ fun SwitchUserContent(
|
|||
Text("Login")
|
||||
}
|
||||
}
|
||||
if (loginAttempts > 2) {
|
||||
Text(
|
||||
text = "Trouble logging in?",
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.navigationManager.navigateTo(Destination.Debug)
|
||||
},
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
) {
|
||||
Text("Show debug info")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -288,11 +309,19 @@ private fun UserStateError(
|
|||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
s.exception?.localizedMessage?.let {
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
if (s.exception != null) {
|
||||
s.exception.localizedMessage?.let {
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
s.exception.cause?.localizedMessage?.let {
|
||||
Text(
|
||||
text = "Cause: $it",
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ class SwitchUserViewModel
|
|||
val addServerState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val switchUserState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
|
||||
val loginAttempts = MutableLiveData(0)
|
||||
|
||||
fun clearAddServerState() {
|
||||
addServerState.value = LoadingState.Pending
|
||||
}
|
||||
|
|
@ -63,6 +65,10 @@ class SwitchUserViewModel
|
|||
switchUserState.value = LoadingState.Pending
|
||||
}
|
||||
|
||||
fun resetAttempts() {
|
||||
loginAttempts.value = 0
|
||||
}
|
||||
|
||||
init {
|
||||
init()
|
||||
}
|
||||
|
|
@ -338,6 +344,7 @@ class SwitchUserViewModel
|
|||
msg: String? = null,
|
||||
ex: Exception? = null,
|
||||
) = withContext(Dispatchers.Main) {
|
||||
loginAttempts.value = (loginAttempts.value ?: 0) + 1
|
||||
switchUserState.value = LoadingState.Error(msg, ex)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,5 +15,8 @@ sealed interface LoadingState {
|
|||
val exception: Throwable? = null,
|
||||
) : LoadingState {
|
||||
constructor(exception: Throwable) : this(null, exception)
|
||||
|
||||
val localizedMessage: String =
|
||||
listOfNotNull(message, exception?.localizedMessage).joinToString(" - ")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package com.github.damontecres.wholphin.util
|
|||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinServer
|
||||
import com.github.damontecres.wholphin.data.model.JellyfinUser
|
||||
import com.github.damontecres.wholphin.hilt.IoCoroutineScope
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.jellyfin.sdk.api.client.ApiClient
|
||||
|
|
@ -27,43 +30,49 @@ class ServerEventListener
|
|||
private val api: ApiClient,
|
||||
@param:IoCoroutineScope private val scope: CoroutineScope,
|
||||
) {
|
||||
init {
|
||||
setupListeners()
|
||||
}
|
||||
private var listenJob: Job? = null
|
||||
|
||||
fun init() {
|
||||
scope.launchIO {
|
||||
api.sessionApi.postCapabilities(
|
||||
playableMediaTypes = listOf(MediaType.VIDEO),
|
||||
supportedCommands =
|
||||
listOf(
|
||||
GeneralCommandType.DISPLAY_MESSAGE,
|
||||
GeneralCommandType.SEND_STRING,
|
||||
),
|
||||
supportsMediaControl = true,
|
||||
)
|
||||
fun init(
|
||||
server: JellyfinServer?,
|
||||
user: JellyfinUser?,
|
||||
) {
|
||||
if (server != null && user != null && api.baseUrl != null && api.accessToken != null) {
|
||||
scope.launchIO {
|
||||
api.sessionApi.postCapabilities(
|
||||
playableMediaTypes = listOf(MediaType.VIDEO),
|
||||
supportedCommands =
|
||||
listOf(
|
||||
GeneralCommandType.DISPLAY_MESSAGE,
|
||||
GeneralCommandType.SEND_STRING,
|
||||
),
|
||||
supportsMediaControl = true,
|
||||
)
|
||||
setupListeners()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setupListeners() {
|
||||
Timber.v("Subscribing to WebSocket")
|
||||
api.webSocket
|
||||
.subscribe<GeneralCommandMessage>()
|
||||
.onEach { message ->
|
||||
if (message.data?.name in
|
||||
setOf(
|
||||
GeneralCommandType.DISPLAY_MESSAGE,
|
||||
GeneralCommandType.SEND_STRING,
|
||||
)
|
||||
) {
|
||||
val header = message.data?.arguments["Header"]
|
||||
val text =
|
||||
message.data?.arguments["Text"] ?: message.data?.arguments["String"]
|
||||
val toast =
|
||||
listOfNotNull(header, text)
|
||||
.joinToString("\n")
|
||||
showToast(context, toast, Toast.LENGTH_LONG)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
listenJob?.cancel()
|
||||
listenJob =
|
||||
api.webSocket
|
||||
.subscribe<GeneralCommandMessage>()
|
||||
.onEach { message ->
|
||||
if (message.data?.name in
|
||||
setOf(
|
||||
GeneralCommandType.DISPLAY_MESSAGE,
|
||||
GeneralCommandType.SEND_STRING,
|
||||
)
|
||||
) {
|
||||
val header = message.data?.arguments["Header"]
|
||||
val text =
|
||||
message.data?.arguments["Text"] ?: message.data?.arguments["String"]
|
||||
val toast =
|
||||
listOfNotNull(header, text)
|
||||
.joinToString("\n")
|
||||
showToast(context, toast, Toast.LENGTH_LONG)
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue