Skip to content

Commit

Permalink
Create new filters for Scenes in the app (#420)
Browse files Browse the repository at this point in the history
Related to #178

Adds ability to create a filter for scenes and optionally &
experimentally save it to the server. Can start with a blank filter or
build up from the currently selected saved filter.

Note: saving to the server requires enabling experiment features in
advanced settings since it is not fully tested

This has several limitations:
- Only supports scene filters right now
- Only a limited subset of available filter options
- UI/UX is okay, but not great
- It takes many clicks to configure things
- Lots of hand written code AKA possibly error prone
  • Loading branch information
damontecres authored Sep 20, 2024
1 parent 7aff8bf commit e5a8dc9
Show file tree
Hide file tree
Showing 32 changed files with 3,355 additions and 34 deletions.
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@
android:name=".UpdateChangelogActivity"
android:exported="false"
android:theme="@style/NoTitleTheme" />
<activity
android:name=".filter.CreateFilterActivity"
android:exported="false"
android:theme="@style/NoTitleTheme" />

<provider
android:name="androidx.core.content.FileProvider"
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/graphql/FindMovies.graphql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
query FindMovies($filter: FindFilterType, $movie_filter: MovieFilterType){
findMovies(filter: $filter, movie_filter: $movie_filter){
query FindMovies($filter: FindFilterType, $movie_filter: MovieFilterType, $ids: [ID!]){
findMovies(filter: $filter, movie_filter: $movie_filter, ids: $ids){
movies{
...MovieData
}
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/graphql/FindSavedFilter.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ fragment SavedFilterData on SavedFilter {
ui_options
__typename
}

mutation SaveFilter($input: SaveFilterInput!) {
saveFilter(input: $input) {
...SavedFilterData
}
}
158 changes: 135 additions & 23 deletions app/src/main/java/com/github/damontecres/stashapp/FilterListActivity.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package com.github.damontecres.stashapp

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.BaseAdapter
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
Expand All @@ -15,6 +19,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.chrynan.parcelable.core.getParcelableExtra
import com.github.damontecres.stashapp.data.DataType
import com.github.damontecres.stashapp.filter.CreateFilterActivity
import com.github.damontecres.stashapp.suppliers.FilterArgs
import com.github.damontecres.stashapp.suppliers.toFilterArgs
import com.github.damontecres.stashapp.util.FilterParser
Expand All @@ -23,6 +28,8 @@ import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
import com.github.damontecres.stashapp.util.StashServer
import com.github.damontecres.stashapp.util.getMaxMeasuredWidth
import com.github.damontecres.stashapp.util.parcelable
import com.github.damontecres.stashapp.util.putDataType
import com.github.damontecres.stashapp.util.putFilterArgs
import com.github.damontecres.stashapp.views.PlayAllOnClickListener
import com.github.damontecres.stashapp.views.SortButtonManager
import com.github.damontecres.stashapp.views.StashOnFocusChangeListener
Expand Down Expand Up @@ -125,7 +132,11 @@ class FilterListActivity : FragmentActivity(R.layout.filter_list) {
val server = StashServer.requireCurrentServer()
val savedFilters =
QueryEngine(server).getSavedFilters(dataType)
if (savedFilters.isEmpty()) {

val createFilterSupported = dataType == DataType.SCENE

// Always show the list for data types supporting create filter
if (savedFilters.isEmpty() && !createFilterSupported) {
filterButton.setOnClickListener {
Toast.makeText(
context,
Expand All @@ -140,12 +151,10 @@ class FilterListActivity : FragmentActivity(R.layout.filter_list) {
null,
android.R.attr.listPopupWindowStyle,
)
val adapter =
ArrayAdapter(
context,
R.layout.popup_item,
savedFilters.map { it.name.ifBlank { getString(dataType.pluralStringId) } },
)
val adapterItems =
savedFilters.map { it.name.ifBlank { getString(dataType.pluralStringId) } }
val adapter = SavedFilterAdapter(context, createFilterSupported, adapterItems)

listPopUp.setAdapter(adapter)
listPopUp.inputMethodMode = ListPopupWindow.INPUT_METHOD_NEEDED
listPopUp.anchorView = filterButton
Expand All @@ -155,22 +164,42 @@ class FilterListActivity : FragmentActivity(R.layout.filter_list) {

val filterParser = FilterParser(server.serverPreferences.serverVersion)
listPopUp.setOnItemClickListener { parent: AdapterView<*>, view: View, position: Int, id: Long ->
Log.v(TAG, "filter list clicked position=$position")
listPopUp.dismiss()
val savedFilter =
savedFilters[position]
try {
val filterArgs =
savedFilter
.toFilterArgs(filterParser)
.withResolvedRandom()
setup(filterArgs)
} catch (ex: Exception) {
Log.e(TAG, "Exception parsing filter ${savedFilter.id}", ex)
Toast.makeText(
this@FilterListActivity,
"Error with filter ${savedFilter.id}! Probably a bug: ${ex.message}",
Toast.LENGTH_LONG,
).show()
val lookupPos =
if (adapter.createEnabled) {
position - 3
} else {
position
}
if (adapter.createEnabled && (position == 0 || position == 1)) {
val intent =
Intent(this@FilterListActivity, CreateFilterActivity::class.java)
.putDataType(dataType)
if (position == 1) {
val fragment =
supportFragmentManager.findFragmentById(R.id.list_fragment) as StashGridFragment
val filter = fragment.filterArgs
intent.putFilterArgs(CreateFilterActivity.INTENT_STARTING_FILTER, filter)
}
this@FilterListActivity.startActivity(intent)
} else {
val savedFilter =
savedFilters[lookupPos]
try {
val filterArgs =
savedFilter
.toFilterArgs(filterParser)
.withResolvedRandom()
setup(filterArgs)
} catch (ex: Exception) {
Log.e(TAG, "Exception parsing filter ${savedFilter.id}", ex)
Toast.makeText(
this@FilterListActivity,
"Error with filter ${savedFilter.id}! Probably a bug: ${ex.message}",
Toast.LENGTH_LONG,
).show()
}
}
}

Expand Down Expand Up @@ -203,6 +232,89 @@ class FilterListActivity : FragmentActivity(R.layout.filter_list) {
sortButtonManager.setUpSortButton(sortButton, filter.dataType, filter.sortAndDirection)
}

private class SavedFilterAdapter(
context: Context,
val createEnabled: Boolean,
val filters: List<String>,
) : BaseAdapter() {
private val inflater = LayoutInflater.from(context)

override fun getCount(): Int {
return if (createEnabled) {
filters.size + 3
} else {
filters.size
}
}

override fun getItem(position: Int): Any {
if (createEnabled) {
return when (position) {
0 -> "Create filter"
1 -> "Create filter from current"
2 ->
if (filters.isEmpty()) {
"No saved filters"
} else {
"Saved filters"
}

else -> filters[position - 3]
}
}
return filters[position]
}

override fun getItemId(position: Int): Long {
return position.toLong()
}

override fun getView(
position: Int,
convertView: View?,
parent: ViewGroup?,
): View {
return if (convertView != null) {
(convertView as TextView).text = getItem(position).toString()
convertView
} else if (createEnabled && position == 2) {
// header
val view = inflater.inflate(R.layout.popup_header, parent, false) as TextView
view.text = getItem(position).toString()
view
} else {
// regular item
val view = inflater.inflate(R.layout.popup_item, parent, false) as TextView
view.text = getItem(position).toString()
view
}
}

override fun areAllItemsEnabled(): Boolean {
return !createEnabled
}

override fun isEnabled(position: Int): Boolean {
return !(createEnabled && position == 2)
}

override fun getItemViewType(position: Int): Int {
return if (isEnabled(position)) {
0
} else {
1
}
}

override fun getViewTypeCount(): Int {
return if (createEnabled) {
2
} else {
1
}
}
}

companion object {
private const val TAG = "FilterListActivity2"
const val INTENT_FILTER_ARGS = "$TAG.filterArgs"
Expand Down
23 changes: 23 additions & 0 deletions app/src/main/java/com/github/damontecres/stashapp/data/DataType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ package com.github.damontecres.stashapp.data
import androidx.annotation.StringRes
import com.github.damontecres.stashapp.R
import com.github.damontecres.stashapp.api.type.FilterMode
import com.github.damontecres.stashapp.api.type.GalleryFilterType
import com.github.damontecres.stashapp.api.type.ImageFilterType
import com.github.damontecres.stashapp.api.type.MovieFilterType
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.SortDirectionEnum
import com.github.damontecres.stashapp.api.type.StashDataFilter
import com.github.damontecres.stashapp.api.type.StudioFilterType
import com.github.damontecres.stashapp.api.type.TagFilterType
import com.github.damontecres.stashapp.presenters.GalleryPresenter
import com.github.damontecres.stashapp.presenters.ImagePresenter
import com.github.damontecres.stashapp.presenters.MarkerPresenter
Expand All @@ -12,6 +21,7 @@ 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
import kotlin.reflect.KClass

enum class DataType(
val filterMode: FilterMode,
Expand Down Expand Up @@ -102,6 +112,19 @@ enum class DataType(
GALLERY -> GalleryPresenter.CARD_WIDTH
}

val filterType
get() =
when (this) {
SCENE -> SceneFilterType::class
MOVIE -> MovieFilterType::class
MARKER -> SceneMarkerFilterType::class
PERFORMER -> PerformerFilterType::class
STUDIO -> StudioFilterType::class
TAG -> TagFilterType::class
IMAGE -> ImageFilterType::class
GALLERY -> GalleryFilterType::class
} as KClass<StashDataFilter>

val defaultCardRatio
get() =
ScenePresenter.CARD_WIDTH.toDouble() / defaultCardWidth
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ data class StashFindFilter(
page = Optional.presentIfNotNull(page),
per_page = Optional.presentIfNotNull(perPage),
)

fun withSort(sort: String): StashFindFilter {
val newSortAndDirection =
sortAndDirection?.copy(sort = sort) ?: SortAndDirection(sort, SortDirectionEnum.ASC)
return this.copy(sortAndDirection = newSortAndDirection)
}

fun withDirection(
direction: SortDirectionEnum,
dataType: DataType,
): StashFindFilter {
val newSortAndDirection =
(sortAndDirection ?: dataType.defaultSort).copy(direction = direction)
return this.copy(sortAndDirection = newSortAndDirection)
}
}

fun FindFilterType.toStashFindFilter(): StashFindFilter =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.github.damontecres.stashapp.filter

import android.os.Bundle
import androidx.activity.viewModels
import androidx.fragment.app.FragmentActivity
import androidx.leanback.app.GuidedStepSupportFragment
import com.github.damontecres.stashapp.R
import com.github.damontecres.stashapp.util.getDataType
import com.github.damontecres.stashapp.util.getFilterArgs

class CreateFilterActivity : FragmentActivity(R.layout.frame_layout) {
private val viewModel by viewModels<CreateFilterViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val dataType = intent.getDataType()
val startingFilter = intent.getFilterArgs(INTENT_STARTING_FILTER)
viewModel.initialize(
dataType,
startingFilter?.objectFilter,
startingFilter?.findFilter,
) {
// This occurs after the items are loaded for labels
GuidedStepSupportFragment.addAsRoot(
this,
CreateFilterStep(),
android.R.id.content,
)
}
}
}

companion object {
private const val TAG = "CreateFilterActivity"
const val INTENT_STARTING_FILTER = "$TAG.startingFilter"
}
}
Loading

0 comments on commit e5a8dc9

Please sign in to comment.