0.2.2 + 0.3.0: remote threading fix, phone-first onboarding, ten-foot settings, self-healing errors, in-app updater, group filter
Some checks failed
Build TV app / build (push) Failing after 2s
Some checks failed
Build TV app / build (push) Failing after 2s
Squashed history rewrite: earlier revisions of the docs carried private hostnames; placeholders throughout history now. - Fix phone remote threading (welcome/get_state/broadcast) — remote connects and mirrors channels, Now/Next, favorites - Phone-first onboarding (ADR-0007): one QR, server URL + IdP login on the phone (closes #11) - Ten-foot settings + Erweitert sub-screen (closes #14), TvTextField D-pad focus fix (closes #9) - EPG grid envelope parsing, Now/Next live (closes #10) - Self-healing error states (closes #13), in-app updater (closes #12) - Channel group filter chips fed by Dispatcharr groups Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package dev.castarr.tv
|
||||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -17,7 +19,7 @@ import dev.castarr.tv.playlist.Channel
|
||||
* main thread (activity callbacks, remote-control listener).
|
||||
*/
|
||||
class AppState(
|
||||
context: Context,
|
||||
private val context: Context,
|
||||
val player: PlayerController,
|
||||
val source: SourceRepository,
|
||||
val auth: DeviceAuth,
|
||||
@@ -35,6 +37,17 @@ class AppState(
|
||||
var durationMs by mutableLongStateOf(0L)
|
||||
var isLive by mutableStateOf(false)
|
||||
var favoritesOnly by mutableStateOf(false)
|
||||
var groupFilter by mutableStateOf<String?>(null)
|
||||
|
||||
/** Phone-first onboarding progress (ADR-0007). */
|
||||
var welcomePhase by mutableStateOf(WelcomePhase.WAIT_PHONE)
|
||||
var welcomeUserCode by mutableStateOf("")
|
||||
|
||||
/** Plain-language fullscreen error state (issue #13). */
|
||||
var appError by mutableStateOf(AppError.NONE)
|
||||
|
||||
/** In-app updater state (issue #12). */
|
||||
var updateAvailable by mutableStateOf<String?>(null)
|
||||
|
||||
var sourceMode by mutableStateOf(
|
||||
if (prefs.getString("source_mode", "generic") == "dispatcharr") SourceMode.DISPATCHARR
|
||||
@@ -42,13 +55,18 @@ class AppState(
|
||||
)
|
||||
private set
|
||||
|
||||
enum class Screen { LIVE, SETTINGS }
|
||||
enum class Screen { WELCOME, LIVE, SETTINGS, ADVANCED }
|
||||
enum class SourceMode { GENERIC, DISPATCHARR }
|
||||
enum class WelcomePhase { WAIT_PHONE, WAIT_URL, WAIT_LOGIN, DONE }
|
||||
enum class AppError { NONE, OFFLINE, SERVER, RELOGIN }
|
||||
|
||||
init {
|
||||
dispatcharr.outputProfile = prefs.getString("output_profile", "").orEmpty()
|
||||
if (sourceMode == SourceMode.DISPATCHARR && auth.isLoggedIn) {
|
||||
dispatcharr.refresh()
|
||||
val configured = auth.isLoggedIn || source.m3uUrl.isNotEmpty()
|
||||
if (!configured) {
|
||||
screen = Screen.WELCOME
|
||||
} else if (sourceMode == SourceMode.DISPATCHARR && auth.isLoggedIn) {
|
||||
dispatcharr.refresh { classifyRefresh(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +76,9 @@ class AppState(
|
||||
"source_mode",
|
||||
if (mode == SourceMode.DISPATCHARR) "dispatcharr" else "generic",
|
||||
).apply()
|
||||
if (mode == SourceMode.DISPATCHARR && auth.isLoggedIn) dispatcharr.refresh()
|
||||
if (mode == SourceMode.DISPATCHARR && auth.isLoggedIn) {
|
||||
dispatcharr.refresh { classifyRefresh(it) }
|
||||
}
|
||||
}
|
||||
|
||||
var outputProfile: String
|
||||
@@ -79,6 +99,44 @@ class AppState(
|
||||
SourceMode.DISPATCHARR -> dispatcharr.nowNext(channel)
|
||||
}
|
||||
|
||||
fun refreshActive() {
|
||||
when (sourceMode) {
|
||||
SourceMode.GENERIC -> source.refresh()
|
||||
SourceMode.DISPATCHARR -> dispatcharr.refresh { classifyRefresh(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/** Map a refresh failure to a family-friendly fullscreen state. */
|
||||
fun classifyRefresh(result: Result<Int>) {
|
||||
result.fold(
|
||||
onSuccess = { appError = AppError.NONE },
|
||||
onFailure = { throwable ->
|
||||
// Cached channels keep the app usable; only surface a
|
||||
// fullscreen state when there is nothing to show.
|
||||
if (activeChannels().isNotEmpty()) return
|
||||
appError = when {
|
||||
throwable.message == "not logged in" -> AppError.RELOGIN
|
||||
!isOnline() -> AppError.OFFLINE
|
||||
else -> AppError.SERVER
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun isOnline(): Boolean {
|
||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val caps = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
|
||||
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
}
|
||||
|
||||
fun startOnboarding() {
|
||||
auth.logout()
|
||||
appError = AppError.NONE
|
||||
welcomePhase = WelcomePhase.WAIT_PHONE
|
||||
welcomeUserCode = ""
|
||||
screen = Screen.WELCOME
|
||||
}
|
||||
|
||||
fun play(channel: Channel) {
|
||||
currentChannel = channel
|
||||
playerVisible = true
|
||||
|
||||
Reference in New Issue
Block a user