From 2a3d0a44a87292a8573fae260d5b78e2cd7714c4 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Thu, 25 Jul 2024 20:09:33 -0400 Subject: [PATCH 01/36] WIP --- app/build.gradle.kts | 30 +++ app/src/main/AndroidManifest.xml | 4 +- .../damontecres/stashapp/RootActivity.kt | 31 ++++ .../damontecres/stashapp/StashApplication.kt | 7 + .../com/github/damontecres/stashapp/ui/App.kt | 175 ++++++++++++++++++ .../github/damontecres/stashapp/ui/Cards.kt | 32 ++++ .../damontecres/stashapp/ui/HomePage.kt | 13 ++ .../damontecres/stashapp/ui/theme/Theme.kt | 22 +++ .../stashapp/util/QueryRepository.kt | 33 ++++ app/src/main/res/values/strings.xml | 3 + build.gradle.kts | 1 + 11 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/RootActivity.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/App.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/QueryRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bbca5640..84edd223 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,6 +11,7 @@ plugins { id("kotlin-parcelize") id("com.apollographql.apollo3") version "3.8.4" id("kotlin-kapt") + id("com.google.dagger.hilt.android") } fun getVersionCode(): Int { @@ -41,6 +42,14 @@ android { } } + buildFeatures { // Enables Jetpack Compose for this module + compose = true + } + composeOptions { + // https://developer.android.com/jetpack/androidx/releases/compose-kotlin + kotlinCompilerExtensionVersion = "1.5.13" + } + defaultConfig { applicationId = "com.github.damontecres.stashapp" minSdk = 23 @@ -125,6 +134,11 @@ tasks.register("generateStrin outputDirectory = File("$buildDir/generated/res/server") } +// Allow references to generated code +kapt { + correctErrorTypes = true +} + // tasks.preBuild.dependsOn("generateStrings") tasks.preBuild.dependsOn("generateStrings") @@ -138,7 +152,21 @@ dependencies { implementation("androidx.leanback:leanback-paging:1.1.0-alpha11") implementation("com.github.bumptech.glide:glide:$glideVersion") implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion") + implementation("com.github.bumptech.glide:compose:1.0.0-beta01") implementation("androidx.leanback:leanback-tab:1.1.0-beta01") + implementation("androidx.navigation:navigation-runtime-ktx:2.7.7") + implementation("androidx.compose.runtime:runtime-android:1.6.8") + implementation("androidx.navigation:navigation-compose:2.7.7") + implementation(platform("androidx.compose:compose-bom:2024.06.00")) + implementation("androidx.activity:activity-compose:1.9.1") + implementation("androidx.compose.ui:ui:1.6.8") + implementation("androidx.compose.ui:ui-tooling-preview:1.6.8") + debugImplementation("androidx.compose.ui:ui-tooling") + implementation("androidx.tv:tv-foundation:1.0.0-alpha11") + implementation("androidx.tv:tv-material:1.0.0-rc01") + implementation("androidx.compose.ui:ui-viewbinding:1.7.0-beta06") + implementation("androidx.paging:paging-compose:3.3.1") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4") kapt("com.github.bumptech.glide:compiler:$glideVersion") implementation("com.caverock:androidsvg-aar:1.4") @@ -161,6 +189,8 @@ dependencies { implementation("com.otaliastudios:zoomlayout:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("io.noties.markwon:core:4.6.2") + implementation("com.google.dagger:hilt-android:2.44") + kapt("com.google.dagger:hilt-android-compiler:2.44") testImplementation("androidx.test:core-ktx:1.6.1") testImplementation("junit:junit:4.13.2") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af6a8182..8babe4e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,13 +31,11 @@ android:theme="@style/Theme.StashAppAndroidTV" android:usesCleartextTraffic="true"> diff --git a/app/src/main/java/com/github/damontecres/stashapp/RootActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/RootActivity.kt new file mode 100644 index 00000000..e2a8f600 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/RootActivity.kt @@ -0,0 +1,31 @@ +package com.github.damontecres.stashapp + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.core.view.WindowCompat +import androidx.tv.material3.Surface +import com.github.damontecres.stashapp.ui.App +import com.github.damontecres.stashapp.ui.theme.AppTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class RootActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + setContent { + AppTheme { + Surface( + shape = RectangleShape, + modifier = Modifier.fillMaxSize(), + ) { + App() + } + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt b/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt index 50bbc816..03d8860c 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt @@ -17,11 +17,14 @@ import androidx.preference.PreferenceManager import com.github.damontecres.stashapp.setup.SetupActivity import com.github.damontecres.stashapp.util.AppUpgradeHandler import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler +import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.Version +import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +@HiltAndroidApp class StashApplication : Application() { private var wasEnterBackground = false private var mainDestroyed = false @@ -165,6 +168,10 @@ class StashApplication : Application() { return application } + fun getCurrentStashServer(): StashServer? { + return StashServer.getCurrentStashServer(application) + } + fun getFont( @FontRes fontId: Int, ): Typeface { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt new file mode 100644 index 00000000..4eb10dea --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -0,0 +1,175 @@ +package com.github.damontecres.stashapp.ui + +import android.widget.Toast +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.tv.foundation.lazy.list.TvLazyColumn +import androidx.tv.foundation.lazy.list.items +import androidx.tv.material3.Button +import androidx.tv.material3.DrawerValue +import androidx.tv.material3.NavigationDrawer +import androidx.tv.material3.NavigationDrawerItem +import androidx.tv.material3.Text +import androidx.tv.material3.rememberDrawerState +import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.ui.DrawerPage.Home +import com.github.damontecres.stashapp.ui.DrawerPage.Scenes + +sealed class DrawerPage( + val route: String, + @StringRes val iconString: Int, + @StringRes val name: Int, +) { + data object Home : DrawerPage("home", R.string.fa_house, R.string.home) + + data object Scenes : DrawerPage("scenes", DataType.SCENE.iconStringId, DataType.SCENE.pluralStringId) +} + +val PAGES = listOf(Home, Scenes) + +@Suppress("ktlint:standard:function-naming") +@Composable +fun App() { + val fontFamily = + FontFamily( + Font( + resId = R.font.fa_solid_900, + ), + ) + + val defaultSelection: DrawerPage = DrawerPage.Home + + var currentScreen by remember { mutableStateOf(defaultSelection) } + + val drawerState = rememberDrawerState(DrawerValue.Closed) + + val collapsedDrawerItemWidth = 56.dp + val paddingValue = 12.dp + + val navController = rememberNavController() + + NavigationDrawer( + drawerState = drawerState, + drawerContent = { + Column( + Modifier + .fillMaxHeight() + .padding(12.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.SpaceBetween, + ) { +// NavigationDrawerItem( +// selected = currentScreen == DrawerScreen.UserAccount, +// onClick = { +// currentScreen = DrawerScreen.UserAccount +// }, +// leadingContent = { +// Icon( +// imageVector = DrawerScreen.UserAccount.icon, +// contentDescription = null, +// ) +// }, +// ) { +// Text(stringResource(id = DrawerScreen.UserAccount.title)) +// } + + // Group of item with same padding + + TvLazyColumn( + Modifier + .selectableGroup(), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically), + ) { + items(PAGES, key = { it.route }) { page -> + NavigationDrawerItem( + selected = navController.currentDestination?.route == page.route, + onClick = { + drawerState.setValue(DrawerValue.Closed) + navController.navigate(page.route) { + // remove the previous Composable from the back stack + popUpTo(navController.currentDestination?.route ?: "") { + inclusive = true + } + } + }, + leadingContent = { + Text(stringResource(id = page.iconString), fontFamily = fontFamily) + }, + ) { + Text(stringResource(id = page.name)) + } + } + } + + // Separated item from other for better UI purpose +// NavigationDrawerItem( +// selected = currentScreen == DrawerScreen.SettingsScreen, +// onClick = { +// currentScreen = DrawerScreen.SettingsScreen +// }, +// leadingContent = { +// Icon( +// imageVector = DrawerScreen.SettingsScreen.icon, +// contentDescription = null, +// ) +// }, +// ) { +// Text(stringResource(id = DrawerScreen.SettingsScreen.title)) +// } + } + }, + ) { + // content + + val context = LocalContext.current + + NavHost( + navController = navController, + startDestination = DrawerPage.Home.route, + modifier = + Modifier, +// .fillMaxSize() +// .padding(start = collapsedDrawerItemWidth), + ) { + composable(route = DrawerPage.Home.route) { + Button( + onClick = { + Toast.makeText(context, "Home clicked", Toast.LENGTH_SHORT).show() + }, + ) { + Text(text = "Home") + } + } + composable(route = DrawerPage.Scenes.route) { + Button( + onClick = { + Toast.makeText(context, "Scenes clicked", Toast.LENGTH_SHORT).show() + }, + ) { + Text(text = "Scenes") + } + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt new file mode 100644 index 00000000..36afba1a --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -0,0 +1,32 @@ +package com.github.damontecres.stashapp.ui + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.tv.material3.ClassicCard +import androidx.tv.material3.Text +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.github.damontecres.stashapp.api.fragment.SlimSceneData +import com.github.damontecres.stashapp.util.titleOrFilename + +@OptIn(ExperimentalGlideComposeApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +fun SceneCard( + scene: SlimSceneData, + onClick: (() -> Unit), +) { + ClassicCard( + onClick = onClick, + image = { + GlideImage( + model = scene.paths.preview, + contentDescription = "", + modifier = Modifier.padding(4.dp), + ) + }, + title = { Text(scene.titleOrFilename ?: "") }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt new file mode 100644 index 00000000..d38281e4 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -0,0 +1,13 @@ +package com.github.damontecres.stashapp.ui + +import androidx.compose.runtime.Composable +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel + +@HiltViewModel +class HomePageViewModel : ViewModel() + +@Suppress("ktlint:standard:function-naming") +@Composable +fun HomePage() { +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt new file mode 100644 index 00000000..04b84917 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt @@ -0,0 +1,22 @@ +package com.github.damontecres.stashapp.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.darkColorScheme +import androidx.tv.material3.lightColorScheme + +@Suppress("ktlint:standard:function-naming") +@Composable +fun AppTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colorScheme = + if (useDarkTheme) { + darkColorScheme() + } else { + lightColorScheme() + } + MaterialTheme(colorScheme = colorScheme, content = content) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/QueryRepository.kt b/app/src/main/java/com/github/damontecres/stashapp/util/QueryRepository.kt new file mode 100644 index 00000000..2bc94ee5 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/util/QueryRepository.kt @@ -0,0 +1,33 @@ +package com.github.damontecres.stashapp.util + +import com.apollographql.apollo3.ApolloCall +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.ApolloResponse +import com.apollographql.apollo3.api.Operation +import com.github.damontecres.stashapp.api.ConfigurationQuery +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class QueryRepository( + private val apolloClient: ApolloClient, +) { + enum class QueryResultStatus { + SUCCESS, + FAILURE, + } + + data class QueryResult(val status: QueryResultStatus, val data: T) + + private suspend fun executeQuery(query: ApolloCall): ApolloResponse = + withContext(Dispatchers.IO) { +// val queryName = query.operation.name() +// val id = QUERY_ID.getAndIncrement() +// Log.v(TAG, "executeQuery $id $queryName") + query.execute() + } + + suspend fun getServerConfiguration() { + val query = ConfigurationQuery() + apolloClient.query(query).execute() + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 053b02ad..5d3a2c39 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,6 +21,7 @@ + Go to Go to scene @@ -33,4 +34,6 @@ Server URL Configure the server URL + Home + diff --git a/build.gradle.kts b/build.gradle.kts index e5640274..af995b2a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,5 @@ plugins { id("com.android.application") version "8.5.1" apply false id("org.jetbrains.kotlin.android") version "1.9.23" apply false id("org.jetbrains.kotlin.jvm") version "1.9.23" apply false + id("com.google.dagger.hilt.android") version "2.44" apply false } From 73cd38f9621be9860a6f5e7797ed7b20121230ce Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:20:30 -0400 Subject: [PATCH 02/36] WIP --- app/build.gradle.kts | 6 +- app/src/main/graphql/Configuration.graphql | 5 ++ .../com/github/damontecres/stashapp/ui/App.kt | 18 ++-- .../github/damontecres/stashapp/ui/Cards.kt | 11 ++- .../damontecres/stashapp/ui/HomePage.kt | 87 ++++++++++++++++++- .../damontecres/stashapp/util/Provider.kt | 17 ++++ .../stashapp/util/QueryRepository.kt | 36 ++++---- build.gradle.kts | 3 +- 8 files changed, 152 insertions(+), 31 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/Provider.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 84edd223..53c25cb9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,6 +12,7 @@ plugins { id("com.apollographql.apollo3") version "3.8.4" id("kotlin-kapt") id("com.google.dagger.hilt.android") + kotlin("kapt") } fun getVersionCode(): Int { @@ -189,8 +190,9 @@ dependencies { implementation("com.otaliastudios:zoomlayout:1.9.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") implementation("io.noties.markwon:core:4.6.2") - implementation("com.google.dagger:hilt-android:2.44") - kapt("com.google.dagger:hilt-android-compiler:2.44") + implementation("com.google.dagger:hilt-android:2.51.1") + implementation("androidx.hilt:hilt-navigation-compose:1.2.0") + kapt("com.google.dagger:hilt-android-compiler:2.51.1") testImplementation("androidx.test:core-ktx:1.6.1") testImplementation("junit:junit:4.13.2") diff --git a/app/src/main/graphql/Configuration.graphql b/app/src/main/graphql/Configuration.graphql index c73a33eb..121ab654 100644 --- a/app/src/main/graphql/Configuration.graphql +++ b/app/src/main/graphql/Configuration.graphql @@ -31,6 +31,11 @@ query Configuration { } ui } + version { + version + hash + build_time + } } query ConfigurationUI { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 4eb10dea..1a12d07d 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -98,7 +99,7 @@ fun App() { TvLazyColumn( Modifier .selectableGroup(), - horizontalAlignment = Alignment.Start, + horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically), ) { items(PAGES, key = { it.route }) { page -> @@ -114,7 +115,12 @@ fun App() { } }, leadingContent = { - Text(stringResource(id = page.iconString), fontFamily = fontFamily) + Text( + stringResource(id = page.iconString), + fontFamily = fontFamily, + textAlign = TextAlign.Center, + modifier = Modifier, + ) }, ) { Text(stringResource(id = page.name)) @@ -153,13 +159,7 @@ fun App() { // .padding(start = collapsedDrawerItemWidth), ) { composable(route = DrawerPage.Home.route) { - Button( - onClick = { - Toast.makeText(context, "Home clicked", Toast.LENGTH_SHORT).show() - }, - ) { - Text(text = "Home") - } + HomePage() } composable(route = DrawerPage.Scenes.route) { Button( diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index 36afba1a..8e857d6b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -1,6 +1,8 @@ package com.github.damontecres.stashapp.ui +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -9,6 +11,7 @@ import androidx.tv.material3.Text import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.github.damontecres.stashapp.api.fragment.SlimSceneData +import com.github.damontecres.stashapp.presenters.ScenePresenter import com.github.damontecres.stashapp.util.titleOrFilename @OptIn(ExperimentalGlideComposeApi::class) @@ -22,9 +25,13 @@ fun SceneCard( onClick = onClick, image = { GlideImage( - model = scene.paths.preview, + model = scene.paths.screenshot, contentDescription = "", - modifier = Modifier.padding(4.dp), + modifier = + Modifier + .padding(0.dp) + .width(ScenePresenter.CARD_WIDTH.dp) + .height(ScenePresenter.CARD_HEIGHT.dp), ) }, title = { Text(scene.titleOrFilename ?: "") }, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index d38281e4..312c3935 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -1,13 +1,98 @@ package com.github.damontecres.stashapp.ui +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel +import com.github.damontecres.stashapp.StashApplication +import com.github.damontecres.stashapp.api.fragment.SlimSceneData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.util.FilterParser +import com.github.damontecres.stashapp.util.FrontPageParser +import com.github.damontecres.stashapp.util.QueryEngine +import com.github.damontecres.stashapp.util.QueryRepository +import com.github.damontecres.stashapp.util.Version +import com.github.damontecres.stashapp.util.getCaseInsensitive +import com.github.damontecres.stashapp.util.titleOrFilename import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject @HiltViewModel -class HomePageViewModel : ViewModel() +class HomePageViewModel + @Inject + constructor( + val queryRepository: QueryRepository, + ) : ViewModel() { + val rows = mutableStateListOf() + + suspend fun fetchFrontPage() { + val config = queryRepository.getServerConfiguration() + if (config != null) { + val version = + Version.tryFromString(config.version.version) ?: Version.MINIMUM_STASH_VERSION + val ui = config.configuration.ui + val frontPageContent = + (ui as Map).getCaseInsensitive("frontPageContent") as List> + val frontPageParser = + FrontPageParser( + QueryEngine(StashApplication.getApplication()), + FilterParser(version), + ) + frontPageParser.parse(frontPageContent).forEach { deferredRow -> + val result = deferredRow.await() + if (result.successful && result.data?.filter?.dataType == DataType.SCENE) { + rows.add(result) + } + } + } + } + } @Suppress("ktlint:standard:function-naming") @Composable fun HomePage() { + val viewModel = hiltViewModel() + + val rows = remember { viewModel.rows } + + LaunchedEffect(Unit) { + viewModel.fetchFrontPage() + } + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier.fillMaxHeight().padding(12.dp), + ) { + items(rows, key = { rows.indexOf(it) }) { row -> + HomePageRow(row) + } + } +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun HomePageRow(row: FrontPageParser.FrontPageRow) { + val context = LocalContext.current + LazyRow { + items(row.data!!.data) { item -> + if (item is SlimSceneData) { + SceneCard(item) { + Toast.makeText(context, "Clicked ${item.titleOrFilename}", Toast.LENGTH_SHORT) + .show() + } + } + } + } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/Provider.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Provider.kt new file mode 100644 index 00000000..eb18c4cc --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Provider.kt @@ -0,0 +1,17 @@ +package com.github.damontecres.stashapp.util + +import com.apollographql.apollo3.ApolloClient +import com.github.damontecres.stashapp.StashApplication +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +object Provider { + @Provides + fun createApolloClient(): ApolloClient { + return StashClient.getApolloClient(StashApplication.getApplication()) + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/QueryRepository.kt b/app/src/main/java/com/github/damontecres/stashapp/util/QueryRepository.kt index 2bc94ee5..7a159690 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/QueryRepository.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/QueryRepository.kt @@ -7,27 +7,31 @@ import com.apollographql.apollo3.api.Operation import com.github.damontecres.stashapp.api.ConfigurationQuery import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import javax.inject.Inject -class QueryRepository( - private val apolloClient: ApolloClient, -) { - enum class QueryResultStatus { - SUCCESS, - FAILURE, - } +class QueryRepository + @Inject + constructor( + private val apolloClient: ApolloClient, + ) { + enum class QueryResultStatus { + SUCCESS, + FAILURE, + } - data class QueryResult(val status: QueryResultStatus, val data: T) + data class QueryResult(val status: QueryResultStatus, val data: T) - private suspend fun executeQuery(query: ApolloCall): ApolloResponse = - withContext(Dispatchers.IO) { + private suspend fun executeQuery(query: ApolloCall): ApolloResponse = + withContext(Dispatchers.IO) { // val queryName = query.operation.name() // val id = QUERY_ID.getAndIncrement() // Log.v(TAG, "executeQuery $id $queryName") - query.execute() - } + query.execute() + } - suspend fun getServerConfiguration() { - val query = ConfigurationQuery() - apolloClient.query(query).execute() + suspend fun getServerConfiguration(): ConfigurationQuery.Data? { + val query = ConfigurationQuery() + val result = apolloClient.query(query).execute() + return result.data + } } -} diff --git a/build.gradle.kts b/build.gradle.kts index af995b2a..785c26ca 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,5 +3,6 @@ plugins { id("com.android.application") version "8.5.1" apply false id("org.jetbrains.kotlin.android") version "1.9.23" apply false id("org.jetbrains.kotlin.jvm") version "1.9.23" apply false - id("com.google.dagger.hilt.android") version "2.44" apply false + id("com.google.dagger.hilt.android") version "2.50" apply false + kotlin("kapt") version "1.9.23" apply false } From f897982463cf87ce09b2c40f4d4dd62b5592e0de Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Fri, 26 Jul 2024 18:48:23 -0400 Subject: [PATCH 03/36] WIP card improvements --- .../stashapp/presenters/StashImageCardView.kt | 6 +- .../com/github/damontecres/stashapp/ui/App.kt | 20 ++- .../github/damontecres/stashapp/ui/Cards.kt | 136 +++++++++++++++++- .../damontecres/stashapp/ui/HomePage.kt | 39 +++-- 4 files changed, 171 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/presenters/StashImageCardView.kt b/app/src/main/java/com/github/damontecres/stashapp/presenters/StashImageCardView.kt index c7054a3f..083ff892 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/presenters/StashImageCardView.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/presenters/StashImageCardView.kt @@ -47,11 +47,11 @@ class StashImageCardView(context: Context) : ImageCardView(context) { companion object { private const val TAG = "StashImageCardView" - private const val ICON_SPACING = " " + const val ICON_SPACING = " " - private val FA_FONT = StashApplication.getFont(R.font.fa_solid_900) + val FA_FONT = StashApplication.getFont(R.font.fa_solid_900) - private val ICON_ORDER = + val ICON_ORDER = listOf( DataType.SCENE, DataType.MOVIE, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 1a12d07d..9bb0c221 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -4,8 +4,12 @@ import android.widget.Toast import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.selectableGroup import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -19,12 +23,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.tv.foundation.lazy.list.TvLazyColumn -import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.Button import androidx.tv.material3.DrawerValue import androidx.tv.material3.NavigationDrawer @@ -75,8 +78,9 @@ fun App() { Column( Modifier .fillMaxHeight() - .padding(12.dp), - horizontalAlignment = Alignment.Start, + .padding(4.dp) + .width(if (drawerState.currentValue == DrawerValue.Closed) collapsedDrawerItemWidth else Dp.Unspecified), + horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, ) { // NavigationDrawerItem( @@ -96,9 +100,11 @@ fun App() { // Group of item with same padding - TvLazyColumn( - Modifier - .selectableGroup(), + LazyColumn( + contentPadding = PaddingValues(8.dp), + modifier = + Modifier + .selectableGroup(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically), ) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index 8e857d6b..2749b35a 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -1,39 +1,165 @@ package com.github.damontecres.stashapp.ui +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.tv.material3.ClassicCard import androidx.tv.material3.Text import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage +import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.SlimSceneData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.PerformerPresenter import com.github.damontecres.stashapp.presenters.ScenePresenter +import com.github.damontecres.stashapp.presenters.StashImageCardView.Companion.ICON_ORDER import com.github.damontecres.stashapp.util.titleOrFilename +import com.github.damontecres.stashapp.views.StashItemViewClickListener +import java.util.EnumMap + +@Suppress("ktlint:standard:function-naming") +@Composable +fun IconRowText( + iconMap: EnumMap, + oCounter: Int, +) { + val faFontFamily = + FontFamily( + Font( + resId = R.font.fa_solid_900, + ), + ) + + val annotatedString = + buildAnnotatedString { + ICON_ORDER.forEach { + val count = iconMap[it] + if (count != null && count > 0) { + withStyle(SpanStyle(fontFamily = faFontFamily)) { + append(stringResource(it.iconStringId)) + } + append(" $count") + append(" ") + } + } + if (oCounter > 0) { + appendInlineContent(id = "ocounter", "O") + append(oCounter.toString()) + } + } + val inlineContentMap = + mapOf( + "ocounter" to + InlineTextContent( + Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter), + ) { + Image( + painterResource(id = R.drawable.sweat_drops), + modifier = Modifier.fillMaxSize(), + contentDescription = "", + ) + }, + ) + Text(annotatedString, inlineContent = inlineContentMap, textAlign = TextAlign.Center) +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun StashCard(item: Any) { + val context = LocalContext.current + val clicker = StashItemViewClickListener(context) + when (item) { + is SlimSceneData -> SceneCard(item, onClick = { clicker.onItemClicked(item) }) + is PerformerData -> PerformerCard(item, onClick = { clicker.onItemClicked(item) }) + } +} @OptIn(ExperimentalGlideComposeApi::class) @Suppress("ktlint:standard:function-naming") @Composable fun SceneCard( - scene: SlimSceneData, + item: SlimSceneData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.TAG] = item.tags.size + dataTypeMap[DataType.PERFORMER] = item.performers.size + dataTypeMap[DataType.MOVIE] = item.movies.size + dataTypeMap[DataType.MARKER] = item.scene_markers.size + dataTypeMap[DataType.GALLERY] = item.galleries.size + + ClassicCard( + onClick = onClick, + image = { + GlideImage( + model = item.paths.screenshot, + contentDescription = "", + modifier = + Modifier + .padding(0.dp) + .width(ScenePresenter.CARD_WIDTH.dp / 2) + .height(ScenePresenter.CARD_HEIGHT.dp / 2), + ) + }, + title = { Text(item.titleOrFilename ?: "") }, + subtitle = { + IconRowText(dataTypeMap, item.o_counter ?: -1) + }, + ) +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +fun PerformerCard( + item: PerformerData, onClick: (() -> Unit), ) { +// val presenter = ScenePresenter() +// +// AndroidView(factory = { context -> +// val cardView = StashImageCardView(context) +// cardView.isFocusable = true +// cardView.isFocusableInTouchMode = true +// cardView.updateCardBackgroundColor(cardView, false) +// cardView.onFocusChangeListener = StashOnFocusChangeListener(context) +// cardView +// }) { view -> +// presenter.onBindViewHolder(Presenter.ViewHolder(view), scene) +// } ClassicCard( onClick = onClick, image = { GlideImage( - model = scene.paths.screenshot, + model = item.image_path, contentDescription = "", modifier = Modifier .padding(0.dp) - .width(ScenePresenter.CARD_WIDTH.dp) - .height(ScenePresenter.CARD_HEIGHT.dp), + .width(PerformerPresenter.CARD_WIDTH.dp / 2) + .height(PerformerPresenter.CARD_HEIGHT.dp / 2), ) }, - title = { Text(scene.titleOrFilename ?: "") }, + title = { Text(item.name) }, ) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index 312c3935..b66f181c 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -1,6 +1,5 @@ package com.github.damontecres.stashapp.ui -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding @@ -12,20 +11,19 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel +import androidx.tv.material3.Text import com.github.damontecres.stashapp.StashApplication -import com.github.damontecres.stashapp.api.fragment.SlimSceneData -import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.api.ServerInfoQuery import com.github.damontecres.stashapp.util.FilterParser import com.github.damontecres.stashapp.util.FrontPageParser import com.github.damontecres.stashapp.util.QueryEngine import com.github.damontecres.stashapp.util.QueryRepository +import com.github.damontecres.stashapp.util.ServerPreferences import com.github.damontecres.stashapp.util.Version import com.github.damontecres.stashapp.util.getCaseInsensitive -import com.github.damontecres.stashapp.util.titleOrFilename import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @@ -33,7 +31,7 @@ import javax.inject.Inject class HomePageViewModel @Inject constructor( - val queryRepository: QueryRepository, + private val queryRepository: QueryRepository, ) : ViewModel() { val rows = mutableStateListOf() @@ -43,6 +41,16 @@ class HomePageViewModel val version = Version.tryFromString(config.version.version) ?: Version.MINIMUM_STASH_VERSION val ui = config.configuration.ui + + // TODO A little hacky + ServerPreferences(StashApplication.getApplication()).updatePreferences( + config.configuration, + ServerInfoQuery.Data( + ServerInfoQuery.Version(config.version.version), + ServerInfoQuery.FindScenes(-1), + ), + ) + val frontPageContent = (ui as Map).getCaseInsensitive("frontPageContent") as List> val frontPageParser = @@ -52,7 +60,7 @@ class HomePageViewModel ) frontPageParser.parse(frontPageContent).forEach { deferredRow -> val result = deferredRow.await() - if (result.successful && result.data?.filter?.dataType == DataType.SCENE) { + if (result.successful) { rows.add(result) } } @@ -73,7 +81,10 @@ fun HomePage() { LazyColumn( verticalArrangement = Arrangement.spacedBy(24.dp), - modifier = Modifier.fillMaxHeight().padding(12.dp), + modifier = + Modifier + .fillMaxHeight() + .padding(12.dp), ) { items(rows, key = { rows.indexOf(it) }) { row -> HomePageRow(row) @@ -84,14 +95,12 @@ fun HomePage() { @Suppress("ktlint:standard:function-naming") @Composable fun HomePageRow(row: FrontPageParser.FrontPageRow) { - val context = LocalContext.current + val rowData = row.data!! + Text(text = rowData.name, modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)) LazyRow { - items(row.data!!.data) { item -> - if (item is SlimSceneData) { - SceneCard(item) { - Toast.makeText(context, "Clicked ${item.titleOrFilename}", Toast.LENGTH_SHORT) - .show() - } + items(rowData.data) { item -> + if (item != null) { + StashCard(item) } } } From 25da549b4f0a4ea1da5787902d4c8c39f0e8418b Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 16:25:11 -0400 Subject: [PATCH 04/36] Add basic filter grid --- app/build.gradle.kts | 2 + .../stashapp/FilterListActivity.kt | 3 + .../damontecres/stashapp/data/FilterType.kt | 11 + .../playback/PlaybackMarkersFragment.kt | 1 + .../com/github/damontecres/stashapp/ui/App.kt | 74 +++-- .../github/damontecres/stashapp/ui/Cards.kt | 36 ++- .../damontecres/stashapp/ui/FilterPage.kt | 301 ++++++++++++++++++ .../damontecres/stashapp/ui/HomePage.kt | 14 +- .../damontecres/stashapp/util/Constants.kt | 5 + .../damontecres/stashapp/util/Provider.kt | 11 +- .../damontecres/stashapp/util/StashServer.kt | 5 + 11 files changed, 418 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 53c25cb9..63ecd7d4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -166,7 +166,9 @@ dependencies { implementation("androidx.tv:tv-foundation:1.0.0-alpha11") implementation("androidx.tv:tv-material:1.0.0-rc01") implementation("androidx.compose.ui:ui-viewbinding:1.7.0-beta06") + implementation("androidx.paging:paging-runtime-ktx:3.3.1") implementation("androidx.paging:paging-compose:3.3.1") + implementation("androidx.compose.runtime:runtime-livedata:1.6.8") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4") kapt("com.github.bumptech.glide:compiler:$glideVersion") implementation("com.caverock:androidsvg-aar:1.4") diff --git a/app/src/main/java/com/github/damontecres/stashapp/FilterListActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/FilterListActivity.kt index 37bc7d88..61be6be5 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/FilterListActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/FilterListActivity.kt @@ -203,6 +203,9 @@ class FilterListActivity : FragmentActivity() { FilterType.APP_FILTER -> { intent.getParcelableExtra("filter") } + FilterType.DEFAULT_FILTER -> { + throw IllegalStateException() + } } setupFragment(filterData, true) } else { diff --git a/app/src/main/java/com/github/damontecres/stashapp/data/FilterType.kt b/app/src/main/java/com/github/damontecres/stashapp/data/FilterType.kt index bf4150d5..30599279 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/data/FilterType.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/data/FilterType.kt @@ -11,6 +11,7 @@ import com.github.damontecres.stashapp.api.type.MultiCriterionInput import com.github.damontecres.stashapp.api.type.PerformerFilterType import com.github.damontecres.stashapp.api.type.SceneFilterType import com.github.damontecres.stashapp.api.type.TagFilterType +import com.github.damontecres.stashapp.util.QueryEngine import com.github.damontecres.stashapp.util.toFind_filter import kotlinx.parcelize.Parcelize @@ -18,6 +19,7 @@ enum class FilterType { CUSTOM_FILTER, SAVED_FILTER, APP_FILTER, + DEFAULT_FILTER, ; companion object { @@ -45,6 +47,15 @@ interface StashFilter : Parcelable { get() = dataType.defaultSort.direction.toString() } +/** + * Represents a default starting filter for a data type. It should be resolved via [QueryEngine.getDefaultFilter]. + */ +@Parcelize +data class StashDefaultFilter(override val dataType: DataType) : StashFilter { + override val filterType: FilterType + get() = FilterType.DEFAULT_FILTER +} + /** * A filter used internally by the app */ diff --git a/app/src/main/java/com/github/damontecres/stashapp/playback/PlaybackMarkersFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/playback/PlaybackMarkersFragment.kt index b591cb18..2d5a9468 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/playback/PlaybackMarkersFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/playback/PlaybackMarkersFragment.kt @@ -117,6 +117,7 @@ class PlaybackMarkersFragment : PlaybackFragment() { filter as StashCustomFilter filter.toSavedFilterData() } + FilterType.DEFAULT_FILTER -> throw IllegalStateException() } if (savedFilter != null) { val newSort = filter.sortBy ?: savedFilter.find_filter?.sort diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 9bb0c221..186fc10a 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -1,6 +1,6 @@ package com.github.damontecres.stashapp.ui -import android.widget.Toast +import android.content.Intent import androidx.annotation.StringRes import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -19,25 +19,32 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat.startActivity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.tv.material3.Button import androidx.tv.material3.DrawerValue +import androidx.tv.material3.Icon import androidx.tv.material3.NavigationDrawer import androidx.tv.material3.NavigationDrawerItem import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.SettingsActivity import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.data.StashDefaultFilter import com.github.damontecres.stashapp.ui.DrawerPage.Home import com.github.damontecres.stashapp.ui.DrawerPage.Scenes +import com.github.damontecres.stashapp.util.StashServer sealed class DrawerPage( val route: String, @@ -51,9 +58,18 @@ sealed class DrawerPage( val PAGES = listOf(Home, Scenes) +class AppViewModel : ViewModel() { + val currentServer = mutableStateOf(StashServer.getCurrentStashServer()) +} + @Suppress("ktlint:standard:function-naming") @Composable fun App() { + val context = LocalContext.current + + val appViewModel = viewModel() + appViewModel.currentServer.value = StashServer.getCurrentStashServer() + val fontFamily = FontFamily( Font( @@ -134,28 +150,29 @@ fun App() { } } - // Separated item from other for better UI purpose -// NavigationDrawerItem( -// selected = currentScreen == DrawerScreen.SettingsScreen, -// onClick = { -// currentScreen = DrawerScreen.SettingsScreen -// }, -// leadingContent = { -// Icon( -// imageVector = DrawerScreen.SettingsScreen.icon, -// contentDescription = null, -// ) -// }, -// ) { -// Text(stringResource(id = DrawerScreen.SettingsScreen.title)) -// } + NavigationDrawerItem( + selected = false, + onClick = { + navController.navigate(DrawerPage.Home.route) { + popUpTo(navController.currentDestination?.route ?: "") { + inclusive = true + } + } + val intent = Intent(context, SettingsActivity::class.java) + startActivity(context, intent, null) + }, + leadingContent = { + Icon( + painter = painterResource(id = R.drawable.vector_settings), + contentDescription = null, + ) + }, + ) { + Text(stringResource(id = R.string.stashapp_settings)) + } } }, ) { - // content - - val context = LocalContext.current - NavHost( navController = navController, startDestination = DrawerPage.Home.route, @@ -168,13 +185,14 @@ fun App() { HomePage() } composable(route = DrawerPage.Scenes.route) { - Button( - onClick = { - Toast.makeText(context, "Scenes clicked", Toast.LENGTH_SHORT).show() - }, - ) { - Text(text = "Scenes") - } + FilterGrid(StashDefaultFilter(DataType.SCENE)) +// Button( +// onClick = { +// Toast.makeText(context, "Scenes clicked", Toast.LENGTH_SHORT).show() +// }, +// ) { +// Text(text = "Scenes") +// } } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index 2749b35a..bb721c37 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -1,7 +1,12 @@ package com.github.damontecres.stashapp.ui +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.MarqueeAnimationMode +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width @@ -21,8 +26,8 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.tv.material3.ClassicCard +import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage @@ -64,14 +69,15 @@ fun IconRowText( } if (oCounter > 0) { appendInlineContent(id = "ocounter", "O") - append(oCounter.toString()) + append(" $oCounter") } } + val fontSize = MaterialTheme.typography.bodySmall.fontSize val inlineContentMap = mapOf( "ocounter" to InlineTextContent( - Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter), + Placeholder(fontSize, fontSize, PlaceholderVerticalAlign.TextCenter), ) { Image( painterResource(id = R.drawable.sweat_drops), @@ -80,7 +86,12 @@ fun IconRowText( ) }, ) - Text(annotatedString, inlineContent = inlineContentMap, textAlign = TextAlign.Center) + Text( + annotatedString, + inlineContent = inlineContentMap, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) } @Suppress("ktlint:standard:function-naming") @@ -94,7 +105,7 @@ fun StashCard(item: Any) { } } -@OptIn(ExperimentalGlideComposeApi::class) +@OptIn(ExperimentalGlideComposeApi::class, ExperimentalFoundationApi::class) @Suppress("ktlint:standard:function-naming") @Composable fun SceneCard( @@ -109,6 +120,11 @@ fun SceneCard( dataTypeMap[DataType.GALLERY] = item.galleries.size ClassicCard( + modifier = + Modifier + .width(ScenePresenter.CARD_WIDTH.dp / 2) + .padding(4.dp), + contentPadding = PaddingValues(4.dp), onClick = onClick, image = { GlideImage( @@ -121,8 +137,14 @@ fun SceneCard( .height(ScenePresenter.CARD_HEIGHT.dp / 2), ) }, - title = { Text(item.titleOrFilename ?: "") }, - subtitle = { + title = { + Text( + item.titleOrFilename ?: "", + modifier = Modifier.basicMarquee(animationMode = MarqueeAnimationMode.WhileFocused), + ) + }, + subtitle = { Text(item.date ?: "") }, + description = { IconRowText(dataTypeMap, item.o_counter ?: -1) }, ) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt new file mode 100644 index 00000000..6b0f2678 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt @@ -0,0 +1,301 @@ +package com.github.damontecres.stashapp.ui + +import android.content.Context +import android.os.Parcelable +import android.util.Log +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.paging.LoadState +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.tv.foundation.ExperimentalTvFoundationApi +import androidx.tv.foundation.lazy.grid.TvGridCells +import androidx.tv.foundation.lazy.grid.TvGridItemSpan +import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid +import androidx.tv.material3.Button +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.ProvideTextStyle +import androidx.tv.material3.Text +import com.apollographql.apollo3.api.Optional +import com.github.damontecres.stashapp.StashApplication +import com.github.damontecres.stashapp.api.fragment.SavedFilterData +import com.github.damontecres.stashapp.api.type.FindFilterType +import com.github.damontecres.stashapp.api.type.SortDirectionEnum +import com.github.damontecres.stashapp.data.AppFilter +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.data.StashCustomFilter +import com.github.damontecres.stashapp.data.StashDefaultFilter +import com.github.damontecres.stashapp.data.StashFilter +import com.github.damontecres.stashapp.data.StashSavedFilter +import com.github.damontecres.stashapp.suppliers.SceneDataSupplier +import com.github.damontecres.stashapp.suppliers.StashPagingSource +import com.github.damontecres.stashapp.util.FilterParser +import com.github.damontecres.stashapp.util.QueryEngine +import com.github.damontecres.stashapp.util.ServerPreferences +import com.github.damontecres.stashapp.util.isNotNullOrBlank +import com.github.damontecres.stashapp.util.parseSortDirection +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.parcelize.Parcelize +import javax.inject.Inject + +@Parcelize +data class ResolvedFindFilter( + val q: String?, + val sort: String?, + val direction: SortDirectionEnum?, +) : Parcelable { + val asFindFilterType: FindFilterType + get() = + FindFilterType( + q = Optional.presentIfNotNull(q), + sort = Optional.presentIfNotNull(sort), + direction = Optional.presentIfNotNull(direction), + ) +} + +data class ResolvedFilter( + val dataType: DataType, + val name: String? = null, + val findFilter: ResolvedFindFilter? = null, + val objectFilter: Any? = null, +) + +val DefaultResolvedFilter = ResolvedFilter(DataType.SCENE) + +val ErrorResolvedFilter = ResolvedFilter(DataType.SCENE) + +fun SavedFilterData.resolve(dataType: DataType): ResolvedFilter = + ResolvedFilter( + dataType = DataType.fromFilterMode(mode) ?: dataType, + name = name, + // TODO need to override sort or direction? + findFilter = + ResolvedFindFilter( + find_filter?.q, + find_filter?.sort, + find_filter?.direction, + ), + objectFilter = object_filter, + ) + +sealed class ResolvedFilterState { + object Loading : ResolvedFilterState() + + data class Success(val filter: ResolvedFilter, val pagingSource: StashPagingSource<*, Any, *>) : ResolvedFilterState() + + object Error : ResolvedFilterState() +} + +@HiltViewModel +class FilterGridViewModel + @Inject + constructor( + @ApplicationContext context: Context, + ) : ViewModel() { + private val queryEngine = QueryEngine(context) + private val filterParser = FilterParser(ServerPreferences(context).serverVersion) + + private val _resolvedFilterState = MutableLiveData() + val resolvedFilterState: LiveData get() = _resolvedFilterState + + val currentFilter = mutableStateOf(DefaultResolvedFilter) +// val _pagingSource = MutableLiveData?>() +// val pagingSource: LiveData?> +// get() = _pagingSource + + suspend fun updateFilter(filter: StashFilter) { + val resolvedFilter = + when (filter) { + is AppFilter -> { + TODO() + } + + is StashDefaultFilter -> { + val savedFilter = queryEngine.getDefaultFilter(filter.dataType) + if (savedFilter != null) { + savedFilter.resolve(filter.dataType) + } else { + _resolvedFilterState.value = ResolvedFilterState.Error + null + } + } + + is StashCustomFilter -> { + ResolvedFilter( + dataType = filter.dataType, + name = filter.description, + findFilter = ResolvedFindFilter(filter.query, filter.sortBy, parseSortDirection(filter.direction)), + objectFilter = null, + ) + } + is StashSavedFilter -> { + val savedFilter = + queryEngine.getSavedFilter(filter.savedFilterId) + if (savedFilter != null) { + savedFilter.resolve(filter.dataType) + } else { + _resolvedFilterState.value = ResolvedFilterState.Error + null + } + } + else -> throw IllegalStateException("Unsupported StashFilter type: $filter") + } + + if (resolvedFilter == null) { + currentFilter.value = ErrorResolvedFilter + } else { + val dataSupplier = + when (resolvedFilter.dataType) { + DataType.SCENE -> { + val objectFilter = + filterParser.convertSceneObjectFilter(resolvedFilter.objectFilter) + SceneDataSupplier( + resolvedFilter.findFilter?.asFindFilterType, + objectFilter, + ) + } + + else -> TODO() + } as StashPagingSource.DataSupplier<*, Any, *> + + _resolvedFilterState.value = + ResolvedFilterState.Success( + resolvedFilter, + StashPagingSource( + StashApplication.getApplication(), + 25, // TODO + dataSupplier = dataSupplier, + useRandom = false, + ), + ) + currentFilter.value = resolvedFilter + } + } + } + +@Suppress("ktlint:standard:function-naming") +@Composable +fun FilterGrid(startingFilter: StashFilter) { + val viewModel = hiltViewModel() + + val resolvedFilterState by viewModel.resolvedFilterState.observeAsState(ResolvedFilterState.Loading) + + LaunchedEffect(Unit) { + viewModel.updateFilter(startingFilter) + } + + when (resolvedFilterState) { + is ResolvedFilterState.Loading -> { + Text( + text = "Querying for filter...", + modifier = + Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + ) + } + is ResolvedFilterState.Error -> { + Text( + text = "Error!!", + modifier = + Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + ) + } + is ResolvedFilterState.Success -> { + ResolvedFilterGrid(resolvedFilterState as ResolvedFilterState.Success) + } + } +} + +@OptIn(ExperimentalTvFoundationApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +fun ResolvedFilterGrid(resolvedFilter: ResolvedFilterState.Success) { +// val viewModel = hiltViewModel() + val pager = + Pager( + PagingConfig( + pageSize = 25, + prefetchDistance = 25 * 2, + initialLoadSize = 25 * 2, + ), + ) { + resolvedFilter.pagingSource + } + Log.v("ResolvedFilterGrid", "resolvedFilter.filter.name=${resolvedFilter.filter.name}") + + val lazyPagingItems = pager.flow.collectAsLazyPagingItems() + + TvLazyVerticalGrid(columns = TvGridCells.Fixed(5)) { + item("header", span = { TvGridItemSpan(this.maxLineSpan) }) { + Column { + ProvideTextStyle(MaterialTheme.typography.titleLarge) { + val filterName = + if (resolvedFilter.filter.name.isNotNullOrBlank()) { + resolvedFilter.filter.name + } else { + stringResource(id = resolvedFilter.filter.dataType.pluralStringId) + } + Text( + modifier = Modifier.fillMaxWidth(), + text = filterName, + ) + } + Button(onClick = { /*TODO*/ }) { + Text(text = "Filters") + } + } + } + if (lazyPagingItems.loadState.refresh == LoadState.Loading) { + item { + Text( + text = "Waiting for items to load from the backend", + modifier = + Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + ) + } + } + items(lazyPagingItems.itemCount) { index -> + val item = lazyPagingItems[index] + if (item != null) { + StashCard(item = item) + } + } + + if (lazyPagingItems.loadState.append == LoadState.Loading) { + item { +// CircularProgressIndicator( +// modifier = +// Modifier.fillMaxWidth() +// .wrapContentWidth(Alignment.CenterHorizontally), +// ) + Text( + text = "Waiting for items to load from the backend", + modifier = + Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + ) + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index b66f181c..952f1977 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -1,11 +1,8 @@ package com.github.damontecres.stashapp.ui import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf @@ -14,6 +11,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel +import androidx.tv.foundation.lazy.list.TvLazyColumn +import androidx.tv.foundation.lazy.list.TvLazyRow +import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.Text import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.api.ServerInfoQuery @@ -79,11 +79,11 @@ fun HomePage() { viewModel.fetchFrontPage() } - LazyColumn( + TvLazyColumn( verticalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier - .fillMaxHeight() + .fillMaxSize() .padding(12.dp), ) { items(rows, key = { rows.indexOf(it) }) { row -> @@ -97,7 +97,7 @@ fun HomePage() { fun HomePageRow(row: FrontPageParser.FrontPageRow) { val rowData = row.data!! Text(text = rowData.name, modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)) - LazyRow { + TvLazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { items(rowData.data) { item -> if (item != null) { StashCard(item) diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt index e8bda9cf..152a2dd1 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt @@ -41,6 +41,7 @@ import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.api.fragment.VideoFileData import com.github.damontecres.stashapp.api.fragment.VideoSceneData import com.github.damontecres.stashapp.api.type.FindFilterType +import com.github.damontecres.stashapp.api.type.SortDirectionEnum import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.util.Constants.STASH_API_HEADER import kotlinx.coroutines.Dispatchers @@ -699,3 +700,7 @@ val ImageData.isGif: Boolean get() = visual_files.firstOrNull()?.onVideoFile != null && visual_files.firstOrNull()?.onVideoFile!!.format == "gif" + +fun parseSortDirection(direction: String?): SortDirectionEnum? { + return SortDirectionEnum.entries.firstOrNull { it.rawValue == direction?.uppercase() } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/Provider.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Provider.kt index eb18c4cc..c6f0e400 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/Provider.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Provider.kt @@ -1,17 +1,22 @@ package com.github.damontecres.stashapp.util +import android.content.Context import com.apollographql.apollo3.ApolloClient -import com.github.damontecres.stashapp.StashApplication import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.components.ViewModelComponent +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.scopes.ViewModelScoped @Module @InstallIn(ViewModelComponent::class) object Provider { @Provides - fun createApolloClient(): ApolloClient { - return StashClient.getApolloClient(StashApplication.getApplication()) + @ViewModelScoped + fun createApolloClient( + @ApplicationContext context: Context, + ): ApolloClient { + return StashClient.getApolloClient(context) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/StashServer.kt b/app/src/main/java/com/github/damontecres/stashapp/util/StashServer.kt index 50167a3c..0fd058e0 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/StashServer.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/StashServer.kt @@ -5,9 +5,14 @@ import android.content.SharedPreferences import androidx.core.content.edit import androidx.preference.PreferenceManager import com.github.damontecres.stashapp.SettingsFragment +import com.github.damontecres.stashapp.StashApplication data class StashServer(val url: String, val apiKey: String?) { companion object { + fun getCurrentStashServer(): StashServer? { + return getCurrentStashServer(StashApplication.getApplication()) + } + fun getCurrentStashServer(context: Context): StashServer? { val manager = PreferenceManager.getDefaultSharedPreferences(context) val url = manager.getString(SettingsFragment.PREF_STASH_URL, null) From 577e2143034f2845593a32b9175c00b6c0caeb9b Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 17:02:43 -0400 Subject: [PATCH 05/36] Add saved filter dropdown --- app/build.gradle.kts | 1 + .../damontecres/stashapp/ui/FilterPage.kt | 131 ++++++++++++------ .../damontecres/stashapp/ui/theme/Theme.kt | 15 ++ 3 files changed, 107 insertions(+), 40 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 63ecd7d4..84673395 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -162,6 +162,7 @@ dependencies { implementation("androidx.activity:activity-compose:1.9.1") implementation("androidx.compose.ui:ui:1.6.8") implementation("androidx.compose.ui:ui-tooling-preview:1.6.8") + implementation("androidx.compose.material3:material3-android:1.2.1") debugImplementation("androidx.compose.ui:ui-tooling") implementation("androidx.tv:tv-foundation:1.0.0-alpha11") implementation("androidx.tv:tv-material:1.0.0-rc01") diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt index 6b0f2678..19a529f1 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt @@ -3,16 +3,25 @@ package com.github.damontecres.stashapp.ui import android.content.Context import android.os.Parcelable import android.util.Log +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.LiveData @@ -43,6 +52,7 @@ import com.github.damontecres.stashapp.data.StashFilter import com.github.damontecres.stashapp.data.StashSavedFilter import com.github.damontecres.stashapp.suppliers.SceneDataSupplier import com.github.damontecres.stashapp.suppliers.StashPagingSource +import com.github.damontecres.stashapp.ui.theme.Material3AppTheme import com.github.damontecres.stashapp.util.FilterParser import com.github.damontecres.stashapp.util.QueryEngine import com.github.damontecres.stashapp.util.ServerPreferences @@ -75,10 +85,6 @@ data class ResolvedFilter( val objectFilter: Any? = null, ) -val DefaultResolvedFilter = ResolvedFilter(DataType.SCENE) - -val ErrorResolvedFilter = ResolvedFilter(DataType.SCENE) - fun SavedFilterData.resolve(dataType: DataType): ResolvedFilter = ResolvedFilter( dataType = DataType.fromFilterMode(mode) ?: dataType, @@ -94,11 +100,11 @@ fun SavedFilterData.resolve(dataType: DataType): ResolvedFilter = ) sealed class ResolvedFilterState { - object Loading : ResolvedFilterState() + data object Loading : ResolvedFilterState() data class Success(val filter: ResolvedFilter, val pagingSource: StashPagingSource<*, Any, *>) : ResolvedFilterState() - object Error : ResolvedFilterState() + data object Error : ResolvedFilterState() } @HiltViewModel @@ -113,12 +119,16 @@ class FilterGridViewModel private val _resolvedFilterState = MutableLiveData() val resolvedFilterState: LiveData get() = _resolvedFilterState - val currentFilter = mutableStateOf(DefaultResolvedFilter) -// val _pagingSource = MutableLiveData?>() -// val pagingSource: LiveData?> -// get() = _pagingSource + private val _savedFilters = mutableStateListOf() + val savedFilters: SnapshotStateList get() = _savedFilters + + suspend fun fetchSavedFilters(dataType: DataType) { + val filters = queryEngine.getSavedFilters(dataType).map { it.resolve(dataType) } + _savedFilters.addAll(filters) + } suspend fun updateFilter(filter: StashFilter) { + _resolvedFilterState.value = ResolvedFilterState.Loading val resolvedFilter = when (filter) { is AppFilter -> { @@ -156,35 +166,37 @@ class FilterGridViewModel else -> throw IllegalStateException("Unsupported StashFilter type: $filter") } - if (resolvedFilter == null) { - currentFilter.value = ErrorResolvedFilter - } else { - val dataSupplier = - when (resolvedFilter.dataType) { - DataType.SCENE -> { - val objectFilter = - filterParser.convertSceneObjectFilter(resolvedFilter.objectFilter) - SceneDataSupplier( - resolvedFilter.findFilter?.asFindFilterType, - objectFilter, - ) - } + if (resolvedFilter != null) { + updateFilter(resolvedFilter) + } + } - else -> TODO() - } as StashPagingSource.DataSupplier<*, Any, *> + fun updateFilter(resolvedFilter: ResolvedFilter) { + _resolvedFilterState.value = ResolvedFilterState.Loading + val dataSupplier = + when (resolvedFilter.dataType) { + DataType.SCENE -> { + val objectFilter = + filterParser.convertSceneObjectFilter(resolvedFilter.objectFilter) + SceneDataSupplier( + resolvedFilter.findFilter?.asFindFilterType, + objectFilter, + ) + } - _resolvedFilterState.value = - ResolvedFilterState.Success( - resolvedFilter, - StashPagingSource( - StashApplication.getApplication(), - 25, // TODO - dataSupplier = dataSupplier, - useRandom = false, - ), - ) - currentFilter.value = resolvedFilter - } + else -> TODO() + } as StashPagingSource.DataSupplier<*, Any, *> + + _resolvedFilterState.value = + ResolvedFilterState.Success( + resolvedFilter, + StashPagingSource( + StashApplication.getApplication(), + 25, // TODO + dataSupplier = dataSupplier, + useRandom = false, + ), + ) } } @@ -198,6 +210,9 @@ fun FilterGrid(startingFilter: StashFilter) { LaunchedEffect(Unit) { viewModel.updateFilter(startingFilter) } + LaunchedEffect(Unit) { + viewModel.fetchSavedFilters(startingFilter.dataType) + } when (resolvedFilterState) { is ResolvedFilterState.Loading -> { @@ -258,9 +273,7 @@ fun ResolvedFilterGrid(resolvedFilter: ResolvedFilterState.Success) { text = filterName, ) } - Button(onClick = { /*TODO*/ }) { - Text(text = "Filters") - } + SavedFilterDropDown() } } if (lazyPagingItems.loadState.refresh == LoadState.Loading) { @@ -299,3 +312,41 @@ fun ResolvedFilterGrid(resolvedFilter: ResolvedFilterState.Success) { } } } + +@Suppress("ktlint:standard:function-naming") +@Composable +fun SavedFilterDropDown() { + val viewModel = hiltViewModel() + val context = LocalContext.current + var expanded by remember { mutableStateOf(false) } + + Box( + modifier = + Modifier.fillMaxWidth() + .wrapContentSize(Alignment.TopEnd), + ) { + Button(onClick = { expanded = !expanded }) { + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + Text("Filters") + } + } + Material3AppTheme { + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + viewModel.savedFilters.forEach { savedFilter -> + if (savedFilter.name != null) { + DropdownMenuItem( + text = { Text(savedFilter.name) }, + onClick = { + expanded = false + viewModel.updateFilter(savedFilter) + }, + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt index 04b84917..bf62f6e4 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt @@ -20,3 +20,18 @@ fun AppTheme( } MaterialTheme(colorScheme = colorScheme, content = content) } + +@Suppress("ktlint:standard:function-naming") +@Composable +fun Material3AppTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colorScheme = + if (useDarkTheme) { + androidx.compose.material3.darkColorScheme() + } else { + androidx.compose.material3.lightColorScheme() + } + androidx.compose.material3.MaterialTheme(colorScheme = colorScheme, content = content) +} From 6c4754828028d7308f41ee547dbb61b519b1eeab Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 17:23:03 -0400 Subject: [PATCH 06/36] Add items to nav drawer --- .../com/github/damontecres/stashapp/ui/App.kt | 112 +++++++++++------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 186fc10a..cd96ec0f 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -1,7 +1,11 @@ package com.github.damontecres.stashapp.ui import android.content.Intent +import android.util.Log import androidx.annotation.StringRes +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.MarqueeAnimationMode +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -39,29 +43,45 @@ import androidx.tv.material3.NavigationDrawerItem import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.SearchActivity import com.github.damontecres.stashapp.SettingsActivity import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashDefaultFilter -import com.github.damontecres.stashapp.ui.DrawerPage.Home -import com.github.damontecres.stashapp.ui.DrawerPage.Scenes import com.github.damontecres.stashapp.util.StashServer -sealed class DrawerPage( +class DrawerPage( val route: String, @StringRes val iconString: Int, @StringRes val name: Int, ) { - data object Home : DrawerPage("home", R.string.fa_house, R.string.home) - - data object Scenes : DrawerPage("scenes", DataType.SCENE.iconStringId, DataType.SCENE.pluralStringId) + companion object { + val HOME_PAGE = DrawerPage("home", R.string.fa_house, R.string.home) + + val SEARCH_PAGE = + DrawerPage( + "search", + R.string.fa_magnifying_glass_plus, + R.string.stashapp_actions_search, + ) + + val PAGES = + buildList { + add(SEARCH_PAGE) + add(HOME_PAGE) + addAll( + DataType.entries.map { dataType -> + DrawerPage(dataType.name, dataType.iconStringId, dataType.pluralStringId) + }, + ) + } + } } -val PAGES = listOf(Home, Scenes) - class AppViewModel : ViewModel() { val currentServer = mutableStateOf(StashServer.getCurrentStashServer()) } +@OptIn(ExperimentalFoundationApi::class) @Suppress("ktlint:standard:function-naming") @Composable fun App() { @@ -77,7 +97,7 @@ fun App() { ), ) - val defaultSelection: DrawerPage = DrawerPage.Home + val defaultSelection: DrawerPage = DrawerPage.HOME_PAGE var currentScreen by remember { mutableStateOf(defaultSelection) } @@ -99,20 +119,23 @@ fun App() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, ) { -// NavigationDrawerItem( -// selected = currentScreen == DrawerScreen.UserAccount, -// onClick = { -// currentScreen = DrawerScreen.UserAccount -// }, -// leadingContent = { -// Icon( -// imageVector = DrawerScreen.UserAccount.icon, -// contentDescription = null, -// ) -// }, -// ) { -// Text(stringResource(id = DrawerScreen.UserAccount.title)) -// } + NavigationDrawerItem( + selected = false, + onClick = { + // TODO + }, + leadingContent = { + Icon( + painterResource(id = R.mipmap.stash_logo), + contentDescription = null, + ) + }, + ) { + Text( + modifier = Modifier.basicMarquee(animationMode = MarqueeAnimationMode.WhileFocused), + text = StashServer.getCurrentStashServer()?.url ?: "No server", + ) + } // Group of item with same padding @@ -124,15 +147,24 @@ fun App() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically), ) { - items(PAGES, key = { it.route }) { page -> + Log.v("App", "DrawerPage.PAGES=${DrawerPage.PAGES}") + items(DrawerPage.PAGES, key = { it.route }) { page -> NavigationDrawerItem( selected = navController.currentDestination?.route == page.route, onClick = { - drawerState.setValue(DrawerValue.Closed) - navController.navigate(page.route) { - // remove the previous Composable from the back stack - popUpTo(navController.currentDestination?.route ?: "") { - inclusive = true + if (page == DrawerPage.SEARCH_PAGE) { + startActivity( + context, + Intent(context, SearchActivity::class.java), + null, + ) + } else { + drawerState.setValue(DrawerValue.Closed) + navController.navigate(page.route) { + // remove the previous Composable from the back stack + popUpTo(navController.currentDestination?.route ?: "") { + inclusive = true + } } } }, @@ -153,7 +185,7 @@ fun App() { NavigationDrawerItem( selected = false, onClick = { - navController.navigate(DrawerPage.Home.route) { + navController.navigate(DrawerPage.HOME_PAGE.route) { popUpTo(navController.currentDestination?.route ?: "") { inclusive = true } @@ -175,24 +207,22 @@ fun App() { ) { NavHost( navController = navController, - startDestination = DrawerPage.Home.route, + startDestination = DrawerPage.HOME_PAGE.route, modifier = Modifier, // .fillMaxSize() // .padding(start = collapsedDrawerItemWidth), ) { - composable(route = DrawerPage.Home.route) { + composable(route = DrawerPage.SEARCH_PAGE.route) { + // TODO + } + composable(route = DrawerPage.HOME_PAGE.route) { HomePage() } - composable(route = DrawerPage.Scenes.route) { - FilterGrid(StashDefaultFilter(DataType.SCENE)) -// Button( -// onClick = { -// Toast.makeText(context, "Scenes clicked", Toast.LENGTH_SHORT).show() -// }, -// ) { -// Text(text = "Scenes") -// } + DataType.entries.forEach { dataType -> + composable(route = dataType.name) { + FilterGrid(StashDefaultFilter(dataType)) + } } } } From 1a006e343637e2ab1daccf398bf417e96551c732 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 18:12:41 -0400 Subject: [PATCH 07/36] Fix marquee & card padding --- .../com/github/damontecres/stashapp/ui/App.kt | 2 + .../github/damontecres/stashapp/ui/Cards.kt | 129 ++++++++++++++---- .../damontecres/stashapp/ui/FilterPage.kt | 93 ++++++++++++- .../damontecres/stashapp/ui/HomePage.kt | 6 +- 4 files changed, 198 insertions(+), 32 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index cd96ec0f..89436ef2 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -6,6 +6,7 @@ import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.MarqueeAnimationMode import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -113,6 +114,7 @@ fun App() { drawerContent = { Column( Modifier + .focusGroup() .fillMaxHeight() .padding(4.dp) .width(if (drawerState.currentValue == DrawerValue.Closed) collapsedDrawerItemWidth else Dp.Unspecified), diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index bb721c37..c990eef7 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -2,8 +2,11 @@ package com.github.damontecres.stashapp.ui import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image -import androidx.compose.foundation.MarqueeAnimationMode import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -13,7 +16,15 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.focus.onFocusChanged +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -26,8 +37,16 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.tv.material3.Card +import androidx.tv.material3.CardBorder +import androidx.tv.material3.CardColors +import androidx.tv.material3.CardDefaults +import androidx.tv.material3.CardGlow +import androidx.tv.material3.CardScale +import androidx.tv.material3.CardShape import androidx.tv.material3.ClassicCard import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.Text import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage @@ -94,6 +113,77 @@ fun IconRowText( ) } +/** + * Main card based on [ClassicCard] + */ +@OptIn(ExperimentalFoundationApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +fun RootCard( + onClick: () -> Unit, + image: @Composable BoxScope.() -> Unit, + title: String, + modifier: Modifier = Modifier, + onLongClick: (() -> Unit)? = null, + subtitle: @Composable () -> Unit = {}, + description: @Composable () -> Unit = {}, + shape: CardShape = CardDefaults.shape(), + colors: CardColors = CardDefaults.colors(), + scale: CardScale = CardDefaults.scale(), + border: CardBorder = CardDefaults.border(), + glow: CardGlow = CardDefaults.glow(), + contentPadding: PaddingValues = PaddingValues(), + interactionSource: MutableInteractionSource? = null, +) { + var focused by remember { mutableStateOf(false) } + + Card( + onClick = onClick, + onLongClick = onLongClick, + modifier = + modifier.onFocusChanged { focusState -> + focused = focusState.isFocused + }, + interactionSource = interactionSource, + shape = shape, + colors = colors, + scale = scale, + border = border, + glow = glow, + ) { + Column(modifier = Modifier.padding(contentPadding)) { + Box(contentAlignment = Alignment.Center, content = image) + Column(modifier = Modifier.padding(6.dp)) { + ProvideTextStyle(MaterialTheme.typography.titleMedium) { + Text( + title, + maxLines = 1, + modifier = + Modifier + .then( + if (focused) { + Modifier.basicMarquee(initialDelayMillis = 250) + } else { + Modifier + }, + ), + ) + } + ProvideTextStyle(MaterialTheme.typography.bodySmall) { + Box(Modifier.graphicsLayer { alpha = 0.6f }) { subtitle.invoke() } + } + ProvideTextStyle(MaterialTheme.typography.bodySmall) { + Box( + Modifier.graphicsLayer { + alpha = 0.8f + }, + ) { description.invoke() } + } + } + } + } +} + @Suppress("ktlint:standard:function-naming") @Composable fun StashCard(item: Any) { @@ -118,13 +208,13 @@ fun SceneCard( dataTypeMap[DataType.MOVIE] = item.movies.size dataTypeMap[DataType.MARKER] = item.scene_markers.size dataTypeMap[DataType.GALLERY] = item.galleries.size - - ClassicCard( + val focusRequester = remember { FocusRequester() } + RootCard( modifier = Modifier - .width(ScenePresenter.CARD_WIDTH.dp / 2) - .padding(4.dp), - contentPadding = PaddingValues(4.dp), + .padding(0.dp) + .width(ScenePresenter.CARD_WIDTH.dp / 2), + contentPadding = PaddingValues(0.dp), onClick = onClick, image = { GlideImage( @@ -137,12 +227,7 @@ fun SceneCard( .height(ScenePresenter.CARD_HEIGHT.dp / 2), ) }, - title = { - Text( - item.titleOrFilename ?: "", - modifier = Modifier.basicMarquee(animationMode = MarqueeAnimationMode.WhileFocused), - ) - }, + title = item.titleOrFilename ?: "", subtitle = { Text(item.date ?: "") }, description = { IconRowText(dataTypeMap, item.o_counter ?: -1) @@ -157,19 +242,11 @@ fun PerformerCard( item: PerformerData, onClick: (() -> Unit), ) { -// val presenter = ScenePresenter() -// -// AndroidView(factory = { context -> -// val cardView = StashImageCardView(context) -// cardView.isFocusable = true -// cardView.isFocusableInTouchMode = true -// cardView.updateCardBackgroundColor(cardView, false) -// cardView.onFocusChangeListener = StashOnFocusChangeListener(context) -// cardView -// }) { view -> -// presenter.onBindViewHolder(Presenter.ViewHolder(view), scene) -// } - ClassicCard( + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(PerformerPresenter.CARD_WIDTH.dp / 2), onClick = onClick, image = { GlideImage( @@ -182,6 +259,6 @@ fun PerformerCard( .height(PerformerPresenter.CARD_HEIGHT.dp / 2), ) }, - title = { Text(item.name) }, + title = item.name, ) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt index 19a529f1..7498e7ae 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt @@ -50,8 +50,15 @@ import com.github.damontecres.stashapp.data.StashCustomFilter import com.github.damontecres.stashapp.data.StashDefaultFilter import com.github.damontecres.stashapp.data.StashFilter import com.github.damontecres.stashapp.data.StashSavedFilter +import com.github.damontecres.stashapp.suppliers.GalleryDataSupplier +import com.github.damontecres.stashapp.suppliers.ImageDataSupplier +import com.github.damontecres.stashapp.suppliers.MarkerDataSupplier +import com.github.damontecres.stashapp.suppliers.MovieDataSupplier +import com.github.damontecres.stashapp.suppliers.PerformerDataSupplier import com.github.damontecres.stashapp.suppliers.SceneDataSupplier import com.github.damontecres.stashapp.suppliers.StashPagingSource +import com.github.damontecres.stashapp.suppliers.StudioDataSupplier +import com.github.damontecres.stashapp.suppliers.TagDataSupplier import com.github.damontecres.stashapp.ui.theme.Material3AppTheme import com.github.damontecres.stashapp.util.FilterParser import com.github.damontecres.stashapp.util.QueryEngine @@ -63,6 +70,8 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.parcelize.Parcelize import javax.inject.Inject +private const val TAG = "FilterPage" + @Parcelize data class ResolvedFindFilter( val q: String?, @@ -111,7 +120,7 @@ sealed class ResolvedFilterState { class FilterGridViewModel @Inject constructor( - @ApplicationContext context: Context, + @ApplicationContext private val context: Context, ) : ViewModel() { private val queryEngine = QueryEngine(context) private val filterParser = FilterParser(ServerPreferences(context).serverVersion) @@ -140,8 +149,19 @@ class FilterGridViewModel if (savedFilter != null) { savedFilter.resolve(filter.dataType) } else { - _resolvedFilterState.value = ResolvedFilterState.Error - null + Log.v(TAG, "No default filter for ${filter.dataType}") + val sortAndDirection = filter.dataType.defaultSort + ResolvedFilter( + dataType = filter.dataType, + name = context.getString(filter.dataType.pluralStringId), + findFilter = + ResolvedFindFilter( + null, + sortAndDirection.sort, + sortAndDirection.direction, + ), + objectFilter = null, + ) } } @@ -159,6 +179,7 @@ class FilterGridViewModel if (savedFilter != null) { savedFilter.resolve(filter.dataType) } else { + Log.v(TAG, "No saved filter for id=${filter.savedFilterId}") _resolvedFilterState.value = ResolvedFilterState.Error null } @@ -183,8 +204,68 @@ class FilterGridViewModel objectFilter, ) } + DataType.TAG -> { + val objectFilter = + filterParser.convertTagObjectFilter(resolvedFilter.objectFilter) + TagDataSupplier( + resolvedFilter.findFilter?.asFindFilterType, + objectFilter, + ) + } + + DataType.STUDIO -> { + val objectFilter = + filterParser.convertStudioObjectFilter(resolvedFilter.objectFilter) + StudioDataSupplier( + resolvedFilter.findFilter?.asFindFilterType, + objectFilter, + ) + } - else -> TODO() + DataType.PERFORMER -> { + val objectFilter = + filterParser.convertPerformerObjectFilter(resolvedFilter.objectFilter) + PerformerDataSupplier( + resolvedFilter.findFilter?.asFindFilterType, + objectFilter, + ) + } + + DataType.IMAGE -> { + val objectFilter = + filterParser.convertImageObjectFilter(resolvedFilter.objectFilter) + ImageDataSupplier( + resolvedFilter.findFilter?.asFindFilterType, + objectFilter, + ) + } + + DataType.MARKER -> { + val objectFilter = + filterParser.convertMarkerObjectFilter(resolvedFilter.objectFilter) + MarkerDataSupplier( + resolvedFilter.findFilter?.asFindFilterType, + objectFilter, + ) + } + + DataType.MOVIE -> { + val objectFilter = + filterParser.convertMovieObjectFilter(resolvedFilter.objectFilter) + MovieDataSupplier( + resolvedFilter.findFilter?.asFindFilterType, + objectFilter, + ) + } + + DataType.GALLERY -> { + val objectFilter = + filterParser.convertGalleryObjectFilter(resolvedFilter.objectFilter) + GalleryDataSupplier( + resolvedFilter.findFilter?.asFindFilterType, + objectFilter, + ) + } } as StashPagingSource.DataSupplier<*, Any, *> _resolvedFilterState.value = @@ -203,6 +284,7 @@ class FilterGridViewModel @Suppress("ktlint:standard:function-naming") @Composable fun FilterGrid(startingFilter: StashFilter) { + Log.v(TAG, "startingFilter=$startingFilter") val viewModel = hiltViewModel() val resolvedFilterState by viewModel.resolvedFilterState.observeAsState(ResolvedFilterState.Loading) @@ -322,7 +404,8 @@ fun SavedFilterDropDown() { Box( modifier = - Modifier.fillMaxWidth() + Modifier + .fillMaxWidth() .wrapContentSize(Alignment.TopEnd), ) { Button(onClick = { expanded = !expanded }) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index 952f1977..5f19f680 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -1,5 +1,6 @@ package com.github.damontecres.stashapp.ui +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -97,7 +98,10 @@ fun HomePage() { fun HomePageRow(row: FrontPageParser.FrontPageRow) { val rowData = row.data!! Text(text = rowData.name, modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)) - TvLazyRow(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + TvLazyRow( + modifier = Modifier.focusGroup(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { items(rowData.data) { item -> if (item != null) { StashCard(item) From 5618b559acd04e1127ee45284c34c493f1c90ad0 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 19:29:45 -0400 Subject: [PATCH 08/36] Add video previews to cards --- .../github/damontecres/stashapp/ui/Cards.kt | 137 +++++++++++++----- 1 file changed, 104 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index c990eef7..8cdfaa73 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -1,17 +1,20 @@ package com.github.damontecres.stashapp.ui +import android.net.Uri +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent @@ -22,7 +25,6 @@ import androidx.compose.runtime.remember 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.focus.onFocusChanged import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext @@ -36,7 +38,17 @@ import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.MimeTypes +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView +import androidx.preference.PreferenceManager import androidx.tv.material3.Card import androidx.tv.material3.CardBorder import androidx.tv.material3.CardColors @@ -51,12 +63,14 @@ import androidx.tv.material3.Text import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.StashExoPlayer import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.presenters.PerformerPresenter import com.github.damontecres.stashapp.presenters.ScenePresenter import com.github.damontecres.stashapp.presenters.StashImageCardView.Companion.ICON_ORDER +import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.titleOrFilename import com.github.damontecres.stashapp.views.StashItemViewClickListener import java.util.EnumMap @@ -65,7 +79,7 @@ import java.util.EnumMap @Composable fun IconRowText( iconMap: EnumMap, - oCounter: Int, + oCounter: Int?, ) { val faFontFamily = FontFamily( @@ -86,7 +100,7 @@ fun IconRowText( append(" ") } } - if (oCounter > 0) { + if (oCounter != null && oCounter > 0) { appendInlineContent(id = "ocounter", "O") append(" $oCounter") } @@ -116,14 +130,18 @@ fun IconRowText( /** * Main card based on [ClassicCard] */ -@OptIn(ExperimentalFoundationApi::class) +@androidx.annotation.OptIn(UnstableApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalGlideComposeApi::class) @Suppress("ktlint:standard:function-naming") @Composable fun RootCard( onClick: () -> Unit, - image: @Composable BoxScope.() -> Unit, title: String, + imageWidth: Dp, + imageHeight: Dp, modifier: Modifier = Modifier, + imageUrl: String? = null, + videoUrl: String? = null, onLongClick: (() -> Unit)? = null, subtitle: @Composable () -> Unit = {}, description: @Composable () -> Unit = {}, @@ -135,15 +153,23 @@ fun RootCard( contentPadding: PaddingValues = PaddingValues(), interactionSource: MutableInteractionSource? = null, ) { + val context = LocalContext.current var focused by remember { mutableStateOf(false) } + val playVideoPreviews = + PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean("playVideoPreviews", true) + Card( onClick = onClick, onLongClick = onLongClick, modifier = - modifier.onFocusChanged { focusState -> - focused = focusState.isFocused - }, + modifier + .onFocusChanged { focusState -> + focused = focusState.isFocused + } + .padding(0.dp) + .width(imageWidth), interactionSource = interactionSource, shape = shape, colors = colors, @@ -152,8 +178,56 @@ fun RootCard( glow = glow, ) { Column(modifier = Modifier.padding(contentPadding)) { - Box(contentAlignment = Alignment.Center, content = image) + // Image/Video + Box( + modifier = modifier.size(imageWidth, imageHeight), + contentAlignment = Alignment.Center, + ) { + if (playVideoPreviews && focused && videoUrl.isNotNullOrBlank()) { + AndroidView(factory = { + PlayerView(context).apply { + hideController() + useController = false + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM + layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + + val exoPlayer = StashExoPlayer.getInstance(context) + player = exoPlayer + + val mediaItem = + MediaItem.Builder() + .setUri(Uri.parse(videoUrl)) + .setMimeType(MimeTypes.VIDEO_MP4) + .build() + + exoPlayer.setMediaItem(mediaItem, C.TIME_UNSET) + if (PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean("videoPreviewAudio", false) + ) { + exoPlayer.volume = 1f + } else { + exoPlayer.volume = 0f + } + exoPlayer.prepare() + exoPlayer.repeatMode = Player.REPEAT_MODE_ONE + exoPlayer.playWhenReady = true + exoPlayer.seekToDefaultPosition() + } + }) + } else { + GlideImage( + model = imageUrl, + contentDescription = "", + modifier = + Modifier + .padding(0.dp) + .width(imageWidth) + .height(imageHeight), + ) + } + } Column(modifier = Modifier.padding(6.dp)) { + // Title ProvideTextStyle(MaterialTheme.typography.titleMedium) { Text( title, @@ -169,9 +243,11 @@ fun RootCard( ), ) } + // Subtitle ProvideTextStyle(MaterialTheme.typography.bodySmall) { Box(Modifier.graphicsLayer { alpha = 0.6f }) { subtitle.invoke() } } + // Description ProvideTextStyle(MaterialTheme.typography.bodySmall) { Box( Modifier.graphicsLayer { @@ -208,7 +284,7 @@ fun SceneCard( dataTypeMap[DataType.MOVIE] = item.movies.size dataTypeMap[DataType.MARKER] = item.scene_markers.size dataTypeMap[DataType.GALLERY] = item.galleries.size - val focusRequester = remember { FocusRequester() } + RootCard( modifier = Modifier @@ -216,17 +292,10 @@ fun SceneCard( .width(ScenePresenter.CARD_WIDTH.dp / 2), contentPadding = PaddingValues(0.dp), onClick = onClick, - image = { - GlideImage( - model = item.paths.screenshot, - contentDescription = "", - modifier = - Modifier - .padding(0.dp) - .width(ScenePresenter.CARD_WIDTH.dp / 2) - .height(ScenePresenter.CARD_HEIGHT.dp / 2), - ) - }, + imageWidth = ScenePresenter.CARD_WIDTH.dp / 2, + imageHeight = ScenePresenter.CARD_HEIGHT.dp / 2, + imageUrl = item.paths.screenshot, + videoUrl = item.paths.preview, title = item.titleOrFilename ?: "", subtitle = { Text(item.date ?: "") }, description = { @@ -242,23 +311,25 @@ fun PerformerCard( item: PerformerData, onClick: (() -> Unit), ) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.SCENE] = item.scene_count + dataTypeMap[DataType.TAG] = item.tags.size + dataTypeMap[DataType.MOVIE] = item.movie_count + dataTypeMap[DataType.IMAGE] = item.image_count + dataTypeMap[DataType.GALLERY] = item.gallery_count + RootCard( modifier = Modifier .padding(0.dp) .width(PerformerPresenter.CARD_WIDTH.dp / 2), onClick = onClick, - image = { - GlideImage( - model = item.image_path, - contentDescription = "", - modifier = - Modifier - .padding(0.dp) - .width(PerformerPresenter.CARD_WIDTH.dp / 2) - .height(PerformerPresenter.CARD_HEIGHT.dp / 2), - ) - }, + imageWidth = PerformerPresenter.CARD_WIDTH.dp / 2, + imageHeight = PerformerPresenter.CARD_HEIGHT.dp / 2, + imageUrl = item.image_path, title = item.name, + description = { + IconRowText(dataTypeMap, item.o_counter ?: -1) + }, ) } From ce6f55e885849a33ae1cd8f91b4271cd1f4f7d60 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 19:56:16 -0400 Subject: [PATCH 09/36] Add image overlays --- .../github/damontecres/stashapp/ui/Cards.kt | 83 +++++++++++++++++-- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index 8cdfaa73..78fdda2d 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -1,13 +1,16 @@ package com.github.damontecres.stashapp.ui +import android.graphics.Color import android.net.Uri import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -33,9 +36,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp @@ -71,10 +76,47 @@ import com.github.damontecres.stashapp.presenters.PerformerPresenter import com.github.damontecres.stashapp.presenters.ScenePresenter import com.github.damontecres.stashapp.presenters.StashImageCardView.Companion.ICON_ORDER import com.github.damontecres.stashapp.util.isNotNullOrBlank +import com.github.damontecres.stashapp.util.resolutionName import com.github.damontecres.stashapp.util.titleOrFilename import com.github.damontecres.stashapp.views.StashItemViewClickListener +import com.github.damontecres.stashapp.views.durationToString +import com.github.damontecres.stashapp.views.getRatingAsDecimalString import java.util.EnumMap +@Suppress("ktlint:standard:function-naming") +@Composable +fun ImageOverlay( + rating100: Int? = null, + favorite: Boolean = false, + content: @Composable BoxScope.() -> Unit = {}, +) { + val context = LocalContext.current + val showRatings = + PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(context.getString(R.string.pref_key_show_rating), true) + + Box(modifier = Modifier.fillMaxSize()) { + if (showRatings && rating100 != null && rating100 >= 0) { + val ratingText = getRatingAsDecimalString(context, rating100) + val text = context.getString(R.string.stashapp_rating) + ": $ratingText" + val ratingColors = context.resources.obtainTypedArray(R.array.rating_colors) + val bgColor = ratingColors.getColor(rating100 / 5, Color.WHITE) + ratingColors.recycle() + + Text( + modifier = + Modifier + .align(Alignment.TopStart) + .background(color = androidx.compose.ui.graphics.Color(bgColor)) + .padding(4.dp), + style = TextStyle(fontWeight = FontWeight.Bold), + text = text, + ) + } + content.invoke(this) + } +} + @Suppress("ktlint:standard:function-naming") @Composable fun IconRowText( @@ -143,6 +185,7 @@ fun RootCard( imageUrl: String? = null, videoUrl: String? = null, onLongClick: (() -> Unit)? = null, + imageOverlay: @Composable BoxScope.() -> Unit = {}, subtitle: @Composable () -> Unit = {}, description: @Composable () -> Unit = {}, shape: CardShape = CardDefaults.shape(), @@ -215,15 +258,20 @@ fun RootCard( } }) } else { - GlideImage( - model = imageUrl, - contentDescription = "", + Box( modifier = Modifier - .padding(0.dp) .width(imageWidth) - .height(imageHeight), - ) + .height(imageHeight) + .padding(0.dp), + ) { + GlideImage( + model = imageUrl, + contentDescription = "", + modifier = Modifier, + ) + imageOverlay.invoke(this) + } } } Column(modifier = Modifier.padding(6.dp)) { @@ -301,6 +349,29 @@ fun SceneCard( description = { IconRowText(dataTypeMap, item.o_counter ?: -1) }, + imageOverlay = { + ImageOverlay(item.rating100) { + val videoFile = item.files.firstOrNull()?.videoFileData + if (videoFile != null) { + val duration = durationToString(videoFile.duration) + Text( + modifier = + Modifier + .align(Alignment.BottomEnd) + .padding(4.dp), + text = duration, + ) + Text( + modifier = + Modifier + .align(Alignment.BottomStart) + .padding(4.dp), + style = TextStyle(fontWeight = FontWeight.Bold), + text = videoFile.resolutionName().toString(), + ) + } + } + }, ) } From 5d7e502874ddc387d09c2799a2a6ed5a2be76fe3 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 20:08:43 -0400 Subject: [PATCH 10/36] More overlays --- .../github/damontecres/stashapp/ui/Cards.kt | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index 78fdda2d..be474d61 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -28,9 +28,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.Placeholder @@ -45,6 +48,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.media3.common.C import androidx.media3.common.MediaItem @@ -113,6 +117,24 @@ fun ImageOverlay( text = text, ) } + if (favorite) { + val faFontFamily = + FontFamily( + Font( + resId = R.font.fa_solid_900, + ), + ) + Text( + modifier = + Modifier + .align(Alignment.TopEnd) + .padding(8.dp), + color = colorResource(android.R.color.holo_red_light), + text = stringResource(R.string.fa_heart), + fontSize = 20.sp, + fontFamily = faFontFamily, + ) + } content.invoke(this) } } @@ -270,7 +292,9 @@ fun RootCard( contentDescription = "", modifier = Modifier, ) - imageOverlay.invoke(this) + if (!focused) { + imageOverlay.invoke(this) + } } } } @@ -369,6 +393,20 @@ fun SceneCard( style = TextStyle(fontWeight = FontWeight.Bold), text = videoFile.resolutionName().toString(), ) + if (item.resume_time != null) { + val percentWatched = item.resume_time / videoFile.duration + Box( + modifier = + Modifier + .align(Alignment.BottomStart) + .background( + androidx.compose.ui.graphics.Color.White, + ) + .clip(RectangleShape) + .height(4.dp) + .width((ScenePresenter.CARD_WIDTH * percentWatched).dp / 2), + ) + } } } }, @@ -402,5 +440,8 @@ fun PerformerCard( description = { IconRowText(dataTypeMap, item.o_counter ?: -1) }, + imageOverlay = { + ImageOverlay(favorite = item.favorite) + }, ) } From ca46f06376c02bed01f2a96532631a31939d00ea Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 20:39:50 -0400 Subject: [PATCH 11/36] Add image card --- .../com/github/damontecres/stashapp/ui/App.kt | 3 +- .../github/damontecres/stashapp/ui/Cards.kt | 64 ++++++++++++++++++- .../damontecres/stashapp/ui/FilterPage.kt | 12 +++- .../damontecres/stashapp/ui/HomePage.kt | 5 +- 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 89436ef2..ef730c79 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -210,8 +210,7 @@ fun App() { NavHost( navController = navController, startDestination = DrawerPage.HOME_PAGE.route, - modifier = - Modifier, + modifier = Modifier, // .fillMaxSize() // .padding(start = collapsedDrawerItemWidth), ) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index be474d61..9a05c7d7 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -2,6 +2,7 @@ package com.github.damontecres.stashapp.ui import android.graphics.Color import android.net.Uri +import android.os.Build import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import androidx.compose.foundation.ExperimentalFoundationApi @@ -73,12 +74,17 @@ import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.StashExoPlayer +import com.github.damontecres.stashapp.api.fragment.ImageData import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.ImagePresenter import com.github.damontecres.stashapp.presenters.PerformerPresenter import com.github.damontecres.stashapp.presenters.ScenePresenter import com.github.damontecres.stashapp.presenters.StashImageCardView.Companion.ICON_ORDER +import com.github.damontecres.stashapp.util.ageInYears +import com.github.damontecres.stashapp.util.concatIfNotBlank +import com.github.damontecres.stashapp.util.isImageClip import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.resolutionName import com.github.damontecres.stashapp.util.titleOrFilename @@ -336,10 +342,12 @@ fun RootCard( @Composable fun StashCard(item: Any) { val context = LocalContext.current + // TODO need to navigate instead val clicker = StashItemViewClickListener(context) when (item) { is SlimSceneData -> SceneCard(item, onClick = { clicker.onItemClicked(item) }) is PerformerData -> PerformerCard(item, onClick = { clicker.onItemClicked(item) }) + is ImageData -> ImageCard(item, onClick = { clicker.onItemClicked(item) }) } } @@ -413,7 +421,6 @@ fun SceneCard( ) } -@OptIn(ExperimentalGlideComposeApi::class) @Suppress("ktlint:standard:function-naming") @Composable fun PerformerCard( @@ -437,6 +444,14 @@ fun PerformerCard( imageHeight = PerformerPresenter.CARD_HEIGHT.dp / 2, imageUrl = item.image_path, title = item.name, + subtitle = { + if (item.birthdate != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val yearsOldStr = stringResource(R.string.stashapp_years_old) + Text(text = "${item.ageInYears} $yearsOldStr") + } else { + Text("") + } + }, description = { IconRowText(dataTypeMap, item.o_counter ?: -1) }, @@ -445,3 +460,50 @@ fun PerformerCard( }, ) } + +@Suppress("ktlint:standard:function-naming") +@Composable +fun ImageCard( + item: ImageData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.TAG] = item.tags.size + dataTypeMap[DataType.PERFORMER] = item.performers.size + dataTypeMap[DataType.GALLERY] = item.galleries.size + + val imageUrl = + if (item.paths.thumbnail.isNotNullOrBlank()) { + item.paths.thumbnail + } else if (item.paths.image.isNotNullOrBlank() && !item.isImageClip) { + item.paths.image + } else { + null + } + + val details = mutableListOf() + details.add(item.studio?.studioData?.name) + details.add(item.date) + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(ImagePresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = ImagePresenter.CARD_WIDTH.dp / 2, + imageHeight = ImagePresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = item.paths.preview, + title = item.title ?: "", + subtitle = { + Text(concatIfNotBlank(" - ", details)) + }, + description = { + IconRowText(dataTypeMap, item.o_counter ?: -1) + }, + imageOverlay = { + ImageOverlay(rating100 = item.rating100) + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt index 7498e7ae..8410171c 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt @@ -3,9 +3,11 @@ package com.github.damontecres.stashapp.ui import android.content.Context import android.os.Parcelable import android.util.Log +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.material3.DropdownMenu @@ -23,6 +25,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -340,9 +343,14 @@ fun ResolvedFilterGrid(resolvedFilter: ResolvedFilterState.Success) { val lazyPagingItems = pager.flow.collectAsLazyPagingItems() - TvLazyVerticalGrid(columns = TvGridCells.Fixed(5)) { + TvLazyVerticalGrid( + modifier = Modifier.padding(16.dp), + columns = TvGridCells.Fixed(5), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { item("header", span = { TvGridItemSpan(this.maxLineSpan) }) { - Column { + Column { // TODO Box? ProvideTextStyle(MaterialTheme.typography.titleLarge) { val filterName = if (resolvedFilter.filter.name.isNotNullOrBlank()) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index 5f19f680..b56fc57b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -81,11 +81,10 @@ fun HomePage() { } TvLazyColumn( - verticalArrangement = Arrangement.spacedBy(24.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier - .fillMaxSize() - .padding(12.dp), + .fillMaxSize(), ) { items(rows, key = { rows.indexOf(it) }) { row -> HomePageRow(row) From 632c829355019aff7240171dedde165a52b49316 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 20:55:00 -0400 Subject: [PATCH 12/36] Add the rest of card types --- .../github/damontecres/stashapp/ui/Cards.kt | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index 9a05c7d7..4b1d229f 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -74,15 +74,27 @@ import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.StashExoPlayer +import com.github.damontecres.stashapp.api.fragment.FullSceneData +import com.github.damontecres.stashapp.api.fragment.GalleryData import com.github.damontecres.stashapp.api.fragment.ImageData +import com.github.damontecres.stashapp.api.fragment.MarkerData +import com.github.damontecres.stashapp.api.fragment.MovieData import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.SlimSceneData +import com.github.damontecres.stashapp.api.fragment.StudioData +import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.GalleryPresenter import com.github.damontecres.stashapp.presenters.ImagePresenter +import com.github.damontecres.stashapp.presenters.MarkerPresenter +import com.github.damontecres.stashapp.presenters.MoviePresenter import com.github.damontecres.stashapp.presenters.PerformerPresenter import com.github.damontecres.stashapp.presenters.ScenePresenter import com.github.damontecres.stashapp.presenters.StashImageCardView.Companion.ICON_ORDER +import com.github.damontecres.stashapp.presenters.StudioPresenter +import com.github.damontecres.stashapp.presenters.TagPresenter import com.github.damontecres.stashapp.util.ageInYears +import com.github.damontecres.stashapp.util.asSlimeSceneData import com.github.damontecres.stashapp.util.concatIfNotBlank import com.github.damontecres.stashapp.util.isImageClip import com.github.damontecres.stashapp.util.isNotNullOrBlank @@ -92,6 +104,8 @@ import com.github.damontecres.stashapp.views.StashItemViewClickListener import com.github.damontecres.stashapp.views.durationToString import com.github.damontecres.stashapp.views.getRatingAsDecimalString import java.util.EnumMap +import kotlin.time.DurationUnit +import kotlin.time.toDuration @Suppress("ktlint:standard:function-naming") @Composable @@ -346,8 +360,18 @@ fun StashCard(item: Any) { val clicker = StashItemViewClickListener(context) when (item) { is SlimSceneData -> SceneCard(item, onClick = { clicker.onItemClicked(item) }) + is FullSceneData -> + SceneCard( + item.asSlimeSceneData, + onClick = { clicker.onItemClicked(item) }, + ) is PerformerData -> PerformerCard(item, onClick = { clicker.onItemClicked(item) }) is ImageData -> ImageCard(item, onClick = { clicker.onItemClicked(item) }) + is GalleryData -> GalleryCard(item, onClick = { clicker.onItemClicked(item) }) + is MarkerData -> MarkerCard(item, onClick = { clicker.onItemClicked(item) }) + is MovieData -> MovieCard(item, onClick = { clicker.onItemClicked(item) }) + is StudioData -> StudioCard(item, onClick = { clicker.onItemClicked(item) }) + is TagData -> TagCard(item, onClick = { clicker.onItemClicked(item) }) } } @@ -507,3 +531,229 @@ fun ImageCard( }, ) } + +@Suppress("ktlint:standard:function-naming") +@Composable +fun GalleryCard( + item: GalleryData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.TAG] = item.tags.size + dataTypeMap[DataType.PERFORMER] = item.performers.size + dataTypeMap[DataType.SCENE] = item.scenes.size + dataTypeMap[DataType.IMAGE] = item.image_count + + val imageUrl = item.cover?.paths?.thumbnail + val videoUrl = item.cover?.paths?.preview + + val details = mutableListOf() + details.add(item.studio?.name) + details.add(item.date) + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(GalleryPresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = GalleryPresenter.CARD_WIDTH.dp / 2, + imageHeight = GalleryPresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = videoUrl, + title = item.title ?: "", + subtitle = { + Text(concatIfNotBlank(" - ", details)) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = { + ImageOverlay(rating100 = item.rating100) + }, + ) +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun MarkerCard( + item: MarkerData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.TAG] = item.tags.size + + val title = + item.title.ifBlank { + item.primary_tag.tagData.name + } + " - ${item.seconds.toInt().toDuration(DurationUnit.SECONDS)}" + + val imageUrl = item.screenshot + val videoUrl = item.preview + + val details = if (item.title.isNotBlank()) item.primary_tag.tagData.name else "" + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(MarkerPresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = MarkerPresenter.CARD_WIDTH.dp / 2, + imageHeight = MarkerPresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = videoUrl, + title = title, + subtitle = { + Text(details) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = {}, + ) +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun MovieCard( + item: MovieData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.SCENE] = item.scene_count + + val title = item.name + val imageUrl = item.front_image_path + val details = item.date ?: "" + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(MoviePresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = MoviePresenter.CARD_WIDTH.dp / 2, + imageHeight = MoviePresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = null, + title = title, + subtitle = { + Text(details) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = { + ImageOverlay(rating100 = item.rating100) + }, + ) +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun StudioCard( + item: StudioData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.SCENE] = item.scene_count + dataTypeMap[DataType.PERFORMER] = item.performer_count + dataTypeMap[DataType.MOVIE] = item.movie_count + dataTypeMap[DataType.IMAGE] = item.image_count + dataTypeMap[DataType.GALLERY] = item.gallery_count + + val title = item.name + val imageUrl = item.image_path + val details = + if (item.parent_studio != null) { + stringResource(R.string.stashapp_part_of, item.parent_studio.name) + } else { + "" + } + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(StudioPresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = StudioPresenter.CARD_WIDTH.dp / 2, + imageHeight = StudioPresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = null, + title = title, + subtitle = { + Text(details) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = { + ImageOverlay(rating100 = item.rating100) + }, + ) +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun TagCard( + item: TagData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.SCENE] = item.scene_count + dataTypeMap[DataType.PERFORMER] = item.performer_count + dataTypeMap[DataType.MARKER] = item.scene_marker_count + dataTypeMap[DataType.IMAGE] = item.image_count + dataTypeMap[DataType.GALLERY] = item.gallery_count + + val title = item.name + val imageUrl = item.image_path + val details = item.description ?: "" + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(TagPresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = TagPresenter.CARD_WIDTH.dp / 2, + imageHeight = TagPresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = null, + title = title, + subtitle = { + Text(details) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = { + ImageOverlay { + if (item.child_count > 0) { + val parentText = + stringResource( + R.string.stashapp_parent_of, + item.child_count.toString(), + ) + Text( + modifier = Modifier.align(Alignment.TopStart), + text = parentText, + ) + } + if (item.parent_count > 0) { + val childText = + stringResource( + R.string.stashapp_sub_tag_of, + item.parent_count.toString(), + ) + Text( + modifier = Modifier.align(Alignment.BottomStart), + text = childText, + ) + } + } + }, + ) +} From 8becb86f0e15543d6c0e7dd20144f40beec394f3 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 21:34:32 -0400 Subject: [PATCH 13/36] Add nav for scene details --- .../com/github/damontecres/stashapp/ui/App.kt | 67 +++++++++++++++++-- .../github/damontecres/stashapp/ui/Cards.kt | 28 ++++---- .../damontecres/stashapp/ui/FilterPage.kt | 14 ++-- .../damontecres/stashapp/ui/HomePage.kt | 11 +-- 4 files changed, 91 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index ef730c79..84b46d20 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -34,9 +34,12 @@ import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavType +import androidx.navigation.activity import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument import androidx.tv.material3.DrawerValue import androidx.tv.material3.Icon import androidx.tv.material3.NavigationDrawer @@ -44,8 +47,10 @@ import androidx.tv.material3.NavigationDrawerItem import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.SceneDetailsActivity import com.github.damontecres.stashapp.SearchActivity import com.github.damontecres.stashapp.SettingsActivity +import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashDefaultFilter import com.github.damontecres.stashapp.util.StashServer @@ -55,6 +60,10 @@ class DrawerPage( @StringRes val iconString: Int, @StringRes val name: Int, ) { + fun idRoute(id: String): String { + return "$route/$id" + } + companion object { val HOME_PAGE = DrawerPage("home", R.string.fa_house, R.string.home) @@ -65,15 +74,21 @@ class DrawerPage( R.string.stashapp_actions_search, ) + val DATA_TYPE_PAGES = + buildMap { + DataType.entries.forEach { dataType -> + put( + dataType, + DrawerPage(dataType.name, dataType.iconStringId, dataType.pluralStringId), + ) + } + } + val PAGES = buildList { add(SEARCH_PAGE) add(HOME_PAGE) - addAll( - DataType.entries.map { dataType -> - DrawerPage(dataType.name, dataType.iconStringId, dataType.pluralStringId) - }, - ) + addAll(DATA_TYPE_PAGES.values) } } } @@ -214,15 +229,53 @@ fun App() { // .fillMaxSize() // .padding(start = collapsedDrawerItemWidth), ) { + val itemOnClick = { item: Any -> + val route = + when (item) { + is SlimSceneData -> { + DrawerPage.DATA_TYPE_PAGES[DataType.SCENE]!!.idRoute(item.id) + } + + else -> throw UnsupportedOperationException() + } + navController.navigate(route = route) + } + composable(route = DrawerPage.SEARCH_PAGE.route) { // TODO } composable(route = DrawerPage.HOME_PAGE.route) { - HomePage() + HomePage(itemOnClick) } DataType.entries.forEach { dataType -> + val drawerPage = DrawerPage.DATA_TYPE_PAGES[dataType]!! + composable(route = dataType.name) { - FilterGrid(StashDefaultFilter(dataType)) + FilterGrid(StashDefaultFilter(dataType), itemOnClick) + } + if (dataType == DataType.SCENE) { + activity( + route = "${drawerPage.route}/{${SceneDetailsActivity.MOVIE}}", + ) { + argument(SceneDetailsActivity.MOVIE) { + type = NavType.StringType + nullable = false + } + activityClass = SceneDetailsActivity::class + } + } else { + composable( + route = "${dataType.name}/{id}", + arguments = + listOf( + navArgument("id") { + type = NavType.StringType + nullable = false + }, + ), + ) { + TODO() + } } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index 4b1d229f..63580a19 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -100,7 +100,6 @@ import com.github.damontecres.stashapp.util.isImageClip import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.resolutionName import com.github.damontecres.stashapp.util.titleOrFilename -import com.github.damontecres.stashapp.views.StashItemViewClickListener import com.github.damontecres.stashapp.views.durationToString import com.github.damontecres.stashapp.views.getRatingAsDecimalString import java.util.EnumMap @@ -354,24 +353,25 @@ fun RootCard( @Suppress("ktlint:standard:function-naming") @Composable -fun StashCard(item: Any) { - val context = LocalContext.current - // TODO need to navigate instead - val clicker = StashItemViewClickListener(context) +fun StashCard( + item: Any, + itemOnClick: (item: Any) -> Unit, +) { when (item) { - is SlimSceneData -> SceneCard(item, onClick = { clicker.onItemClicked(item) }) + is SlimSceneData -> SceneCard(item, onClick = { itemOnClick(item) }) is FullSceneData -> SceneCard( item.asSlimeSceneData, - onClick = { clicker.onItemClicked(item) }, + onClick = { itemOnClick(item) }, ) - is PerformerData -> PerformerCard(item, onClick = { clicker.onItemClicked(item) }) - is ImageData -> ImageCard(item, onClick = { clicker.onItemClicked(item) }) - is GalleryData -> GalleryCard(item, onClick = { clicker.onItemClicked(item) }) - is MarkerData -> MarkerCard(item, onClick = { clicker.onItemClicked(item) }) - is MovieData -> MovieCard(item, onClick = { clicker.onItemClicked(item) }) - is StudioData -> StudioCard(item, onClick = { clicker.onItemClicked(item) }) - is TagData -> TagCard(item, onClick = { clicker.onItemClicked(item) }) + + is PerformerData -> PerformerCard(item, onClick = { itemOnClick(item) }) + is ImageData -> ImageCard(item, onClick = { itemOnClick(item) }) + is GalleryData -> GalleryCard(item, onClick = { itemOnClick(item) }) + is MarkerData -> MarkerCard(item, onClick = { itemOnClick(item) }) + is MovieData -> MovieCard(item, onClick = { itemOnClick(item) }) + is StudioData -> StudioCard(item, onClick = { itemOnClick(item) }) + is TagData -> TagCard(item, onClick = { itemOnClick(item) }) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt index 8410171c..f2bdb263 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt @@ -286,7 +286,10 @@ class FilterGridViewModel @Suppress("ktlint:standard:function-naming") @Composable -fun FilterGrid(startingFilter: StashFilter) { +fun FilterGrid( + startingFilter: StashFilter, + itemOnClick: (item: Any) -> Unit, +) { Log.v(TAG, "startingFilter=$startingFilter") val viewModel = hiltViewModel() @@ -319,7 +322,7 @@ fun FilterGrid(startingFilter: StashFilter) { ) } is ResolvedFilterState.Success -> { - ResolvedFilterGrid(resolvedFilterState as ResolvedFilterState.Success) + ResolvedFilterGrid(resolvedFilterState as ResolvedFilterState.Success, itemOnClick) } } } @@ -327,7 +330,10 @@ fun FilterGrid(startingFilter: StashFilter) { @OptIn(ExperimentalTvFoundationApi::class) @Suppress("ktlint:standard:function-naming") @Composable -fun ResolvedFilterGrid(resolvedFilter: ResolvedFilterState.Success) { +fun ResolvedFilterGrid( + resolvedFilter: ResolvedFilterState.Success, + itemOnClick: (item: Any) -> Unit, +) { // val viewModel = hiltViewModel() val pager = Pager( @@ -380,7 +386,7 @@ fun ResolvedFilterGrid(resolvedFilter: ResolvedFilterState.Success) { items(lazyPagingItems.itemCount) { index -> val item = lazyPagingItems[index] if (item != null) { - StashCard(item = item) + StashCard(item = item, itemOnClick) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index b56fc57b..9e27b338 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -71,7 +71,7 @@ class HomePageViewModel @Suppress("ktlint:standard:function-naming") @Composable -fun HomePage() { +fun HomePage(itemOnClick: (Any) -> Unit) { val viewModel = hiltViewModel() val rows = remember { viewModel.rows } @@ -87,14 +87,17 @@ fun HomePage() { .fillMaxSize(), ) { items(rows, key = { rows.indexOf(it) }) { row -> - HomePageRow(row) + HomePageRow(row, itemOnClick) } } } @Suppress("ktlint:standard:function-naming") @Composable -fun HomePageRow(row: FrontPageParser.FrontPageRow) { +fun HomePageRow( + row: FrontPageParser.FrontPageRow, + itemOnClick: (Any) -> Unit, +) { val rowData = row.data!! Text(text = rowData.name, modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)) TvLazyRow( @@ -103,7 +106,7 @@ fun HomePageRow(row: FrontPageParser.FrontPageRow) { ) { items(rowData.data) { item -> if (item != null) { - StashCard(item) + StashCard(item, itemOnClick) } } } From 6e004e23a5020fa2a9cde55446d917860d92d108 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 22:20:58 -0400 Subject: [PATCH 14/36] WIP nav w/ existing fragments --- app/build.gradle.kts | 1 + .../damontecres/stashapp/SearchActivity.kt | 27 ++++++- .../stashapp/StashSearchFragment.kt | 10 ++- .../com/github/damontecres/stashapp/ui/App.kt | 20 +++-- .../stashapp/ui/StashNavItemClickListener.kt | 78 +++++++++++++++++++ .../damontecres/stashapp/util/Constants.kt | 1 + 6 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 84673395..f406a76d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -158,6 +158,7 @@ dependencies { implementation("androidx.navigation:navigation-runtime-ktx:2.7.7") implementation("androidx.compose.runtime:runtime-android:1.6.8") implementation("androidx.navigation:navigation-compose:2.7.7") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") implementation(platform("androidx.compose:compose-bom:2024.06.00")) implementation("androidx.activity:activity-compose:1.9.1") implementation("androidx.compose.ui:ui:1.6.8") diff --git a/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt index 567de6b8..cc2961f1 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt @@ -1,15 +1,38 @@ package com.github.damontecres.stashapp import android.os.Bundle +import android.view.View import androidx.fragment.app.FragmentActivity +import androidx.navigation.fragment.NavHostFragment +import com.github.damontecres.stashapp.util.Constants + +class NavFragment() : NavHostFragment() { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + childFragmentManager.beginTransaction() + .add(view.id, StashSearchFragment()) + .commitNow() + } +} class SearchActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (savedInstanceState == null) { - supportFragmentManager.beginTransaction() - .replace(R.id.main_browse_fragment, StashSearchFragment()).commitNow() + if (intent.getBooleanExtra(Constants.USE_NAV_CONTROLLER, false)) { + val navFragment = NavFragment() + supportFragmentManager.beginTransaction() + .replace(R.id.main_browse_fragment, navFragment) + .commitNow() + } else { + supportFragmentManager.beginTransaction() + .replace(R.id.main_browse_fragment, StashSearchFragment()) + .commitNow() + } } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/StashSearchFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/StashSearchFragment.kt index 923506e3..a71f7c2c 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/StashSearchFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/StashSearchFragment.kt @@ -11,12 +11,15 @@ import androidx.leanback.widget.ListRowPresenter import androidx.leanback.widget.ObjectAdapter import androidx.leanback.widget.SparseArrayObjectAdapter import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.apollographql.apollo3.api.Optional import com.github.damontecres.stashapp.api.type.FindFilterType import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashCustomFilter import com.github.damontecres.stashapp.presenters.StashPresenter +import com.github.damontecres.stashapp.ui.StashNavItemClickListener +import com.github.damontecres.stashapp.util.Constants import com.github.damontecres.stashapp.util.QueryEngine import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler import com.github.damontecres.stashapp.views.StashItemViewClickListener @@ -32,7 +35,12 @@ class StashSearchFragment : SearchSupportFragment(), SearchSupportFragment.Searc override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setSearchResultProvider(this) - setOnItemViewClickedListener(StashItemViewClickListener(requireActivity())) + if (requireActivity().intent.getBooleanExtra(Constants.USE_NAV_CONTROLLER, false)) { + val navController = findNavController() + setOnItemViewClickedListener(StashNavItemClickListener(navController)) + } else { + setOnItemViewClickedListener(StashItemViewClickListener(requireActivity())) + } } override fun getResultsAdapter(): ObjectAdapter { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 84b46d20..c26d23bf 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -53,6 +53,7 @@ import com.github.damontecres.stashapp.SettingsActivity import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashDefaultFilter +import com.github.damontecres.stashapp.util.Constants import com.github.damontecres.stashapp.util.StashServer class DrawerPage( @@ -170,11 +171,12 @@ fun App() { selected = navController.currentDestination?.route == page.route, onClick = { if (page == DrawerPage.SEARCH_PAGE) { - startActivity( - context, - Intent(context, SearchActivity::class.java), - null, - ) + drawerState.setValue(DrawerValue.Closed) + navController.navigate(DrawerPage.SEARCH_PAGE.route) { + popUpTo(navController.currentDestination?.route ?: "") { + inclusive = true + } + } } else { drawerState.setValue(DrawerValue.Closed) navController.navigate(page.route) { @@ -241,8 +243,12 @@ fun App() { navController.navigate(route = route) } - composable(route = DrawerPage.SEARCH_PAGE.route) { - // TODO + activity(route = DrawerPage.SEARCH_PAGE.route) { + activityClass = SearchActivity::class + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } } composable(route = DrawerPage.HOME_PAGE.route) { HomePage(itemOnClick) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt new file mode 100644 index 00000000..0ec888d8 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt @@ -0,0 +1,78 @@ +package com.github.damontecres.stashapp.ui + +import android.util.Log +import androidx.leanback.widget.OnItemViewClickedListener +import androidx.leanback.widget.Presenter +import androidx.leanback.widget.Row +import androidx.leanback.widget.RowPresenter +import androidx.navigation.NavController +import com.github.damontecres.stashapp.actions.StashAction +import com.github.damontecres.stashapp.actions.StashActionClickedListener +import com.github.damontecres.stashapp.api.fragment.GalleryData +import com.github.damontecres.stashapp.api.fragment.ImageData +import com.github.damontecres.stashapp.api.fragment.MarkerData +import com.github.damontecres.stashapp.api.fragment.MovieData +import com.github.damontecres.stashapp.api.fragment.PerformerData +import com.github.damontecres.stashapp.api.fragment.SlimSceneData +import com.github.damontecres.stashapp.api.fragment.StudioData +import com.github.damontecres.stashapp.api.fragment.TagData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.data.OCounter +import com.github.damontecres.stashapp.data.StashCustomFilter +import com.github.damontecres.stashapp.data.StashSavedFilter + +/** + * A OnItemViewClickedListener that starts activities for scenes, performers, etc + */ +class StashNavItemClickListener( + private val navController: NavController, + private val actionListener: StashActionClickedListener? = null, +) : OnItemViewClickedListener { + fun onItemClicked(item: Any) { + onItemClicked(null, item, null, null) + } + + override fun onItemClicked( + itemViewHolder: Presenter.ViewHolder?, + item: Any, + rowViewHolder: RowPresenter.ViewHolder?, + row: Row?, + ) { + if (item is SlimSceneData) { + navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.SCENE]!!.idRoute(item.id)) + } else if (item is PerformerData) { + navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.PERFORMER]!!.idRoute(item.id)) + } else if (item is TagData) { + navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.TAG]!!.idRoute(item.id)) + } else if (item is StudioData) { + navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.STUDIO]!!.idRoute(item.id)) + } else if (item is MovieData) { + navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.MOVIE]!!.idRoute(item.id)) + } else if (item is MarkerData) { + navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.MARKER]!!.idRoute(item.id)) + } else if (item is ImageData) { + // TODO handle image switches + navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.IMAGE]!!.idRoute(item.id)) + } else if (item is GalleryData) { + navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.GALLERY]!!.idRoute(item.id)) + } else if (item is StashSavedFilter) { + throw UnsupportedOperationException() + } else if (item is StashCustomFilter) { + throw UnsupportedOperationException() + } else if (item is StashAction) { + if (actionListener != null) { + actionListener.onClicked(item) + } else { + throw RuntimeException("Action $item clicked, but no actionListener was provided!") + } + } else if (item is OCounter) { + actionListener!!.incrementOCounter(item) + } else { + Log.e(TAG, "Unknown item type: $item") + } + } + + companion object { + private const val TAG = "StashNavItemClickListener" + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt index 152a2dd1..84cc6275 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt @@ -70,6 +70,7 @@ object Constants { const val STASH_API_HEADER = "ApiKey" const val TAG = "Constants" const val OK_HTTP_CACHE_DIR = "okhttpcache" + const val USE_NAV_CONTROLLER = "useNavController" fun getNetworkCache(context: Context): Cache { val cacheSize = From a5c198d63ed78e4b880d57b35ed0cf890ac24f99 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sat, 27 Jul 2024 22:33:39 -0400 Subject: [PATCH 15/36] Generalize marquee --- .../com/github/damontecres/stashapp/ui/App.kt | 12 +++++++++--- .../com/github/damontecres/stashapp/ui/Cards.kt | 9 +-------- .../github/damontecres/stashapp/ui/Extensions.kt | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/Extensions.kt diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index c26d23bf..3fd67df4 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -4,8 +4,6 @@ import android.content.Intent import android.util.Log import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.MarqueeAnimationMode -import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -23,6 +21,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -137,7 +136,12 @@ fun App() { horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, ) { + var serverFocused by remember { mutableStateOf(false) } NavigationDrawerItem( + modifier = + Modifier.onFocusChanged { + serverFocused = it.isFocused + }, selected = false, onClick = { // TODO @@ -150,8 +154,10 @@ fun App() { }, ) { Text( - modifier = Modifier.basicMarquee(animationMode = MarqueeAnimationMode.WhileFocused), + modifier = + Modifier.enableMarquee(serverFocused), text = StashServer.getCurrentStashServer()?.url ?: "No server", + maxLines = 1, ) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt index 63580a19..c7b925ad 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt @@ -8,7 +8,6 @@ import android.widget.FrameLayout import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -325,13 +324,7 @@ fun RootCard( maxLines = 1, modifier = Modifier - .then( - if (focused) { - Modifier.basicMarquee(initialDelayMillis = 250) - } else { - Modifier - }, - ), + .enableMarquee(focused), ) } // Subtitle diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Extensions.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/Extensions.kt new file mode 100644 index 00000000..87b404f9 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/Extensions.kt @@ -0,0 +1,15 @@ +package com.github.damontecres.stashapp.ui + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.MarqueeAnimationMode +import androidx.compose.foundation.basicMarquee +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalFoundationApi::class) +fun Modifier.enableMarquee(focused: Boolean) = + if (focused) { + basicMarquee(initialDelayMillis = 250, animationMode = MarqueeAnimationMode.Immediately, velocity = 40.dp) + } else { + basicMarquee(animationMode = MarqueeAnimationMode.WhileFocused) + } From 4ae485a8e58de69021c6683a97190aed7dc9c378 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 08:19:25 -0400 Subject: [PATCH 16/36] Refactor performer activity to use ID --- .../damontecres/stashapp/PerformerActivity.kt | 27 +-- .../damontecres/stashapp/PerformerFragment.kt | 64 +++--- .../damontecres/stashapp/data/Performer.kt | 21 -- .../com/github/damontecres/stashapp/ui/App.kt | 195 +++++++++++------- .../stashapp/ui/StashNavItemClickListener.kt | 6 +- .../damontecres/stashapp/util/Constants.kt | 4 + .../views/StashItemViewClickListener.kt | 3 +- 7 files changed, 175 insertions(+), 145 deletions(-) delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/data/Performer.kt diff --git a/app/src/main/java/com/github/damontecres/stashapp/PerformerActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/PerformerActivity.kt index f8461ce2..d6566949 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/PerformerActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/PerformerActivity.kt @@ -23,7 +23,6 @@ import com.github.damontecres.stashapp.api.type.PerformerFilterType import com.github.damontecres.stashapp.api.type.SceneFilterType import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.PerformTogetherAppFilter -import com.github.damontecres.stashapp.data.Performer import com.github.damontecres.stashapp.data.PerformerWithTagAppFilter import com.github.damontecres.stashapp.presenters.PerformerPresenter import com.github.damontecres.stashapp.presenters.StashPresenter @@ -45,13 +44,13 @@ import com.github.damontecres.stashapp.util.getInt import com.github.damontecres.stashapp.views.StashItemViewClickListener class PerformerActivity : FragmentActivity() { - private lateinit var performer: Performer + private lateinit var performerId: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_performer) if (savedInstanceState == null) { - performer = this.intent.getParcelableExtra("performer")!! + performerId = this.intent.getStringExtra("id")!! val cardSize = PreferenceManager.getDefaultSharedPreferences(this) @@ -101,7 +100,7 @@ class PerformerActivity : FragmentActivity() { performers = Optional.present( MultiCriterionInput( - value = Optional.present(listOf(performer.id)), + value = Optional.present(listOf(performerId)), modifier = CriterionModifier.INCLUDES_ALL, ), ), @@ -117,7 +116,7 @@ class PerformerActivity : FragmentActivity() { performers = Optional.present( MultiCriterionInput( - value = Optional.present(listOf(performer.id)), + value = Optional.present(listOf(performerId)), modifier = CriterionModifier.INCLUDES_ALL, ), ), @@ -133,7 +132,7 @@ class PerformerActivity : FragmentActivity() { performers = Optional.present( MultiCriterionInput( - value = Optional.present(listOf(performer.id)), + value = Optional.present(listOf(performerId)), modifier = CriterionModifier.INCLUDES_ALL, ), ), @@ -149,7 +148,7 @@ class PerformerActivity : FragmentActivity() { performers = Optional.present( MultiCriterionInput( - value = Optional.present(listOf(performer.id)), + value = Optional.present(listOf(performerId)), modifier = CriterionModifier.INCLUDES_ALL, ), ), @@ -167,7 +166,7 @@ class PerformerActivity : FragmentActivity() { StashGridFragment( presenter, TagComparator, - PerformerTagDataSupplier(performer.id), + PerformerTagDataSupplier(performerId), getColumns(DataType.TAG), ) } else if (position == 5) { @@ -175,7 +174,7 @@ class PerformerActivity : FragmentActivity() { ClassPresenterSelector() .addClassPresenter( PerformerData::class.java, - PerformerPresenter(PerformTogetherLongClickCallback(performer)), + PerformerPresenter(PerformTogetherLongClickCallback(performerId)), ) StashGridFragment( presenter, @@ -185,7 +184,7 @@ class PerformerActivity : FragmentActivity() { performers = Optional.present( MultiCriterionInput( - value = Optional.present(listOf(performer.id)), + value = Optional.present(listOf(performerId)), modifier = CriterionModifier.INCLUDES_ALL, ), ), @@ -199,7 +198,7 @@ class PerformerActivity : FragmentActivity() { } } - private class PerformTogetherLongClickCallback(val performer: Performer) : + private class PerformTogetherLongClickCallback(val performerId: String) : StashPresenter.LongClickCallBack { override fun getPopUpItems( context: Context, @@ -222,8 +221,10 @@ class PerformerActivity : FragmentActivity() { } 1L -> { - val performerIds = listOf(performer.id, item.id) - val name = "${performer.name} & ${item.name}" + val performerIds = listOf(performerId, item.id) + // TODO +// val name = "${performer.name} & ${item.name}" + val name = "" val appFilter = PerformTogetherAppFilter(name, performerIds) val intent = Intent(context, FilterListActivity::class.java) intent.putExtra("filter", appFilter) diff --git a/app/src/main/java/com/github/damontecres/stashapp/PerformerFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/PerformerFragment.kt index a3b4b42a..fa5e1779 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/PerformerFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/PerformerFragment.kt @@ -1,6 +1,5 @@ package com.github.damontecres.stashapp -import android.content.Intent import android.os.Build import android.os.Bundle import android.util.Log @@ -16,7 +15,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.type.CircumisedEnum -import com.github.damontecres.stashapp.data.Performer import com.github.damontecres.stashapp.presenters.PerformerPresenter import com.github.damontecres.stashapp.presenters.StashPresenter import com.github.damontecres.stashapp.util.MutationEngine @@ -63,44 +61,40 @@ class PerformerFragment : Fragment(R.layout.performer_view) { favoriteButton = view.findViewById(R.id.favorite_button) favoriteButton.onFocusChangeListener = StashOnFocusChangeListener(requireContext()) - val performer = requireActivity().intent.getParcelableExtra("performer") - if (performer != null) { - mPerformerName.text = performer.name - mPerformerDisambiguation.text = performer.disambiguation - - val lock = ReentrantReadWriteLock() - queryEngine = QueryEngine(requireContext(), true, lock) - mutationEngine = MutationEngine(requireContext(), true, lock) - - val exceptionHandler = - CoroutineExceptionHandler { _, ex -> - Log.e(TAG, "Error fetching data", ex) - Toast.makeText( - requireContext(), - "Error fetching data: ${ex.message}", - Toast.LENGTH_LONG, - ).show() - } - viewLifecycleOwner.lifecycleScope.launch(exceptionHandler) { - val perf = queryEngine.getPerformer(performer.id) - if (perf == null) { - Toast.makeText( - requireContext(), - "Performer not found: ${performer.id}", - Toast.LENGTH_LONG, - ).show() - return@launch - } else { - updateUi(perf) - } + val performerId = requireActivity().intent.getStringExtra("id")!! + + val lock = ReentrantReadWriteLock() + queryEngine = QueryEngine(requireContext(), true, lock) + mutationEngine = MutationEngine(requireContext(), true, lock) + + val exceptionHandler = + CoroutineExceptionHandler { _, ex -> + Log.e(TAG, "Error fetching data", ex) + Toast.makeText( + requireContext(), + "Error fetching data: ${ex.message}", + Toast.LENGTH_LONG, + ).show() + } + viewLifecycleOwner.lifecycleScope.launch(exceptionHandler) { + val perf = queryEngine.getPerformer(performerId) + if (perf == null) { + Toast.makeText( + requireContext(), + "Performer not found: $performerId", + Toast.LENGTH_LONG, + ).show() + return@launch + } else { + updateUi(perf) } - } else { - val intent = Intent(requireActivity(), MainActivity::class.java) - startActivity(intent) } } private fun updateUi(perf: PerformerData) { + mPerformerName.text = perf.name + mPerformerDisambiguation.text = perf.disambiguation + favoriteButton.isFocusable = true if (perf.favorite) { favoriteButton.setTextColor( diff --git a/app/src/main/java/com/github/damontecres/stashapp/data/Performer.kt b/app/src/main/java/com/github/damontecres/stashapp/data/Performer.kt deleted file mode 100644 index 67bd7b8a..00000000 --- a/app/src/main/java/com/github/damontecres/stashapp/data/Performer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.damontecres.stashapp.data - -import android.os.Parcelable -import com.github.damontecres.stashapp.api.fragment.PerformerData -import com.github.damontecres.stashapp.api.type.GenderEnum -import kotlinx.parcelize.Parcelize - -@Parcelize -data class Performer( - val id: String, - val name: String, - val disambiguation: String?, - val gender: GenderEnum?, -) : Parcelable { - constructor(p: PerformerData) : this( - id = p.id, - name = p.name, - disambiguation = p.disambiguation, - gender = p.gender, - ) -} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 3fd67df4..c0fab6e1 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -1,6 +1,5 @@ package com.github.damontecres.stashapp.ui -import android.content.Intent import android.util.Log import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi @@ -30,7 +29,6 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat.startActivity import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavType @@ -38,22 +36,32 @@ import androidx.navigation.activity import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument import androidx.tv.material3.DrawerValue import androidx.tv.material3.Icon import androidx.tv.material3.NavigationDrawer import androidx.tv.material3.NavigationDrawerItem import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState +import com.github.damontecres.stashapp.PerformerActivity import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.SceneDetailsActivity +import com.github.damontecres.stashapp.SceneDetailsFragment.Companion.POSITION_ARG import com.github.damontecres.stashapp.SearchActivity import com.github.damontecres.stashapp.SettingsActivity +import com.github.damontecres.stashapp.api.fragment.GalleryData +import com.github.damontecres.stashapp.api.fragment.ImageData +import com.github.damontecres.stashapp.api.fragment.MarkerData +import com.github.damontecres.stashapp.api.fragment.MovieData +import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.SlimSceneData +import com.github.damontecres.stashapp.api.fragment.StudioData +import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashDefaultFilter +import com.github.damontecres.stashapp.playback.PlaybackActivity import com.github.damontecres.stashapp.util.Constants import com.github.damontecres.stashapp.util.StashServer +import com.github.damontecres.stashapp.util.secondsMs class DrawerPage( val route: String, @@ -74,6 +82,13 @@ class DrawerPage( R.string.stashapp_actions_search, ) + val SETTINGS_PAGE = + DrawerPage( + "settings", + R.string.fa_arrow_right_arrow_left, // Ignored + R.string.stashapp_settings, + ) + val DATA_TYPE_PAGES = buildMap { DataType.entries.forEach { dataType -> @@ -84,15 +99,34 @@ class DrawerPage( } } + fun dataType(dataType: DataType): DrawerPage { + return DATA_TYPE_PAGES[dataType]!! + } + val PAGES = buildList { add(SEARCH_PAGE) add(HOME_PAGE) addAll(DATA_TYPE_PAGES.values) + add(SETTINGS_PAGE) } } } +sealed class Routes { + companion object { + val PLAYBACK = + "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE}}/play/{$POSITION_ARG]" + + fun playback( + sceneId: String, + position: Long, + ): String { + return "${DrawerPage.dataType(DataType.SCENE).route}/$sceneId/play/$position" + } + } +} + class AppViewModel : ViewModel() { val currentServer = mutableStateOf(StashServer.getCurrentStashServer()) } @@ -176,57 +210,34 @@ fun App() { NavigationDrawerItem( selected = navController.currentDestination?.route == page.route, onClick = { - if (page == DrawerPage.SEARCH_PAGE) { - drawerState.setValue(DrawerValue.Closed) - navController.navigate(DrawerPage.SEARCH_PAGE.route) { - popUpTo(navController.currentDestination?.route ?: "") { - inclusive = true - } - } - } else { - drawerState.setValue(DrawerValue.Closed) - navController.navigate(page.route) { - // remove the previous Composable from the back stack - popUpTo(navController.currentDestination?.route ?: "") { - inclusive = true - } + drawerState.setValue(DrawerValue.Closed) + navController.navigate(page.route) { + // remove the previous Composable from the back stack + popUpTo(navController.currentDestination?.route ?: "") { + inclusive = true } } }, leadingContent = { - Text( - stringResource(id = page.iconString), - fontFamily = fontFamily, - textAlign = TextAlign.Center, - modifier = Modifier, - ) + if (page != DrawerPage.SETTINGS_PAGE) { + Text( + stringResource(id = page.iconString), + fontFamily = fontFamily, + textAlign = TextAlign.Center, + modifier = Modifier, + ) + } else { + Icon( + painter = painterResource(id = R.drawable.vector_settings), + contentDescription = null, + ) + } }, ) { Text(stringResource(id = page.name)) } } } - - NavigationDrawerItem( - selected = false, - onClick = { - navController.navigate(DrawerPage.HOME_PAGE.route) { - popUpTo(navController.currentDestination?.route ?: "") { - inclusive = true - } - } - val intent = Intent(context, SettingsActivity::class.java) - startActivity(context, intent, null) - }, - leadingContent = { - Icon( - painter = painterResource(id = R.drawable.vector_settings), - contentDescription = null, - ) - }, - ) { - Text(stringResource(id = R.string.stashapp_settings)) - } } }, ) { @@ -241,7 +252,35 @@ fun App() { val route = when (item) { is SlimSceneData -> { - DrawerPage.DATA_TYPE_PAGES[DataType.SCENE]!!.idRoute(item.id) + DrawerPage.dataType(DataType.SCENE).idRoute(item.id) + } + + is GalleryData -> { + DrawerPage.dataType(DataType.GALLERY).idRoute(item.id) + } + + is ImageData -> { + DrawerPage.dataType(DataType.IMAGE).idRoute(item.id) + } + + is MarkerData -> { + Routes.playback(item.scene.videoSceneData.id, item.secondsMs) + } + + is MovieData -> { + DrawerPage.dataType(DataType.MOVIE).idRoute(item.id) + } + + is PerformerData -> { + DrawerPage.dataType(DataType.PERFORMER).idRoute(item.id) + } + + is StudioData -> { + DrawerPage.dataType(DataType.STUDIO).idRoute(item.id) + } + + is TagData -> { + DrawerPage.dataType(DataType.TAG).idRoute(item.id) } else -> throw UnsupportedOperationException() @@ -249,6 +288,9 @@ fun App() { navController.navigate(route = route) } + composable(route = DrawerPage.HOME_PAGE.route) { + HomePage(itemOnClick) + } activity(route = DrawerPage.SEARCH_PAGE.route) { activityClass = SearchActivity::class argument(Constants.USE_NAV_CONTROLLER) { @@ -256,39 +298,48 @@ fun App() { defaultValue = true } } - composable(route = DrawerPage.HOME_PAGE.route) { - HomePage(itemOnClick) + activity(route = DrawerPage.SETTINGS_PAGE.route) { + activityClass = SettingsActivity::class + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } } DataType.entries.forEach { dataType -> - val drawerPage = DrawerPage.DATA_TYPE_PAGES[dataType]!! - composable(route = dataType.name) { FilterGrid(StashDefaultFilter(dataType), itemOnClick) } - if (dataType == DataType.SCENE) { - activity( - route = "${drawerPage.route}/{${SceneDetailsActivity.MOVIE}}", - ) { - argument(SceneDetailsActivity.MOVIE) { - type = NavType.StringType - nullable = false - } - activityClass = SceneDetailsActivity::class - } - } else { - composable( - route = "${dataType.name}/{id}", - arguments = - listOf( - navArgument("id") { - type = NavType.StringType - nullable = false - }, - ), - ) { - TODO() - } + } + + activity( + route = "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE}}", + ) { + argument(SceneDetailsActivity.MOVIE) { + type = NavType.StringType + nullable = false + } + activityClass = SceneDetailsActivity::class + } + + activity(route = "${DrawerPage.dataType(DataType.PERFORMER).route}/{id}") { + argument("id") { + type = NavType.StringType + nullable = false + } + activityClass = PerformerActivity::class + } + + activity(route = Routes.PLAYBACK) { + argument(SceneDetailsActivity.MOVIE) { + type = NavType.StringType + nullable = false + } + argument(POSITION_ARG) { + type = NavType.LongType + nullable = false + defaultValue = 0L } + activityClass = PlaybackActivity::class } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt index 0ec888d8..6b1da1d8 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt @@ -41,7 +41,7 @@ class StashNavItemClickListener( if (item is SlimSceneData) { navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.SCENE]!!.idRoute(item.id)) } else if (item is PerformerData) { - navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.PERFORMER]!!.idRoute(item.id)) + navController.navigate(DrawerPage.dataType(DataType.PERFORMER).idRoute(item.id)) } else if (item is TagData) { navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.TAG]!!.idRoute(item.id)) } else if (item is StudioData) { @@ -49,7 +49,9 @@ class StashNavItemClickListener( } else if (item is MovieData) { navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.MOVIE]!!.idRoute(item.id)) } else if (item is MarkerData) { - navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.MARKER]!!.idRoute(item.id)) + val route = + Routes.playback(item.scene.videoSceneData.id, (item.seconds * 1000).toLong()) + navController.navigate(route) } else if (item is ImageData) { // TODO handle image switches navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.IMAGE]!!.idRoute(item.id)) diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt index 84cc6275..a2bf0ac4 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt @@ -33,6 +33,7 @@ import com.github.damontecres.stashapp.api.ServerInfoQuery import com.github.damontecres.stashapp.api.fragment.FullSceneData import com.github.damontecres.stashapp.api.fragment.GalleryData import com.github.damontecres.stashapp.api.fragment.ImageData +import com.github.damontecres.stashapp.api.fragment.MarkerData import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.SavedFilterData import com.github.damontecres.stashapp.api.fragment.SlimSceneData @@ -702,6 +703,9 @@ val ImageData.isGif: Boolean visual_files.firstOrNull()?.onVideoFile != null && visual_files.firstOrNull()?.onVideoFile!!.format == "gif" +val MarkerData.secondsMs: Long + get() = (seconds * 1000).toLong() + fun parseSortDirection(direction: String?): SortDirectionEnum? { return SortDirectionEnum.entries.firstOrNull { it.rawValue == direction?.uppercase() } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt b/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt index 58c46218..c135db0e 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt @@ -29,7 +29,6 @@ import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.Movie import com.github.damontecres.stashapp.data.OCounter -import com.github.damontecres.stashapp.data.Performer import com.github.damontecres.stashapp.data.StashCustomFilter import com.github.damontecres.stashapp.data.StashSavedFilter import com.github.damontecres.stashapp.playback.PlaybackActivity @@ -59,7 +58,7 @@ class StashItemViewClickListener( context.startActivity(intent) } else if (item is PerformerData) { val intent = Intent(context, PerformerActivity::class.java) - intent.putExtra("performer", Performer(item)) + intent.putExtra("id", item.id) context.startActivity(intent, null) } else if (item is TagData) { val intent = Intent(context, TagActivity::class.java) From 1e9f2416972acd15a3360e55b1f108fa3e7145e4 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 08:22:59 -0400 Subject: [PATCH 17/36] Fix marker playback --- app/src/main/java/com/github/damontecres/stashapp/ui/App.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index c0fab6e1..e83378bc 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -116,7 +116,7 @@ class DrawerPage( sealed class Routes { companion object { val PLAYBACK = - "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE}}/play/{$POSITION_ARG]" + "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE_ID}}/play/{$POSITION_ARG}" fun playback( sceneId: String, @@ -330,7 +330,7 @@ fun App() { } activity(route = Routes.PLAYBACK) { - argument(SceneDetailsActivity.MOVIE) { + argument(SceneDetailsActivity.MOVIE_ID) { type = NavType.StringType nullable = false } From 6a84534a17f9a69b52578dce3b162f018a83f7b1 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 08:32:36 -0400 Subject: [PATCH 18/36] Add toggle for compose UI --- app/src/main/AndroidManifest.xml | 6 +++++- .../{RootActivity.kt => MainComposeActivity.kt} | 2 +- .../com/github/damontecres/stashapp/PinActivity.kt | 12 +++++++++++- app/src/main/res/values/preferences.xml | 1 + app/src/main/res/xml/advanced_preferences.xml | 6 ++++++ 5 files changed, 24 insertions(+), 3 deletions(-) rename app/src/main/java/com/github/damontecres/stashapp/{RootActivity.kt => MainComposeActivity.kt} (95%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8babe4e8..27c06ee9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -31,7 +31,7 @@ android:theme="@style/Theme.StashAppAndroidTV" android:usesCleartextTraffic="true"> + forcedDirectContainers playback.showDebugInfo playback.experimentalFeatures + useComposeUI diff --git a/app/src/main/res/xml/advanced_preferences.xml b/app/src/main/res/xml/advanced_preferences.xml index 25b9efa5..1bfb3a15 100644 --- a/app/src/main/res/xml/advanced_preferences.xml +++ b/app/src/main/res/xml/advanced_preferences.xml @@ -163,6 +163,12 @@ app:summaryOn="Enabled" app:summaryOff="Disabled" app:defaultValue="false" /> + Date: Sun, 28 Jul 2024 10:26:20 -0400 Subject: [PATCH 19/36] Hook up all data types --- .../damontecres/stashapp/GalleryActivity.kt | 4 +- .../damontecres/stashapp/ImageActivity.kt | 2 +- .../damontecres/stashapp/MovieActivity.kt | 81 ++++++++++--------- .../damontecres/stashapp/MovieFragment.kt | 69 +++++++--------- .../damontecres/stashapp/StudioActivity.kt | 4 +- .../damontecres/stashapp/TagActivity.kt | 6 +- .../com/github/damontecres/stashapp/ui/App.kt | 54 ++++++++++++- .../stashapp/ui/StashNavItemClickListener.kt | 14 ++-- .../views/StashItemViewClickListener.kt | 11 +-- 9 files changed, 142 insertions(+), 103 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt index ead01294..d16f3807 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt @@ -53,7 +53,6 @@ class GalleryActivity : FragmentActivity() { setContentView(R.layout.activity_gallery) if (savedInstanceState == null) { val galleryId = intent.getStringExtra(INTENT_GALLERY_ID)!! - val galleryName = intent.getStringExtra(INTENT_GALLERY_NAME) val cardSize = PreferenceManager.getDefaultSharedPreferences(this) @@ -274,7 +273,6 @@ class GalleryActivity : FragmentActivity() { } companion object { - const val INTENT_GALLERY_ID = "gallery.id" - const val INTENT_GALLERY_NAME = "gallery.name" + const val INTENT_GALLERY_ID = "id" } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt index 2f103de1..3c1dbe29 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt @@ -247,7 +247,7 @@ class ImageActivity : FragmentActivity() { companion object { const val TAG = "ImageActivity" - const val INTENT_IMAGE_ID = "image.id" + const val INTENT_IMAGE_ID = "id" const val INTENT_IMAGE_URL = "image.url" const val INTENT_IMAGE_SIZE = "image.size" diff --git a/app/src/main/java/com/github/damontecres/stashapp/MovieActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/MovieActivity.kt index 2c8d17e3..07b5c6b7 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/MovieActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/MovieActivity.kt @@ -3,6 +3,7 @@ package com.github.damontecres.stashapp import android.os.Bundle import androidx.fragment.app.FragmentActivity import androidx.leanback.widget.ObjectAdapter +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import com.apollographql.apollo3.api.Optional import com.github.damontecres.stashapp.api.type.CriterionModifier @@ -10,56 +11,62 @@ import com.github.damontecres.stashapp.api.type.FindFilterType import com.github.damontecres.stashapp.api.type.MultiCriterionInput import com.github.damontecres.stashapp.api.type.SceneFilterType import com.github.damontecres.stashapp.api.type.SortDirectionEnum -import com.github.damontecres.stashapp.data.Movie import com.github.damontecres.stashapp.suppliers.SceneDataSupplier +import com.github.damontecres.stashapp.util.QueryEngine import com.github.damontecres.stashapp.util.SceneComparator +import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler import com.github.damontecres.stashapp.util.getInt +import kotlinx.coroutines.launch class MovieActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_movie) if (savedInstanceState == null) { - val movie = this.intent.getParcelableExtra("movie") - val cardSize = - PreferenceManager.getDefaultSharedPreferences(this) - .getInt("cardSize", getString(R.string.card_size_default)) - // At medium size, 3 scenes fit in the space vs 5 normally - val columns = cardSize * 3 / 5 + val movieId = intent.getStringExtra("id")!! + lifecycleScope.launch(StashCoroutineExceptionHandler()) { + val queryEngine = QueryEngine(this@MovieActivity) + val movie = queryEngine.getMovie(movieId)!! + val cardSize = + PreferenceManager.getDefaultSharedPreferences(this@MovieActivity) + .getInt("cardSize", getString(R.string.card_size_default)) + // At medium size, 3 scenes fit in the space vs 5 normally + val columns = cardSize * 3 / 5 - val sceneFragment = - StashGridFragment( - SceneComparator, - SceneDataSupplier( - FindFilterType( - sort = Optional.present("movie_scene_number"), - direction = Optional.present(SortDirectionEnum.ASC), - ), - SceneFilterType( - movies = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(movie?.id.toString())), - modifier = CriterionModifier.INCLUDES, + val sceneFragment = + StashGridFragment( + SceneComparator, + SceneDataSupplier( + FindFilterType( + sort = Optional.present("movie_scene_number"), + direction = Optional.present(SortDirectionEnum.ASC), + ), + SceneFilterType( + movies = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(movie?.id.toString())), + modifier = CriterionModifier.INCLUDES, + ), ), - ), + ), ), - ), - columns, - ) + columns, + ) - supportFragmentManager.beginTransaction() - .replace(R.id.movie_fragment, MovieFragment()) - .replace(R.id.movie_list_fragment, sceneFragment) - .commitNow() - sceneFragment.pagingAdapter.registerObserver( - object : ObjectAdapter.DataObserver() { - override fun onChanged() { - sceneFragment.view!!.requestFocus() - sceneFragment.pagingAdapter.unregisterObserver(this) - } - }, - ) + supportFragmentManager.beginTransaction() + .replace(R.id.movie_fragment, MovieFragment(movie)) + .replace(R.id.movie_list_fragment, sceneFragment) + .commitNow() + sceneFragment.pagingAdapter.registerObserver( + object : ObjectAdapter.DataObserver() { + override fun onChanged() { + sceneFragment.view!!.requestFocus() + sceneFragment.pagingAdapter.unregisterObserver(this) + } + }, + ) + } } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/MovieFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/MovieFragment.kt index a3465a08..90f446e7 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/MovieFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/MovieFragment.kt @@ -1,6 +1,5 @@ package com.github.damontecres.stashapp -import android.content.Intent import android.os.Bundle import android.view.View import android.widget.ImageView @@ -8,19 +7,15 @@ import android.widget.TableLayout import android.widget.TableRow import android.widget.TextView import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import com.github.damontecres.stashapp.data.Movie +import com.github.damontecres.stashapp.api.fragment.MovieData import com.github.damontecres.stashapp.presenters.MoviePresenter import com.github.damontecres.stashapp.presenters.StashPresenter -import com.github.damontecres.stashapp.util.QueryEngine -import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler import com.github.damontecres.stashapp.util.StashGlide import com.github.damontecres.stashapp.views.parseTimeToString -import kotlinx.coroutines.launch import kotlin.time.DurationUnit import kotlin.time.toDuration -class MovieFragment : Fragment(R.layout.movie_view) { +class MovieFragment(private val movie: MovieData) : Fragment(R.layout.movie_view) { private lateinit var frontImage: ImageView private lateinit var backImage: ImageView private lateinit var titleText: TextView @@ -38,42 +33,32 @@ class MovieFragment : Fragment(R.layout.movie_view) { table = view.findViewById(R.id.movie_table) - val movie = requireActivity().intent.getParcelableExtra("movie") - if (movie != null) { - titleText.text = movie.name - if (movie.frontImagePath != null) { - configureLayout(frontImage) - StashGlide.with(requireActivity(), movie.frontImagePath) - .optionalCenterCrop() - .error(StashPresenter.glideError(requireContext())) - .into(frontImage) - } - if (movie.backImagePath != null) { - configureLayout(backImage) - StashGlide.with(requireActivity(), movie.backImagePath) - .optionalCenterCrop() - .error(StashPresenter.glideError(requireContext())) - .into(backImage) - } - viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) { - val queryEngine = QueryEngine(requireContext()) - val movie = queryEngine.getMovie(movie.id) - addRow( - R.string.stashapp_duration, - movie?.duration?.toDuration(DurationUnit.MINUTES)?.toString(), - ) - addRow(R.string.stashapp_date, movie?.date) - addRow(R.string.stashapp_studio, movie?.studio?.name) - addRow(R.string.stashapp_director, movie?.director) - addRow(R.string.stashapp_synopsis, movie?.synopsis) - addRow(R.string.stashapp_created_at, parseTimeToString(movie?.created_at)) - addRow(R.string.stashapp_updated_at, parseTimeToString(movie?.updated_at)) - table.setColumnShrinkable(1, true) - } - } else { - val intent = Intent(requireActivity(), MainActivity::class.java) - startActivity(intent) + titleText.text = movie.name + if (movie.front_image_path != null) { + configureLayout(frontImage) + StashGlide.with(requireActivity(), movie.front_image_path) + .optionalCenterCrop() + .error(StashPresenter.glideError(requireContext())) + .into(frontImage) } + if (movie.back_image_path != null) { + configureLayout(backImage) + StashGlide.with(requireActivity(), movie.back_image_path) + .optionalCenterCrop() + .error(StashPresenter.glideError(requireContext())) + .into(backImage) + } + addRow( + R.string.stashapp_duration, + movie?.duration?.toDuration(DurationUnit.MINUTES)?.toString(), + ) + addRow(R.string.stashapp_date, movie?.date) + addRow(R.string.stashapp_studio, movie?.studio?.name) + addRow(R.string.stashapp_director, movie?.director) + addRow(R.string.stashapp_synopsis, movie?.synopsis) + addRow(R.string.stashapp_created_at, parseTimeToString(movie?.created_at)) + addRow(R.string.stashapp_updated_at, parseTimeToString(movie?.updated_at)) + table.setColumnShrinkable(1, true) } private fun configureLayout(view: ImageView) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/StudioActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/StudioActivity.kt index 21ba4a97..3f9c99cc 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/StudioActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/StudioActivity.kt @@ -34,7 +34,7 @@ class StudioActivity : TabbedGridFragmentActivity() { } override fun getPagerAdapter(): PagerAdapter { - val studioId = this.intent.getIntExtra("studioId", -1) + val studioId = this.intent.getStringExtra("id")!! val tabTitles = listOf( getString(DataType.SCENE.pluralStringId), @@ -44,7 +44,7 @@ class StudioActivity : TabbedGridFragmentActivity() { getString(DataType.MOVIE.pluralStringId), getString(R.string.stashapp_subsidiary_studios), ) - return StudioPagerAdapter(tabTitles, studioId.toString(), supportFragmentManager) + return StudioPagerAdapter(tabTitles, studioId, supportFragmentManager) } class StudioPagerAdapter( diff --git a/app/src/main/java/com/github/damontecres/stashapp/TagActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/TagActivity.kt index 6708711b..02be8da8 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/TagActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/TagActivity.kt @@ -29,11 +29,13 @@ import com.github.damontecres.stashapp.util.TagComparator class TagActivity : TabbedGridFragmentActivity() { override fun getTitleText(): CharSequence? { - return intent.getStringExtra("tagName") + // TODO + return null +// return intent.getStringExtra("tagName") } override fun getPagerAdapter(): PagerAdapter { - val tagId = intent.getStringExtra("tagId")!! + val tagId = intent.getStringExtra("id")!! val includeSubTags = intent.getBooleanExtra("includeSubTags", false) val tabs = listOf( diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index e83378bc..955a2d9d 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -42,12 +42,17 @@ import androidx.tv.material3.NavigationDrawer import androidx.tv.material3.NavigationDrawerItem import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState +import com.github.damontecres.stashapp.GalleryActivity +import com.github.damontecres.stashapp.ImageActivity +import com.github.damontecres.stashapp.MovieActivity import com.github.damontecres.stashapp.PerformerActivity import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.SceneDetailsActivity import com.github.damontecres.stashapp.SceneDetailsFragment.Companion.POSITION_ARG import com.github.damontecres.stashapp.SearchActivity import com.github.damontecres.stashapp.SettingsActivity +import com.github.damontecres.stashapp.StudioActivity +import com.github.damontecres.stashapp.TagActivity import com.github.damontecres.stashapp.api.fragment.GalleryData import com.github.damontecres.stashapp.api.fragment.ImageData import com.github.damontecres.stashapp.api.fragment.MarkerData @@ -124,6 +129,13 @@ sealed class Routes { ): String { return "${DrawerPage.dataType(DataType.SCENE).route}/$sceneId/play/$position" } + + fun dataType( + dataType: DataType, + id: String, + ): String { + return "${DrawerPage.dataType(dataType).route}/$id" + } } } @@ -306,7 +318,7 @@ fun App() { } } DataType.entries.forEach { dataType -> - composable(route = dataType.name) { + composable(route = DrawerPage.dataType(dataType).route) { FilterGrid(StashDefaultFilter(dataType), itemOnClick) } } @@ -329,6 +341,46 @@ fun App() { activityClass = PerformerActivity::class } + activity(route = "${DrawerPage.dataType(DataType.GALLERY).route}/{id}") { + argument("id") { + type = NavType.StringType + nullable = false + } + activityClass = GalleryActivity::class + } + + activity(route = "${DrawerPage.dataType(DataType.IMAGE).route}/{id}") { + argument("id") { + type = NavType.StringType + nullable = false + } + activityClass = ImageActivity::class + } + + activity(route = "${DrawerPage.dataType(DataType.MOVIE).route}/{id}") { + argument("id") { + type = NavType.StringType + nullable = false + } + activityClass = MovieActivity::class + } + + activity(route = "${DrawerPage.dataType(DataType.STUDIO).route}/{id}") { + argument("id") { + type = NavType.StringType + nullable = false + } + activityClass = StudioActivity::class + } + + activity(route = "${DrawerPage.dataType(DataType.TAG).route}/{id}") { + argument("id") { + type = NavType.StringType + nullable = false + } + activityClass = TagActivity::class + } + activity(route = Routes.PLAYBACK) { argument(SceneDetailsActivity.MOVIE_ID) { type = NavType.StringType diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt index 6b1da1d8..9d415fe8 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt @@ -39,24 +39,24 @@ class StashNavItemClickListener( row: Row?, ) { if (item is SlimSceneData) { - navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.SCENE]!!.idRoute(item.id)) + navController.navigate(Routes.dataType(DataType.SCENE, item.id)) } else if (item is PerformerData) { - navController.navigate(DrawerPage.dataType(DataType.PERFORMER).idRoute(item.id)) + navController.navigate(Routes.dataType(DataType.PERFORMER, item.id)) } else if (item is TagData) { - navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.TAG]!!.idRoute(item.id)) + navController.navigate(Routes.dataType(DataType.TAG, item.id)) } else if (item is StudioData) { - navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.STUDIO]!!.idRoute(item.id)) + navController.navigate(Routes.dataType(DataType.STUDIO, item.id)) } else if (item is MovieData) { - navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.MOVIE]!!.idRoute(item.id)) + navController.navigate(Routes.dataType(DataType.MOVIE, item.id)) } else if (item is MarkerData) { val route = Routes.playback(item.scene.videoSceneData.id, (item.seconds * 1000).toLong()) navController.navigate(route) } else if (item is ImageData) { // TODO handle image switches - navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.IMAGE]!!.idRoute(item.id)) + navController.navigate(Routes.dataType(DataType.IMAGE, item.id)) } else if (item is GalleryData) { - navController.navigate(DrawerPage.DATA_TYPE_PAGES[DataType.GALLERY]!!.idRoute(item.id)) + navController.navigate(Routes.dataType(DataType.GALLERY, item.id)) } else if (item is StashSavedFilter) { throw UnsupportedOperationException() } else if (item is StashCustomFilter) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt b/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt index c135db0e..8ee5edfe 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt @@ -27,13 +27,11 @@ import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.api.fragment.StudioData import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.DataType -import com.github.damontecres.stashapp.data.Movie import com.github.damontecres.stashapp.data.OCounter import com.github.damontecres.stashapp.data.StashCustomFilter import com.github.damontecres.stashapp.data.StashSavedFilter import com.github.damontecres.stashapp.playback.PlaybackActivity import com.github.damontecres.stashapp.util.addToIntent -import com.github.damontecres.stashapp.util.name /** * A OnItemViewClickedListener that starts activities for scenes, performers, etc @@ -62,17 +60,15 @@ class StashItemViewClickListener( context.startActivity(intent, null) } else if (item is TagData) { val intent = Intent(context, TagActivity::class.java) - intent.putExtra("tagId", item.id) - intent.putExtra("tagName", item.name) + intent.putExtra("id", item.id) context.startActivity(intent) } else if (item is StudioData) { val intent = Intent(context, StudioActivity::class.java) - intent.putExtra("studioId", item.id.toInt()) - intent.putExtra("studioName", item.name) + intent.putExtra("id", item.id) context.startActivity(intent) } else if (item is MovieData) { val intent = Intent(context, MovieActivity::class.java) - intent.putExtra("movie", Movie(item)) + intent.putExtra("id", item.id) context.startActivity(intent) } else if (item is MarkerData) { val intent = Intent(context, PlaybackActivity::class.java) @@ -86,7 +82,6 @@ class StashItemViewClickListener( } else if (item is GalleryData) { val intent = Intent(context, GalleryActivity::class.java) intent.putExtra(GalleryActivity.INTENT_GALLERY_ID, item.id) - intent.putExtra(GalleryActivity.INTENT_GALLERY_NAME, item.name) context.startActivity(intent) } else if (item is StashSavedFilter) { val intent = Intent(context, FilterListActivity::class.java) From d22e29515ba8e16bac7cf5ae89492c1e8140bbf1 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 10:47:31 -0400 Subject: [PATCH 20/36] Wrap fragments for compose navigation --- .../damontecres/stashapp/GalleryActivity.kt | 113 +++++----- .../damontecres/stashapp/ImageActivity.kt | 28 ++- .../damontecres/stashapp/MovieActivity.kt | 18 +- .../damontecres/stashapp/NavFragment.kt | 21 ++ .../damontecres/stashapp/PerformerActivity.kt | 202 ++++++++++-------- .../stashapp/SceneDetailsActivity.kt | 13 +- .../damontecres/stashapp/SearchActivity.kt | 20 +- .../damontecres/stashapp/StashGridFragment.kt | 9 +- .../damontecres/stashapp/StudioActivity.kt | 167 ++++++++------- .../damontecres/stashapp/TagActivity.kt | 181 ++++++++-------- .../com/github/damontecres/stashapp/ui/App.kt | 32 +++ .../damontecres/stashapp/util/Constants.kt | 4 + 12 files changed, 464 insertions(+), 344 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/NavFragment.kt diff --git a/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt index d16f3807..61e52141 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt @@ -41,6 +41,7 @@ import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler import com.github.damontecres.stashapp.util.StashGlide import com.github.damontecres.stashapp.util.TagComparator import com.github.damontecres.stashapp.util.getInt +import com.github.damontecres.stashapp.util.isNavHostActive import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.name import com.github.damontecres.stashapp.util.onlyScrollIfNeeded @@ -72,10 +73,15 @@ class GalleryActivity : FragmentActivity() { tabLayout.nextFocusDownId = R.id.gallery_view_pager tabLayout.children.forEach { it.nextFocusDownId = R.id.gallery_view_pager } tabLayout.children.first().requestFocus() - - supportFragmentManager.beginTransaction() - .replace(R.id.gallery_details, GalleryFragment(gallery)) - .commitNow() + if (isNavHostActive()) { + supportFragmentManager.beginTransaction() + .replace(R.id.gallery_details, NavFragment(GalleryFragment(gallery))) + .commitNow() + } else { + supportFragmentManager.beginTransaction() + .replace(R.id.gallery_details, GalleryFragment(gallery)) + .commitNow() + } } } } @@ -99,14 +105,39 @@ class GalleryActivity : FragmentActivity() { } override fun getItem(position: Int): Fragment { - return when (position) { - 0 -> { - val fragment = + val createdFragment = + when (position) { + 0 -> { + val fragment = + StashGridFragment( + ImageComparator, + ImageDataSupplier( + DataType.IMAGE.asDefaultFindFilterType, + ImageFilterType( + galleries = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(gallery.id)), + modifier = CriterionModifier.INCLUDES_ALL, + ), + ), + ), + ), + getColumns(DataType.IMAGE), + ) + fragment.onItemViewClickedListener = + ImageGridClickedListener(this@GalleryActivity, fragment) { + it.putExtra(ImageActivity.INTENT_GALLERY_ID, gallery.id) + } + fragment + } + + 1 -> StashGridFragment( - ImageComparator, - ImageDataSupplier( - DataType.IMAGE.asDefaultFindFilterType, - ImageFilterType( + SceneComparator, + SceneDataSupplier( + DataType.SCENE.asDefaultFindFilterType, + SceneFilterType( galleries = Optional.present( MultiCriterionInput( @@ -116,47 +147,29 @@ class GalleryActivity : FragmentActivity() { ), ), ), - getColumns(DataType.IMAGE), + getColumns(DataType.SCENE), + ) + + 2 -> + StashGridFragment( + PerformerComparator, + GalleryPerformerDataSupplier(gallery), + getColumns(DataType.PERFORMER), ) - fragment.onItemViewClickedListener = - ImageGridClickedListener(this@GalleryActivity, fragment) { - it.putExtra(ImageActivity.INTENT_GALLERY_ID, gallery.id) - } - fragment + + 3 -> + StashGridFragment( + TagComparator, + GalleryTagDataSupplier(gallery), + getColumns(DataType.TAG), + ) + + else -> throw IllegalArgumentException() } - 1 -> - StashGridFragment( - SceneComparator, - SceneDataSupplier( - DataType.SCENE.asDefaultFindFilterType, - SceneFilterType( - galleries = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(gallery.id)), - modifier = CriterionModifier.INCLUDES_ALL, - ), - ), - ), - ), - getColumns(DataType.SCENE), - ) - - 2 -> - StashGridFragment( - PerformerComparator, - GalleryPerformerDataSupplier(gallery), - getColumns(DataType.PERFORMER), - ) - - 3 -> - StashGridFragment( - TagComparator, - GalleryTagDataSupplier(gallery), - getColumns(DataType.TAG), - ) - - else -> throw IllegalArgumentException() + return if (isNavHostActive()) { + NavFragment(createdFragment) + } else { + createdFragment } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt index 3c1dbe29..a7bd9b16 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt @@ -52,6 +52,7 @@ import com.github.damontecres.stashapp.util.concatIfNotBlank import com.github.damontecres.stashapp.util.height import com.github.damontecres.stashapp.util.isGif import com.github.damontecres.stashapp.util.isImageClip +import com.github.damontecres.stashapp.util.isNavHostActive import com.github.damontecres.stashapp.util.maxFileSize import com.github.damontecres.stashapp.util.showSetRatingToast import com.github.damontecres.stashapp.util.toFindFilterType @@ -88,9 +89,15 @@ class ImageActivity : FragmentActivity() { } else { ImageFragment(image) } - supportFragmentManager.beginTransaction() - .replace(R.id.image_fragment, imageFragment as Fragment) - .commitNow() + if (isNavHostActive()) { + supportFragmentManager.beginTransaction() + .replace(R.id.image_fragment, NavFragment(imageFragment as Fragment)) + .commitNow() + } else { + supportFragmentManager.beginTransaction() + .replace(R.id.image_fragment, imageFragment as Fragment) + .commitNow() + } val pageSize = PreferenceManager.getDefaultSharedPreferences(this@ImageActivity) @@ -231,9 +238,18 @@ class ImageActivity : FragmentActivity() { } else { ImageFragment(image) } - supportFragmentManager.beginTransaction() - .replace(R.id.image_fragment, imageFragment as Fragment) - .commitNow() + if (isNavHostActive()) { + supportFragmentManager.beginTransaction() + .replace( + R.id.image_fragment, + NavFragment(imageFragment as Fragment), + ) + .commitNow() + } else { + supportFragmentManager.beginTransaction() + .replace(R.id.image_fragment, imageFragment as Fragment) + .commitNow() + } } else if (image == null) { Toast.makeText(this@ImageActivity, "No more images", Toast.LENGTH_SHORT) .show() diff --git a/app/src/main/java/com/github/damontecres/stashapp/MovieActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/MovieActivity.kt index 07b5c6b7..959ae0a0 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/MovieActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/MovieActivity.kt @@ -16,6 +16,7 @@ import com.github.damontecres.stashapp.util.QueryEngine import com.github.damontecres.stashapp.util.SceneComparator import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler import com.github.damontecres.stashapp.util.getInt +import com.github.damontecres.stashapp.util.isNavHostActive import kotlinx.coroutines.launch class MovieActivity : FragmentActivity() { @@ -53,11 +54,6 @@ class MovieActivity : FragmentActivity() { ), columns, ) - - supportFragmentManager.beginTransaction() - .replace(R.id.movie_fragment, MovieFragment(movie)) - .replace(R.id.movie_list_fragment, sceneFragment) - .commitNow() sceneFragment.pagingAdapter.registerObserver( object : ObjectAdapter.DataObserver() { override fun onChanged() { @@ -66,6 +62,18 @@ class MovieActivity : FragmentActivity() { } }, ) + + if (isNavHostActive()) { + supportFragmentManager.beginTransaction() + .replace(R.id.movie_fragment, NavFragment(MovieFragment(movie))) + .replace(R.id.movie_list_fragment, NavFragment(sceneFragment)) + .commitNow() + } else { + supportFragmentManager.beginTransaction() + .replace(R.id.movie_fragment, MovieFragment(movie)) + .replace(R.id.movie_list_fragment, sceneFragment) + .commitNow() + } } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/NavFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/NavFragment.kt new file mode 100644 index 00000000..c03fb9aa --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/NavFragment.kt @@ -0,0 +1,21 @@ +package com.github.damontecres.stashapp + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.NavHostFragment + +/** + * Wraps a fragment for navigation + */ +class NavFragment(private val fragment: Fragment) : NavHostFragment() { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { + super.onViewCreated(view, savedInstanceState) + childFragmentManager.beginTransaction() + .add(view.id, fragment) + .commitNow() + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/PerformerActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/PerformerActivity.kt index d6566949..1f828e07 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/PerformerActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/PerformerActivity.kt @@ -41,6 +41,7 @@ import com.github.damontecres.stashapp.util.PerformerComparator import com.github.damontecres.stashapp.util.SceneComparator import com.github.damontecres.stashapp.util.TagComparator import com.github.damontecres.stashapp.util.getInt +import com.github.damontecres.stashapp.util.isNavHostActive import com.github.damontecres.stashapp.views.StashItemViewClickListener class PerformerActivity : FragmentActivity() { @@ -65,10 +66,15 @@ class PerformerActivity : FragmentActivity() { tabLayout.nextFocusDownId = R.id.performer_view_pager tabLayout.children.forEach { it.nextFocusDownId = R.id.performer_view_pager } - - supportFragmentManager.beginTransaction() - .replace(R.id.performer_details, PerformerFragment()) - .commitNow() + if (isNavHostActive()) { + supportFragmentManager.beginTransaction() + .replace(R.id.performer_details, NavFragment(PerformerFragment())) + .commitNow() + } else { + supportFragmentManager.beginTransaction() + .replace(R.id.performer_details, PerformerFragment()) + .commitNow() + } } } @@ -92,108 +98,114 @@ class PerformerActivity : FragmentActivity() { } override fun getItem(position: Int): Fragment { - return if (position == 0) { - StashGridFragment( - SceneComparator, - SceneDataSupplier( - SceneFilterType( - performers = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(performerId)), - modifier = CriterionModifier.INCLUDES_ALL, + val createdFragment = + if (position == 0) { + StashGridFragment( + SceneComparator, + SceneDataSupplier( + SceneFilterType( + performers = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(performerId)), + modifier = CriterionModifier.INCLUDES_ALL, + ), ), - ), + ), ), - ), - getColumns(DataType.SCENE), - ) - } else if (position == 1) { - StashGridFragment( - GalleryComparator, - GalleryDataSupplier( - GalleryFilterType( - performers = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(performerId)), - modifier = CriterionModifier.INCLUDES_ALL, + getColumns(DataType.SCENE), + ) + } else if (position == 1) { + StashGridFragment( + GalleryComparator, + GalleryDataSupplier( + GalleryFilterType( + performers = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(performerId)), + modifier = CriterionModifier.INCLUDES_ALL, + ), ), - ), + ), ), - ), - getColumns(DataType.GALLERY), - ) - } else if (position == 2) { - StashGridFragment( - ImageComparator, - ImageDataSupplier( - ImageFilterType( - performers = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(performerId)), - modifier = CriterionModifier.INCLUDES_ALL, + getColumns(DataType.GALLERY), + ) + } else if (position == 2) { + StashGridFragment( + ImageComparator, + ImageDataSupplier( + ImageFilterType( + performers = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(performerId)), + modifier = CriterionModifier.INCLUDES_ALL, + ), ), - ), + ), ), - ), - getColumns(DataType.IMAGE), - ) - } else if (position == 3) { - StashGridFragment( - MovieComparator, - MovieDataSupplier( - MovieFilterType( - performers = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(performerId)), - modifier = CriterionModifier.INCLUDES_ALL, + getColumns(DataType.IMAGE), + ) + } else if (position == 3) { + StashGridFragment( + MovieComparator, + MovieDataSupplier( + MovieFilterType( + performers = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(performerId)), + modifier = CriterionModifier.INCLUDES_ALL, + ), ), - ), + ), ), - ), - getColumns(DataType.MOVIE), - ) - } else if (position == 4) { - val presenter = - ClassPresenterSelector() - .addClassPresenter( - TagData::class.java, - TagPresenter(PerformersWithTagLongClickCallback()), - ) - StashGridFragment( - presenter, - TagComparator, - PerformerTagDataSupplier(performerId), - getColumns(DataType.TAG), - ) - } else if (position == 5) { - val presenter = - ClassPresenterSelector() - .addClassPresenter( - PerformerData::class.java, - PerformerPresenter(PerformTogetherLongClickCallback(performerId)), - ) - StashGridFragment( - presenter, - PerformerComparator, - PerformerDataSupplier( - PerformerFilterType( - performers = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(performerId)), - modifier = CriterionModifier.INCLUDES_ALL, + getColumns(DataType.MOVIE), + ) + } else if (position == 4) { + val presenter = + ClassPresenterSelector() + .addClassPresenter( + TagData::class.java, + TagPresenter(PerformersWithTagLongClickCallback()), + ) + StashGridFragment( + presenter, + TagComparator, + PerformerTagDataSupplier(performerId), + getColumns(DataType.TAG), + ) + } else if (position == 5) { + val presenter = + ClassPresenterSelector() + .addClassPresenter( + PerformerData::class.java, + PerformerPresenter(PerformTogetherLongClickCallback(performerId)), + ) + StashGridFragment( + presenter, + PerformerComparator, + PerformerDataSupplier( + PerformerFilterType( + performers = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(performerId)), + modifier = CriterionModifier.INCLUDES_ALL, + ), ), - ), + ), ), - ), - getColumns(DataType.PERFORMER), - ) + getColumns(DataType.PERFORMER), + ) + } else { + throw IllegalStateException() + } + return if (this@PerformerActivity.isNavHostActive()) { + NavFragment(createdFragment) } else { - throw IllegalStateException() + createdFragment } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsActivity.kt index 72897f3e..f4355b3f 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsActivity.kt @@ -2,6 +2,7 @@ package com.github.damontecres.stashapp import android.os.Bundle import androidx.fragment.app.FragmentActivity +import com.github.damontecres.stashapp.util.isNavHostActive /** * Details activity class that loads [SceneDetailsFragment] class. @@ -11,9 +12,15 @@ class SceneDetailsActivity : FragmentActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_details) if (savedInstanceState == null) { - supportFragmentManager.beginTransaction() - .replace(R.id.details_fragment, SceneDetailsFragment()) - .commitNow() + if (isNavHostActive()) { + supportFragmentManager.beginTransaction() + .replace(R.id.details_fragment, NavFragment(SceneDetailsFragment())) + .commitNow() + } else { + supportFragmentManager.beginTransaction() + .replace(R.id.details_fragment, SceneDetailsFragment()) + .commitNow() + } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt index cc2961f1..cdf13fd2 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt @@ -1,30 +1,16 @@ package com.github.damontecres.stashapp import android.os.Bundle -import android.view.View import androidx.fragment.app.FragmentActivity -import androidx.navigation.fragment.NavHostFragment -import com.github.damontecres.stashapp.util.Constants - -class NavFragment() : NavHostFragment() { - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - childFragmentManager.beginTransaction() - .add(view.id, StashSearchFragment()) - .commitNow() - } -} +import com.github.damontecres.stashapp.util.isNavHostActive class SearchActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) if (savedInstanceState == null) { - if (intent.getBooleanExtra(Constants.USE_NAV_CONTROLLER, false)) { - val navFragment = NavFragment() + if (isNavHostActive()) { + val navFragment = NavFragment(StashSearchFragment()) supportFragmentManager.beginTransaction() .replace(R.id.main_browse_fragment, navFragment) .commitNow() diff --git a/app/src/main/java/com/github/damontecres/stashapp/StashGridFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/StashGridFragment.kt index 27b78997..a610cece 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/StashGridFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/StashGridFragment.kt @@ -12,6 +12,7 @@ import androidx.leanback.widget.FocusHighlight import androidx.leanback.widget.PresenterSelector import androidx.leanback.widget.VerticalGridPresenter import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn @@ -20,9 +21,11 @@ import androidx.recyclerview.widget.DiffUtil import com.apollographql.apollo3.api.Query import com.github.damontecres.stashapp.presenters.StashPresenter import com.github.damontecres.stashapp.suppliers.StashPagingSource +import com.github.damontecres.stashapp.ui.StashNavItemClickListener import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler import com.github.damontecres.stashapp.util.animateToVisible import com.github.damontecres.stashapp.util.getInt +import com.github.damontecres.stashapp.util.isNavHostActive import com.github.damontecres.stashapp.views.StashItemViewClickListener import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest @@ -73,7 +76,11 @@ class StashGridFragment( totalCountTextView = view.findViewById(R.id.total_count_text) if (onItemViewClickedListener == null) { - onItemViewClickedListener = StashItemViewClickListener(requireActivity()) + if (requireActivity().isNavHostActive()) { + onItemViewClickedListener = StashNavItemClickListener(findNavController()) + } else { + onItemViewClickedListener = StashItemViewClickListener(requireActivity()) + } } val pageSize = diff --git a/app/src/main/java/com/github/damontecres/stashapp/StudioActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/StudioActivity.kt index 3f9c99cc..cdb110a8 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/StudioActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/StudioActivity.kt @@ -27,6 +27,7 @@ import com.github.damontecres.stashapp.util.MovieComparator import com.github.damontecres.stashapp.util.PerformerComparator import com.github.damontecres.stashapp.util.SceneComparator import com.github.damontecres.stashapp.util.StudioComparator +import com.github.damontecres.stashapp.util.isNavHostActive class StudioActivity : TabbedGridFragmentActivity() { override fun getTitleText(): CharSequence? { @@ -47,105 +48,111 @@ class StudioActivity : TabbedGridFragmentActivity() { return StudioPagerAdapter(tabTitles, studioId, supportFragmentManager) } - class StudioPagerAdapter( + inner class StudioPagerAdapter( tabTitles: List, private val studioId: String, fm: FragmentManager, ) : ListFragmentPagerAdapter(tabTitles, fm) { override fun getItem(position: Int): Fragment { - return if (position == 0) { - StashGridFragment( - SceneComparator, - SceneDataSupplier( - SceneFilterType( - studios = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(studioId)), - modifier = CriterionModifier.INCLUDES, + val createdFragment = + if (position == 0) { + StashGridFragment( + SceneComparator, + SceneDataSupplier( + SceneFilterType( + studios = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(studioId)), + modifier = CriterionModifier.INCLUDES, + ), ), - ), + ), ), - ), - ) - } else if (position == 1) { - StashGridFragment( - GalleryComparator, - GalleryDataSupplier( - GalleryFilterType( - studios = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(studioId)), - modifier = CriterionModifier.INCLUDES, + ) + } else if (position == 1) { + StashGridFragment( + GalleryComparator, + GalleryDataSupplier( + GalleryFilterType( + studios = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(studioId)), + modifier = CriterionModifier.INCLUDES, + ), ), - ), + ), ), - ), - ) - } else if (position == 2) { - StashGridFragment( - ImageComparator, - ImageDataSupplier( - ImageFilterType( - studios = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(studioId)), - modifier = CriterionModifier.INCLUDES, + ) + } else if (position == 2) { + StashGridFragment( + ImageComparator, + ImageDataSupplier( + ImageFilterType( + studios = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(studioId)), + modifier = CriterionModifier.INCLUDES, + ), ), - ), + ), ), - ), - ) - } else if (position == 3) { - StashGridFragment( - PerformerComparator, - PerformerDataSupplier( - PerformerFilterType( - studios = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(studioId)), - modifier = CriterionModifier.INCLUDES, + ) + } else if (position == 3) { + StashGridFragment( + PerformerComparator, + PerformerDataSupplier( + PerformerFilterType( + studios = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(studioId)), + modifier = CriterionModifier.INCLUDES, + ), ), - ), + ), ), - ), - ) - } else if (position == 4) { - StashGridFragment( - MovieComparator, - MovieDataSupplier( - MovieFilterType( - studios = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(studioId)), - modifier = CriterionModifier.INCLUDES, + ) + } else if (position == 4) { + StashGridFragment( + MovieComparator, + MovieDataSupplier( + MovieFilterType( + studios = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(studioId)), + modifier = CriterionModifier.INCLUDES, + ), ), - ), + ), ), - ), - ) - } else if (position == 5) { - StashGridFragment( - StudioComparator, - StudioDataSupplier( - StudioFilterType( - parents = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(studioId)), - modifier = CriterionModifier.INCLUDES, + ) + } else if (position == 5) { + StashGridFragment( + StudioComparator, + StudioDataSupplier( + StudioFilterType( + parents = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(studioId)), + modifier = CriterionModifier.INCLUDES, + ), ), - ), + ), ), - ), - ) + ) + } else { + throw IllegalStateException() + } + return if (this@StudioActivity.isNavHostActive()) { + NavFragment(createdFragment) } else { - throw IllegalStateException() + createdFragment } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/TagActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/TagActivity.kt index 02be8da8..f58bab59 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/TagActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/TagActivity.kt @@ -26,6 +26,7 @@ import com.github.damontecres.stashapp.util.MarkerComparator import com.github.damontecres.stashapp.util.PerformerComparator import com.github.damontecres.stashapp.util.SceneComparator import com.github.damontecres.stashapp.util.TagComparator +import com.github.damontecres.stashapp.util.isNavHostActive class TagActivity : TabbedGridFragmentActivity() { override fun getTitleText(): CharSequence? { @@ -50,7 +51,7 @@ class TagActivity : TabbedGridFragmentActivity() { return TabPageAdapter(tabs, tagId, includeSubTags, supportFragmentManager) } - class TabPageAdapter( + inner class TabPageAdapter( tabs: List, private val tagId: String, private val includeSubTags: Boolean, @@ -59,105 +60,111 @@ class TagActivity : TabbedGridFragmentActivity() { ListFragmentPagerAdapter(tabs, fm) { override fun getItem(position: Int): Fragment { val depth = Optional.present(if (includeSubTags) -1 else 0) - return if (position == 0) { - StashGridFragment( - SceneComparator, - SceneDataSupplier( - SceneFilterType( - tags = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(tagId)), - modifier = CriterionModifier.INCLUDES_ALL, - depth = depth, + val createdFragment = + if (position == 0) { + StashGridFragment( + SceneComparator, + SceneDataSupplier( + SceneFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tagId)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = depth, + ), ), - ), + ), ), - ), - ) - } else if (position == 1) { - StashGridFragment( - GalleryComparator, - GalleryDataSupplier( - GalleryFilterType( - tags = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(tagId)), - modifier = CriterionModifier.INCLUDES, - depth = depth, + ) + } else if (position == 1) { + StashGridFragment( + GalleryComparator, + GalleryDataSupplier( + GalleryFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tagId)), + modifier = CriterionModifier.INCLUDES, + depth = depth, + ), ), - ), + ), ), - ), - ) - } else if (position == 2) { - StashGridFragment( - ImageComparator, - ImageDataSupplier( - ImageFilterType( - tags = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(tagId)), - modifier = CriterionModifier.INCLUDES, - depth = depth, + ) + } else if (position == 2) { + StashGridFragment( + ImageComparator, + ImageDataSupplier( + ImageFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tagId)), + modifier = CriterionModifier.INCLUDES, + depth = depth, + ), ), - ), + ), ), - ), - ) - } else if (position == 3) { - StashGridFragment( - MarkerComparator, - MarkerDataSupplier( - SceneMarkerFilterType( - tags = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(tagId)), - modifier = CriterionModifier.INCLUDES_ALL, - depth = depth, + ) + } else if (position == 3) { + StashGridFragment( + MarkerComparator, + MarkerDataSupplier( + SceneMarkerFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tagId)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = depth, + ), ), - ), + ), ), - ), - ) - } else if (position == 4) { - StashGridFragment( - PerformerComparator, - PerformerDataSupplier( - PerformerFilterType( - tags = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(tagId)), - modifier = CriterionModifier.INCLUDES_ALL, - depth = depth, + ) + } else if (position == 4) { + StashGridFragment( + PerformerComparator, + PerformerDataSupplier( + PerformerFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tagId)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = depth, + ), ), - ), + ), ), - ), - ) - } else if (position == 5) { - StashGridFragment( - TagComparator, - TagDataSupplier( - DataType.TAG.asDefaultFindFilterType, - TagFilterType( - parents = - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(tagId)), - modifier = CriterionModifier.INCLUDES_ALL, - depth = depth, + ) + } else if (position == 5) { + StashGridFragment( + TagComparator, + TagDataSupplier( + DataType.TAG.asDefaultFindFilterType, + TagFilterType( + parents = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tagId)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = depth, + ), ), - ), + ), ), - ), - ) + ) + } else { + throw IllegalStateException() + } + return if (this@TagActivity.isNavHostActive()) { + NavFragment(createdFragment) } else { - throw IllegalStateException() + createdFragment } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 955a2d9d..2f0468e9 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -330,6 +330,10 @@ fun App() { type = NavType.StringType nullable = false } + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } activityClass = SceneDetailsActivity::class } @@ -338,6 +342,10 @@ fun App() { type = NavType.StringType nullable = false } + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } activityClass = PerformerActivity::class } @@ -346,6 +354,10 @@ fun App() { type = NavType.StringType nullable = false } + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } activityClass = GalleryActivity::class } @@ -354,6 +366,10 @@ fun App() { type = NavType.StringType nullable = false } + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } activityClass = ImageActivity::class } @@ -362,6 +378,10 @@ fun App() { type = NavType.StringType nullable = false } + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } activityClass = MovieActivity::class } @@ -370,6 +390,10 @@ fun App() { type = NavType.StringType nullable = false } + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } activityClass = StudioActivity::class } @@ -378,6 +402,10 @@ fun App() { type = NavType.StringType nullable = false } + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } activityClass = TagActivity::class } @@ -391,6 +419,10 @@ fun App() { nullable = false defaultValue = 0L } + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } activityClass = PlaybackActivity::class } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt index a2bf0ac4..63c6f008 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt @@ -709,3 +709,7 @@ val MarkerData.secondsMs: Long fun parseSortDirection(direction: String?): SortDirectionEnum? { return SortDirectionEnum.entries.firstOrNull { it.rawValue == direction?.uppercase() } } + +fun Activity.isNavHostActive(): Boolean { + return intent.getBooleanExtra(Constants.USE_NAV_CONTROLLER, false) +} From d314531892bccba0af2247f6177cd3c4a02f3602 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 10:52:50 -0400 Subject: [PATCH 21/36] Refactors cards into files --- .../damontecres/stashapp/ui/FilterPage.kt | 1 + .../damontecres/stashapp/ui/HomePage.kt | 1 + .../stashapp/ui/{ => cards}/Cards.kt | 405 +----------------- .../stashapp/ui/cards/GalleryCard.kt | 55 +++ .../stashapp/ui/cards/ImageCard.kt | 62 +++ .../stashapp/ui/cards/MarkerCard.kt | 54 +++ .../stashapp/ui/cards/MovieCard.kt | 48 +++ .../stashapp/ui/cards/PerformerCard.kt | 56 +++ .../stashapp/ui/cards/SceneCard.kt | 94 ++++ .../stashapp/ui/cards/StudioCard.kt | 59 +++ .../damontecres/stashapp/ui/cards/TagCard.kt | 78 ++++ 11 files changed, 510 insertions(+), 403 deletions(-) rename app/src/main/java/com/github/damontecres/stashapp/ui/{ => cards}/Cards.kt (50%) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/cards/GalleryCard.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/cards/ImageCard.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/cards/MarkerCard.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/cards/MovieCard.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/cards/PerformerCard.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/cards/SceneCard.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/cards/StudioCard.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/cards/TagCard.kt diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt index f2bdb263..1cd9f16c 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt @@ -62,6 +62,7 @@ import com.github.damontecres.stashapp.suppliers.SceneDataSupplier import com.github.damontecres.stashapp.suppliers.StashPagingSource import com.github.damontecres.stashapp.suppliers.StudioDataSupplier import com.github.damontecres.stashapp.suppliers.TagDataSupplier +import com.github.damontecres.stashapp.ui.cards.StashCard import com.github.damontecres.stashapp.ui.theme.Material3AppTheme import com.github.damontecres.stashapp.util.FilterParser import com.github.damontecres.stashapp.util.QueryEngine diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index 9e27b338..324b3508 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -18,6 +18,7 @@ import androidx.tv.foundation.lazy.list.items import androidx.tv.material3.Text import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.api.ServerInfoQuery +import com.github.damontecres.stashapp.ui.cards.StashCard import com.github.damontecres.stashapp.util.FilterParser import com.github.damontecres.stashapp.util.FrontPageParser import com.github.damontecres.stashapp.util.QueryEngine diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt similarity index 50% rename from app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt rename to app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt index c7b925ad..5932946e 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt @@ -1,8 +1,7 @@ -package com.github.damontecres.stashapp.ui +package com.github.damontecres.stashapp.ui.cards import android.graphics.Color import android.net.Uri -import android.os.Build import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.widget.FrameLayout import androidx.compose.foundation.ExperimentalFoundationApi @@ -28,9 +27,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource @@ -83,27 +80,12 @@ import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.api.fragment.StudioData import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.DataType -import com.github.damontecres.stashapp.presenters.GalleryPresenter -import com.github.damontecres.stashapp.presenters.ImagePresenter -import com.github.damontecres.stashapp.presenters.MarkerPresenter -import com.github.damontecres.stashapp.presenters.MoviePresenter -import com.github.damontecres.stashapp.presenters.PerformerPresenter -import com.github.damontecres.stashapp.presenters.ScenePresenter import com.github.damontecres.stashapp.presenters.StashImageCardView.Companion.ICON_ORDER -import com.github.damontecres.stashapp.presenters.StudioPresenter -import com.github.damontecres.stashapp.presenters.TagPresenter -import com.github.damontecres.stashapp.util.ageInYears +import com.github.damontecres.stashapp.ui.enableMarquee import com.github.damontecres.stashapp.util.asSlimeSceneData -import com.github.damontecres.stashapp.util.concatIfNotBlank -import com.github.damontecres.stashapp.util.isImageClip import com.github.damontecres.stashapp.util.isNotNullOrBlank -import com.github.damontecres.stashapp.util.resolutionName -import com.github.damontecres.stashapp.util.titleOrFilename -import com.github.damontecres.stashapp.views.durationToString import com.github.damontecres.stashapp.views.getRatingAsDecimalString import java.util.EnumMap -import kotlin.time.DurationUnit -import kotlin.time.toDuration @Suppress("ktlint:standard:function-naming") @Composable @@ -367,386 +349,3 @@ fun StashCard( is TagData -> TagCard(item, onClick = { itemOnClick(item) }) } } - -@OptIn(ExperimentalGlideComposeApi::class, ExperimentalFoundationApi::class) -@Suppress("ktlint:standard:function-naming") -@Composable -fun SceneCard( - item: SlimSceneData, - onClick: (() -> Unit), -) { - val dataTypeMap = EnumMap(DataType::class.java) - dataTypeMap[DataType.TAG] = item.tags.size - dataTypeMap[DataType.PERFORMER] = item.performers.size - dataTypeMap[DataType.MOVIE] = item.movies.size - dataTypeMap[DataType.MARKER] = item.scene_markers.size - dataTypeMap[DataType.GALLERY] = item.galleries.size - - RootCard( - modifier = - Modifier - .padding(0.dp) - .width(ScenePresenter.CARD_WIDTH.dp / 2), - contentPadding = PaddingValues(0.dp), - onClick = onClick, - imageWidth = ScenePresenter.CARD_WIDTH.dp / 2, - imageHeight = ScenePresenter.CARD_HEIGHT.dp / 2, - imageUrl = item.paths.screenshot, - videoUrl = item.paths.preview, - title = item.titleOrFilename ?: "", - subtitle = { Text(item.date ?: "") }, - description = { - IconRowText(dataTypeMap, item.o_counter ?: -1) - }, - imageOverlay = { - ImageOverlay(item.rating100) { - val videoFile = item.files.firstOrNull()?.videoFileData - if (videoFile != null) { - val duration = durationToString(videoFile.duration) - Text( - modifier = - Modifier - .align(Alignment.BottomEnd) - .padding(4.dp), - text = duration, - ) - Text( - modifier = - Modifier - .align(Alignment.BottomStart) - .padding(4.dp), - style = TextStyle(fontWeight = FontWeight.Bold), - text = videoFile.resolutionName().toString(), - ) - if (item.resume_time != null) { - val percentWatched = item.resume_time / videoFile.duration - Box( - modifier = - Modifier - .align(Alignment.BottomStart) - .background( - androidx.compose.ui.graphics.Color.White, - ) - .clip(RectangleShape) - .height(4.dp) - .width((ScenePresenter.CARD_WIDTH * percentWatched).dp / 2), - ) - } - } - } - }, - ) -} - -@Suppress("ktlint:standard:function-naming") -@Composable -fun PerformerCard( - item: PerformerData, - onClick: (() -> Unit), -) { - val dataTypeMap = EnumMap(DataType::class.java) - dataTypeMap[DataType.SCENE] = item.scene_count - dataTypeMap[DataType.TAG] = item.tags.size - dataTypeMap[DataType.MOVIE] = item.movie_count - dataTypeMap[DataType.IMAGE] = item.image_count - dataTypeMap[DataType.GALLERY] = item.gallery_count - - RootCard( - modifier = - Modifier - .padding(0.dp) - .width(PerformerPresenter.CARD_WIDTH.dp / 2), - onClick = onClick, - imageWidth = PerformerPresenter.CARD_WIDTH.dp / 2, - imageHeight = PerformerPresenter.CARD_HEIGHT.dp / 2, - imageUrl = item.image_path, - title = item.name, - subtitle = { - if (item.birthdate != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val yearsOldStr = stringResource(R.string.stashapp_years_old) - Text(text = "${item.ageInYears} $yearsOldStr") - } else { - Text("") - } - }, - description = { - IconRowText(dataTypeMap, item.o_counter ?: -1) - }, - imageOverlay = { - ImageOverlay(favorite = item.favorite) - }, - ) -} - -@Suppress("ktlint:standard:function-naming") -@Composable -fun ImageCard( - item: ImageData, - onClick: (() -> Unit), -) { - val dataTypeMap = EnumMap(DataType::class.java) - dataTypeMap[DataType.TAG] = item.tags.size - dataTypeMap[DataType.PERFORMER] = item.performers.size - dataTypeMap[DataType.GALLERY] = item.galleries.size - - val imageUrl = - if (item.paths.thumbnail.isNotNullOrBlank()) { - item.paths.thumbnail - } else if (item.paths.image.isNotNullOrBlank() && !item.isImageClip) { - item.paths.image - } else { - null - } - - val details = mutableListOf() - details.add(item.studio?.studioData?.name) - details.add(item.date) - - RootCard( - modifier = - Modifier - .padding(0.dp) - .width(ImagePresenter.CARD_WIDTH.dp / 2), - onClick = onClick, - imageWidth = ImagePresenter.CARD_WIDTH.dp / 2, - imageHeight = ImagePresenter.CARD_HEIGHT.dp / 2, - imageUrl = imageUrl, - videoUrl = item.paths.preview, - title = item.title ?: "", - subtitle = { - Text(concatIfNotBlank(" - ", details)) - }, - description = { - IconRowText(dataTypeMap, item.o_counter ?: -1) - }, - imageOverlay = { - ImageOverlay(rating100 = item.rating100) - }, - ) -} - -@Suppress("ktlint:standard:function-naming") -@Composable -fun GalleryCard( - item: GalleryData, - onClick: (() -> Unit), -) { - val dataTypeMap = EnumMap(DataType::class.java) - dataTypeMap[DataType.TAG] = item.tags.size - dataTypeMap[DataType.PERFORMER] = item.performers.size - dataTypeMap[DataType.SCENE] = item.scenes.size - dataTypeMap[DataType.IMAGE] = item.image_count - - val imageUrl = item.cover?.paths?.thumbnail - val videoUrl = item.cover?.paths?.preview - - val details = mutableListOf() - details.add(item.studio?.name) - details.add(item.date) - - RootCard( - modifier = - Modifier - .padding(0.dp) - .width(GalleryPresenter.CARD_WIDTH.dp / 2), - onClick = onClick, - imageWidth = GalleryPresenter.CARD_WIDTH.dp / 2, - imageHeight = GalleryPresenter.CARD_HEIGHT.dp / 2, - imageUrl = imageUrl, - videoUrl = videoUrl, - title = item.title ?: "", - subtitle = { - Text(concatIfNotBlank(" - ", details)) - }, - description = { - IconRowText(dataTypeMap, null) - }, - imageOverlay = { - ImageOverlay(rating100 = item.rating100) - }, - ) -} - -@Suppress("ktlint:standard:function-naming") -@Composable -fun MarkerCard( - item: MarkerData, - onClick: (() -> Unit), -) { - val dataTypeMap = EnumMap(DataType::class.java) - dataTypeMap[DataType.TAG] = item.tags.size - - val title = - item.title.ifBlank { - item.primary_tag.tagData.name - } + " - ${item.seconds.toInt().toDuration(DurationUnit.SECONDS)}" - - val imageUrl = item.screenshot - val videoUrl = item.preview - - val details = if (item.title.isNotBlank()) item.primary_tag.tagData.name else "" - - RootCard( - modifier = - Modifier - .padding(0.dp) - .width(MarkerPresenter.CARD_WIDTH.dp / 2), - onClick = onClick, - imageWidth = MarkerPresenter.CARD_WIDTH.dp / 2, - imageHeight = MarkerPresenter.CARD_HEIGHT.dp / 2, - imageUrl = imageUrl, - videoUrl = videoUrl, - title = title, - subtitle = { - Text(details) - }, - description = { - IconRowText(dataTypeMap, null) - }, - imageOverlay = {}, - ) -} - -@Suppress("ktlint:standard:function-naming") -@Composable -fun MovieCard( - item: MovieData, - onClick: (() -> Unit), -) { - val dataTypeMap = EnumMap(DataType::class.java) - dataTypeMap[DataType.SCENE] = item.scene_count - - val title = item.name - val imageUrl = item.front_image_path - val details = item.date ?: "" - - RootCard( - modifier = - Modifier - .padding(0.dp) - .width(MoviePresenter.CARD_WIDTH.dp / 2), - onClick = onClick, - imageWidth = MoviePresenter.CARD_WIDTH.dp / 2, - imageHeight = MoviePresenter.CARD_HEIGHT.dp / 2, - imageUrl = imageUrl, - videoUrl = null, - title = title, - subtitle = { - Text(details) - }, - description = { - IconRowText(dataTypeMap, null) - }, - imageOverlay = { - ImageOverlay(rating100 = item.rating100) - }, - ) -} - -@Suppress("ktlint:standard:function-naming") -@Composable -fun StudioCard( - item: StudioData, - onClick: (() -> Unit), -) { - val dataTypeMap = EnumMap(DataType::class.java) - dataTypeMap[DataType.SCENE] = item.scene_count - dataTypeMap[DataType.PERFORMER] = item.performer_count - dataTypeMap[DataType.MOVIE] = item.movie_count - dataTypeMap[DataType.IMAGE] = item.image_count - dataTypeMap[DataType.GALLERY] = item.gallery_count - - val title = item.name - val imageUrl = item.image_path - val details = - if (item.parent_studio != null) { - stringResource(R.string.stashapp_part_of, item.parent_studio.name) - } else { - "" - } - - RootCard( - modifier = - Modifier - .padding(0.dp) - .width(StudioPresenter.CARD_WIDTH.dp / 2), - onClick = onClick, - imageWidth = StudioPresenter.CARD_WIDTH.dp / 2, - imageHeight = StudioPresenter.CARD_HEIGHT.dp / 2, - imageUrl = imageUrl, - videoUrl = null, - title = title, - subtitle = { - Text(details) - }, - description = { - IconRowText(dataTypeMap, null) - }, - imageOverlay = { - ImageOverlay(rating100 = item.rating100) - }, - ) -} - -@Suppress("ktlint:standard:function-naming") -@Composable -fun TagCard( - item: TagData, - onClick: (() -> Unit), -) { - val dataTypeMap = EnumMap(DataType::class.java) - dataTypeMap[DataType.SCENE] = item.scene_count - dataTypeMap[DataType.PERFORMER] = item.performer_count - dataTypeMap[DataType.MARKER] = item.scene_marker_count - dataTypeMap[DataType.IMAGE] = item.image_count - dataTypeMap[DataType.GALLERY] = item.gallery_count - - val title = item.name - val imageUrl = item.image_path - val details = item.description ?: "" - - RootCard( - modifier = - Modifier - .padding(0.dp) - .width(TagPresenter.CARD_WIDTH.dp / 2), - onClick = onClick, - imageWidth = TagPresenter.CARD_WIDTH.dp / 2, - imageHeight = TagPresenter.CARD_HEIGHT.dp / 2, - imageUrl = imageUrl, - videoUrl = null, - title = title, - subtitle = { - Text(details) - }, - description = { - IconRowText(dataTypeMap, null) - }, - imageOverlay = { - ImageOverlay { - if (item.child_count > 0) { - val parentText = - stringResource( - R.string.stashapp_parent_of, - item.child_count.toString(), - ) - Text( - modifier = Modifier.align(Alignment.TopStart), - text = parentText, - ) - } - if (item.parent_count > 0) { - val childText = - stringResource( - R.string.stashapp_sub_tag_of, - item.parent_count.toString(), - ) - Text( - modifier = Modifier.align(Alignment.BottomStart), - text = childText, - ) - } - } - }, - ) -} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/GalleryCard.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/GalleryCard.kt new file mode 100644 index 00000000..587c133d --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/GalleryCard.kt @@ -0,0 +1,55 @@ +package com.github.damontecres.stashapp.ui.cards + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.api.fragment.GalleryData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.GalleryPresenter +import com.github.damontecres.stashapp.util.concatIfNotBlank +import java.util.EnumMap + +@Suppress("ktlint:standard:function-naming") +@Composable +fun GalleryCard( + item: GalleryData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.TAG] = item.tags.size + dataTypeMap[DataType.PERFORMER] = item.performers.size + dataTypeMap[DataType.SCENE] = item.scenes.size + dataTypeMap[DataType.IMAGE] = item.image_count + + val imageUrl = item.cover?.paths?.thumbnail + val videoUrl = item.cover?.paths?.preview + + val details = mutableListOf() + details.add(item.studio?.name) + details.add(item.date) + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(GalleryPresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = GalleryPresenter.CARD_WIDTH.dp / 2, + imageHeight = GalleryPresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = videoUrl, + title = item.title ?: "", + subtitle = { + Text(concatIfNotBlank(" - ", details)) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = { + ImageOverlay(rating100 = item.rating100) + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/ImageCard.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/ImageCard.kt new file mode 100644 index 00000000..82cfcf68 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/ImageCard.kt @@ -0,0 +1,62 @@ +package com.github.damontecres.stashapp.ui.cards + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.api.fragment.ImageData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.ImagePresenter +import com.github.damontecres.stashapp.util.concatIfNotBlank +import com.github.damontecres.stashapp.util.isImageClip +import com.github.damontecres.stashapp.util.isNotNullOrBlank +import java.util.EnumMap + +@Suppress("ktlint:standard:function-naming") +@Composable +fun ImageCard( + item: ImageData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.TAG] = item.tags.size + dataTypeMap[DataType.PERFORMER] = item.performers.size + dataTypeMap[DataType.GALLERY] = item.galleries.size + + val imageUrl = + if (item.paths.thumbnail.isNotNullOrBlank()) { + item.paths.thumbnail + } else if (item.paths.image.isNotNullOrBlank() && !item.isImageClip) { + item.paths.image + } else { + null + } + + val details = mutableListOf() + details.add(item.studio?.studioData?.name) + details.add(item.date) + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(ImagePresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = ImagePresenter.CARD_WIDTH.dp / 2, + imageHeight = ImagePresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = item.paths.preview, + title = item.title ?: "", + subtitle = { + Text(concatIfNotBlank(" - ", details)) + }, + description = { + IconRowText(dataTypeMap, item.o_counter ?: -1) + }, + imageOverlay = { + ImageOverlay(rating100 = item.rating100) + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/MarkerCard.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/MarkerCard.kt new file mode 100644 index 00000000..70378518 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/MarkerCard.kt @@ -0,0 +1,54 @@ +package com.github.damontecres.stashapp.ui.cards + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.api.fragment.MarkerData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.MarkerPresenter +import java.util.EnumMap +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +@Suppress("ktlint:standard:function-naming") +@Composable +fun MarkerCard( + item: MarkerData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.TAG] = item.tags.size + + val title = + item.title.ifBlank { + item.primary_tag.tagData.name + } + " - ${item.seconds.toInt().toDuration(DurationUnit.SECONDS)}" + + val imageUrl = item.screenshot + val videoUrl = item.preview + + val details = if (item.title.isNotBlank()) item.primary_tag.tagData.name else "" + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(MarkerPresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = MarkerPresenter.CARD_WIDTH.dp / 2, + imageHeight = MarkerPresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = videoUrl, + title = title, + subtitle = { + Text(details) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = {}, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/MovieCard.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/MovieCard.kt new file mode 100644 index 00000000..bbf1522a --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/MovieCard.kt @@ -0,0 +1,48 @@ +package com.github.damontecres.stashapp.ui.cards + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.api.fragment.MovieData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.MoviePresenter +import java.util.EnumMap + +@Suppress("ktlint:standard:function-naming") +@Composable +fun MovieCard( + item: MovieData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.SCENE] = item.scene_count + + val title = item.name + val imageUrl = item.front_image_path + val details = item.date ?: "" + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(MoviePresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = MoviePresenter.CARD_WIDTH.dp / 2, + imageHeight = MoviePresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = null, + title = title, + subtitle = { + Text(details) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = { + ImageOverlay(rating100 = item.rating100) + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/PerformerCard.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/PerformerCard.kt new file mode 100644 index 00000000..da0ca2b7 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/PerformerCard.kt @@ -0,0 +1,56 @@ +package com.github.damontecres.stashapp.ui.cards + +import android.os.Build +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.api.fragment.PerformerData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.PerformerPresenter +import com.github.damontecres.stashapp.util.ageInYears +import java.util.EnumMap + +@Suppress("ktlint:standard:function-naming") +@Composable +fun PerformerCard( + item: PerformerData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.SCENE] = item.scene_count + dataTypeMap[DataType.TAG] = item.tags.size + dataTypeMap[DataType.MOVIE] = item.movie_count + dataTypeMap[DataType.IMAGE] = item.image_count + dataTypeMap[DataType.GALLERY] = item.gallery_count + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(PerformerPresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = PerformerPresenter.CARD_WIDTH.dp / 2, + imageHeight = PerformerPresenter.CARD_HEIGHT.dp / 2, + imageUrl = item.image_path, + title = item.name, + subtitle = { + if (item.birthdate != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val yearsOldStr = stringResource(R.string.stashapp_years_old) + Text(text = "${item.ageInYears} $yearsOldStr") + } else { + Text("") + } + }, + description = { + IconRowText(dataTypeMap, item.o_counter ?: -1) + }, + imageOverlay = { + ImageOverlay(favorite = item.favorite) + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/SceneCard.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/SceneCard.kt new file mode 100644 index 00000000..ad9a858b --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/SceneCard.kt @@ -0,0 +1,94 @@ +package com.github.damontecres.stashapp.ui.cards + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.api.fragment.SlimSceneData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.ScenePresenter +import com.github.damontecres.stashapp.util.resolutionName +import com.github.damontecres.stashapp.util.titleOrFilename +import com.github.damontecres.stashapp.views.durationToString +import java.util.EnumMap + +@Suppress("ktlint:standard:function-naming") +@Composable +fun SceneCard( + item: SlimSceneData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.TAG] = item.tags.size + dataTypeMap[DataType.PERFORMER] = item.performers.size + dataTypeMap[DataType.MOVIE] = item.movies.size + dataTypeMap[DataType.MARKER] = item.scene_markers.size + dataTypeMap[DataType.GALLERY] = item.galleries.size + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(ScenePresenter.CARD_WIDTH.dp / 2), + contentPadding = PaddingValues(0.dp), + onClick = onClick, + imageWidth = ScenePresenter.CARD_WIDTH.dp / 2, + imageHeight = ScenePresenter.CARD_HEIGHT.dp / 2, + imageUrl = item.paths.screenshot, + videoUrl = item.paths.preview, + title = item.titleOrFilename ?: "", + subtitle = { Text(item.date ?: "") }, + description = { + IconRowText(dataTypeMap, item.o_counter ?: -1) + }, + imageOverlay = { + ImageOverlay(item.rating100) { + val videoFile = item.files.firstOrNull()?.videoFileData + if (videoFile != null) { + val duration = durationToString(videoFile.duration) + Text( + modifier = + Modifier + .align(Alignment.BottomEnd) + .padding(4.dp), + text = duration, + ) + Text( + modifier = + Modifier + .align(Alignment.BottomStart) + .padding(4.dp), + style = TextStyle(fontWeight = FontWeight.Bold), + text = videoFile.resolutionName().toString(), + ) + if (item.resume_time != null) { + val percentWatched = item.resume_time / videoFile.duration + Box( + modifier = + Modifier + .align(Alignment.BottomStart) + .background( + Color.White, + ) + .clip(RectangleShape) + .height(4.dp) + .width((ScenePresenter.CARD_WIDTH * percentWatched).dp / 2), + ) + } + } + } + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/StudioCard.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/StudioCard.kt new file mode 100644 index 00000000..dbaeaba0 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/StudioCard.kt @@ -0,0 +1,59 @@ +package com.github.damontecres.stashapp.ui.cards + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.api.fragment.StudioData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.StudioPresenter +import java.util.EnumMap + +@Suppress("ktlint:standard:function-naming") +@Composable +fun StudioCard( + item: StudioData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.SCENE] = item.scene_count + dataTypeMap[DataType.PERFORMER] = item.performer_count + dataTypeMap[DataType.MOVIE] = item.movie_count + dataTypeMap[DataType.IMAGE] = item.image_count + dataTypeMap[DataType.GALLERY] = item.gallery_count + + val title = item.name + val imageUrl = item.image_path + val details = + if (item.parent_studio != null) { + stringResource(R.string.stashapp_part_of, item.parent_studio.name) + } else { + "" + } + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(StudioPresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = StudioPresenter.CARD_WIDTH.dp / 2, + imageHeight = StudioPresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = null, + title = title, + subtitle = { + Text(details) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = { + ImageOverlay(rating100 = item.rating100) + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/TagCard.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/TagCard.kt new file mode 100644 index 00000000..486eb524 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/TagCard.kt @@ -0,0 +1,78 @@ +package com.github.damontecres.stashapp.ui.cards + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.api.fragment.TagData +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.presenters.TagPresenter +import java.util.EnumMap + +@Suppress("ktlint:standard:function-naming") +@Composable +fun TagCard( + item: TagData, + onClick: (() -> Unit), +) { + val dataTypeMap = EnumMap(DataType::class.java) + dataTypeMap[DataType.SCENE] = item.scene_count + dataTypeMap[DataType.PERFORMER] = item.performer_count + dataTypeMap[DataType.MARKER] = item.scene_marker_count + dataTypeMap[DataType.IMAGE] = item.image_count + dataTypeMap[DataType.GALLERY] = item.gallery_count + + val title = item.name + val imageUrl = item.image_path + val details = item.description ?: "" + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(TagPresenter.CARD_WIDTH.dp / 2), + onClick = onClick, + imageWidth = TagPresenter.CARD_WIDTH.dp / 2, + imageHeight = TagPresenter.CARD_HEIGHT.dp / 2, + imageUrl = imageUrl, + videoUrl = null, + title = title, + subtitle = { + Text(details) + }, + description = { + IconRowText(dataTypeMap, null) + }, + imageOverlay = { + ImageOverlay { + if (item.child_count > 0) { + val parentText = + stringResource( + R.string.stashapp_parent_of, + item.child_count.toString(), + ) + Text( + modifier = Modifier.align(Alignment.TopStart), + text = parentText, + ) + } + if (item.parent_count > 0) { + val childText = + stringResource( + R.string.stashapp_sub_tag_of, + item.parent_count.toString(), + ) + Text( + modifier = Modifier.align(Alignment.BottomStart), + text = childText, + ) + } + } + }, + ) +} From 2206a18ebcbe2f3b09f2b4dfa049dd07fed13118 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:57:51 -0400 Subject: [PATCH 22/36] Add basic scene details page --- .../com/github/damontecres/stashapp/ui/App.kt | 30 +- .../damontecres/stashapp/ui/ScenePage.kt | 364 ++++++++++++++++++ .../damontecres/stashapp/ui/cards/Cards.kt | 1 + 3 files changed, 387 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/ScenePage.kt diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 2f0468e9..8b7d69f7 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -1,6 +1,7 @@ package com.github.damontecres.stashapp.ui import android.util.Log +import android.widget.Toast import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.focusGroup @@ -323,18 +324,31 @@ fun App() { } } - activity( - route = "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE}}", - ) { +// activity( +// route = "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE}}", +// ) { +// argument(SceneDetailsActivity.MOVIE) { +// type = NavType.StringType +// nullable = false +// } +// argument(Constants.USE_NAV_CONTROLLER) { +// type = NavType.BoolType +// defaultValue = true +// } +// activityClass = SceneDetailsActivity::class +// } + composable(route = "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE}}") { argument(SceneDetailsActivity.MOVIE) { type = NavType.StringType nullable = false } - argument(Constants.USE_NAV_CONTROLLER) { - type = NavType.BoolType - defaultValue = true - } - activityClass = SceneDetailsActivity::class + ScenePage(itemClick = { item -> + Toast.makeText( + context, + "Item clicked ${item.javaClass.name}", + Toast.LENGTH_SHORT, + ).show() + }) } activity(route = "${DrawerPage.dataType(DataType.PERFORMER).route}/{id}") { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/ScenePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/ScenePage.kt new file mode 100644 index 00000000..a3f56795 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/ScenePage.kt @@ -0,0 +1,364 @@ +package com.github.damontecres.stashapp.ui + +import android.content.Context +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.mutableStateListOf +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import androidx.preference.PreferenceManager +import androidx.tv.foundation.lazy.list.TvLazyColumn +import androidx.tv.foundation.lazy.list.TvLazyRow +import androidx.tv.foundation.lazy.list.items +import androidx.tv.material3.Button +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.ProvideTextStyle +import androidx.tv.material3.Text +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage +import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.SceneDetailsActivity +import com.github.damontecres.stashapp.api.fragment.FullSceneData +import com.github.damontecres.stashapp.api.fragment.MarkerData +import com.github.damontecres.stashapp.api.fragment.PerformerData +import com.github.damontecres.stashapp.playback.CodecSupport +import com.github.damontecres.stashapp.ui.cards.StashCard +import com.github.damontecres.stashapp.util.QueryEngine +import com.github.damontecres.stashapp.util.asVideoSceneData +import com.github.damontecres.stashapp.util.concatIfNotBlank +import com.github.damontecres.stashapp.util.isNotNullOrBlank +import com.github.damontecres.stashapp.util.resolutionName +import com.github.damontecres.stashapp.util.titleOrFilename +import com.github.damontecres.stashapp.views.durationToString +import com.github.damontecres.stashapp.views.parseTimeToString +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +sealed class SceneUiState { + data class Success(val scene: FullSceneData) : SceneUiState() + + data object Loading : SceneUiState() + + data class Error(val message: String, val cause: Exception? = null) : SceneUiState() +} + +@HiltViewModel +class SceneViewModel + @Inject + constructor( + @ApplicationContext context: Context, + savedStateHandle: SavedStateHandle, + ) : ViewModel() { + private val queryEngine = QueryEngine(context) + + val uiState = + savedStateHandle + .getStateFlow(SceneDetailsActivity.MOVIE, null) + .map { id -> + if (id == null) { + SceneUiState.Error("sceneId cannot be null") + } else { + val scene = queryEngine.getScene(id) + if (scene != null) { + SceneUiState.Success(scene) + } else { + SceneUiState.Error("No scene with id=$id") + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = SceneUiState.Loading, + ) + + private val _performers = mutableStateListOf() + val performers: SnapshotStateList get() = _performers + + suspend fun fetchPerformers(scene: FullSceneData) { + val ids = scene.performers.map { it.id } + val results = queryEngine.findPerformers(performerIds = ids) + _performers.addAll(results) + } + } + +@Suppress("ktlint:standard:function-naming") +@Composable +fun ScenePage( + itemClick: (Any) -> Unit, + viewModel: SceneViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + when (val s = uiState) { + is SceneUiState.Loading -> { + Text(text = "Loading...") + } + + is SceneUiState.Error -> { + Text(text = "Error: ${s.message}") + } + + is SceneUiState.Success -> { + LaunchedEffect(Unit) { + viewModel.fetchPerformers(s.scene) + } + SceneDetails( + s.scene, + itemClick, + Modifier + .fillMaxSize() + .animateContentSize(), + ) + } + } +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +private fun SceneDetails( + scene: FullSceneData, + itemClick: (Any) -> Unit, + modifier: Modifier = Modifier, + viewModel: SceneViewModel = hiltViewModel(), +) { + val context = LocalContext.current + + val createdAt = + stringResource(R.string.stashapp_created_at) + ": " + + parseTimeToString( + scene.created_at, + ) + val updatedAt = + stringResource(R.string.stashapp_updated_at) + ": " + + parseTimeToString( + scene.updated_at, + ) + val file = scene.files.firstOrNull() + val subtitle = + if (file != null) { + val resolution = file.videoFileData.resolutionName() + val duration = durationToString(file.videoFileData.duration) + concatIfNotBlank( + " - ", + scene.studio?.studioData?.name, + scene.date, + duration, + resolution, + ) + } else { + null + } + val debugInfo = + if (file != null && + PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + stringResource(R.string.pref_key_show_playback_debug_info), + false, + ) + ) { + val videoFile = file.videoFileData + val supportedCodecs = CodecSupport.getSupportedCodecs(context) + val videoSupported = supportedCodecs.isVideoSupported(videoFile.video_codec) + val audioSupported = supportedCodecs.isAudioSupported(videoFile.audio_codec) + val containerSupported = + supportedCodecs.isContainerFormatSupported(videoFile.format) + + val video = + if (videoSupported) { + "Video: ${videoFile.video_codec}" + } else { + "Video: ${videoFile.video_codec} (unsupported)" + } + val audio = + if (audioSupported) { + "Audio: ${videoFile.audio_codec}" + } else { + "Audio: ${videoFile.audio_codec} (unsupported)" + } + val format = + if (containerSupported) { + "Format: ${videoFile.format}" + } else { + "Format: ${videoFile.format} (unsupported)" + } + + listOf(video, audio, format).joinToString(", ") + } else { + null + } + val playCount = + if (scene.play_count != null && scene.play_count > 0) { + stringResource(R.string.stashapp_play_count) + ": " + scene.play_count.toString() + } else { + null + } + val playDuration = + if (scene.play_duration != null && scene.play_duration >= 1.0) { + stringResource(R.string.stashapp_play_duration) + ": " + durationToString(scene.play_duration) + } else { + null + } + + val playHistory = + if (playCount != null || playDuration != null) { + concatIfNotBlank( + ", ", + playCount, + playDuration, + ) + } else { + null + } + + val body = + listOfNotNull(debugInfo, playHistory, createdAt, updatedAt) + .joinToString("\n") + + TvLazyColumn( + contentPadding = PaddingValues(bottom = 135.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = + modifier + .focusGroup(), + ) { + item { + Box( + modifier = + Modifier + .fillMaxWidth() + .height(350.dp), + ) { + if (scene.paths.screenshot != null) { + GlideImage( + model = scene.paths.screenshot, + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = + Modifier + .align(Alignment.TopCenter) + .fillMaxSize(), + ) + } + } + } + + item { + Button(onClick = { /*TODO*/ }) { + Text(text = "Play") + } + } + + item { + ProvideTextStyle(MaterialTheme.typography.headlineLarge) { + Text( + text = scene.titleOrFilename ?: "", + ) + } + ProvideTextStyle(MaterialTheme.typography.bodyLarge) { + if (subtitle != null) { + Text(modifier = Modifier, text = subtitle) + } + } + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + if (scene.details != null) { + Text(modifier = Modifier, text = scene.details) + } + } + ProvideTextStyle(MaterialTheme.typography.bodySmall) { + if (body.isNotNullOrBlank()) { + Text(modifier = Modifier, text = body) + } + } + } +// private const val MARKER_POS = DETAILS_POS + 1 +// private const val MOVIE_POS = MARKER_POS + 1 +// private const val STUDIO_POS = MOVIE_POS + 1 +// private const val PERFORMER_POS = STUDIO_POS + 1 +// private const val TAG_POS = PERFORMER_POS + 1 +// private const val GALLERY_POS = TAG_POS + 1 +// private const val ACTIONS_POS = GALLERY_POS + 1 + item { + ItemRow( + name = stringResource(R.string.stashapp_markers), + items = scene.scene_markers.map { convertMarker(scene, it) }, + itemClick, + ) + } + item { + ItemRow(name = stringResource(R.string.stashapp_performers), items = viewModel.performers, itemClick) + } + item { + ItemRow(name = stringResource(R.string.stashapp_tags), items = scene.tags.map { it.tagData }, itemClick) + } + } +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun ItemRow( + name: String, + items: List, + itemOnClick: (Any) -> Unit, +) { + if (items.isNotEmpty()) { + Text( + modifier = Modifier.padding(top = 20.dp, bottom = 20.dp), + text = name, + ) + TvLazyRow( + modifier = + Modifier + .focusGroup() + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(items) { item -> + StashCard(item, itemOnClick) + } + } + } +} + +fun convertMarker( + scene: FullSceneData, + m: FullSceneData.Scene_marker, +): MarkerData { + return MarkerData( + id = m.id, + title = m.title, + created_at = "", + updated_at = "", + stream = m.stream, + screenshot = m.screenshot, + seconds = m.seconds, + preview = "", + primary_tag = MarkerData.Primary_tag("", m.primary_tag.tagData), + scene = MarkerData.Scene(scene.id, scene.asVideoSceneData), + tags = m.tags.map { MarkerData.Tag("", it.tagData) }, + __typename = "", + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt index 5932946e..0cea7722 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt @@ -347,5 +347,6 @@ fun StashCard( is MovieData -> MovieCard(item, onClick = { itemOnClick(item) }) is StudioData -> StudioCard(item, onClick = { itemOnClick(item) }) is TagData -> TagCard(item, onClick = { itemOnClick(item) }) + else -> throw UnsupportedOperationException("Item with class ${item.javaClass} not supported.") } } From 718aa4419476787ec1b1771d1136a7853b920a0a Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:08:47 -0400 Subject: [PATCH 23/36] Scale card images better --- .../java/com/github/damontecres/stashapp/ui/cards/Cards.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt index 0cea7722..ae16d50a 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource @@ -290,7 +291,8 @@ fun RootCard( GlideImage( model = imageUrl, contentDescription = "", - modifier = Modifier, + contentScale = ContentScale.Crop, // TODO or ContentScale.Fit ? + modifier = Modifier.fillMaxSize(), ) if (!focused) { imageOverlay.invoke(this) From 1fac3d84472d5c2446f7197eb3b2273caea3ffba Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 20:05:12 -0400 Subject: [PATCH 24/36] Allow for override overall light/dark theme --- .../damontecres/stashapp/SettingsFragment.kt | 15 +++++ .../damontecres/stashapp/ui/theme/Theme.kt | 59 ++++++++++++++----- app/src/main/res/values/preferences.xml | 11 ++++ app/src/main/res/xml/root_preferences.xml | 4 ++ app/src/main/res/xml/ui_preferences.xml | 15 +++++ 5 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 app/src/main/res/xml/ui_preferences.xml diff --git a/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt index 6e740244..2cf9099b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt @@ -238,6 +238,12 @@ class SettingsFragment : LeanbackSettingsFragmentCompat() { findPreference("skip_back_time")!!.min = 5 findPreference("skip_forward_time")!!.min = 5 + val uiPreferences = findPreference("uiPreferences")!! + uiPreferences.setOnPreferenceClickListener { + startPreferenceFragmentFunc(UiPreferencesFragment()) + true + } + val advancedPreferences = findPreference("advancedPreferences")!! advancedPreferences.setOnPreferenceClickListener { startPreferenceFragmentFunc(AdvancedPreferencesFragment()) @@ -327,6 +333,15 @@ class SettingsFragment : LeanbackSettingsFragmentCompat() { } } + class UiPreferencesFragment : LeanbackPreferenceFragmentCompat() { + override fun onCreatePreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { + setPreferencesFromResource(R.xml.ui_preferences, rootKey) + } + } + class AdvancedPreferencesFragment : LeanbackPreferenceFragmentCompat() { override fun onCreatePreferences( savedInstanceState: Bundle?, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt index bf62f6e4..d81c08fd 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt @@ -2,36 +2,63 @@ package com.github.damontecres.stashapp.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.preference.PreferenceManager import androidx.tv.material3.MaterialTheme import androidx.tv.material3.darkColorScheme import androidx.tv.material3.lightColorScheme +import com.github.damontecres.stashapp.R @Suppress("ktlint:standard:function-naming") @Composable -fun AppTheme( - useDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit, -) { +fun AppTheme(content: @Composable () -> Unit) { + val context = LocalContext.current + val themeChoice = + PreferenceManager.getDefaultSharedPreferences(context) + .getString( + stringResource(R.string.pref_key_ui_theme_dark_appearance), + stringResource(R.string.ui_theme_dark_appearance_choice_default), + ) + val colorScheme = - if (useDarkTheme) { - darkColorScheme() - } else { - lightColorScheme() + when (themeChoice) { + stringResource(id = R.string.ui_theme_dark_appearance_choice_light) -> lightColorScheme() + stringResource(id = R.string.ui_theme_dark_appearance_choice_dark) -> darkColorScheme() + else -> { + if (isSystemInDarkTheme()) { + darkColorScheme() + } else { + lightColorScheme() + } + } } MaterialTheme(colorScheme = colorScheme, content = content) } @Suppress("ktlint:standard:function-naming") @Composable -fun Material3AppTheme( - useDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit, -) { +fun Material3AppTheme(content: @Composable () -> Unit) { + val context = LocalContext.current + val themeChoice = + PreferenceManager.getDefaultSharedPreferences(context) + .getString( + stringResource(R.string.pref_key_ui_theme_dark_appearance), + stringResource(R.string.ui_theme_dark_appearance_choice_default), + ) + val colorScheme = - if (useDarkTheme) { - androidx.compose.material3.darkColorScheme() - } else { - androidx.compose.material3.lightColorScheme() + when (themeChoice) { + stringResource(id = R.string.ui_theme_dark_appearance_choice_light) -> androidx.compose.material3.lightColorScheme() + stringResource(id = R.string.ui_theme_dark_appearance_choice_dark) -> androidx.compose.material3.darkColorScheme() + else -> { + if (isSystemInDarkTheme()) { + androidx.compose.material3.darkColorScheme() + } else { + androidx.compose.material3.lightColorScheme() + } + } } + androidx.compose.material3.MaterialTheme(colorScheme = colorScheme, content = content) } diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml index 32a3001e..5012e0a7 100644 --- a/app/src/main/res/values/preferences.xml +++ b/app/src/main/res/values/preferences.xml @@ -15,4 +15,15 @@ playback.showDebugInfo playback.experimentalFeatures useComposeUI + + ui.theme.darkAppearance + + System + Light + Dark + + @string/ui_theme_dark_appearance_choice_default + @string/ui_theme_dark_appearance_choice_light + @string/ui_theme_dark_appearance_choice_dark + diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 4e3096e3..3bf5c8db 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -97,6 +97,10 @@ + + + + + + + + From 0810de727e89fb7f11a6bcfbc52aecf204acfec6 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 20:24:18 -0400 Subject: [PATCH 25/36] Adjust layout sizes for home/nav drawer --- .../com/github/damontecres/stashapp/ui/App.kt | 8 ++++- .../damontecres/stashapp/ui/HomePage.kt | 31 ++++++++++++++----- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 8b7d69f7..84e4f9b3 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -38,9 +39,11 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.tv.material3.DrawerValue +import androidx.tv.material3.Glow import androidx.tv.material3.Icon import androidx.tv.material3.NavigationDrawer import androidx.tv.material3.NavigationDrawerItem +import androidx.tv.material3.NavigationDrawerItemDefaults import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState import com.github.damontecres.stashapp.GalleryActivity @@ -166,7 +169,7 @@ fun App() { val drawerState = rememberDrawerState(DrawerValue.Closed) - val collapsedDrawerItemWidth = 56.dp + val collapsedDrawerItemWidth = 48.dp val paddingValue = 12.dp val navController = rememberNavController() @@ -221,6 +224,9 @@ fun App() { Log.v("App", "DrawerPage.PAGES=${DrawerPage.PAGES}") items(DrawerPage.PAGES, key = { it.route }) { page -> NavigationDrawerItem( + modifier = Modifier, + shape = NavigationDrawerItemDefaults.shape(shape = RoundedCornerShape(50)), + glow = NavigationDrawerItemDefaults.glow(Glow.None), selected = navController.currentDestination?.route == page.route, onClick = { drawerState.setValue(DrawerValue.Closed) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index 324b3508..b45938ed 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -2,6 +2,8 @@ package com.github.damontecres.stashapp.ui import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable @@ -15,6 +17,8 @@ import androidx.lifecycle.ViewModel import androidx.tv.foundation.lazy.list.TvLazyColumn import androidx.tv.foundation.lazy.list.TvLazyRow import androidx.tv.foundation.lazy.list.items +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.Text import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.api.ServerInfoQuery @@ -83,6 +87,7 @@ fun HomePage(itemOnClick: (Any) -> Unit) { TvLazyColumn( verticalArrangement = Arrangement.spacedBy(20.dp), +// contentPadding = PaddingValues(16.dp), modifier = Modifier .fillMaxSize(), @@ -100,14 +105,24 @@ fun HomePageRow( itemOnClick: (Any) -> Unit, ) { val rowData = row.data!! - Text(text = rowData.name, modifier = Modifier.padding(top = 20.dp, bottom = 20.dp)) - TvLazyRow( - modifier = Modifier.focusGroup(), - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - items(rowData.data) { item -> - if (item != null) { - StashCard(item, itemOnClick) + Column(modifier = Modifier) { + ProvideTextStyle(MaterialTheme.typography.titleLarge) { + Text( + modifier = Modifier.padding(top = 20.dp, bottom = 10.dp, start = 16.dp), + text = rowData.name, + ) + } + TvLazyRow( + modifier = + Modifier + .focusGroup(), + contentPadding = PaddingValues(start = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(rowData.data) { item -> + if (item != null) { + StashCard(item, itemOnClick) + } } } } From 729d9a7a7f2946fad70eb4d0230c9074d10ae031 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 20:32:44 -0400 Subject: [PATCH 26/36] Minor renaming --- app/src/main/java/com/github/damontecres/stashapp/ui/App.kt | 1 + .../main/java/com/github/damontecres/stashapp/ui/HomePage.kt | 4 +++- .../github/damontecres/stashapp/ui/{ => details}/ScenePage.kt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) rename app/src/main/java/com/github/damontecres/stashapp/ui/{ => details}/ScenePage.kt (99%) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 84e4f9b3..5eeb5bf6 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -68,6 +68,7 @@ import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashDefaultFilter import com.github.damontecres.stashapp.playback.PlaybackActivity +import com.github.damontecres.stashapp.ui.details.ScenePage import com.github.damontecres.stashapp.util.Constants import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.secondsMs diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index b45938ed..e05ee822 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues 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 @@ -115,7 +116,8 @@ fun HomePageRow( TvLazyRow( modifier = Modifier - .focusGroup(), + .focusGroup() + .fillMaxWidth(), contentPadding = PaddingValues(start = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), ) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/ScenePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt similarity index 99% rename from app/src/main/java/com/github/damontecres/stashapp/ui/ScenePage.kt rename to app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt index a3f56795..e3f27907 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/ScenePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.stashapp.ui +package com.github.damontecres.stashapp.ui.details import android.content.Context import androidx.compose.animation.animateContentSize From 0fd5289bc713eb1859e3f834b6a406153b86428e Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Sun, 28 Jul 2024 22:25:52 -0400 Subject: [PATCH 27/36] Add tag page --- .../com/github/damontecres/stashapp/ui/App.kt | 35 +- .../damontecres/stashapp/ui/FilterPage.kt | 47 ++- .../damontecres/stashapp/ui/HomePage.kt | 2 +- .../stashapp/ui/details/ScenePage.kt | 108 ++++-- .../stashapp/ui/details/TagPage.kt | 323 ++++++++++++++++++ 5 files changed, 439 insertions(+), 76 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 5eeb5bf6..61b360d1 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -1,7 +1,6 @@ package com.github.damontecres.stashapp.ui import android.util.Log -import android.widget.Toast import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.focusGroup @@ -56,7 +55,6 @@ import com.github.damontecres.stashapp.SceneDetailsFragment.Companion.POSITION_A import com.github.damontecres.stashapp.SearchActivity import com.github.damontecres.stashapp.SettingsActivity import com.github.damontecres.stashapp.StudioActivity -import com.github.damontecres.stashapp.TagActivity import com.github.damontecres.stashapp.api.fragment.GalleryData import com.github.damontecres.stashapp.api.fragment.ImageData import com.github.damontecres.stashapp.api.fragment.MarkerData @@ -69,6 +67,7 @@ import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashDefaultFilter import com.github.damontecres.stashapp.playback.PlaybackActivity import com.github.damontecres.stashapp.ui.details.ScenePage +import com.github.damontecres.stashapp.ui.details.TagPage import com.github.damontecres.stashapp.util.Constants import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.secondsMs @@ -305,7 +304,8 @@ fun App() { else -> throw UnsupportedOperationException() } - navController.navigate(route = route) + navController.navigate(route = route) { + } } composable(route = DrawerPage.HOME_PAGE.route) { @@ -331,31 +331,12 @@ fun App() { } } -// activity( -// route = "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE}}", -// ) { -// argument(SceneDetailsActivity.MOVIE) { -// type = NavType.StringType -// nullable = false -// } -// argument(Constants.USE_NAV_CONTROLLER) { -// type = NavType.BoolType -// defaultValue = true -// } -// activityClass = SceneDetailsActivity::class -// } composable(route = "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE}}") { argument(SceneDetailsActivity.MOVIE) { type = NavType.StringType nullable = false } - ScenePage(itemClick = { item -> - Toast.makeText( - context, - "Item clicked ${item.javaClass.name}", - Toast.LENGTH_SHORT, - ).show() - }) + ScenePage(itemOnClick = itemOnClick) } activity(route = "${DrawerPage.dataType(DataType.PERFORMER).route}/{id}") { @@ -418,16 +399,12 @@ fun App() { activityClass = StudioActivity::class } - activity(route = "${DrawerPage.dataType(DataType.TAG).route}/{id}") { + composable(route = "${DrawerPage.dataType(DataType.TAG).route}/{id}") { argument("id") { type = NavType.StringType nullable = false } - argument(Constants.USE_NAV_CONTROLLER) { - type = NavType.BoolType - defaultValue = true - } - activityClass = TagActivity::class + TagPage(itemOnClick) } activity(route = Routes.PLAYBACK) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt index 1cd9f16c..f5297615 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt @@ -6,6 +6,8 @@ import android.util.Log import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize @@ -23,7 +25,6 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -323,7 +324,10 @@ fun FilterGrid( ) } is ResolvedFilterState.Success -> { - ResolvedFilterGrid(resolvedFilterState as ResolvedFilterState.Success, itemOnClick) + ResolvedFilterGrid( + resolvedFilterState as ResolvedFilterState.Success, + itemOnClick = itemOnClick, + ) } } } @@ -333,6 +337,9 @@ fun FilterGrid( @Composable fun ResolvedFilterGrid( resolvedFilter: ResolvedFilterState.Success, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(), + showHeader: Boolean = true, itemOnClick: (item: Any) -> Unit, ) { // val viewModel = hiltViewModel() @@ -351,26 +358,31 @@ fun ResolvedFilterGrid( val lazyPagingItems = pager.flow.collectAsLazyPagingItems() TvLazyVerticalGrid( - modifier = Modifier.padding(16.dp), + modifier = + modifier + .padding(16.dp) + .fillMaxSize(), columns = TvGridCells.Fixed(5), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { - item("header", span = { TvGridItemSpan(this.maxLineSpan) }) { - Column { // TODO Box? - ProvideTextStyle(MaterialTheme.typography.titleLarge) { - val filterName = - if (resolvedFilter.filter.name.isNotNullOrBlank()) { - resolvedFilter.filter.name - } else { - stringResource(id = resolvedFilter.filter.dataType.pluralStringId) - } - Text( - modifier = Modifier.fillMaxWidth(), - text = filterName, - ) + if (showHeader) { + item("header", span = { TvGridItemSpan(this.maxLineSpan) }) { + Column { // TODO Box? + ProvideTextStyle(MaterialTheme.typography.titleLarge) { + val filterName = + if (resolvedFilter.filter.name.isNotNullOrBlank()) { + resolvedFilter.filter.name + } else { + stringResource(id = resolvedFilter.filter.dataType.pluralStringId) + } + Text( + modifier = Modifier.fillMaxWidth(), + text = filterName, + ) + } + SavedFilterDropDown() } - SavedFilterDropDown() } } if (lazyPagingItems.loadState.refresh == LoadState.Loading) { @@ -414,7 +426,6 @@ fun ResolvedFilterGrid( @Composable fun SavedFilterDropDown() { val viewModel = hiltViewModel() - val context = LocalContext.current var expanded by remember { mutableStateOf(false) } Box( diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index e05ee822..869ebb0b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -88,7 +88,7 @@ fun HomePage(itemOnClick: (Any) -> Unit) { TvLazyColumn( verticalArrangement = Arrangement.spacedBy(20.dp), -// contentPadding = PaddingValues(16.dp), + contentPadding = PaddingValues(bottom = 75.dp), modifier = Modifier .fillMaxSize(), diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt index e3f27907..d47e9be1 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.animateContentSize import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -39,6 +40,7 @@ import com.bumptech.glide.integration.compose.GlideImage import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.SceneDetailsActivity import com.github.damontecres.stashapp.api.fragment.FullSceneData +import com.github.damontecres.stashapp.api.fragment.GalleryData import com.github.damontecres.stashapp.api.fragment.MarkerData import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.playback.CodecSupport @@ -103,12 +105,21 @@ class SceneViewModel val results = queryEngine.findPerformers(performerIds = ids) _performers.addAll(results) } + + private val _galleries = mutableStateListOf() + val galleries: SnapshotStateList get() = _galleries + + suspend fun fetchGalleries(scene: FullSceneData) { + val ids = scene.galleries.map { it.id } + val results = queryEngine.getGalleries(ids) + _galleries.addAll(results) + } } @Suppress("ktlint:standard:function-naming") @Composable fun ScenePage( - itemClick: (Any) -> Unit, + itemOnClick: (Any) -> Unit, viewModel: SceneViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -125,10 +136,11 @@ fun ScenePage( is SceneUiState.Success -> { LaunchedEffect(Unit) { viewModel.fetchPerformers(s.scene) + viewModel.fetchGalleries(s.scene) } SceneDetails( s.scene, - itemClick, + itemOnClick, Modifier .fillMaxSize() .animateContentSize(), @@ -142,7 +154,7 @@ fun ScenePage( @Composable private fun SceneDetails( scene: FullSceneData, - itemClick: (Any) -> Unit, + itemOnClick: (Any) -> Unit, modifier: Modifier = Modifier, viewModel: SceneViewModel = hiltViewModel(), ) { @@ -239,8 +251,8 @@ private fun SceneDetails( .joinToString("\n") TvLazyColumn( - contentPadding = PaddingValues(bottom = 135.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = modifier .focusGroup(), @@ -294,25 +306,60 @@ private fun SceneDetails( } } } -// private const val MARKER_POS = DETAILS_POS + 1 -// private const val MOVIE_POS = MARKER_POS + 1 -// private const val STUDIO_POS = MOVIE_POS + 1 -// private const val PERFORMER_POS = STUDIO_POS + 1 -// private const val TAG_POS = PERFORMER_POS + 1 -// private const val GALLERY_POS = TAG_POS + 1 -// private const val ACTIONS_POS = GALLERY_POS + 1 + // Markers item { ItemRow( name = stringResource(R.string.stashapp_markers), items = scene.scene_markers.map { convertMarker(scene, it) }, - itemClick, + itemOnClick, + ) + } + + // Studio + if (scene.studio != null) { + item { + ItemRow( + name = stringResource(R.string.stashapp_studio), + items = listOf(scene.studio.studioData), + itemOnClick, + ) + } + } + + // Movies + item { + ItemRow( + name = stringResource(R.string.stashapp_movies), + items = scene.movies.sortedBy { it.scene_index }.map { it.movie.movieData }, + itemOnClick, + ) + } + + // Performers + item { + ItemRow( + name = stringResource(R.string.stashapp_performers), + items = viewModel.performers, + itemOnClick, ) } + + // Tags item { - ItemRow(name = stringResource(R.string.stashapp_performers), items = viewModel.performers, itemClick) + ItemRow( + name = stringResource(R.string.stashapp_tags), + items = scene.tags.map { it.tagData }, + itemOnClick, + ) } + + // Galleries item { - ItemRow(name = stringResource(R.string.stashapp_tags), items = scene.tags.map { it.tagData }, itemClick) + ItemRow( + name = stringResource(R.string.stashapp_galleries), + items = viewModel.galleries, + itemOnClick, + ) } } } @@ -325,19 +372,24 @@ fun ItemRow( itemOnClick: (Any) -> Unit, ) { if (items.isNotEmpty()) { - Text( - modifier = Modifier.padding(top = 20.dp, bottom = 20.dp), - text = name, - ) - TvLazyRow( - modifier = - Modifier - .focusGroup() - .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - items(items) { item -> - StashCard(item, itemOnClick) + Column { + ProvideTextStyle(MaterialTheme.typography.titleLarge) { + Text( + modifier = Modifier.padding(top = 20.dp, bottom = 10.dp, start = 16.dp), + text = name, + ) + } + TvLazyRow( + contentPadding = PaddingValues(start = 16.dp), + modifier = + Modifier + .focusGroup() + .fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(items) { item -> + StashCard(item, itemOnClick) + } } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt new file mode 100644 index 00000000..d89bbf45 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt @@ -0,0 +1,323 @@ +package com.github.damontecres.stashapp.ui.details + +import android.content.Context +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.key +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusRestorer +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.ProvideTextStyle +import androidx.tv.material3.Tab +import androidx.tv.material3.TabRow +import androidx.tv.material3.Text +import com.apollographql.apollo3.api.Optional +import com.apollographql.apollo3.api.Query +import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.StashApplication +import com.github.damontecres.stashapp.api.fragment.TagData +import com.github.damontecres.stashapp.api.type.CriterionModifier +import com.github.damontecres.stashapp.api.type.GalleryFilterType +import com.github.damontecres.stashapp.api.type.HierarchicalMultiCriterionInput +import com.github.damontecres.stashapp.api.type.ImageFilterType +import com.github.damontecres.stashapp.api.type.PerformerFilterType +import com.github.damontecres.stashapp.api.type.SceneFilterType +import com.github.damontecres.stashapp.api.type.SceneMarkerFilterType +import com.github.damontecres.stashapp.api.type.TagFilterType +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.suppliers.GalleryDataSupplier +import com.github.damontecres.stashapp.suppliers.ImageDataSupplier +import com.github.damontecres.stashapp.suppliers.MarkerDataSupplier +import com.github.damontecres.stashapp.suppliers.PerformerDataSupplier +import com.github.damontecres.stashapp.suppliers.SceneDataSupplier +import com.github.damontecres.stashapp.suppliers.StashPagingSource +import com.github.damontecres.stashapp.suppliers.TagDataSupplier +import com.github.damontecres.stashapp.ui.ResolvedFilter +import com.github.damontecres.stashapp.ui.ResolvedFilterGrid +import com.github.damontecres.stashapp.ui.ResolvedFilterState +import com.github.damontecres.stashapp.util.QueryEngine +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +sealed class TagUiState { + data class Success(val tag: TagData) : TagUiState() + + data object Loading : TagUiState() + + data class Error(val message: String, val cause: Exception? = null) : TagUiState() +} + +@HiltViewModel +class TagViewModel + @Inject + constructor( + @ApplicationContext context: Context, + savedStateHandle: SavedStateHandle, + ) : ViewModel() { + private val queryEngine = QueryEngine(context) + + val uiState = + savedStateHandle + .getStateFlow("id", null) + .map { id -> + if (id == null) { + TagUiState.Error("TagId cannot be null") + } else { + val tag = queryEngine.getTags(listOf(id)).firstOrNull() + if (tag != null) { + TagUiState.Success(tag) + } else { + TagUiState.Error("No Tag with id=$id") + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = TagUiState.Loading, + ) + } + +@Suppress("ktlint:standard:function-naming") +@Composable +fun TagPage( + itemOnClick: (Any) -> Unit, + viewModel: TagViewModel = hiltViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + when (val s = uiState) { + is TagUiState.Loading -> { + Text(text = "Loading...") + } + + is TagUiState.Error -> { + Text(text = "Error: ${s.message}") + } + + is TagUiState.Success -> { + LaunchedEffect(Unit) { + } + TagDetails( + s.tag, + itemOnClick, + Modifier + .fillMaxSize() + .animateContentSize(), + ) + } + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +fun TagDetails( + tag: TagData, + itemOnClick: (Any) -> Unit, + modifier: Modifier, +) { + val tabs = + listOf( + stringResource(DataType.SCENE.pluralStringId), + stringResource(DataType.GALLERY.pluralStringId), + stringResource(DataType.IMAGE.pluralStringId), + stringResource(DataType.MARKER.pluralStringId), + stringResource(DataType.PERFORMER.pluralStringId), + stringResource(R.string.stashapp_sub_tags), + ) + var selectedTabIndex by remember { mutableIntStateOf(0) } + + Box( + modifier = + Modifier + .fillMaxSize(), + ) { + Column { + ProvideTextStyle(MaterialTheme.typography.headlineLarge) { + Text( + text = tag.name, + modifier = + Modifier + .align(Alignment.CenterHorizontally), + ) + } + // https://developer.android.com/reference/kotlin/androidx/tv/material3/package-summary#TabRow(kotlin.Int,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function0,kotlin.Function2,kotlin.Function1) + // TODO center tabs? + TabRow( + selectedTabIndex = selectedTabIndex, + modifier = + Modifier + .align(Alignment.CenterHorizontally) + .fillMaxWidth() + .focusRestorer(), + ) { + tabs.forEachIndexed { index, tab -> + key(index) { + Tab( + selected = index == selectedTabIndex, + onFocus = { selectedTabIndex = index }, + modifier = + Modifier + .align(Alignment.CenterHorizontally), + ) { + ProvideTextStyle(MaterialTheme.typography.titleMedium) { + Text( + text = tab, + modifier = + Modifier.padding( + horizontal = 16.dp, + vertical = 6.dp, + ), + ) + } + } + } + } + } + val resolvedFilter = + ResolvedFilterState.Success(ResolvedFilter(DataType.TAG), getPagingSource(selectedTabIndex, tag)) + ResolvedFilterGrid( + resolvedFilter, + showHeader = false, + itemOnClick = itemOnClick, + contentPadding = PaddingValues(top = 16.dp), + modifier = Modifier, + ) + } + } +} + +private fun getPagingSource( + index: Int, + tag: TagData, + includeSubTags: Boolean = false, +): StashPagingSource { + val depth = Optional.present(if (includeSubTags) -1 else 0) + + val dataSupplier = + when (index) { + 0 -> { + SceneDataSupplier( + SceneFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tag.id)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = depth, + ), + ), + ), + ) + } + + 1 -> { + GalleryDataSupplier( + GalleryFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tag.id)), + modifier = CriterionModifier.INCLUDES, + depth = depth, + ), + ), + ), + ) + } + + 2 -> { + ImageDataSupplier( + ImageFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tag.id)), + modifier = CriterionModifier.INCLUDES, + depth = depth, + ), + ), + ), + ) + } + + 3 -> { + MarkerDataSupplier( + SceneMarkerFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tag.id)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = depth, + ), + ), + ), + ) + } + + 4 -> { + PerformerDataSupplier( + PerformerFilterType( + tags = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tag.id)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = depth, + ), + ), + ), + ) + } + + 5 -> { + TagDataSupplier( + DataType.TAG.asDefaultFindFilterType, + TagFilterType( + parents = + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(tag.id)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = depth, + ), + ), + ), + ) + } + + else -> throw IllegalStateException("selectedTabIndex=$index") + } as StashPagingSource.DataSupplier<*, Any, *> + + return StashPagingSource( + StashApplication.getApplication(), + 25, // TODO + dataSupplier = dataSupplier, + useRandom = false, + ) +} From 46f9e32d11d7b6b023e58f41796939df45d6552e Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Mon, 29 Jul 2024 17:31:28 -0400 Subject: [PATCH 28/36] Generalize tabbed grid --- .../com/github/damontecres/stashapp/ui/App.kt | 108 +++++++-------- .../damontecres/stashapp/ui/FilterPage.kt | 23 +++- .../damontecres/stashapp/ui/HomePage.kt | 16 ++- .../damontecres/stashapp/ui/TabbedGrid.kt | 98 ++++++++++++++ .../stashapp/ui/details/TagPage.kt | 123 +++--------------- .../damontecres/stashapp/util/Constants.kt | 78 +++++++++++ 6 files changed, 283 insertions(+), 163 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/TabbedGrid.kt diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 61b360d1..b77d616f 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -13,14 +13,18 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -38,11 +42,9 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.tv.material3.DrawerValue -import androidx.tv.material3.Glow import androidx.tv.material3.Icon import androidx.tv.material3.NavigationDrawer import androidx.tv.material3.NavigationDrawerItem -import androidx.tv.material3.NavigationDrawerItemDefaults import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState import com.github.damontecres.stashapp.GalleryActivity @@ -55,14 +57,7 @@ import com.github.damontecres.stashapp.SceneDetailsFragment.Companion.POSITION_A import com.github.damontecres.stashapp.SearchActivity import com.github.damontecres.stashapp.SettingsActivity import com.github.damontecres.stashapp.StudioActivity -import com.github.damontecres.stashapp.api.fragment.GalleryData -import com.github.damontecres.stashapp.api.fragment.ImageData import com.github.damontecres.stashapp.api.fragment.MarkerData -import com.github.damontecres.stashapp.api.fragment.MovieData -import com.github.damontecres.stashapp.api.fragment.PerformerData -import com.github.damontecres.stashapp.api.fragment.SlimSceneData -import com.github.damontecres.stashapp.api.fragment.StudioData -import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashDefaultFilter import com.github.damontecres.stashapp.playback.PlaybackActivity @@ -70,9 +65,13 @@ import com.github.damontecres.stashapp.ui.details.ScenePage import com.github.damontecres.stashapp.ui.details.TagPage import com.github.damontecres.stashapp.util.Constants import com.github.damontecres.stashapp.util.StashServer +import com.github.damontecres.stashapp.util.getDataType +import com.github.damontecres.stashapp.util.getId import com.github.damontecres.stashapp.util.secondsMs -class DrawerPage( +private const val TAG = "Compose.App" + +data class DrawerPage( val route: String, @StringRes val iconString: Int, @StringRes val name: Int, @@ -147,7 +146,7 @@ class AppViewModel : ViewModel() { val currentServer = mutableStateOf(StashServer.getCurrentStashServer()) } -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @Suppress("ktlint:standard:function-naming") @Composable fun App() { @@ -173,6 +172,8 @@ fun App() { val paddingValue = 12.dp val navController = rememberNavController() + val focusRequester = remember { FocusRequester() } + val focusRequesters = remember { DrawerPage.PAGES.associateWith { FocusRequester() } } NavigationDrawer( drawerState = drawerState, @@ -182,7 +183,28 @@ fun App() { .focusGroup() .fillMaxHeight() .padding(4.dp) - .width(if (drawerState.currentValue == DrawerValue.Closed) collapsedDrawerItemWidth else Dp.Unspecified), + .width(if (drawerState.currentValue == DrawerValue.Closed) collapsedDrawerItemWidth else Dp.Unspecified) + .focusRequester(focusRequester) + .focusProperties { + enter = { focusDirection -> + if (focusDirection == FocusDirection.Left) { + val currentPage = + DrawerPage.PAGES.firstOrNull { page -> + navController.currentDestination?.route?.startsWith( + page.route, + ) ?: false + } + Log.v(TAG, "focus enter currentPage=$currentPage") + if (currentPage != null) { + focusRequesters[currentPage]!! + } else { + focusRequesters[DrawerPage.HOME_PAGE]!! + } + } else { + FocusRequester.Default + } + } + }, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.SpaceBetween, ) { @@ -214,22 +236,27 @@ fun App() { // Group of item with same padding LazyColumn( - contentPadding = PaddingValues(8.dp), + contentPadding = PaddingValues(0.dp), modifier = Modifier .selectableGroup(), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterVertically), + verticalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterVertically), ) { - Log.v("App", "DrawerPage.PAGES=${DrawerPage.PAGES}") + Log.v(TAG, "DrawerPage.PAGES=${DrawerPage.PAGES}") items(DrawerPage.PAGES, key = { it.route }) { page -> NavigationDrawerItem( - modifier = Modifier, - shape = NavigationDrawerItemDefaults.shape(shape = RoundedCornerShape(50)), - glow = NavigationDrawerItemDefaults.glow(Glow.None), - selected = navController.currentDestination?.route == page.route, + modifier = + Modifier + .focusRequester(focusRequesters[page]!!), + // shape = NavigationDrawerItemDefaults.shape(shape = RoundedCornerShape(50)), +// glow = NavigationDrawerItemDefaults.glow(Glow.None), + selected = + navController.currentDestination?.route?.startsWith(page.route) + ?: false, onClick = { drawerState.setValue(DrawerValue.Closed) + Log.v(TAG, "Navigating to ${page.route}") navController.navigate(page.route) { // remove the previous Composable from the back stack popUpTo(navController.currentDestination?.route ?: "") { @@ -268,41 +295,14 @@ fun App() { // .padding(start = collapsedDrawerItemWidth), ) { val itemOnClick = { item: Any -> - val route = - when (item) { - is SlimSceneData -> { - DrawerPage.dataType(DataType.SCENE).idRoute(item.id) - } - - is GalleryData -> { - DrawerPage.dataType(DataType.GALLERY).idRoute(item.id) - } - - is ImageData -> { - DrawerPage.dataType(DataType.IMAGE).idRoute(item.id) - } + val dataType = getDataType(item) - is MarkerData -> { - Routes.playback(item.scene.videoSceneData.id, item.secondsMs) - } - - is MovieData -> { - DrawerPage.dataType(DataType.MOVIE).idRoute(item.id) - } - - is PerformerData -> { - DrawerPage.dataType(DataType.PERFORMER).idRoute(item.id) - } - - is StudioData -> { - DrawerPage.dataType(DataType.STUDIO).idRoute(item.id) - } - - is TagData -> { - DrawerPage.dataType(DataType.TAG).idRoute(item.id) - } - - else -> throw UnsupportedOperationException() + val route = + if (dataType == DataType.MARKER) { + item as MarkerData + Routes.playback(item.scene.videoSceneData.id, item.secondsMs) + } else { + Routes.dataType(dataType, getId(item)) } navController.navigate(route = route) { } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt index f5297615..bc14a01f 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt @@ -3,6 +3,7 @@ package com.github.damontecres.stashapp.ui import android.content.Context import android.os.Parcelable import android.util.Log +import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -25,6 +26,8 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment 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.hilt.navigation.compose.hiltViewModel @@ -356,12 +359,15 @@ fun ResolvedFilterGrid( Log.v("ResolvedFilterGrid", "resolvedFilter.filter.name=${resolvedFilter.filter.name}") val lazyPagingItems = pager.flow.collectAsLazyPagingItems() + val focusRequester = remember { FocusRequester() } TvLazyVerticalGrid( modifier = modifier .padding(16.dp) - .fillMaxSize(), + .fillMaxSize() + .focusGroup() + .focusRequester(focusRequester), columns = TvGridCells.Fixed(5), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), @@ -395,12 +401,27 @@ fun ResolvedFilterGrid( .wrapContentWidth(Alignment.CenterHorizontally), ) } + } else if (lazyPagingItems.itemCount == 0) { + item { + Text( + text = "No items found", + modifier = + Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + ) + } } + items(lazyPagingItems.itemCount) { index -> val item = lazyPagingItems[index] if (item != null) { StashCard(item = item, itemOnClick) } + // TODO is this inefficient? + if (index == 0) { + focusRequester.requestFocus() + } } if (lazyPagingItems.loadState.append == LoadState.Loading) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index 869ebb0b..a0fb11e8 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -11,7 +11,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel @@ -40,7 +43,8 @@ class HomePageViewModel constructor( private val queryRepository: QueryRepository, ) : ViewModel() { - val rows = mutableStateListOf() + private val _rows = mutableStateListOf() + val rows: SnapshotStateList get() = _rows suspend fun fetchFrontPage() { val config = queryRepository.getServerConfiguration() @@ -68,7 +72,7 @@ class HomePageViewModel frontPageParser.parse(frontPageContent).forEach { deferredRow -> val result = deferredRow.await() if (result.successful) { - rows.add(result) + _rows.add(result) } } } @@ -81,6 +85,7 @@ fun HomePage(itemOnClick: (Any) -> Unit) { val viewModel = hiltViewModel() val rows = remember { viewModel.rows } + val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { viewModel.fetchFrontPage() @@ -91,12 +96,17 @@ fun HomePage(itemOnClick: (Any) -> Unit) { contentPadding = PaddingValues(bottom = 75.dp), modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .focusGroup() + .focusRequester(focusRequester), ) { items(rows, key = { rows.indexOf(it) }) { row -> HomePageRow(row, itemOnClick) } } +// if (rows.isNotEmpty()) { +// focusRequester.requestFocus() +// } } @Suppress("ktlint:standard:function-naming") diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/TabbedGrid.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/TabbedGrid.kt new file mode 100644 index 00000000..99419670 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/TabbedGrid.kt @@ -0,0 +1,98 @@ +package com.github.damontecres.stashapp.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +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.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusRestorer +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.ProvideTextStyle +import androidx.tv.material3.Tab +import androidx.tv.material3.TabRow +import androidx.tv.material3.Text +import com.apollographql.apollo3.api.Query +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.suppliers.StashPagingSource + +@Suppress("ktlint:standard:function-naming") +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun TabbedFilterGrid( + name: String, + tabs: List, + contentProvider: (Int) -> StashPagingSource, + itemOnClick: (Any) -> Unit, + modifier: Modifier = Modifier, +) { + var selectedTabIndex by remember { mutableIntStateOf(0) } + Column( + modifier = + Modifier + .fillMaxSize(), + ) { + ProvideTextStyle(MaterialTheme.typography.headlineLarge) { + Text( + text = name, + modifier = + Modifier + .align(Alignment.CenterHorizontally), + ) + } + // https://developer.android.com/reference/kotlin/androidx/tv/material3/package-summary#TabRow(kotlin.Int,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function0,kotlin.Function2,kotlin.Function1) + // TODO center tabs? + TabRow( + selectedTabIndex = selectedTabIndex, + modifier = + Modifier + .align(Alignment.CenterHorizontally) + .fillMaxWidth() + .focusRestorer(), + ) { + tabs.forEachIndexed { index, tab -> + key(index) { + Tab( + selected = index == selectedTabIndex, + onFocus = { selectedTabIndex = index }, + modifier = + Modifier + .align(Alignment.CenterHorizontally), + ) { + ProvideTextStyle(MaterialTheme.typography.titleMedium) { + Text( + text = tab, + modifier = + Modifier.padding( + horizontal = 16.dp, + vertical = 6.dp, + ), + ) + } + } + } + } + } + val resolvedFilter = + ResolvedFilterState.Success( + ResolvedFilter(DataType.SCENE), + contentProvider(selectedTabIndex), + ) + ResolvedFilterGrid( + resolvedFilter, + showHeader = false, + itemOnClick = itemOnClick, + contentPadding = PaddingValues(top = 16.dp), + modifier = modifier, + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt index d89bbf45..69866d17 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt @@ -2,34 +2,16 @@ package com.github.damontecres.stashapp.ui.details import android.content.Context import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues 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.key -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewModelScope -import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.ProvideTextStyle -import androidx.tv.material3.Tab -import androidx.tv.material3.TabRow import androidx.tv.material3.Text import com.apollographql.apollo3.api.Optional import com.apollographql.apollo3.api.Query @@ -52,9 +34,7 @@ import com.github.damontecres.stashapp.suppliers.PerformerDataSupplier import com.github.damontecres.stashapp.suppliers.SceneDataSupplier import com.github.damontecres.stashapp.suppliers.StashPagingSource import com.github.damontecres.stashapp.suppliers.TagDataSupplier -import com.github.damontecres.stashapp.ui.ResolvedFilter -import com.github.damontecres.stashapp.ui.ResolvedFilterGrid -import com.github.damontecres.stashapp.ui.ResolvedFilterState +import com.github.damontecres.stashapp.ui.TabbedFilterGrid import com.github.damontecres.stashapp.util.QueryEngine import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext @@ -119,93 +99,26 @@ fun TagPage( } is TagUiState.Success -> { - LaunchedEffect(Unit) { - } - TagDetails( - s.tag, - itemOnClick, - Modifier - .fillMaxSize() - .animateContentSize(), - ) - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Suppress("ktlint:standard:function-naming") -@Composable -fun TagDetails( - tag: TagData, - itemOnClick: (Any) -> Unit, - modifier: Modifier, -) { - val tabs = - listOf( - stringResource(DataType.SCENE.pluralStringId), - stringResource(DataType.GALLERY.pluralStringId), - stringResource(DataType.IMAGE.pluralStringId), - stringResource(DataType.MARKER.pluralStringId), - stringResource(DataType.PERFORMER.pluralStringId), - stringResource(R.string.stashapp_sub_tags), - ) - var selectedTabIndex by remember { mutableIntStateOf(0) } - - Box( - modifier = - Modifier - .fillMaxSize(), - ) { - Column { - ProvideTextStyle(MaterialTheme.typography.headlineLarge) { - Text( - text = tag.name, - modifier = - Modifier - .align(Alignment.CenterHorizontally), + val tabs = + listOf( + stringResource(DataType.SCENE.pluralStringId), + stringResource(DataType.GALLERY.pluralStringId), + stringResource(DataType.IMAGE.pluralStringId), + stringResource(DataType.MARKER.pluralStringId), + stringResource(DataType.PERFORMER.pluralStringId), + stringResource(R.string.stashapp_sub_tags), ) - } - // https://developer.android.com/reference/kotlin/androidx/tv/material3/package-summary#TabRow(kotlin.Int,androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androidx.compose.ui.graphics.Color,kotlin.Function0,kotlin.Function2,kotlin.Function1) - // TODO center tabs? - TabRow( - selectedTabIndex = selectedTabIndex, + TabbedFilterGrid( + name = s.tag.name, + tabs = tabs, + contentProvider = { index -> + getPagingSource(index, s.tag) + }, + itemOnClick = itemOnClick, modifier = Modifier - .align(Alignment.CenterHorizontally) - .fillMaxWidth() - .focusRestorer(), - ) { - tabs.forEachIndexed { index, tab -> - key(index) { - Tab( - selected = index == selectedTabIndex, - onFocus = { selectedTabIndex = index }, - modifier = - Modifier - .align(Alignment.CenterHorizontally), - ) { - ProvideTextStyle(MaterialTheme.typography.titleMedium) { - Text( - text = tab, - modifier = - Modifier.padding( - horizontal = 16.dp, - vertical = 6.dp, - ), - ) - } - } - } - } - } - val resolvedFilter = - ResolvedFilterState.Success(ResolvedFilter(DataType.TAG), getPagingSource(selectedTabIndex, tag)) - ResolvedFilterGrid( - resolvedFilter, - showHeader = false, - itemOnClick = itemOnClick, - contentPadding = PaddingValues(top = 16.dp), - modifier = Modifier, + .fillMaxSize() + .animateContentSize(), ) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt index 63c6f008..fe73d79d 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt @@ -34,10 +34,12 @@ import com.github.damontecres.stashapp.api.fragment.FullSceneData import com.github.damontecres.stashapp.api.fragment.GalleryData import com.github.damontecres.stashapp.api.fragment.ImageData import com.github.damontecres.stashapp.api.fragment.MarkerData +import com.github.damontecres.stashapp.api.fragment.MovieData import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.SavedFilterData import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.api.fragment.SlimTagData +import com.github.damontecres.stashapp.api.fragment.StudioData import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.api.fragment.VideoFileData import com.github.damontecres.stashapp.api.fragment.VideoSceneData @@ -713,3 +715,79 @@ fun parseSortDirection(direction: String?): SortDirectionEnum? { fun Activity.isNavHostActive(): Boolean { return intent.getBooleanExtra(Constants.USE_NAV_CONTROLLER, false) } + +fun getDataType(item: Any): DataType { + return when (item) { + is SlimSceneData -> { + DataType.SCENE + } + + is GalleryData -> { + DataType.GALLERY + } + + is ImageData -> { + DataType.IMAGE + } + + is MarkerData -> { + DataType.MARKER + } + + is MovieData -> { + DataType.MOVIE + } + + is PerformerData -> { + DataType.PERFORMER + } + + is StudioData -> { + DataType.STUDIO + } + + is TagData -> { + DataType.TAG + } + + else -> throw IllegalArgumentException("Item of type ${item.javaClass} is not supported") + } +} + +fun getId(item: Any): String { + return when (item) { + is SlimSceneData -> { + item.id + } + + is GalleryData -> { + item.id + } + + is ImageData -> { + item.id + } + + is MarkerData -> { + item.id + } + + is MovieData -> { + item.id + } + + is PerformerData -> { + item.id + } + + is StudioData -> { + item.id + } + + is TagData -> { + item.id + } + + else -> throw IllegalArgumentException("Item of type ${item.javaClass} is not supported") + } +} From 36eb7654a71c243da0b37e028812b74d73a37be8 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:38:17 -0400 Subject: [PATCH 29/36] Refactor filters so that they don't store generated code --- .../stashapp/data/StashCustomFilter.kt | 17 ++++++++++++++--- .../stashapp/data/StashSavedFilter.kt | 16 +++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/data/StashCustomFilter.kt b/app/src/main/java/com/github/damontecres/stashapp/data/StashCustomFilter.kt index c8ba06fe..7a47bf2b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/data/StashCustomFilter.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/data/StashCustomFilter.kt @@ -8,19 +8,30 @@ import com.github.damontecres.stashapp.api.type.FindFilterType import com.github.damontecres.stashapp.api.type.SortDirectionEnum import com.github.damontecres.stashapp.util.toFind_filter import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable +@Serializable @Parcelize data class StashCustomFilter( - val mode: FilterMode, + override val dataType: DataType, override val direction: String?, override val sortBy: String?, val description: String, val query: String? = null, ) : Parcelable, StashFilter { + constructor( + mode: FilterMode, + direction: String?, + sortBy: String?, + description: String, + query: String? = null, + ) : + this(DataType.fromFilterMode(mode)!!, direction, sortBy, description, query) + override val filterType: FilterType get() = FilterType.CUSTOM_FILTER - override val dataType: DataType - get() = DataType.fromFilterMode(mode)!! + val mode: FilterMode + get() = dataType.filterMode fun asFindFilterType(): FindFilterType { val direction = diff --git a/app/src/main/java/com/github/damontecres/stashapp/data/StashSavedFilter.kt b/app/src/main/java/com/github/damontecres/stashapp/data/StashSavedFilter.kt index dba301b2..17f12435 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/data/StashSavedFilter.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/data/StashSavedFilter.kt @@ -4,14 +4,23 @@ import android.os.Parcelable import com.github.damontecres.stashapp.api.fragment.SavedFilterData import com.github.damontecres.stashapp.api.type.FilterMode import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable +@Serializable @Parcelize data class StashSavedFilter( val savedFilterId: String, - val mode: FilterMode, + override val dataType: DataType, override val sortBy: String? = null, override val direction: String? = null, ) : Parcelable, StashFilter { + constructor( + savedFilterId: String, + mode: FilterMode, + sortBy: String? = null, + direction: String? = null, + ) : this(savedFilterId, DataType.fromFilterMode(mode)!!, sortBy, direction) + constructor(savedFilterData: SavedFilterData) : this( savedFilterData.id, savedFilterData.mode, @@ -21,6 +30,7 @@ data class StashSavedFilter( override val filterType: FilterType get() = FilterType.SAVED_FILTER - override val dataType: DataType - get() = DataType.fromFilterMode(mode)!! + + val mode: FilterMode + get() = dataType.filterMode } From efa2b1a55267c9893d1b618fab4d12e8c33a363b Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:38:58 -0400 Subject: [PATCH 30/36] Add View All card to home page rows --- app/build.gradle.kts | 13 +-- .../damontecres/stashapp/data/FilterType.kt | 2 + .../com/github/damontecres/stashapp/ui/App.kt | 21 ++++- .../damontecres/stashapp/ui/HomePage.kt | 6 ++ .../damontecres/stashapp/ui/cards/Cards.kt | 16 ++-- .../stashapp/ui/cards/FilterCards.kt | 84 +++++++++++++++++++ .../damontecres/stashapp/util/Constants.kt | 4 +- build.gradle.kts | 15 +++- 8 files changed, 144 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/cards/FilterCards.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f406a76d..815eabcc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,6 +13,8 @@ plugins { id("kotlin-kapt") id("com.google.dagger.hilt.android") kotlin("kapt") + id("androidx.navigation.safeargs.kotlin") + kotlin("plugin.serialization") version "1.9.25" } fun getVersionCode(): Int { @@ -48,7 +50,7 @@ android { } composeOptions { // https://developer.android.com/jetpack/androidx/releases/compose-kotlin - kotlinCompilerExtensionVersion = "1.5.13" + kotlinCompilerExtensionVersion = "1.5.14" } defaultConfig { @@ -145,6 +147,7 @@ tasks.preBuild.dependsOn("generateStrings") val mediaVersion = "1.3.1" val glideVersion = "4.16.0" +val navVersion = "2.8.0-beta06" dependencies { implementation("androidx.core:core-ktx:1.13.1") @@ -155,10 +158,10 @@ dependencies { implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion") implementation("com.github.bumptech.glide:compose:1.0.0-beta01") implementation("androidx.leanback:leanback-tab:1.1.0-beta01") - implementation("androidx.navigation:navigation-runtime-ktx:2.7.7") + implementation("androidx.navigation:navigation-runtime-ktx:$navVersion") implementation("androidx.compose.runtime:runtime-android:1.6.8") - implementation("androidx.navigation:navigation-compose:2.7.7") - implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") + implementation("androidx.navigation:navigation-compose:$navVersion") + implementation("androidx.navigation:navigation-fragment-ktx:$navVersion") implementation(platform("androidx.compose:compose-bom:2024.06.00")) implementation("androidx.activity:activity-compose:1.9.1") implementation("androidx.compose.ui:ui:1.6.8") @@ -192,7 +195,7 @@ dependencies { implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.lifecycle:lifecycle-process:2.8.3") implementation("com.otaliastudios:zoomlayout:1.9.0") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") implementation("io.noties.markwon:core:4.6.2") implementation("com.google.dagger:hilt-android:2.51.1") implementation("androidx.hilt:hilt-navigation-compose:1.2.0") diff --git a/app/src/main/java/com/github/damontecres/stashapp/data/FilterType.kt b/app/src/main/java/com/github/damontecres/stashapp/data/FilterType.kt index 30599279..b345b4b1 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/data/FilterType.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/data/FilterType.kt @@ -14,6 +14,7 @@ import com.github.damontecres.stashapp.api.type.TagFilterType import com.github.damontecres.stashapp.util.QueryEngine import com.github.damontecres.stashapp.util.toFind_filter import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable enum class FilterType { CUSTOM_FILTER, @@ -93,6 +94,7 @@ interface AppFilter : StashFilter { } } +@Serializable @Parcelize data class PerformTogetherAppFilter(override val name: String, val performerIds: List) : AppFilter { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index b77d616f..fdca024f 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -41,6 +41,7 @@ import androidx.navigation.activity import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute import androidx.tv.material3.DrawerValue import androidx.tv.material3.Icon import androidx.tv.material3.NavigationDrawer @@ -59,7 +60,9 @@ import com.github.damontecres.stashapp.SettingsActivity import com.github.damontecres.stashapp.StudioActivity import com.github.damontecres.stashapp.api.fragment.MarkerData import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.data.StashCustomFilter import com.github.damontecres.stashapp.data.StashDefaultFilter +import com.github.damontecres.stashapp.data.StashSavedFilter import com.github.damontecres.stashapp.playback.PlaybackActivity import com.github.damontecres.stashapp.ui.details.ScenePage import com.github.damontecres.stashapp.ui.details.TagPage @@ -68,6 +71,7 @@ import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.getDataType import com.github.damontecres.stashapp.util.getId import com.github.damontecres.stashapp.util.secondsMs +import kotlin.reflect.typeOf private const val TAG = "Compose.App" @@ -301,8 +305,10 @@ fun App() { if (dataType == DataType.MARKER) { item as MarkerData Routes.playback(item.scene.videoSceneData.id, item.secondsMs) - } else { + } else if (dataType != null) { Routes.dataType(dataType, getId(item)) + } else { + item } navController.navigate(route = route) { } @@ -423,6 +429,19 @@ fun App() { } activityClass = PlaybackActivity::class } + + val typeMap = + mapOf( + typeOf() to NavType.EnumType(DataType::class.java), + ) + composable(typeMap = typeMap) { backStackEntry -> + val filter: StashCustomFilter = backStackEntry.toRoute() + FilterGrid(startingFilter = filter, itemOnClick = itemOnClick) + } + composable(typeMap = typeMap) { backStackEntry -> + val filter: StashSavedFilter = backStackEntry.toRoute() + FilterGrid(startingFilter = filter, itemOnClick = itemOnClick) + } } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt index a0fb11e8..b2e9e940 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -27,6 +27,7 @@ import androidx.tv.material3.Text import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.api.ServerInfoQuery import com.github.damontecres.stashapp.ui.cards.StashCard +import com.github.damontecres.stashapp.ui.cards.ViewAllCard import com.github.damontecres.stashapp.util.FilterParser import com.github.damontecres.stashapp.util.FrontPageParser import com.github.damontecres.stashapp.util.QueryEngine @@ -136,6 +137,11 @@ fun HomePageRow( StashCard(item, itemOnClick) } } + if (rowData.data.isNotEmpty()) { + item { + ViewAllCard(filter = row.data.filter, itemOnClick) + } + } } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt index ae16d50a..1452fa61 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt @@ -206,6 +206,7 @@ fun RootCard( imageHeight: Dp, modifier: Modifier = Modifier, imageUrl: String? = null, + imageContent: @Composable BoxScope.() -> Unit = {}, videoUrl: String? = null, onLongClick: (() -> Unit)? = null, imageOverlay: @Composable BoxScope.() -> Unit = {}, @@ -288,12 +289,15 @@ fun RootCard( .height(imageHeight) .padding(0.dp), ) { - GlideImage( - model = imageUrl, - contentDescription = "", - contentScale = ContentScale.Crop, // TODO or ContentScale.Fit ? - modifier = Modifier.fillMaxSize(), - ) + if (imageUrl.isNotNullOrBlank()) { + GlideImage( + model = imageUrl, + contentDescription = "", + contentScale = ContentScale.Crop, // TODO or ContentScale.Fit ? + modifier = Modifier.fillMaxSize(), + ) + } + imageContent.invoke(this) if (!focused) { imageOverlay.invoke(this) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/cards/FilterCards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/FilterCards.kt new file mode 100644 index 00000000..9b624547 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/FilterCards.kt @@ -0,0 +1,84 @@ +package com.github.damontecres.stashapp.ui.cards + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.data.StashFilter +import com.github.damontecres.stashapp.presenters.GalleryPresenter +import com.github.damontecres.stashapp.presenters.ImagePresenter +import com.github.damontecres.stashapp.presenters.MarkerPresenter +import com.github.damontecres.stashapp.presenters.MoviePresenter +import com.github.damontecres.stashapp.presenters.PerformerPresenter +import com.github.damontecres.stashapp.presenters.ScenePresenter +import com.github.damontecres.stashapp.presenters.StudioPresenter +import com.github.damontecres.stashapp.presenters.TagPresenter + +@Suppress("ktlint:standard:function-naming") +@Composable +fun ViewAllCard( + filter: StashFilter, + itemOnClick: (Any) -> Unit, +) { + val width = + when (filter.dataType) { + DataType.GALLERY -> GalleryPresenter.CARD_WIDTH + DataType.IMAGE -> ImagePresenter.CARD_WIDTH + DataType.MARKER -> MarkerPresenter.CARD_WIDTH + DataType.MOVIE -> MoviePresenter.CARD_WIDTH + DataType.PERFORMER -> PerformerPresenter.CARD_WIDTH + DataType.SCENE -> ScenePresenter.CARD_WIDTH + DataType.STUDIO -> StudioPresenter.CARD_WIDTH + DataType.TAG -> TagPresenter.CARD_WIDTH + } + val height = + when (filter.dataType) { + DataType.GALLERY -> GalleryPresenter.CARD_HEIGHT + DataType.IMAGE -> ImagePresenter.CARD_HEIGHT + DataType.MARKER -> MarkerPresenter.CARD_HEIGHT + DataType.MOVIE -> MoviePresenter.CARD_HEIGHT + DataType.PERFORMER -> PerformerPresenter.CARD_HEIGHT + DataType.SCENE -> ScenePresenter.CARD_HEIGHT + DataType.STUDIO -> StudioPresenter.CARD_HEIGHT + DataType.TAG -> TagPresenter.CARD_HEIGHT + } + + RootCard( + modifier = + Modifier + .padding(0.dp) + .width(width.dp / 2), + onClick = { + itemOnClick(filter) + }, + imageWidth = width.dp / 2, + imageContent = { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.baseline_camera_indoor_48), + contentDescription = "", + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxSize(), + ) + }, + imageHeight = height.dp / 2, + imageUrl = null, + videoUrl = null, + title = stringResource(R.string.stashapp_view_all), + subtitle = { + Text("") + }, + description = { + Text("") + }, + ) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt index fe73d79d..a634b787 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt @@ -716,7 +716,7 @@ fun Activity.isNavHostActive(): Boolean { return intent.getBooleanExtra(Constants.USE_NAV_CONTROLLER, false) } -fun getDataType(item: Any): DataType { +fun getDataType(item: Any): DataType? { return when (item) { is SlimSceneData -> { DataType.SCENE @@ -750,7 +750,7 @@ fun getDataType(item: Any): DataType { DataType.TAG } - else -> throw IllegalArgumentException("Item of type ${item.javaClass} is not supported") + else -> null } } diff --git a/build.gradle.kts b/build.gradle.kts index 785c26ca..c3932d40 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,17 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id("com.android.application") version "8.5.1" apply false - id("org.jetbrains.kotlin.android") version "1.9.23" apply false - id("org.jetbrains.kotlin.jvm") version "1.9.23" apply false + id("org.jetbrains.kotlin.android") version "1.9.24" apply false + id("org.jetbrains.kotlin.jvm") version "1.9.24" apply false id("com.google.dagger.hilt.android") version "2.50" apply false - kotlin("kapt") version "1.9.23" apply false + kotlin("kapt") version "1.9.24" apply false +} +buildscript { + repositories { + google() + } + dependencies { + val navVersion = "2.8.0-beta06" + classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion") + } } From c5fdf2a6c8f862fb703a10216aebae31fec23581 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Mon, 29 Jul 2024 20:44:55 -0400 Subject: [PATCH 31/36] Fix routing --- .../com/github/damontecres/stashapp/ui/App.kt | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index fdca024f..48dce56f 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -62,6 +62,7 @@ import com.github.damontecres.stashapp.api.fragment.MarkerData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashCustomFilter import com.github.damontecres.stashapp.data.StashDefaultFilter +import com.github.damontecres.stashapp.data.StashFilter import com.github.damontecres.stashapp.data.StashSavedFilter import com.github.damontecres.stashapp.playback.PlaybackActivity import com.github.damontecres.stashapp.ui.details.ScenePage @@ -301,16 +302,21 @@ fun App() { val itemOnClick = { item: Any -> val dataType = getDataType(item) - val route = - if (dataType == DataType.MARKER) { - item as MarkerData - Routes.playback(item.scene.videoSceneData.id, item.secondsMs) - } else if (dataType != null) { - Routes.dataType(dataType, getId(item)) - } else { - item + if (dataType != null) { + val route = + if (dataType == DataType.MARKER) { + item as MarkerData + Routes.playback(item.scene.videoSceneData.id, item.secondsMs) + } else { + Routes.dataType(dataType, getId(item)) + } + navController.navigate(route = route) { + } + } else if (item is StashFilter) { + navController.navigate(route = item) { } - navController.navigate(route = route) { + } else { + throw IllegalArgumentException("Unknown item clicked: $item") } } From d057578998e81a4d6b0afd6507d80797eab4d740 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Tue, 30 Jul 2024 10:01:45 -0400 Subject: [PATCH 32/36] WIP using safe args routes --- .../stashapp/SceneDetailsActivity.kt | 4 +- .../stashapp/SceneDetailsFragment.kt | 7 +- .../stashapp/presenters/ScenePresenter.kt | 9 +- .../com/github/damontecres/stashapp/ui/App.kt | 103 +++++++++--------- .../stashapp/ui/details/ScenePage.kt | 50 ++++----- .../stashapp/ui/details/TagPage.kt | 50 ++++----- .../views/StashItemViewClickListener.kt | 2 +- 7 files changed, 107 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsActivity.kt index f4355b3f..b461d35a 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsActivity.kt @@ -26,7 +26,9 @@ class SceneDetailsActivity : FragmentActivity() { companion object { const val SHARED_ELEMENT_NAME = "hero" + + @Deprecated("Use an id instead") const val MOVIE = "Movie" - const val MOVIE_ID = "MovieID" + const val MOVIE_ID = "id" } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsFragment.kt index 23c5870d..c50a16af 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsFragment.kt @@ -44,7 +44,6 @@ import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.Marker import com.github.damontecres.stashapp.data.OCounter -import com.github.damontecres.stashapp.data.Scene import com.github.damontecres.stashapp.playback.PlaybackActivity import com.github.damontecres.stashapp.presenters.ActionPresenter import com.github.damontecres.stashapp.presenters.CreateMarkerActionPresenter @@ -227,7 +226,7 @@ class SceneDetailsFragment : DetailsSupportFragment() { ) { super.onViewCreated(view, savedInstanceState) - val sceneId = requireActivity().intent.getStringExtra(SceneDetailsActivity.MOVIE) + val sceneId = requireActivity().intent.getStringExtra(SceneDetailsActivity.MOVIE_ID) if (sceneId == null) { Log.w(TAG, "No scene found in intent") val intent = Intent(requireActivity(), MainActivity::class.java) @@ -452,8 +451,8 @@ class SceneDetailsFragment : DetailsSupportFragment() { ) { val intent = Intent(requireActivity(), PlaybackActivity::class.java) intent.putExtra( - SceneDetailsActivity.MOVIE, - Scene.fromFullSceneData(mSelectedMovie!!), + SceneDetailsActivity.MOVIE_ID, + mSelectedMovie!!.id, ) if (action.id == ACTION_RESUME_SCENE || action.id == ACTION_TRANSCODE_RESUME_SCENE || diff --git a/app/src/main/java/com/github/damontecres/stashapp/presenters/ScenePresenter.kt b/app/src/main/java/com/github/damontecres/stashapp/presenters/ScenePresenter.kt index 5fdc1ff1..d7e72914 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/presenters/ScenePresenter.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/presenters/ScenePresenter.kt @@ -7,7 +7,6 @@ import com.github.damontecres.stashapp.SceneDetailsActivity import com.github.damontecres.stashapp.SceneDetailsFragment import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.data.DataType -import com.github.damontecres.stashapp.data.Scene import com.github.damontecres.stashapp.playback.PlaybackActivity import com.github.damontecres.stashapp.util.ServerPreferences import com.github.damontecres.stashapp.util.concatIfNotBlank @@ -114,8 +113,8 @@ class ScenePresenter(callback: LongClickCallBack? = null) : // Resume val intent = Intent(context, PlaybackActivity::class.java) intent.putExtra( - SceneDetailsActivity.MOVIE, - Scene.fromSlimSceneData(item), + SceneDetailsActivity.MOVIE_ID, + item.id, ) if (item.resume_time != null) { intent.putExtra( @@ -130,8 +129,8 @@ class ScenePresenter(callback: LongClickCallBack? = null) : // Restart/Play val intent = Intent(context, PlaybackActivity::class.java) intent.putExtra( - SceneDetailsActivity.MOVIE, - Scene.fromSlimSceneData(item), + SceneDetailsActivity.MOVIE_ID, + item.id, ) context.startActivity(intent) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 48dce56f..f993a741 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -71,6 +71,7 @@ import com.github.damontecres.stashapp.util.Constants import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.getDataType import com.github.damontecres.stashapp.util.getId +import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.secondsMs import kotlin.reflect.typeOf @@ -126,25 +127,18 @@ data class DrawerPage( } } -sealed class Routes { - companion object { - val PLAYBACK = - "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE_ID}}/play/{$POSITION_ARG}" - - fun playback( - sceneId: String, - position: Long, - ): String { - return "${DrawerPage.dataType(DataType.SCENE).route}/$sceneId/play/$position" - } +sealed class Route { + data class DataTypeRoute(val dataType: DataType, val id: String? = null) : Route() - fun dataType( - dataType: DataType, - id: String, - ): String { - return "${DrawerPage.dataType(dataType).route}/$id" - } - } + data class Playback(val id: String, val position: Long = 0) : Route() + + data class Filter(val filter: StashFilter) : Route() + + data object Home : Route() + + data object Search : Route() + + data object Settings : Route() } class AppViewModel : ViewModel() { @@ -302,53 +296,62 @@ fun App() { val itemOnClick = { item: Any -> val dataType = getDataType(item) - if (dataType != null) { - val route = - if (dataType == DataType.MARKER) { - item as MarkerData - Routes.playback(item.scene.videoSceneData.id, item.secondsMs) - } else { - Routes.dataType(dataType, getId(item)) - } - navController.navigate(route = route) { - } - } else if (item is StashFilter) { - navController.navigate(route = item) { + val route = + if (dataType == DataType.MARKER) { + item as MarkerData + Route.Playback(item.scene.videoSceneData.id, item.secondsMs) + } else if (dataType != null) { + Route.DataTypeRoute(dataType, getId(item)) + } else if (item is StashFilter) { + item + } else { + throw IllegalArgumentException("Unknown item clicked: $item") } - } else { - throw IllegalArgumentException("Unknown item clicked: $item") + navController.navigate(route = route) { } } - composable(route = DrawerPage.HOME_PAGE.route) { + composable { HomePage(itemOnClick) } - activity(route = DrawerPage.SEARCH_PAGE.route) { + activity { activityClass = SearchActivity::class argument(Constants.USE_NAV_CONTROLLER) { type = NavType.BoolType defaultValue = true } } - activity(route = DrawerPage.SETTINGS_PAGE.route) { + activity { activityClass = SettingsActivity::class argument(Constants.USE_NAV_CONTROLLER) { type = NavType.BoolType defaultValue = true } } - DataType.entries.forEach { dataType -> - composable(route = DrawerPage.dataType(dataType).route) { - FilterGrid(StashDefaultFilter(dataType), itemOnClick) - } - } - composable(route = "${DrawerPage.dataType(DataType.SCENE).route}/{${SceneDetailsActivity.MOVIE}}") { - argument(SceneDetailsActivity.MOVIE) { - type = NavType.StringType - nullable = false + val composedDataTypes = listOf(DataType.SCENE, DataType.TAG) + + composable { + val dataTypeRoute = it.toRoute() + if (dataTypeRoute.dataType == DataType.MARKER) { + throw IllegalArgumentException("Cannot pass DataType.Marker in a DataTypeRoute") + } + if (dataTypeRoute.id.isNotNullOrBlank()) { + FilterGrid(StashDefaultFilter(dataTypeRoute.dataType), itemOnClick) + } else if (dataTypeRoute.id != null && dataTypeRoute.dataType in composedDataTypes) { + when (dataTypeRoute.dataType) { + DataType.SCENE -> + ScenePage( + sceneId = dataTypeRoute.id, + itemOnClick = itemOnClick, + ) + + DataType.TAG -> TagPage(dataTypeRoute.id, itemOnClick = itemOnClick) + else -> throw IllegalStateException() + } + } else { + throw IllegalStateException() } - ScenePage(itemOnClick = itemOnClick) } activity(route = "${DrawerPage.dataType(DataType.PERFORMER).route}/{id}") { @@ -411,15 +414,7 @@ fun App() { activityClass = StudioActivity::class } - composable(route = "${DrawerPage.dataType(DataType.TAG).route}/{id}") { - argument("id") { - type = NavType.StringType - nullable = false - } - TagPage(itemOnClick) - } - - activity(route = Routes.PLAYBACK) { + activity { argument(SceneDetailsActivity.MOVIE_ID) { type = NavType.StringType nullable = false diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt index d47e9be1..78d03490 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt @@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.height 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.livedata.observeAsState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment @@ -23,10 +23,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import androidx.tv.foundation.lazy.list.TvLazyColumn import androidx.tv.foundation.lazy.list.TvLazyRow @@ -38,7 +38,6 @@ import androidx.tv.material3.Text import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage import com.github.damontecres.stashapp.R -import com.github.damontecres.stashapp.SceneDetailsActivity import com.github.damontecres.stashapp.api.fragment.FullSceneData import com.github.damontecres.stashapp.api.fragment.GalleryData import com.github.damontecres.stashapp.api.fragment.MarkerData @@ -55,9 +54,6 @@ import com.github.damontecres.stashapp.views.durationToString import com.github.damontecres.stashapp.views.parseTimeToString import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import javax.inject.Inject sealed class SceneUiState { @@ -77,25 +73,18 @@ class SceneViewModel ) : ViewModel() { private val queryEngine = QueryEngine(context) - val uiState = - savedStateHandle - .getStateFlow(SceneDetailsActivity.MOVIE, null) - .map { id -> - if (id == null) { - SceneUiState.Error("sceneId cannot be null") - } else { - val scene = queryEngine.getScene(id) - if (scene != null) { - SceneUiState.Success(scene) - } else { - SceneUiState.Error("No scene with id=$id") - } - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = SceneUiState.Loading, - ) + private val _uiState = MutableLiveData(SceneUiState.Loading) + val uiState: LiveData get() = _uiState + + suspend fun fetchScene(sceneId: String) { + val scene = queryEngine.getScene(sceneId) + _uiState.value = + if (scene != null) { + SceneUiState.Success(scene) + } else { + SceneUiState.Error("No scene with id=$sceneId") + } + } private val _performers = mutableStateListOf() val performers: SnapshotStateList get() = _performers @@ -119,10 +108,15 @@ class SceneViewModel @Suppress("ktlint:standard:function-naming") @Composable fun ScenePage( + sceneId: String, itemOnClick: (Any) -> Unit, viewModel: SceneViewModel = hiltViewModel(), ) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState = viewModel.uiState.observeAsState().value + + LaunchedEffect(Unit) { + viewModel.fetchScene(sceneId) + } when (val s = uiState) { is SceneUiState.Loading -> { @@ -146,6 +140,8 @@ fun ScenePage( .animateContentSize(), ) } + + null -> throw IllegalStateException() } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt index 69866d17..0f9d0000 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt @@ -4,14 +4,15 @@ import android.content.Context import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewModelScope import androidx.tv.material3.Text import com.apollographql.apollo3.api.Optional import com.apollographql.apollo3.api.Query @@ -38,9 +39,6 @@ import com.github.damontecres.stashapp.ui.TabbedFilterGrid import com.github.damontecres.stashapp.util.QueryEngine import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn import javax.inject.Inject sealed class TagUiState { @@ -60,34 +58,32 @@ class TagViewModel ) : ViewModel() { private val queryEngine = QueryEngine(context) - val uiState = - savedStateHandle - .getStateFlow("id", null) - .map { id -> - if (id == null) { - TagUiState.Error("TagId cannot be null") - } else { - val tag = queryEngine.getTags(listOf(id)).firstOrNull() - if (tag != null) { - TagUiState.Success(tag) - } else { - TagUiState.Error("No Tag with id=$id") - } - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5_000), - initialValue = TagUiState.Loading, - ) + private val _uiState = MutableLiveData(TagUiState.Loading) + val uiState: LiveData get() = _uiState + + suspend fun fetchTag(tagId: String) { + val tag = queryEngine.getTags(listOf(tagId)).firstOrNull() + _uiState.value = + if (tag != null) { + TagUiState.Success(tag) + } else { + TagUiState.Error("No Tag with id=$tagId") + } + } } @Suppress("ktlint:standard:function-naming") @Composable fun TagPage( + tagId: String, itemOnClick: (Any) -> Unit, viewModel: TagViewModel = hiltViewModel(), ) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val uiState = viewModel.uiState.observeAsState().value + + LaunchedEffect(Unit) { + viewModel.fetchTag(tagId) + } when (val s = uiState) { is TagUiState.Loading -> { @@ -121,6 +117,8 @@ fun TagPage( .animateContentSize(), ) } + + null -> throw IllegalStateException() } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt b/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt index 8ee5edfe..8bfb7514 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/views/StashItemViewClickListener.kt @@ -52,7 +52,7 @@ class StashItemViewClickListener( ) { if (item is SlimSceneData) { val intent = Intent(context, SceneDetailsActivity::class.java) - intent.putExtra(SceneDetailsActivity.MOVIE, item.id) + intent.putExtra(SceneDetailsActivity.MOVIE_ID, item.id) context.startActivity(intent) } else if (item is PerformerData) { val intent = Intent(context, PerformerActivity::class.java) From 34c0e0dfefd2aaf68ab9b721b39da365df66d025 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Fri, 2 Aug 2024 19:55:38 -0400 Subject: [PATCH 33/36] Some fixes to navigation --- .../com/github/damontecres/stashapp/ui/App.kt | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index f993a741..4736e21e 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -73,6 +73,7 @@ import com.github.damontecres.stashapp.util.getDataType import com.github.damontecres.stashapp.util.getId import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.secondsMs +import kotlinx.serialization.Serializable import kotlin.reflect.typeOf private const val TAG = "Compose.App" @@ -127,17 +128,24 @@ data class DrawerPage( } } +@Serializable sealed class Route { + @Serializable data class DataTypeRoute(val dataType: DataType, val id: String? = null) : Route() + @Serializable data class Playback(val id: String, val position: Long = 0) : Route() + @Serializable data class Filter(val filter: StashFilter) : Route() + @Serializable data object Home : Route() + @Serializable data object Search : Route() + @Serializable data object Settings : Route() } @@ -288,11 +296,16 @@ fun App() { ) { NavHost( navController = navController, - startDestination = DrawerPage.HOME_PAGE.route, + startDestination = Route.Home, modifier = Modifier, // .fillMaxSize() // .padding(start = collapsedDrawerItemWidth), ) { + val typeMap = + mapOf( + typeOf() to NavType.EnumType(DataType::class.java), + ) + val itemOnClick = { item: Any -> val dataType = getDataType(item) @@ -331,7 +344,7 @@ fun App() { val composedDataTypes = listOf(DataType.SCENE, DataType.TAG) - composable { + composable(typeMap) { val dataTypeRoute = it.toRoute() if (dataTypeRoute.dataType == DataType.MARKER) { throw IllegalArgumentException("Cannot pass DataType.Marker in a DataTypeRoute") @@ -430,11 +443,6 @@ fun App() { } activityClass = PlaybackActivity::class } - - val typeMap = - mapOf( - typeOf() to NavType.EnumType(DataType::class.java), - ) composable(typeMap = typeMap) { backStackEntry -> val filter: StashCustomFilter = backStackEntry.toRoute() FilterGrid(startingFilter = filter, itemOnClick = itemOnClick) From c8c4b2dd3b1fbaadbcf6f3bd620b87264c816b80 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Fri, 2 Aug 2024 20:10:53 -0400 Subject: [PATCH 34/36] More fixes to navigation --- .../com/github/damontecres/stashapp/ui/App.kt | 60 ++++++++++--------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 4736e21e..0c659359 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -1,5 +1,6 @@ package com.github.damontecres.stashapp.ui +import android.os.Parcelable import android.util.Log import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi @@ -71,35 +72,31 @@ import com.github.damontecres.stashapp.util.Constants import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.getDataType import com.github.damontecres.stashapp.util.getId -import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.secondsMs +import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import kotlin.reflect.typeOf private const val TAG = "Compose.App" data class DrawerPage( - val route: String, + val route: Route, @StringRes val iconString: Int, @StringRes val name: Int, ) { - fun idRoute(id: String): String { - return "$route/$id" - } - companion object { - val HOME_PAGE = DrawerPage("home", R.string.fa_house, R.string.home) + val HOME_PAGE = DrawerPage(Route.Home, R.string.fa_house, R.string.home) val SEARCH_PAGE = DrawerPage( - "search", + Route.Search, R.string.fa_magnifying_glass_plus, R.string.stashapp_actions_search, ) val SETTINGS_PAGE = DrawerPage( - "settings", + Route.Settings, R.string.fa_arrow_right_arrow_left, // Ignored R.string.stashapp_settings, ) @@ -109,7 +106,11 @@ data class DrawerPage( DataType.entries.forEach { dataType -> put( dataType, - DrawerPage(dataType.name, dataType.iconStringId, dataType.pluralStringId), + DrawerPage( + Route.DataTypeRoute(dataType), + dataType.iconStringId, + dataType.pluralStringId, + ), ) } } @@ -129,7 +130,8 @@ data class DrawerPage( } @Serializable -sealed class Route { +@Parcelize +sealed class Route : Parcelable { @Serializable data class DataTypeRoute(val dataType: DataType, val id: String? = null) : Route() @@ -195,18 +197,19 @@ fun App() { .focusProperties { enter = { focusDirection -> if (focusDirection == FocusDirection.Left) { - val currentPage = - DrawerPage.PAGES.firstOrNull { page -> - navController.currentDestination?.route?.startsWith( - page.route, - ) ?: false - } - Log.v(TAG, "focus enter currentPage=$currentPage") - if (currentPage != null) { - focusRequesters[currentPage]!! - } else { - focusRequesters[DrawerPage.HOME_PAGE]!! - } +// val currentPage = +// DrawerPage.PAGES.firstOrNull { page -> +// navController.currentDestination?.route?.startsWith( +// page.route, +// ) ?: false +// } +// Log.v(TAG, "focus enter currentPage=$currentPage") +// if (currentPage != null) { +// focusRequesters[currentPage]!! +// } else { +// focusRequesters[DrawerPage.HOME_PAGE]!! +// } + FocusRequester.Default } else { FocusRequester.Default } @@ -258,9 +261,9 @@ fun App() { .focusRequester(focusRequesters[page]!!), // shape = NavigationDrawerItemDefaults.shape(shape = RoundedCornerShape(50)), // glow = NavigationDrawerItemDefaults.glow(Glow.None), - selected = - navController.currentDestination?.route?.startsWith(page.route) - ?: false, + selected = false, +// navController.currentDestination?.route?.startsWith(page.route) +// ?: false, onClick = { drawerState.setValue(DrawerValue.Closed) Log.v(TAG, "Navigating to ${page.route}") @@ -346,12 +349,13 @@ fun App() { composable(typeMap) { val dataTypeRoute = it.toRoute() + Log.v(TAG, "dataTypeRoute=$dataTypeRoute") if (dataTypeRoute.dataType == DataType.MARKER) { throw IllegalArgumentException("Cannot pass DataType.Marker in a DataTypeRoute") } - if (dataTypeRoute.id.isNotNullOrBlank()) { + if (dataTypeRoute.id.isNullOrBlank()) { FilterGrid(StashDefaultFilter(dataTypeRoute.dataType), itemOnClick) - } else if (dataTypeRoute.id != null && dataTypeRoute.dataType in composedDataTypes) { + } else if (dataTypeRoute.dataType in composedDataTypes) { when (dataTypeRoute.dataType) { DataType.SCENE -> ScenePage( From 449abfcd3492598191e13f37f4841caf754d890f Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Fri, 2 Aug 2024 20:40:54 -0400 Subject: [PATCH 35/36] Launch activities for non-compose data types --- .../com/github/damontecres/stashapp/ui/App.kt | 110 ++++++++---------- 1 file changed, 50 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index 0c659359..e9bee348 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -1,5 +1,6 @@ package com.github.damontecres.stashapp.ui +import android.content.Intent import android.os.Parcelable import android.util.Log import androidx.annotation.StringRes @@ -355,7 +356,7 @@ fun App() { } if (dataTypeRoute.id.isNullOrBlank()) { FilterGrid(StashDefaultFilter(dataTypeRoute.dataType), itemOnClick) - } else if (dataTypeRoute.dataType in composedDataTypes) { + } else { when (dataTypeRoute.dataType) { DataType.SCENE -> ScenePage( @@ -364,71 +365,60 @@ fun App() { ) DataType.TAG -> TagPage(dataTypeRoute.id, itemOnClick = itemOnClick) - else -> throw IllegalStateException() - } - } else { - throw IllegalStateException() - } - } - activity(route = "${DrawerPage.dataType(DataType.PERFORMER).route}/{id}") { - argument("id") { - type = NavType.StringType - nullable = false - } - argument(Constants.USE_NAV_CONTROLLER) { - type = NavType.BoolType - defaultValue = true - } - activityClass = PerformerActivity::class - } + DataType.PERFORMER -> { + context.startActivity( + Intent( + context, + PerformerActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } - activity(route = "${DrawerPage.dataType(DataType.GALLERY).route}/{id}") { - argument("id") { - type = NavType.StringType - nullable = false - } - argument(Constants.USE_NAV_CONTROLLER) { - type = NavType.BoolType - defaultValue = true - } - activityClass = GalleryActivity::class - } + DataType.STUDIO -> { + context.startActivity( + Intent( + context, + StudioActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } - activity(route = "${DrawerPage.dataType(DataType.IMAGE).route}/{id}") { - argument("id") { - type = NavType.StringType - nullable = false - } - argument(Constants.USE_NAV_CONTROLLER) { - type = NavType.BoolType - defaultValue = true - } - activityClass = ImageActivity::class - } + DataType.MOVIE -> { + context.startActivity( + Intent( + context, + MovieActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } - activity(route = "${DrawerPage.dataType(DataType.MOVIE).route}/{id}") { - argument("id") { - type = NavType.StringType - nullable = false - } - argument(Constants.USE_NAV_CONTROLLER) { - type = NavType.BoolType - defaultValue = true - } - activityClass = MovieActivity::class - } + DataType.IMAGE -> { + context.startActivity( + Intent( + context, + ImageActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } - activity(route = "${DrawerPage.dataType(DataType.STUDIO).route}/{id}") { - argument("id") { - type = NavType.StringType - nullable = false - } - argument(Constants.USE_NAV_CONTROLLER) { - type = NavType.BoolType - defaultValue = true + DataType.GALLERY -> { + context.startActivity( + Intent( + context, + GalleryActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } + + DataType.MARKER -> throw IllegalStateException() + } } - activityClass = StudioActivity::class } activity { From 540db72fbd9072c143ea9b60446752733366aa86 Mon Sep 17 00:00:00 2001 From: Damontecres <154766448+damontecres@users.noreply.github.com> Date: Fri, 2 Aug 2024 20:44:29 -0400 Subject: [PATCH 36/36] Add playback callback --- .../main/java/com/github/damontecres/stashapp/ui/App.kt | 8 ++++++++ .../github/damontecres/stashapp/ui/details/ScenePage.kt | 7 ++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt index e9bee348..bf293388 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -362,6 +362,14 @@ fun App() { ScenePage( sceneId = dataTypeRoute.id, itemOnClick = itemOnClick, + playbackCallback = { position -> + navController.navigate( + Route.Playback( + dataTypeRoute.id, + position, + ), + ) + }, ) DataType.TAG -> TagPage(dataTypeRoute.id, itemOnClick = itemOnClick) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt index 78d03490..08b21b84 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt @@ -110,6 +110,7 @@ class SceneViewModel fun ScenePage( sceneId: String, itemOnClick: (Any) -> Unit, + playbackCallback: (Long) -> Unit, viewModel: SceneViewModel = hiltViewModel(), ) { val uiState = viewModel.uiState.observeAsState().value @@ -135,6 +136,7 @@ fun ScenePage( SceneDetails( s.scene, itemOnClick, + playbackCallback, Modifier .fillMaxSize() .animateContentSize(), @@ -151,6 +153,7 @@ fun ScenePage( private fun SceneDetails( scene: FullSceneData, itemOnClick: (Any) -> Unit, + playbackCallback: (Long) -> Unit, modifier: Modifier = Modifier, viewModel: SceneViewModel = hiltViewModel(), ) { @@ -275,7 +278,9 @@ private fun SceneDetails( } item { - Button(onClick = { /*TODO*/ }) { + Button(onClick = { + playbackCallback.invoke(0L) + }) { Text(text = "Play") } }