From d35d85c65ae3158ed3198e071a7015e80a3fafb0 Mon Sep 17 00:00:00 2001
From: damontecres <154766448+damontecres@users.noreply.github.com>
Date: Mon, 4 Mar 2024 19:53:50 -0500
Subject: [PATCH] Add support for images & galleries (#173)
This is a work in progress
Adds support for showing images & galleries
Minimum features:
- [x] Display an image
- [x] Display list of images (w/ filter)
- [x] Support images on main page
- [x] Add gallery count & icon to card
- [x] Add overlay with details
Extra features
- [ ] Move between images w/ left & right
- [ ] Add controls to zoom and move image
---
app/src/main/AndroidManifest.xml | 8 +
app/src/main/graphql/FindGalleries.graphql | 52 +++++
app/src/main/graphql/FindImages.graphql | 63 ++++++
app/src/main/graphql/Fragments.graphql | 2 +
.../stashapp/FilterListActivity.kt | 36 +++
.../damontecres/stashapp/GalleryActivity.kt | 52 +++++
.../damontecres/stashapp/ImageActivity.kt | 213 ++++++++++++++++++
.../damontecres/stashapp/MainFragment.kt | 18 ++
.../damontecres/stashapp/MainTitleView.kt | 20 ++
.../damontecres/stashapp/SearchForFragment.kt | 3 +
.../damontecres/stashapp/SettingsFragment.kt | 26 ++-
.../stashapp/StashItemViewClickListener.kt | 19 ++
.../damontecres/stashapp/data/DataType.kt | 2 +
.../stashapp/presenters/GalleryPresenter.kt | 51 +++++
.../stashapp/presenters/ImagePresenter.kt | 48 ++++
.../presenters/StashFilterPresenter.kt | 7 +
.../stashapp/presenters/StashImageCardView.kt | 10 +
.../stashapp/presenters/StashPresenter.kt | 4 +
.../stashapp/suppliers/GalleryDataSupplier.kt | 36 +++
.../stashapp/suppliers/ImageDataSupplier.kt | 35 +++
.../stashapp/suppliers/TagDataSupplier.kt | 4 +-
.../damontecres/stashapp/util/Comparators.kt | 34 +++
.../damontecres/stashapp/util/Constants.kt | 25 +-
.../damontecres/stashapp/util/FilterParser.kt | 95 ++++++++
.../damontecres/stashapp/util/QueryEngine.kt | 45 ++++
.../stashapp/util/ServerPreferences.kt | 11 +-
.../damontecres/stashapp/util/StashGlide.kt | 33 +++
.../stashapp/util/StashGlideModule.kt | 10 +
app/src/main/res/layout/activity_image.xml | 9 +
.../main/res/layout/image_card_icon_row.xml | 24 ++
app/src/main/res/layout/image_layout.xml | 35 +++
app/src/main/res/layout/title.xml | 34 +--
app/src/main/res/values/colors.xml | 1 +
app/src/main/res/values/dimens.xml | 2 +-
app/src/main/res/values/strings.xml | 1 +
app/src/main/res/values/styles.xml | 5 +-
36 files changed, 1046 insertions(+), 27 deletions(-)
create mode 100644 app/src/main/graphql/FindGalleries.graphql
create mode 100644 app/src/main/graphql/FindImages.graphql
create mode 100644 app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt
create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt
create mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/GalleryPresenter.kt
create mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/ImagePresenter.kt
create mode 100644 app/src/main/java/com/github/damontecres/stashapp/suppliers/GalleryDataSupplier.kt
create mode 100644 app/src/main/java/com/github/damontecres/stashapp/suppliers/ImageDataSupplier.kt
create mode 100644 app/src/main/res/layout/activity_image.xml
create mode 100644 app/src/main/res/layout/image_layout.xml
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 24c96a23..82f67de2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -76,6 +76,14 @@
android:name=".MovieActivity"
android:exported="false"
android:theme="@style/NoTitleTheme" />
+
+
{
+ val imageFilter =
+ if (objectFilter is ImageFilterType) {
+ objectFilter
+ } else {
+ FilterParser.instance.convertImageObjectFilter(objectFilter)
+ }
+ StashGridFragment(
+ ImageComparator,
+ ImageDataSupplier(findFilter, imageFilter),
+ null,
+ name,
+ )
+ }
+
+ DataType.GALLERY -> {
+ val galleryFilter =
+ if (objectFilter is GalleryFilterType) {
+ objectFilter
+ } else {
+ FilterParser.instance.convertGalleryObjectFilter(objectFilter)
+ }
+ StashGridFragment(
+ GalleryComparator,
+ GalleryDataSupplier(findFilter, galleryFilter),
+ null,
+ name,
+ )
+ }
}
}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt
new file mode 100644
index 00000000..936baa8a
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/GalleryActivity.kt
@@ -0,0 +1,52 @@
+package com.github.damontecres.stashapp
+
+import android.os.Bundle
+import android.widget.TextView
+import androidx.fragment.app.FragmentActivity
+import com.apollographql.apollo3.api.Optional
+import com.github.damontecres.stashapp.api.type.CriterionModifier
+import com.github.damontecres.stashapp.api.type.FindFilterType
+import com.github.damontecres.stashapp.api.type.ImageFilterType
+import com.github.damontecres.stashapp.api.type.MultiCriterionInput
+import com.github.damontecres.stashapp.api.type.SortDirectionEnum
+import com.github.damontecres.stashapp.suppliers.ImageDataSupplier
+import com.github.damontecres.stashapp.util.ImageComparator
+
+class GalleryActivity : FragmentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.grid_view)
+ if (savedInstanceState == null) {
+ val galleryId = intent.getStringExtra(INTENT_GALLERY_ID)!!
+ val galleryName = intent.getStringExtra(INTENT_GALLERY_NAME)
+ findViewById(R.id.grid_title).text = galleryName
+ supportFragmentManager.beginTransaction()
+ .replace(
+ R.id.grid_fragment,
+ StashGridFragment(
+ ImageComparator,
+ ImageDataSupplier(
+ FindFilterType(
+ sort = Optional.present("path"),
+ direction = Optional.present(SortDirectionEnum.ASC),
+ ),
+ ImageFilterType(
+ galleries =
+ Optional.present(
+ MultiCriterionInput(
+ value = Optional.present(listOf(galleryId)),
+ modifier = CriterionModifier.INCLUDES,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ).commitNow()
+ }
+ }
+
+ companion object {
+ const val INTENT_GALLERY_ID = "gallery.id"
+ const val INTENT_GALLERY_NAME = "gallery.name"
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt
new file mode 100644
index 00000000..121d94dc
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/ImageActivity.kt
@@ -0,0 +1,213 @@
+package com.github.damontecres.stashapp
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.annotation.SuppressLint
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.KeyEvent
+import android.view.View
+import android.widget.ImageView
+import android.widget.TableLayout
+import android.widget.TableRow
+import android.widget.TextView
+import android.widget.Toast
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.lifecycleScope
+import com.bumptech.glide.load.DataSource
+import com.bumptech.glide.load.engine.GlideException
+import com.bumptech.glide.request.RequestListener
+import com.bumptech.glide.request.target.Target
+import com.github.damontecres.stashapp.api.fragment.ImageData
+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.util.concatIfNotBlank
+import kotlinx.coroutines.launch
+import kotlin.properties.Delegates
+
+class ImageActivity : FragmentActivity() {
+ private val imageFragment = ImageFragment()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_image)
+ if (savedInstanceState == null) {
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.image_fragment, imageFragment)
+ .commitNow()
+ }
+ }
+
+ @SuppressLint("RestrictedApi")
+ override fun dispatchKeyEvent(event: KeyEvent): Boolean {
+ if (event.action == KeyEvent.ACTION_DOWN) {
+ if (event.keyCode == KeyEvent.KEYCODE_BACK) {
+ if (imageFragment.isOverlayVisible()) {
+ imageFragment.hideOverlay()
+ return true
+ }
+ } else if (isDpadKey(event.keyCode) && !imageFragment.isOverlayVisible()) {
+ imageFragment.showOverlay()
+ return true
+ }
+ }
+ return super.dispatchKeyEvent(event)
+ }
+
+ private fun isDpadKey(keyCode: Int): Boolean {
+ return keyCode == KeyEvent.KEYCODE_DPAD_UP ||
+ keyCode == KeyEvent.KEYCODE_DPAD_RIGHT ||
+ keyCode == KeyEvent.KEYCODE_DPAD_DOWN ||
+ keyCode == KeyEvent.KEYCODE_DPAD_LEFT ||
+ keyCode == KeyEvent.KEYCODE_DPAD_CENTER ||
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
+ (
+ keyCode == KeyEvent.KEYCODE_DPAD_UP_RIGHT ||
+ keyCode == KeyEvent.KEYCODE_DPAD_DOWN_RIGHT ||
+ keyCode == KeyEvent.KEYCODE_DPAD_DOWN_LEFT ||
+ keyCode == KeyEvent.KEYCODE_DPAD_UP_LEFT
+ )
+ }
+
+ companion object {
+ const val TAG = "ImageActivity"
+ const val INTENT_IMAGE_ID = "image.id"
+ const val INTENT_IMAGE_URL = "image.url"
+ const val INTENT_IMAGE_SIZE = "image.size"
+ }
+
+ class ImageFragment : Fragment(R.layout.image_layout) {
+ lateinit var titleText: TextView
+ lateinit var table: TableLayout
+ lateinit var image: ImageData
+
+ private var animationDuration by Delegates.notNull()
+
+ override fun onViewCreated(
+ view: View,
+ savedInstanceState: Bundle?,
+ ) {
+ animationDuration =
+ resources.getInteger(android.R.integer.config_mediumAnimTime).toLong()
+ titleText = view.findViewById(R.id.image_view_title)
+ val mainImage = view.findViewById(R.id.image_view_image)
+ table = view.findViewById(R.id.image_view_table)
+ val imageUrl = requireActivity().intent.getStringExtra(INTENT_IMAGE_URL)!!
+ val imageId = requireActivity().intent.getStringExtra(INTENT_IMAGE_ID)!!
+ val imageSize = requireActivity().intent.getIntExtra(INTENT_IMAGE_SIZE, -1)
+ Log.v(TAG, "imageId=$imageId")
+ viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
+ val queryEngine = QueryEngine(requireContext())
+ image = queryEngine.getImage(imageId)!!
+ Log.v(TAG, "image.id=${image.id}")
+ titleText.text = image.title
+
+ addRow(R.string.stashapp_studio, image.studio?.studioData?.name)
+ addRow(R.string.stashapp_date, image.date)
+ addRow(R.string.stashapp_photographer, image.photographer)
+ addRow(R.string.stashapp_details, image.details)
+ addRow(
+ R.string.stashapp_tags,
+ concatIfNotBlank(", ", image.tags.map { it.tagData.name }),
+ )
+ addRow(
+ R.string.stashapp_performers,
+ concatIfNotBlank(", ", image.performers.map { it.performerData.name }),
+ )
+ }
+
+ StashGlide.with(requireContext(), imageUrl, imageSize)
+ .listener(
+ object : RequestListener {
+ override fun onLoadFailed(
+ e: GlideException?,
+ model: Any?,
+ target: Target,
+ isFirstResource: Boolean,
+ ): Boolean {
+ Log.v(TAG, "onLoadFailed for $imageUrl")
+ Toast.makeText(
+ requireContext(),
+ "Image loading failed!",
+ Toast.LENGTH_LONG,
+ ).show()
+ return true
+ }
+
+ override fun onResourceReady(
+ resource: Drawable,
+ model: Any,
+ target: Target?,
+ dataSource: DataSource,
+ isFirstResource: Boolean,
+ ): Boolean {
+ return false
+ }
+ },
+ )
+ .into(mainImage)
+ }
+
+ fun isOverlayVisible(): Boolean {
+ return titleText.isVisible
+ }
+
+ fun showOverlay() {
+ listOf(titleText, table).forEach {
+ it.alpha = 0.0f
+ it.visibility = View.VISIBLE
+ it.animate()
+ .alpha(1f)
+ .setDuration(animationDuration)
+ .setListener(null)
+ }
+ }
+
+ fun hideOverlay() {
+ listOf(titleText, table).forEach {
+ it.alpha = 1.0f
+ it.visibility = View.VISIBLE
+ it.animate()
+ .alpha(0f)
+ .setDuration(animationDuration)
+ .setListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ it.visibility = View.GONE
+ }
+ },
+ )
+ }
+ }
+
+ private fun addRow(
+ key: Int,
+ value: String?,
+ ) {
+ if (value.isNullOrBlank()) {
+ return
+ }
+ val keyString = getString(key) + ":"
+
+ val row =
+ requireActivity().layoutInflater.inflate(
+ R.layout.table_row,
+ table,
+ false,
+ ) as TableRow
+
+ val keyView = row.findViewById(R.id.table_row_key)
+ keyView.text = keyString
+
+ val valueView = row.findViewById(R.id.table_row_value)
+ valueView.text = value
+
+ table.addView(row)
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt
index 2949e29c..c36d02d7 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt
@@ -481,6 +481,24 @@ class MainFragment : BrowseSupportFragment() {
)
}
+ FilterMode.IMAGES -> {
+ val imageFilter =
+ FilterParser.instance.convertImageObjectFilter(objectFilter)
+ adapter.addAll(
+ 0,
+ queryEngine.findImages(filter, imageFilter, useRandom = false),
+ )
+ }
+
+ FilterMode.GALLERIES -> {
+ val galleryFilter =
+ FilterParser.instance.convertGalleryObjectFilter(objectFilter)
+ adapter.addAll(
+ 0,
+ queryEngine.findGalleries(filter, galleryFilter, useRandom = false),
+ )
+ }
+
else -> {
Log.w(
TAG,
diff --git a/app/src/main/java/com/github/damontecres/stashapp/MainTitleView.kt b/app/src/main/java/com/github/damontecres/stashapp/MainTitleView.kt
index 6312d4c4..58c54597 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/MainTitleView.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/MainTitleView.kt
@@ -25,6 +25,8 @@ class MainTitleView(context: Context, attrs: AttributeSet) :
private val tagsButton: Button
private val moviesButton: Button
private val markersButton: Button
+ private val imagesButton: Button
+ private val galleriesButton: Button
private val mTitleViewAdapter =
object : TitleViewAdapter() {
@@ -53,6 +55,14 @@ class MainTitleView(context: Context, attrs: AttributeSet) :
}
scenesButton.onFocusChangeListener = onFocusChangeListener
+ imagesButton = root.findViewById