Build the Wolfskin TV shell and navigation

This commit is contained in:
justinzeus 2026-11-05 14:38:38 +01:00
parent de452448ba
commit f60a22e435
97 changed files with 4363 additions and 91 deletions

View file

@ -2,7 +2,7 @@ package com.github.damontecres.wholphin.ui
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import com.github.damontecres.wholphin.ui.theme.colors.WolfskinThemeColors
import org.justinvisser.wolfskin.design.WolfskinColors
import org.junit.Assert.assertTrue
import org.junit.Test
@ -10,8 +10,8 @@ import org.junit.Test
class WolfskinThemeColorsTest {
@Test
fun darkPaletteHasReadableNormalAndFocusedText() {
val television = WolfskinThemeColors.darkScheme
val material = WolfskinThemeColors.darkSchemeMaterial
val television = WolfskinColors.darkScheme
val material = WolfskinColors.darkSchemeMaterial
val textAndBackgrounds =
listOf(
television.onBackground to television.background,

View file

@ -0,0 +1,76 @@
package org.justinvisser.wolfskin.tv
import android.app.Application
import android.view.View
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.SeerrRatingsService
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.components.LocalSeerrRatingsService
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.ui.main.settings.HomePageSettingsState
import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsPage
import com.github.damontecres.wholphin.ui.main.settings.HomeSettingsViewModel
import com.github.damontecres.wholphin.util.LoadingState
import com.github.takahirom.roborazzi.captureRoboImage
import io.mockk.every
import io.mockk.mockk
import java.io.File
import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.justinvisser.wolfskin.design.WolfskinTheme
import org.justinvisser.wolfskin.design.WolfskinTokens
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
/** The frozen st-094 state opens User profile settings, not the separate Playback press path. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34], application = Application::class, qualifiers = "w960dp-h540dp-xhdpi")
@GraphicsMode(GraphicsMode.Mode.NATIVE)
class SettingsHandOffCaptureTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Before fun fullscreen() {
rule.activity.window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
@Suppress("DEPRECATION")
rule.activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
}
@Test fun st094_realUserProfileHomeSettingsHandOff() {
val model = mockk<HomeSettingsViewModel>(relaxed = true)
every { model.state } returns MutableStateFlow(HomePageSettingsState.EMPTY.copy(loading = LoadingState.Success))
every { model.serverPluginActive } returns MutableStateFlow(false)
every { model.settingsChanged } returns MutableStateFlow(false)
val preferences = UserPreferences(AppPreferences.getDefaultInstance(), null)
val ratings = SeerrRatingsService(mockk(relaxed = true), mockk(relaxed = true))
val images = mockk<ImageUrlService>(relaxed = true)
rule.setContent {
WolfskinTheme {
CompositionLocalProvider(LocalSeerrRatingsService provides ratings, LocalImageUrlService provides images) {
HomeSettingsPage(preferences, Modifier.fillMaxSize().background(WolfskinTokens.Color.bg), viewModel = model)
}
}
}
rule.mainClock.advanceTimeBy(1000)
rule.waitForIdle()
rule.onNodeWithText(rule.activity.getString(R.string.customize_home)).assertIsDisplayed()
val directory = File(requireNotNull(System.getProperty("wolfskin.captureDirectory")))
directory.mkdirs()
rule.onRoot().captureRoboImage(File(directory, "st-094.png").absolutePath)
}
}

View file

@ -0,0 +1,57 @@
package org.justinvisser.wolfskin.tv
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
import java.util.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/** The actual Advanced preference persists its value and changes the shell's renderer policy. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34], application = Application::class)
class ShellRoutingTest {
private val context: Application get() = ApplicationProvider.getApplicationContext()
private val movies = Destination.MediaItem(UUID.fromString("00000000-0000-0000-0000-000000000001"), BaseItemKind.COLLECTION_FOLDER, CollectionType.MOVIES)
private val libraries = mapOf("movies" to movies, "shows" to movies, "anime" to movies)
@Before fun initialize() { WolfskinScreenPreference.initialize(context); WolfskinScreenPreference.enabled = true }
@After fun restore() { WolfskinScreenPreference.enabled = true }
@Test fun advancedSwitchRestoresUpstreamHomeAndLibrariesAndPersistsAcrossReload() {
val preference = AppPreference.WolfskinScreens
val preferences = AppPreferences.getDefaultInstance()
assertNull(resolveUpstreamDestination(Destination.Home(), true, libraries))
assertNull(resolveUpstreamDestination(Destination.Wolfskin("movies"), true, libraries))
preference.setter(preferences, false)
WolfskinScreenPreference.initialize(context)
assertFalse(preference.getter(preferences))
assertTrue(resolveUpstreamDestination(Destination.Home(), WolfskinScreenPreference.enabled, libraries) is Destination.Home)
listOf("movies", "shows", "anime").forEach { route ->
assertEquals(movies, resolveUpstreamDestination(Destination.Wolfskin(route), WolfskinScreenPreference.enabled, libraries))
}
preference.setter(preferences, true)
assertNull(resolveUpstreamDestination(Destination.Home(), WolfskinScreenPreference.enabled, libraries))
assertNull(resolveUpstreamDestination(Destination.Wolfskin("movies"), WolfskinScreenPreference.enabled, libraries))
}
@Test fun settingsAndPickerNeverBecomeUpstreamScreensButUnrelatedRoutesPassThrough() {
val settings = Destination.Wolfskin("settings")
val upstream = Destination.Settings(PreferenceScreenOption.ADVANCED)
for (enabled in listOf(false, true)) {
assertNull(resolveUpstreamDestination(settings, enabled, libraries))
assertEquals(upstream, resolveUpstreamDestination(upstream, enabled, libraries))
assertEquals(Destination.Search("name"), resolveUpstreamDestination(Destination.Search("name"), enabled, libraries))
}
}
}