diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d64a0363..05d8c6d9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,6 +11,10 @@ plugins { id("kotlin-parcelize") id("com.apollographql.apollo3") version "3.8.4" 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 { @@ -43,8 +47,12 @@ android { buildFeatures { buildConfig = true + compose = true + } + composeOptions { + // https://developer.android.com/jetpack/androidx/releases/compose-kotlin + kotlinCompilerExtensionVersion = "1.5.14" } - defaultConfig { applicationId = "com.github.damontecres.stashapp" minSdk = 23 @@ -129,12 +137,18 @@ 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") val mediaVersion = "1.3.1" val glideVersion = "4.16.0" val acraVersion = "5.11.3" +val navVersion = "2.8.0-beta06" dependencies { implementation("androidx.core:core-ktx:1.13.1") @@ -143,7 +157,25 @@ 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:$navVersion") + implementation("androidx.compose.runtime:runtime-android:1.6.8") + 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") + 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") + 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") implementation("androidx.test.ext:junit-ktx:1.2.1") kapt("com.github.bumptech.glide:compiler:$glideVersion") implementation("com.caverock:androidsvg-aar:1.4") @@ -165,8 +197,11 @@ 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") + kapt("com.google.dagger:hilt-android-compiler:2.51.1") implementation("ch.acra:acra-http:$acraVersion") implementation("ch.acra:acra-dialog:$acraVersion") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af6a8182..27c06ee9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,8 +36,6 @@ android:exported="true" android:icon="@mipmap/stash_logo" android:logo="@mipmap/stash_logo" - android:noHistory="true" - android:screenOrientation="landscape" android:theme="@style/NoTitleTheme"> @@ -49,6 +47,10 @@ android:name=".MainActivity" android:exported="false" android:launchMode="singleTop" /> + { intent.getParcelableExtra("filter") } + FilterType.DEFAULT_FILTER -> { + throw IllegalStateException() + } } setupFragment(filterData, true) } else { 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..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 @@ -53,7 +54,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) @@ -73,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() + } } } } @@ -100,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( @@ -117,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 } } } @@ -274,7 +286,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..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() @@ -247,7 +263,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/MainComposeActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/MainComposeActivity.kt new file mode 100644 index 00000000..4c33ae36 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/MainComposeActivity.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 MainComposeActivity : 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/MovieActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/MovieActivity.kt index 2c8d17e3..959ae0a0 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,70 @@ 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 com.github.damontecres.stashapp.util.isNavHostActive +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, + ) + sceneFragment.pagingAdapter.registerObserver( + object : ObjectAdapter.DataObserver() { + override fun onChanged() { + sceneFragment.view!!.requestFocus() + sceneFragment.pagingAdapter.unregisterObserver(this) + } + }, ) - 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) - } - }, - ) + 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/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/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 f8461ce2..1f828e07 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 @@ -42,16 +41,17 @@ 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() { - 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) @@ -66,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() + } } } @@ -93,113 +98,119 @@ 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(performer.id)), - 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(performer.id)), - 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(performer.id)), - 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(performer.id)), - 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(performer.id), - getColumns(DataType.TAG), - ) - } else if (position == 5) { - val presenter = - ClassPresenterSelector() - .addClassPresenter( - PerformerData::class.java, - PerformerPresenter(PerformTogetherLongClickCallback(performer)), - ) - StashGridFragment( - presenter, - PerformerComparator, - PerformerDataSupplier( - PerformerFilterType( - performers = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(performer.id)), - 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 } } } - private class PerformTogetherLongClickCallback(val performer: Performer) : + private class PerformTogetherLongClickCallback(val performerId: String) : StashPresenter.LongClickCallBack { override fun getPopUpItems( context: Context, @@ -222,8 +233,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/PinActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/PinActivity.kt index 8345792a..64673f21 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/PinActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/PinActivity.kt @@ -109,7 +109,17 @@ class PinActivity() : FragmentActivity() { if (requireActivity().isTaskRoot || mainDestroyed) { // This is the task root when invoked from the start // mainDestroyed is true when the MainActivity was destroyed, but not the entire app - val intent = Intent(requireContext(), MainActivity::class.java) + + val mainClass = + if (PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getBoolean(getString(R.string.pref_key_use_compose_ui), false) + ) { + MainComposeActivity::class.java + } else { + MainActivity::class.java + } + + val intent = Intent(requireContext(), mainClass) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) } 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..b461d35a 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,15 +12,23 @@ 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() + } } } 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/SearchActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt index 567de6b8..cdf13fd2 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/SearchActivity.kt @@ -2,14 +2,23 @@ package com.github.damontecres.stashapp import android.os.Bundle import androidx.fragment.app.FragmentActivity +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) { - supportFragmentManager.beginTransaction() - .replace(R.id.main_browse_fragment, StashSearchFragment()).commitNow() + if (isNavHostActive()) { + val navFragment = NavFragment(StashSearchFragment()) + 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/SettingsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt index f79b3555..78f06b9a 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt @@ -239,6 +239,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()) @@ -341,6 +347,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/StashApplication.kt b/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt index 5a2d9174..c6e4a79b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt @@ -18,7 +18,9 @@ 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 @@ -28,6 +30,7 @@ import org.acra.config.dialog import org.acra.data.StringFormat import org.acra.ktx.initAcra +@HiltAndroidApp class StashApplication : Application() { private var wasEnterBackground = false private var mainDestroyed = false @@ -207,6 +210,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/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/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/StudioActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/StudioActivity.kt index 21ba4a97..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? { @@ -34,7 +35,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,108 +45,114 @@ 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( + 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 6708711b..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,14 +26,17 @@ 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? { - 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( @@ -48,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, @@ -57,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/data/FilterType.kt b/app/src/main/java/com/github/damontecres/stashapp/data/FilterType.kt index bf4150d5..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 @@ -11,13 +11,16 @@ 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 +import kotlinx.serialization.Serializable enum class FilterType { CUSTOM_FILTER, SAVED_FILTER, APP_FILTER, + DEFAULT_FILTER, ; companion object { @@ -45,6 +48,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 */ @@ -82,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/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/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 } 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/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/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 new file mode 100644 index 00000000..bf293388 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/App.kt @@ -0,0 +1,458 @@ +package com.github.damontecres.stashapp.ui + +import android.content.Intent +import android.os.Parcelable +import android.util.Log +import androidx.annotation.StringRes +import androidx.compose.foundation.ExperimentalFoundationApi +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.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 +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 +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.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.toRoute +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.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.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 +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 +import kotlinx.parcelize.Parcelize +import kotlinx.serialization.Serializable +import kotlin.reflect.typeOf + +private const val TAG = "Compose.App" + +data class DrawerPage( + val route: Route, + @StringRes val iconString: Int, + @StringRes val name: Int, +) { + companion object { + val HOME_PAGE = DrawerPage(Route.Home, R.string.fa_house, R.string.home) + + val SEARCH_PAGE = + DrawerPage( + Route.Search, + R.string.fa_magnifying_glass_plus, + R.string.stashapp_actions_search, + ) + + val SETTINGS_PAGE = + DrawerPage( + Route.Settings, + R.string.fa_arrow_right_arrow_left, // Ignored + R.string.stashapp_settings, + ) + + val DATA_TYPE_PAGES = + buildMap { + DataType.entries.forEach { dataType -> + put( + dataType, + DrawerPage( + Route.DataTypeRoute(dataType), + dataType.iconStringId, + dataType.pluralStringId, + ), + ) + } + } + + 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) + } + } +} + +@Serializable +@Parcelize +sealed class Route : Parcelable { + @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() +} + +class AppViewModel : ViewModel() { + val currentServer = mutableStateOf(StashServer.getCurrentStashServer()) +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +fun App() { + val context = LocalContext.current + + val appViewModel = viewModel() + appViewModel.currentServer.value = StashServer.getCurrentStashServer() + + val fontFamily = + FontFamily( + Font( + resId = R.font.fa_solid_900, + ), + ) + + val defaultSelection: DrawerPage = DrawerPage.HOME_PAGE + + var currentScreen by remember { mutableStateOf(defaultSelection) } + + val drawerState = rememberDrawerState(DrawerValue.Closed) + + val collapsedDrawerItemWidth = 48.dp + val paddingValue = 12.dp + + val navController = rememberNavController() + val focusRequester = remember { FocusRequester() } + val focusRequesters = remember { DrawerPage.PAGES.associateWith { FocusRequester() } } + + NavigationDrawer( + drawerState = drawerState, + drawerContent = { + Column( + Modifier + .focusGroup() + .fillMaxHeight() + .padding(4.dp) + .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]!! +// } + FocusRequester.Default + } else { + FocusRequester.Default + } + } + }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + ) { + var serverFocused by remember { mutableStateOf(false) } + NavigationDrawerItem( + modifier = + Modifier.onFocusChanged { + serverFocused = it.isFocused + }, + selected = false, + onClick = { + // TODO + }, + leadingContent = { + Icon( + painterResource(id = R.mipmap.stash_logo), + contentDescription = null, + ) + }, + ) { + Text( + modifier = + Modifier.enableMarquee(serverFocused), + text = StashServer.getCurrentStashServer()?.url ?: "No server", + maxLines = 1, + ) + } + + // Group of item with same padding + + LazyColumn( + contentPadding = PaddingValues(0.dp), + modifier = + Modifier + .selectableGroup(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterVertically), + ) { + Log.v(TAG, "DrawerPage.PAGES=${DrawerPage.PAGES}") + items(DrawerPage.PAGES, key = { it.route }) { page -> + NavigationDrawerItem( + modifier = + Modifier + .focusRequester(focusRequesters[page]!!), + // shape = NavigationDrawerItemDefaults.shape(shape = RoundedCornerShape(50)), +// glow = NavigationDrawerItemDefaults.glow(Glow.None), + selected = false, +// 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 ?: "") { + inclusive = true + } + } + }, + leadingContent = { + 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)) + } + } + } + } + }, + ) { + NavHost( + navController = navController, + 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) + + 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") + } + navController.navigate(route = route) { + } + } + + composable { + HomePage(itemOnClick) + } + activity { + activityClass = SearchActivity::class + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } + } + activity { + activityClass = SettingsActivity::class + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } + } + + val composedDataTypes = listOf(DataType.SCENE, DataType.TAG) + + 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.isNullOrBlank()) { + FilterGrid(StashDefaultFilter(dataTypeRoute.dataType), itemOnClick) + } else { + when (dataTypeRoute.dataType) { + DataType.SCENE -> + ScenePage( + sceneId = dataTypeRoute.id, + itemOnClick = itemOnClick, + playbackCallback = { position -> + navController.navigate( + Route.Playback( + dataTypeRoute.id, + position, + ), + ) + }, + ) + + DataType.TAG -> TagPage(dataTypeRoute.id, itemOnClick = itemOnClick) + + DataType.PERFORMER -> { + context.startActivity( + Intent( + context, + PerformerActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } + + DataType.STUDIO -> { + context.startActivity( + Intent( + context, + StudioActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } + + DataType.MOVIE -> { + context.startActivity( + Intent( + context, + MovieActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } + + DataType.IMAGE -> { + context.startActivity( + Intent( + context, + ImageActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } + + DataType.GALLERY -> { + context.startActivity( + Intent( + context, + GalleryActivity::class.java, + ).putExtra("id", dataTypeRoute.id) + .putExtra(Constants.USE_NAV_CONTROLLER, true), + ) + } + + DataType.MARKER -> throw IllegalStateException() + } + } + } + + activity { + argument(SceneDetailsActivity.MOVIE_ID) { + type = NavType.StringType + nullable = false + } + argument(POSITION_ARG) { + type = NavType.LongType + nullable = false + defaultValue = 0L + } + argument(Constants.USE_NAV_CONTROLLER) { + type = NavType.BoolType + defaultValue = true + } + activityClass = PlaybackActivity::class + } + 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/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) + } 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..bc14a01f --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterPage.kt @@ -0,0 +1,482 @@ +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 +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 +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.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 +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.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.cards.StashCard +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 +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 + +private const val TAG = "FilterPage" + +@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, +) + +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 { + data object Loading : ResolvedFilterState() + + data class Success(val filter: ResolvedFilter, val pagingSource: StashPagingSource<*, Any, *>) : ResolvedFilterState() + + data object Error : ResolvedFilterState() +} + +@HiltViewModel +class FilterGridViewModel + @Inject + constructor( + @ApplicationContext private val context: Context, + ) : ViewModel() { + private val queryEngine = QueryEngine(context) + private val filterParser = FilterParser(ServerPreferences(context).serverVersion) + + private val _resolvedFilterState = MutableLiveData() + val resolvedFilterState: LiveData get() = _resolvedFilterState + + 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 -> { + TODO() + } + + is StashDefaultFilter -> { + val savedFilter = queryEngine.getDefaultFilter(filter.dataType) + if (savedFilter != null) { + savedFilter.resolve(filter.dataType) + } else { + 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, + ) + } + } + + 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 { + Log.v(TAG, "No saved filter for id=${filter.savedFilterId}") + _resolvedFilterState.value = ResolvedFilterState.Error + null + } + } + else -> throw IllegalStateException("Unsupported StashFilter type: $filter") + } + + if (resolvedFilter != null) { + updateFilter(resolvedFilter) + } + } + + 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, + ) + } + 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, + ) + } + + 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 = + ResolvedFilterState.Success( + resolvedFilter, + StashPagingSource( + StashApplication.getApplication(), + 25, // TODO + dataSupplier = dataSupplier, + useRandom = false, + ), + ) + } + } + +@Suppress("ktlint:standard:function-naming") +@Composable +fun FilterGrid( + startingFilter: StashFilter, + itemOnClick: (item: Any) -> Unit, +) { + Log.v(TAG, "startingFilter=$startingFilter") + val viewModel = hiltViewModel() + + val resolvedFilterState by viewModel.resolvedFilterState.observeAsState(ResolvedFilterState.Loading) + + LaunchedEffect(Unit) { + viewModel.updateFilter(startingFilter) + } + LaunchedEffect(Unit) { + viewModel.fetchSavedFilters(startingFilter.dataType) + } + + 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, + itemOnClick = itemOnClick, + ) + } + } +} + +@OptIn(ExperimentalTvFoundationApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +fun ResolvedFilterGrid( + resolvedFilter: ResolvedFilterState.Success, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(), + showHeader: Boolean = true, + itemOnClick: (item: Any) -> Unit, +) { +// 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() + val focusRequester = remember { FocusRequester() } + + TvLazyVerticalGrid( + modifier = + modifier + .padding(16.dp) + .fillMaxSize() + .focusGroup() + .focusRequester(focusRequester), + columns = TvGridCells.Fixed(5), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + 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() + } + } + } + if (lazyPagingItems.loadState.refresh == LoadState.Loading) { + item { + Text( + text = "Waiting for items to load from the backend", + modifier = + Modifier + .fillMaxWidth() + .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) { + item { +// CircularProgressIndicator( +// modifier = +// Modifier.fillMaxWidth() +// .wrapContentWidth(Alignment.CenterHorizontally), +// ) + Text( + text = "Waiting for items to load from the backend", + modifier = + Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + ) + } + } + } +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun SavedFilterDropDown() { + val viewModel = hiltViewModel() + 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/HomePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt new file mode 100644 index 00000000..151909a4 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/HomePage.kt @@ -0,0 +1,139 @@ +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +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 +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.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 +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 dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class HomePageViewModel + @Inject + constructor( + private val queryRepository: QueryRepository, + ) : ViewModel() { + private val _rows = mutableStateListOf() + val rows: SnapshotStateList get() = _rows + + 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 + + ServerPreferences(StashApplication.getApplication()).updatePreferences(config) + + 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) { + _rows.add(result) + } + } + } + } + } + +@Suppress("ktlint:standard:function-naming") +@Composable +fun HomePage(itemOnClick: (Any) -> Unit) { + val viewModel = hiltViewModel() + + val rows = remember { viewModel.rows } + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + viewModel.fetchFrontPage() + } + + TvLazyColumn( + verticalArrangement = Arrangement.spacedBy(20.dp), + contentPadding = PaddingValues(bottom = 75.dp), + modifier = + Modifier + .fillMaxSize() + .focusGroup() + .focusRequester(focusRequester), + ) { + items(rows, key = { rows.indexOf(it) }) { row -> + HomePageRow(row, itemOnClick) + } + } +// if (rows.isNotEmpty()) { +// focusRequester.requestFocus() +// } +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun HomePageRow( + row: FrontPageParser.FrontPageRow, + itemOnClick: (Any) -> Unit, +) { + val rowData = row.data!! + 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() + .fillMaxWidth(), + contentPadding = PaddingValues(start = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(rowData.data) { item -> + if (item != null) { + 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/StashNavItemClickListener.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt new file mode 100644 index 00000000..d27f5367 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/StashNavItemClickListener.kt @@ -0,0 +1,80 @@ +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(Route.DataTypeRoute(DataType.SCENE, item.id)) + } else if (item is PerformerData) { + navController.navigate(Route.DataTypeRoute(DataType.PERFORMER, item.id)) + } else if (item is TagData) { + navController.navigate(Route.DataTypeRoute(DataType.TAG, item.id)) + } else if (item is StudioData) { + navController.navigate(Route.DataTypeRoute(DataType.STUDIO, item.id)) + } else if (item is MovieData) { + navController.navigate(Route.DataTypeRoute(DataType.MOVIE, item.id)) + } else if (item is MarkerData) { + val route = + Route.Playback(item.scene.videoSceneData.id, (item.seconds * 1000).toLong()) + navController.navigate(route) + } else if (item is ImageData) { + // TODO handle image switches + navController.navigate(Route.DataTypeRoute(DataType.IMAGE, item.id)) + } else if (item is GalleryData) { + navController.navigate(Route.DataTypeRoute(DataType.GALLERY, 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/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/cards/Cards.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt new file mode 100644 index 00000000..1452fa61 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/cards/Cards.kt @@ -0,0 +1,358 @@ +package com.github.damontecres.stashapp.ui.cards + +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.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 +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.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 +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 +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 +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 +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 +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.StashImageCardView.Companion.ICON_ORDER +import com.github.damontecres.stashapp.ui.enableMarquee +import com.github.damontecres.stashapp.util.asSlimeSceneData +import com.github.damontecres.stashapp.util.isNotNullOrBlank +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, + ) + } + 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) + } +} + +@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 != null && oCounter > 0) { + appendInlineContent(id = "ocounter", "O") + append(" $oCounter") + } + } + val fontSize = MaterialTheme.typography.bodySmall.fontSize + val inlineContentMap = + mapOf( + "ocounter" to + InlineTextContent( + Placeholder(fontSize, fontSize, PlaceholderVerticalAlign.TextCenter), + ) { + Image( + painterResource(id = R.drawable.sweat_drops), + modifier = Modifier.fillMaxSize(), + contentDescription = "", + ) + }, + ) + Text( + annotatedString, + inlineContent = inlineContentMap, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) +} + +/** + * Main card based on [ClassicCard] + */ +@androidx.annotation.OptIn(UnstableApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalGlideComposeApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +fun RootCard( + onClick: () -> Unit, + title: String, + imageWidth: Dp, + imageHeight: Dp, + modifier: Modifier = Modifier, + imageUrl: String? = null, + imageContent: @Composable BoxScope.() -> Unit = {}, + videoUrl: String? = null, + onLongClick: (() -> Unit)? = null, + imageOverlay: @Composable BoxScope.() -> Unit = {}, + 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, +) { + 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 + } + .padding(0.dp) + .width(imageWidth), + interactionSource = interactionSource, + shape = shape, + colors = colors, + scale = scale, + border = border, + glow = glow, + ) { + Column(modifier = Modifier.padding(contentPadding)) { + // 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 { + Box( + modifier = + Modifier + .width(imageWidth) + .height(imageHeight) + .padding(0.dp), + ) { + 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) + } + } + } + } + Column(modifier = Modifier.padding(6.dp)) { + // Title + ProvideTextStyle(MaterialTheme.typography.titleMedium) { + Text( + title, + maxLines = 1, + modifier = + Modifier + .enableMarquee(focused), + ) + } + // Subtitle + ProvideTextStyle(MaterialTheme.typography.bodySmall) { + Box(Modifier.graphicsLayer { alpha = 0.6f }) { subtitle.invoke() } + } + // Description + ProvideTextStyle(MaterialTheme.typography.bodySmall) { + Box( + Modifier.graphicsLayer { + alpha = 0.8f + }, + ) { description.invoke() } + } + } + } + } +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun StashCard( + item: Any, + itemOnClick: (item: Any) -> Unit, +) { + when (item) { + is SlimSceneData -> SceneCard(item, onClick = { itemOnClick(item) }) + is FullSceneData -> + SceneCard( + item.asSlimeSceneData, + onClick = { itemOnClick(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) }) + else -> throw UnsupportedOperationException("Item with class ${item.javaClass} not supported.") + } +} 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/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, + ) + } + } + }, + ) +} 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 new file mode 100644 index 00000000..08b21b84 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/details/ScenePage.kt @@ -0,0 +1,417 @@ +package com.github.damontecres.stashapp.ui.details + +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.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.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.livedata.observeAsState +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.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +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.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 +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 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) + + 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 + + suspend fun fetchPerformers(scene: FullSceneData) { + val ids = scene.performers.map { it.id } + 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( + sceneId: String, + itemOnClick: (Any) -> Unit, + playbackCallback: (Long) -> Unit, + viewModel: SceneViewModel = hiltViewModel(), +) { + val uiState = viewModel.uiState.observeAsState().value + + LaunchedEffect(Unit) { + viewModel.fetchScene(sceneId) + } + + 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) + viewModel.fetchGalleries(s.scene) + } + SceneDetails( + s.scene, + itemOnClick, + playbackCallback, + Modifier + .fillMaxSize() + .animateContentSize(), + ) + } + + null -> throw IllegalStateException() + } +} + +@OptIn(ExperimentalGlideComposeApi::class) +@Suppress("ktlint:standard:function-naming") +@Composable +private fun SceneDetails( + scene: FullSceneData, + itemOnClick: (Any) -> Unit, + playbackCallback: (Long) -> 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(16.dp), + verticalArrangement = Arrangement.spacedBy(16.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 = { + playbackCallback.invoke(0L) + }) { + 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) + } + } + } + // Markers + item { + ItemRow( + name = stringResource(R.string.stashapp_markers), + items = scene.scene_markers.map { convertMarker(scene, it) }, + 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_tags), + items = scene.tags.map { it.tagData }, + itemOnClick, + ) + } + + // Galleries + item { + ItemRow( + name = stringResource(R.string.stashapp_galleries), + items = viewModel.galleries, + itemOnClick, + ) + } + } +} + +@Suppress("ktlint:standard:function-naming") +@Composable +fun ItemRow( + name: String, + items: List, + itemOnClick: (Any) -> Unit, +) { + if (items.isNotEmpty()) { + 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) + } + } + } + } +} + +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/details/TagPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt new file mode 100644 index 00000000..0f9d0000 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/details/TagPage.kt @@ -0,0 +1,234 @@ +package com.github.damontecres.stashapp.ui.details + +import android.content.Context +import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +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.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.TabbedFilterGrid +import com.github.damontecres.stashapp.util.QueryEngine +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +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) + + 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 = viewModel.uiState.observeAsState().value + + LaunchedEffect(Unit) { + viewModel.fetchTag(tagId) + } + + when (val s = uiState) { + is TagUiState.Loading -> { + Text(text = "Loading...") + } + + is TagUiState.Error -> { + Text(text = "Error: ${s.message}") + } + + is TagUiState.Success -> { + 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), + ) + TabbedFilterGrid( + name = s.tag.name, + tabs = tabs, + contentProvider = { index -> + getPagingSource(index, s.tag) + }, + itemOnClick = itemOnClick, + modifier = + Modifier + .fillMaxSize() + .animateContentSize(), + ) + } + + null -> throw IllegalStateException() + } +} + +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, + ) +} 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..d81c08fd --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/theme/Theme.kt @@ -0,0 +1,64 @@ +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(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 = + 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(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 = + 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/java/com/github/damontecres/stashapp/util/Constants.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt index e8bda9cf..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 @@ -33,14 +33,18 @@ 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.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 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 @@ -69,6 +73,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 = @@ -699,3 +704,90 @@ val ImageData.isGif: Boolean get() = 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() } +} + +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 -> null + } +} + +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") + } +} 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..c6f0e400 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Provider.kt @@ -0,0 +1,22 @@ +package com.github.damontecres.stashapp.util + +import android.content.Context +import com.apollographql.apollo3.ApolloClient +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 + @ViewModelScoped + fun createApolloClient( + @ApplicationContext context: Context, + ): ApolloClient { + return StashClient.getApolloClient(context) + } +} 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..7a159690 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/util/QueryRepository.kt @@ -0,0 +1,37 @@ +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 +import javax.inject.Inject + +class QueryRepository + @Inject + constructor( + 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(): ConfigurationQuery.Data? { + val query = ConfigurationQuery() + val result = apolloClient.query(query).execute() + return result.data + } + } 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) 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..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 @@ -27,14 +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.Performer 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 @@ -55,25 +52,23 @@ 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) - 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) - 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) @@ -87,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) diff --git a/app/src/main/res/values/preferences.xml b/app/src/main/res/values/preferences.xml index ba69fde4..5012e0a7 100644 --- a/app/src/main/res/values/preferences.xml +++ b/app/src/main/res/values/preferences.xml @@ -14,4 +14,16 @@ forcedDirectContainers 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/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/app/src/main/res/xml/advanced_preferences.xml b/app/src/main/res/xml/advanced_preferences.xml index 3d1652e6..a12cfcf2 100644 --- a/app/src/main/res/xml/advanced_preferences.xml +++ b/app/src/main/res/xml/advanced_preferences.xml @@ -169,6 +169,12 @@ app:summaryOn="Enabled" app:summaryOff="Disabled" app:defaultValue="false" /> + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts index e5640274..c3932d40 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +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.24" apply false +} +buildscript { + repositories { + google() + } + dependencies { + val navVersion = "2.8.0-beta06" + classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion") + } }