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