diff --git a/README.md b/README.md
index e22801a..4f88b40 100644
--- a/README.md
+++ b/README.md
@@ -330,6 +330,23 @@ with a packet capture.
## TV settings screen
+### First run
+
+The first launch after installing opens a three-step setup instead of the settings screen:
+
+1. **Welcome** explains what the app does and shows the address this TV listens on.
+2. **Permission** walks through the overlay permission. When the TV offers a system page for it, a
+ "Try system settings" button opens it; otherwise the adb command is shown on screen. The step
+ re-checks every few seconds and moves on by itself once the permission is granted. It can be
+ skipped, but every following screen then states that TibeePost is unusable until the permission
+ is granted, and the settings screen keeps saying so on every launch.
+3. **Test** sends a real notification through the local server so you can see a card appear before
+ any remote client is involved.
+
+The setup runs once. A "Run setup again" row at the bottom of the settings screen brings it back.
+
+### Settings
+
Open TibeePost from the launcher. Everything is reachable with the D-pad; there are no text
fields.
@@ -342,6 +359,7 @@ fields.
- **Default sound** None or chime, with a Preview button.
- **Default card width, duration, dim, position, background, text color, accent** Press left
and right on a row to step the value. These become the defaults for fields a `POST` omits.
+- **Run setup again** Replays the first-run flow.
## Web client
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/MainActivity.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/MainActivity.kt
index 53f5f4b..230ebed 100644
--- a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/MainActivity.kt
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/MainActivity.kt
@@ -3,16 +3,32 @@ package com.zorenkonte.tibeepost
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
import com.zorenkonte.tibeepost.service.ServiceStarter
+import com.zorenkonte.tibeepost.settings.Settings
import com.zorenkonte.tibeepost.ui.SettingsScreen
import com.zorenkonte.tibeepost.ui.TibeeTheme
+import com.zorenkonte.tibeepost.ui.onboarding.OnboardingScreen
+import com.zorenkonte.tibeepost.ui.rememberSettingsState
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ServiceStarter.start(this)
+ val setupPending = !Settings(this).onboardingDone
setContent {
- TibeeTheme { SettingsScreen() }
+ TibeeTheme {
+ val state = rememberSettingsState()
+ var showSetup by rememberSaveable { mutableStateOf(setupPending) }
+ if (showSetup) {
+ OnboardingScreen(state, onFinished = { showSetup = false })
+ } else {
+ SettingsScreen(state, onRerunSetup = { showSetup = true })
+ }
+ }
}
}
}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/settings/Settings.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/settings/Settings.kt
index 27eece5..403c4f1 100644
--- a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/settings/Settings.kt
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/settings/Settings.kt
@@ -15,6 +15,10 @@ class Settings(context: Context) {
get() = prefs.getString(SettingsKeys.TOKEN, "") ?: ""
set(value) = prefs.edit().putString(SettingsKeys.TOKEN, value).apply()
+ var onboardingDone: Boolean
+ get() = prefs.getBoolean(SettingsKeys.ONBOARDING_DONE, false)
+ set(value) = prefs.edit().putBoolean(SettingsKeys.ONBOARDING_DONE, value).apply()
+
var autostart: Boolean
get() = prefs.getBoolean(SettingsKeys.AUTOSTART, true)
set(value) = prefs.edit().putBoolean(SettingsKeys.AUTOSTART, value).apply()
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/settings/SettingsKeys.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/settings/SettingsKeys.kt
index 5765365..9562862 100644
--- a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/settings/SettingsKeys.kt
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/settings/SettingsKeys.kt
@@ -12,4 +12,5 @@ object SettingsKeys {
const val ACCENT = "default_accent"
const val POSITION = "default_position"
const val SOUND = "default_sound"
+ const val ONBOARDING_DONE = "onboarding_done"
}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/SettingsScreen.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/SettingsScreen.kt
index 3bc0a65..ac1e1c9 100644
--- a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/SettingsScreen.kt
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/SettingsScreen.kt
@@ -31,7 +31,7 @@ import com.zorenkonte.tibeepost.sound.ChimePlayer
import kotlinx.coroutines.launch
@Composable
-fun SettingsScreen(state: SettingsState = rememberSettingsState()) {
+fun SettingsScreen(state: SettingsState = rememberSettingsState(), onRerunSetup: () -> Unit = {}) {
val context = LocalContext.current
val chime = remember { ChimePlayer(context) }
DisposableEffect(chime) { onDispose { chime.release() } }
@@ -139,6 +139,17 @@ fun SettingsScreen(state: SettingsState = rememberSettingsState()) {
decoration = { ColorSwatch(state.accent) },
)
}
+ item {
+ ListItem(
+ selected = false,
+ onClick = {
+ state.resetOnboarding()
+ onRerunSetup()
+ },
+ headlineContent = { Text(stringResource(R.string.row_rerun_setup), style = MaterialTheme.typography.titleLarge) },
+ supportingContent = { Text(stringResource(R.string.row_rerun_setup_body), style = MaterialTheme.typography.bodyMedium) },
+ )
+ }
}
}
}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/SettingsState.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/SettingsState.kt
index 4fb7827..3fdba6f 100644
--- a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/SettingsState.kt
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/SettingsState.kt
@@ -63,6 +63,14 @@ class SettingsState(context: Context) {
fun stopObservingPreferences() = settings.stopObserving(preferenceListener)
+ fun completeOnboarding() {
+ settings.onboardingDone = true
+ }
+
+ fun resetOnboarding() {
+ settings.onboardingDone = false
+ }
+
fun generateToken() {
settings.token = TokenGenerator.generate()
}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/DoneStep.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/DoneStep.kt
new file mode 100644
index 0000000..6ca7b6e
--- /dev/null
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/DoneStep.kt
@@ -0,0 +1,37 @@
+package com.zorenkonte.tibeepost.ui.onboarding
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.tv.material3.Button
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.Text
+import com.zorenkonte.tibeepost.R
+import com.zorenkonte.tibeepost.ui.SettingsState
+
+@Composable
+fun DoneStep(state: SettingsState, primaryFocus: FocusRequester, onFinish: () -> Unit) {
+ val usable = state.overlayPermitted
+ val address = "http://${state.ipAddress ?: stringResource(R.string.status_no_network)}:${state.port}"
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text(
+ text = stringResource(if (usable) R.string.setup_done_title else R.string.setup_done_unusable_title),
+ style = MaterialTheme.typography.displayMedium,
+ )
+ if (!usable) UnusableWarning(packageName = state.packageName)
+ Text(stringResource(R.string.setup_done_address), style = MaterialTheme.typography.titleMedium, color = Color(0xFF9CA3AF))
+ Text(address, style = MaterialTheme.typography.displayMedium.copy(fontSize = 52.sp, lineHeight = 60.sp))
+ Text(stringResource(R.string.setup_done_body), style = MaterialTheme.typography.bodyLarge)
+ Button(onClick = onFinish, modifier = Modifier.focusRequester(primaryFocus)) {
+ Text(stringResource(if (usable) R.string.setup_finish else R.string.setup_finish_anyway))
+ }
+ }
+}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/OnboardingScreen.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/OnboardingScreen.kt
new file mode 100644
index 0000000..b4078b4
--- /dev/null
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/OnboardingScreen.kt
@@ -0,0 +1,95 @@
+package com.zorenkonte.tibeepost.ui.onboarding
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.Text
+import com.zorenkonte.tibeepost.R
+import com.zorenkonte.tibeepost.ui.SettingsState
+
+@Composable
+fun OnboardingScreen(state: SettingsState, onFinished: () -> Unit) {
+ var step by rememberSaveable { mutableStateOf(OnboardingStep.WELCOME) }
+ var permissionSkipped by rememberSaveable { mutableStateOf(false) }
+ val primaryFocus = remember { FocusRequester() }
+
+ LaunchedEffect(step) { primaryFocus.requestFocus() }
+
+ val permittedOnEntry = remember(step) { state.overlayPermitted }
+ LaunchedEffect(step, state.overlayPermitted) {
+ if (step == OnboardingStep.PERMISSION && !permittedOnEntry && state.overlayPermitted) {
+ permissionSkipped = false
+ step = OnboardingStep.TEST
+ }
+ }
+
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(Color(0xFF0B1020))
+ .padding(horizontal = 96.dp, vertical = 56.dp),
+ contentAlignment = Alignment.CenterStart,
+ ) {
+ Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(20.dp)) {
+ if (step != OnboardingStep.DONE) {
+ Text(
+ text = stringResource(R.string.setup_step, step.number),
+ style = MaterialTheme.typography.titleMedium,
+ color = Color(0xFF9CA3AF),
+ )
+ }
+ when (step) {
+ OnboardingStep.WELCOME -> WelcomeStep(
+ state = state,
+ primaryFocus = primaryFocus,
+ onContinue = { step = OnboardingStep.PERMISSION },
+ )
+ OnboardingStep.PERMISSION -> PermissionStep(
+ state = state,
+ primaryFocus = primaryFocus,
+ onContinue = {
+ permissionSkipped = false
+ step = OnboardingStep.TEST
+ },
+ onSkip = {
+ permissionSkipped = true
+ step = OnboardingStep.TEST
+ },
+ )
+ OnboardingStep.TEST -> TestStep(
+ state = state,
+ permissionMissing = permissionSkipped && !state.overlayPermitted,
+ primaryFocus = primaryFocus,
+ onBackToPermission = { step = OnboardingStep.PERMISSION },
+ onContinue = { step = OnboardingStep.DONE },
+ )
+ OnboardingStep.DONE -> DoneStep(
+ state = state,
+ primaryFocus = primaryFocus,
+ onFinish = {
+ state.completeOnboarding()
+ onFinished()
+ },
+ )
+ }
+ }
+ }
+}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/OnboardingStep.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/OnboardingStep.kt
new file mode 100644
index 0000000..f17e67c
--- /dev/null
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/OnboardingStep.kt
@@ -0,0 +1,8 @@
+package com.zorenkonte.tibeepost.ui.onboarding
+
+enum class OnboardingStep(val number: Int) {
+ WELCOME(1),
+ PERMISSION(2),
+ TEST(3),
+ DONE(3),
+}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/PermissionStep.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/PermissionStep.kt
new file mode 100644
index 0000000..acea7f0
--- /dev/null
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/PermissionStep.kt
@@ -0,0 +1,76 @@
+package com.zorenkonte.tibeepost.ui.onboarding
+
+import android.content.Intent
+import android.net.Uri
+import android.provider.Settings
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.Button
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.Text
+import com.zorenkonte.tibeepost.R
+import com.zorenkonte.tibeepost.ui.SettingsState
+
+@Composable
+fun PermissionStep(
+ state: SettingsState,
+ primaryFocus: FocusRequester,
+ onContinue: () -> Unit,
+ onSkip: () -> Unit,
+) {
+ if (state.overlayPermitted) {
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text(stringResource(R.string.setup_permission_granted_title), style = MaterialTheme.typography.displayMedium)
+ Text(stringResource(R.string.setup_permission_granted_body), style = MaterialTheme.typography.bodyLarge)
+ Button(onClick = onContinue, modifier = Modifier.focusRequester(primaryFocus)) {
+ Text(stringResource(R.string.setup_continue))
+ }
+ }
+ return
+ }
+
+ val context = LocalContext.current
+ val systemSettings = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:${state.packageName}"))
+ val canOpenSystemSettings = systemSettings.resolveActivity(context.packageManager) != null
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text(stringResource(R.string.setup_permission_title), style = MaterialTheme.typography.displayMedium)
+ Text(stringResource(R.string.setup_permission_body), style = MaterialTheme.typography.bodyLarge)
+ Text(stringResource(R.string.setup_permission_system), style = MaterialTheme.typography.bodyLarge)
+ Text(
+ text = "adb shell appops set ${state.packageName} SYSTEM_ALERT_WINDOW allow",
+ style = MaterialTheme.typography.titleMedium.copy(fontFamily = FontFamily.Monospace),
+ color = Color(0xFFFDE68A),
+ )
+ Text(
+ text = stringResource(R.string.setup_permission_recheck),
+ style = MaterialTheme.typography.bodyMedium,
+ color = Color(0xFF9CA3AF),
+ )
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ if (canOpenSystemSettings) {
+ Button(
+ onClick = { context.startActivity(systemSettings) },
+ modifier = Modifier.focusRequester(primaryFocus),
+ ) {
+ Text(stringResource(R.string.overlay_open_settings))
+ }
+ Button(onClick = onSkip) { Text(stringResource(R.string.setup_skip)) }
+ } else {
+ Button(onClick = onSkip, modifier = Modifier.focusRequester(primaryFocus)) {
+ Text(stringResource(R.string.setup_skip))
+ }
+ }
+ }
+ }
+}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/TestStep.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/TestStep.kt
new file mode 100644
index 0000000..596d80e
--- /dev/null
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/TestStep.kt
@@ -0,0 +1,56 @@
+package com.zorenkonte.tibeepost.ui.onboarding
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.Button
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.Text
+import com.zorenkonte.tibeepost.R
+import com.zorenkonte.tibeepost.ui.SettingsState
+import com.zorenkonte.tibeepost.ui.TestNotificationSender
+import kotlinx.coroutines.launch
+
+@Composable
+fun TestStep(
+ state: SettingsState,
+ permissionMissing: Boolean,
+ primaryFocus: FocusRequester,
+ onBackToPermission: () -> Unit,
+ onContinue: () -> Unit,
+) {
+ val scope = rememberCoroutineScope()
+
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ if (permissionMissing) UnusableWarning(packageName = null)
+ Text(stringResource(R.string.setup_test_title), style = MaterialTheme.typography.displayMedium)
+ Text(stringResource(R.string.setup_test_body), style = MaterialTheme.typography.bodyLarge)
+ if (state.testResult.isNotEmpty()) {
+ Text(
+ text = if (permissionMissing) stringResource(R.string.setup_test_blocked) else state.testResult,
+ style = MaterialTheme.typography.titleMedium,
+ color = if (permissionMissing) Color(0xFFFCA5A5) else Color(0xFF34D399),
+ )
+ }
+ Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ Button(
+ onClick = { scope.launch { state.testResult = TestNotificationSender.send(state.token) } },
+ modifier = Modifier.focusRequester(primaryFocus),
+ ) {
+ Text(stringResource(R.string.setup_test_send))
+ }
+ Button(onClick = onContinue) { Text(stringResource(R.string.setup_continue)) }
+ if (permissionMissing) {
+ Button(onClick = onBackToPermission) { Text(stringResource(R.string.setup_back_to_permission)) }
+ }
+ }
+ }
+}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/UnusableWarning.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/UnusableWarning.kt
new file mode 100644
index 0000000..160bec1
--- /dev/null
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/UnusableWarning.kt
@@ -0,0 +1,41 @@
+package com.zorenkonte.tibeepost.ui.onboarding
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.Text
+import com.zorenkonte.tibeepost.R
+
+@Composable
+fun UnusableWarning(packageName: String?, modifier: Modifier = Modifier) {
+ Column(
+ modifier
+ .fillMaxWidth()
+ .background(Color(0xFF3F1D1D), RoundedCornerShape(12.dp))
+ .padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ Text(
+ text = stringResource(R.string.setup_unusable),
+ style = MaterialTheme.typography.titleLarge,
+ color = Color(0xFFFCA5A5),
+ )
+ if (packageName != null) {
+ Text(
+ text = "adb shell appops set $packageName SYSTEM_ALERT_WINDOW allow",
+ style = MaterialTheme.typography.titleMedium.copy(fontFamily = FontFamily.Monospace),
+ color = Color(0xFFFDE68A),
+ )
+ }
+ }
+}
diff --git a/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/WelcomeStep.kt b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/WelcomeStep.kt
new file mode 100644
index 0000000..9790729
--- /dev/null
+++ b/tv/app/src/main/kotlin/com/zorenkonte/tibeepost/ui/onboarding/WelcomeStep.kt
@@ -0,0 +1,29 @@
+package com.zorenkonte.tibeepost.ui.onboarding
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.tv.material3.Button
+import androidx.tv.material3.MaterialTheme
+import androidx.tv.material3.Text
+import com.zorenkonte.tibeepost.R
+import com.zorenkonte.tibeepost.ui.SettingsState
+
+@Composable
+fun WelcomeStep(state: SettingsState, primaryFocus: FocusRequester, onContinue: () -> Unit) {
+ val address = "http://${state.ipAddress ?: stringResource(R.string.status_no_network)}:${state.port}"
+ Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
+ Text(stringResource(R.string.setup_welcome_title), style = MaterialTheme.typography.displayMedium)
+ Text(stringResource(R.string.setup_welcome_line1), style = MaterialTheme.typography.bodyLarge)
+ Text(stringResource(R.string.setup_welcome_line2, address), style = MaterialTheme.typography.bodyLarge)
+ Text(stringResource(R.string.setup_welcome_line3), style = MaterialTheme.typography.bodyLarge)
+ Button(onClick = onContinue, modifier = Modifier.focusRequester(primaryFocus)) {
+ Text(stringResource(R.string.setup_continue))
+ }
+ }
+}
diff --git a/tv/app/src/main/res/values/strings.xml b/tv/app/src/main/res/values/strings.xml
index 63e210a..7c3b095 100644
--- a/tv/app/src/main/res/values/strings.xml
+++ b/tv/app/src/main/res/values/strings.xml
@@ -17,7 +17,7 @@
Overlay permission
granted
missing
- Cards cannot be drawn yet
+ TibeePost is unusable until this permission is granted
This TV has no settings page for the overlay permission. Connect with adb and run:
Try system settings
Send test notification
@@ -39,4 +39,31 @@
Default background
Default text color
Default accent
+ Step %1$d of 3
+ Continue
+ Welcome to TibeePost
+ Anything on your network can put a notification on this TV with a single HTTP request: a script, a shop system, a smart home hub, or the TibeePost web client.
+ This TV listens at %1$s. Write it down or leave the app open; it is shown on the settings screen too.
+ Cards are drawn over whatever is playing. Android only allows that after you grant the "display over other apps" permission, which is the next step.
+ Allow TibeePost to draw over other apps
+ Without this permission the server still answers requests, but no card can ever appear, so the app is unusable. Grant it now if you can.
+ Press Try system settings if the button is shown and toggle TibeePost on. If your TV has no such page, connect a computer with adb on the same network and run the command below.
+ This screen checks again every few seconds and moves on by itself once the permission is granted.
+ Overlay permission granted
+ TibeePost can draw cards over other apps. Next, send yourself a test card.
+ Skip for now
+ Back to permission
+ Send yourself a test card
+ This posts a notification to the server running on this TV, exactly like a remote client would. With the permission granted, a card appears over this screen for a few seconds and the chime plays.
+ Send test card
+ The server accepted the request, but no card could be drawn because the overlay permission is missing.
+ Cards cannot be shown. The overlay permission is missing, so TibeePost is unusable until it is granted.
+ You are all set
+ Setup finished, but TibeePost is not usable yet
+ Send notifications to
+ The settings screen shows this address, lets you require a bearer token, and sets the default look, sound and duration for cards that do not specify their own.
+ Finish
+ Finish anyway
+ Run setup again
+ Walk through the welcome, permission and test steps once more.