diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 6a0e8f5dc..52662f676 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -31,6 +31,8 @@ plugins {
alias(libs.plugins.room)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.protobuf)
+ alias(libs.plugins.koin.compiler)
+ alias(libs.plugins.aboutLibraries)
}
val gitTags =
@@ -233,20 +235,19 @@ protobuf {
}
dependencies {
+ implementation(libs.androidx.activity.ktx)
+ implementation(libs.androidx.appcompat)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.leanback)
- implementation(libs.androidx.leanback.preference)
- implementation(libs.androidx.leanback.tab)
-
- implementation(libs.glide)
- implementation(libs.glide.okhttp3.integration)
- ksp(libs.glide.ksp)
-
+ implementation(libs.androidx.navigation3.runtime)
+ implementation(libs.androidx.navigation3.ui)
+ implementation(libs.androidx.lifecycle.viewmodel.navigation3)
+ implementation(libs.androidx.material3.adaptive.navigation3)
implementation(libs.android.material)
-
implementation(libs.androidx.swiperefreshlayout)
+ implementation(libs.aboutlibraries.core)
+ implementation(libs.aboutlibraries.compose.m3)
- implementation(libs.androidsvg.aar)
implementation(kotlin("reflect"))
implementation(libs.androidx.preference.ktx)
@@ -264,11 +265,8 @@ dependencies {
implementation(libs.androidx.media3.datasource.okhttp)
implementation(libs.androidx.media3.effect)
implementation(libs.androidx.media3.transformer)
- implementation(libs.previewseekbar)
- implementation(libs.previewseekbar.media3)
implementation(libs.androidx.constraintlayout)
- implementation(libs.zoomlayout)
implementation(libs.compose.wheel.picker)
implementation(libs.kotlinx.serialization.json)
implementation(libs.markwon.core)
@@ -300,17 +298,13 @@ dependencies {
implementation(libs.androidx.runtime.livedata)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.android.material)
- implementation(libs.androidx.swiperefreshlayout)
- implementation(libs.androidsvg.aar)
implementation(libs.coil.compose)
implementation(libs.coil.network.okhttp)
implementation(libs.coil.network.cachecontrol)
implementation(libs.coil.svg)
implementation(libs.coil.gif)
- implementation(libs.navigation.reimagined)
coreLibraryDesugaring(libs.desugar.jdk.libs)
implementation(libs.restring)
- implementation(libs.viewpump)
implementation(libs.reword)
implementation(libs.androidx.datastore)
implementation(libs.protobuf.kotlin.lite)
@@ -319,6 +313,16 @@ dependencies {
implementation(libs.timber)
implementation(libs.slf4j2.timber)
+ implementation(libs.kermit)
+
+ implementation(platform(libs.koin.bom))
+ implementation(libs.koin.core)
+ implementation(libs.koin.annotations)
+ implementation(libs.koin.android)
+ implementation(libs.koin.androidx.compose)
+ implementation(libs.koin.compose)
+ implementation(libs.koin.compose.viewmodel)
+
if (ffmpegModuleExists.get()) {
logger.info("Using local ffmpeg decoder")
implementation(files("libs/lib-decoder-ffmpeg-release.aar"))
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f2f9857b4..b10dd3b53 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,24 +27,26 @@
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:largeHeap="true"
android:supportsRtl="true"
android:theme="@style/Theme.StashAppAndroidTV"
- android:usesCleartextTraffic="true"
- android:largeHeap="true">
+ android:usesCleartextTraffic="true">
+ android:logo="@mipmap/ic_launcher">
+
+
@@ -57,16 +59,13 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/app/src/main/assets/licenses/glide.txt b/app/src/main/assets/licenses/glide.txt
deleted file mode 100644
index 2387132ec..000000000
--- a/app/src/main/assets/licenses/glide.txt
+++ /dev/null
@@ -1,96 +0,0 @@
-Glide included from https://github.com/bumptech/glide
-
-License for everything not in third_party and not otherwise marked:
-
-Copyright 2014 Google, Inc. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification, are
-permitted provided that the following conditions are met:
-
- 1. Redistributions of source code must retain the above copyright notice, this list of
- conditions and the following disclaimer.
-
- 2. Redistributions in binary form must reproduce the above copyright notice, this list
- of conditions and the following disclaimer in the documentation and/or other materials
- provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY GOOGLE, INC. ``AS IS'' AND ANY EXPRESS OR IMPLIED
-WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR
-CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
-NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
-ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-The views and conclusions contained in the software and documentation are those of the
-authors and should not be interpreted as representing official policies, either expressed
-or implied, of Google, Inc.
----------------------------------------------------------------------------------------------
-License for third_party/disklrucache:
-
-Copyright 2012 Jake Wharton
-Copyright 2011 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
----------------------------------------------------------------------------------------------
-License for third_party/gif_decoder:
-
-Copyright (c) 2013 Xcellent Creations, Inc.
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
----------------------------------------------------------------------------------------------
-License for third_party/gif_encoder/AnimatedGifEncoder.java and
-third_party/gif_encoder/LZWEncoder.java:
-
-No copyright asserted on the source code of this class. May be used for any
-purpose, however, refer to the Unisys LZW patent for restrictions on use of
-the associated LZWEncoder class. Please forward any corrections to
-kweiner@fmsware.com.
-
------------------------------------------------------------------------------
-License for third_party/gif_encoder/NeuQuant.java
-
-Copyright (c) 1994 Anthony Dekker
-
-NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994. See
-"Kohonen neural networks for optimal colour quantization" in "Network:
-Computation in Neural Systems" Vol. 5 (1994) pp 351-367. for a discussion of
-the algorithm.
-
-Any party obtaining a copy of these files from the author, directly or
-indirectly, is granted, free of charge, a full and unrestricted irrevocable,
-world-wide, paid up, royalty-free, nonexclusive right and license to deal in
-this software and documentation files (the "Software"), including without
-limitation the rights to use, copy, modify, merge, publish, distribute,
-sublicense, and/or sell copies of the Software, and to permit persons who
-receive copies from any such party to do so, with the only requirement being
-that this copyright notice remain intact.
diff --git a/app/src/main/graphql/FindSceneMarkers.graphql b/app/src/main/graphql/FindSceneMarkers.graphql
index fa3043903..964b2377e 100644
--- a/app/src/main/graphql/FindSceneMarkers.graphql
+++ b/app/src/main/graphql/FindSceneMarkers.graphql
@@ -76,6 +76,10 @@ fragment VideoSceneData on Scene {
captions {
...Caption
}
+ studio {
+ id
+ image_path
+ }
}
fragment MarkerData on SceneMarker {
diff --git a/app/src/main/java/com/github/damontecres/stashapp/DebugFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/DebugFragment.kt
deleted file mode 100644
index 068ba0631..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/DebugFragment.kt
+++ /dev/null
@@ -1,382 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.app.ActivityManager
-import android.content.Context.ACTIVITY_SERVICE
-import android.graphics.Color
-import android.graphics.Typeface
-import android.media.MediaCodecList
-import android.os.Build
-import android.os.Bundle
-import android.util.Log
-import android.view.Gravity
-import android.view.View
-import android.widget.Button
-import android.widget.TableLayout
-import android.widget.TableRow
-import android.widget.TextView
-import android.widget.Toast
-import androidx.fragment.app.Fragment
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.apollographql.apollo.api.Query
-import com.github.damontecres.stashapp.api.fragment.StashData
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.playback.CodecSupport
-import com.github.damontecres.stashapp.suppliers.DataSupplierFactory
-import com.github.damontecres.stashapp.suppliers.toFilterArgs
-import com.github.damontecres.stashapp.util.FilterParser
-import com.github.damontecres.stashapp.util.QueryEngine
-import com.github.damontecres.stashapp.util.StashClient
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.plugin.CompanionPlugin
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-class DebugFragment : Fragment(R.layout.debug) {
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- val prefTable = view.findViewById(R.id.preferences_table)
- val serverPrefTable = view.findViewById(R.id.server_prefs_table)
- val formatSupportedTable = view.findViewById(R.id.supported_formats_table)
- val codecsTable = view.findViewById(R.id.codecs_table)
- val otherTable = view.findViewById(R.id.other_table)
- val logTextView = view.findViewById(R.id.logs)
-
- val prefManager = PreferenceManager.getDefaultSharedPreferences(requireContext()).all
- prefManager.keys.sorted().forEach {
- val row = createRow(it, prefManager[it].toString())
- prefTable.addView(row)
- }
- prefTable.isStretchAllColumns = true
-
- val serverPrefs = StashServer.requireCurrentServer().serverPreferences
- val serverPrefsRaw = serverPrefs.preferences.all
- serverPrefsRaw.keys.sorted().forEach {
- val row = createRow(it, serverPrefsRaw[it].toString())
- serverPrefTable.addView(row)
- }
- serverPrefTable.isStretchAllColumns = true
-
- val codecs = CodecSupport.getSupportedCodecs(requireContext())
- formatSupportedTable.addView(
- createRow(
- "Video Codecs",
- codecs.videoCodecs.sorted().joinToString(", "),
- ),
- )
- formatSupportedTable.addView(
- createRow(
- "Audio Codecs",
- codecs.audioCodecs.sorted().joinToString(", "),
- ),
- )
- formatSupportedTable.addView(
- createRow(
- "Container Formats",
- codecs.containers.sorted().joinToString(", "),
- ),
- )
- formatSupportedTable.isStretchAllColumns = true
-
- populateCodecsTable(codecsTable)
- codecsTable.isStretchAllColumns = true
-
- otherTable.addView(
- createRow("Build type", BuildConfig.BUILD_TYPE),
- )
- val server = StashServer.getCurrentStashServer()
- if (server != null) {
- otherTable.addView(
- createRow(
- "Current server URL",
- server.url,
- ),
- )
- otherTable.addView(
- createRow(
- "Current server API Key",
- server.apiKey,
- ),
- )
- otherTable.addView(
- createRow(
- "Current server URL (resolved endpoint)",
- StashClient.cleanServerUrl(server.url),
- ),
- )
- otherTable.addView(
- createRow(
- "Current server URL (root)",
- StashClient.getServerRoot(server.url),
- ),
- )
- }
- otherTable.addView(
- createRow(
- "User-Agent",
- StashClient.createUserAgent(requireContext()),
- ),
- )
-
- val activityManager = requireContext().getSystemService(ACTIVITY_SERVICE) as ActivityManager
- otherTable.addView(createRow("isLowRam", activityManager.isLowRamDevice.toString()))
-
- otherTable.isStretchAllColumns = true
-
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler {
- Toast.makeText(
- requireContext(),
- "Exception getting logs: ${it.message}",
- Toast.LENGTH_LONG,
- )
- },
- ) {
- val logs = CompanionPlugin.getLogCatLines(true).joinToString("\n")
- logTextView.text = logs
- }
-
- view.findViewById(R.id.button_test_saved_filters).setOnClickListener {
- testSavedFilters()
- }
- val dbTable = view.findViewById(R.id.database_table)
- dbTable.removeAllViews()
- dbTable.addView(createRow("DataType", "ID", "VideoFilter"))
- if (server != null) {
- viewLifecycleOwner.lifecycleScope.launch(
- Dispatchers.IO +
- StashCoroutineExceptionHandler(
- true,
- ),
- ) {
- val effects =
- StashApplication
- .getDatabase()
- .playbackEffectsDao()
- .getPlaybackEffects(server.url)
- Log.i(TAG, "Found ${effects.size} effects")
- withContext(Dispatchers.Main) {
- effects.forEach {
- dbTable.addView(
- createRow(
- it.dataType.name,
- it.id,
- it.videoFilter.toString(),
- ),
- )
- }
- }
- }
- }
- }
-
- private fun testSavedFilters() {
- val table = requireView().findViewById(R.id.debug_test_table)
- table.removeAllViews()
- table.addView(createRow("Data Type", "ID", "Name", "Parsed", "Query"))
-
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- try {
- val server = StashServer.requireCurrentServer()
- val queryEngine = QueryEngine(server)
- val filterParser = FilterParser(server.serverPreferences.serverVersion)
- DataType.entries.forEach { dataType ->
- val filters = queryEngine.getSavedFilters(dataType)
- filters.forEach { filter ->
- val args =
- try {
- filter.toFilterArgs(filterParser)
- } catch (ex: Exception) {
- Log.w(TAG, "Test saved filter failed parsing: id=${filter.id}", ex)
- table.addView(
- createRow(
- dataType.name,
- filter.id,
- filter.name,
- ex.message?.ifBlank { ex.cause?.message },
- null,
- ),
- )
- null
- }
- if (args != null) {
- try {
- val dataSupplier =
- DataSupplierFactory(server.version).create(
- args,
- )
- val countQuery = dataSupplier.createCountQuery(null)
- val count =
- queryEngine.executeQuery(countQuery).data?.let {
- dataSupplier.parseCountQuery(
- it,
- )
- }
- table.addView(
- createRow(
- dataType.name,
- filter.id,
- filter.name,
- "Success",
- count?.toString(),
- ),
- )
- } catch (ex: Exception) {
- Log.w(TAG, "Test saved filter failed query: id=${filter.id}", ex)
- table.addView(
- createRow(
- dataType.name,
- filter.id,
- filter.name,
- ex.message?.ifBlank { ex.cause?.message },
- "Error",
- ),
- )
- }
- }
- }
- }
- } catch (ex: Exception) {
- Log.e(TAG, "Exception during test", ex)
- }
- }
- }
-
- private fun createRow(
- key: String,
- vararg values: String?,
- ): TableRow {
- val row = TableRow(requireContext())
- row.layoutParams =
- TableLayout.LayoutParams(
- TableLayout.LayoutParams.MATCH_PARENT,
- TableLayout.LayoutParams.WRAP_CONTENT,
- )
- row.gravity = Gravity.CENTER_HORIZONTAL
-// val lp = TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT)
-// row.layoutParams = lp
-
- val keyView = TextView(requireContext())
- keyView.text = key
- keyView.textSize = TABLE_TEXT_SIZE
- keyView.setTextColor(Color.WHITE)
- keyView.textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START
- keyView.setPadding(5, 3, 5, 3)
-// keyView.isSingleLine=false
-// keyView.maxLines=8
-// keyView.maxWidth=400
-
- row.addView(keyView)
-
- val isApiKey = key.contains("apikey", true) || key.contains("api key", true)
-
- values.forEach { value ->
- val valueView = TextView(requireContext())
- valueView.text =
- if (isApiKey && value != null) {
- value.take(4) + "..." + value.takeLast(8)
- } else {
- value
- }
- if (isApiKey) {
- valueView.typeface = Typeface.MONOSPACE
- }
- valueView.textSize = TABLE_TEXT_SIZE
- valueView.setTextColor(Color.WHITE)
- valueView.textAlignment = TextView.TEXT_ALIGNMENT_VIEW_START
- valueView.setPadding(15, 3, 5, 3)
-// valueView.isSingleLine=false
-// valueView.maxLines=8
-// valueView.maxWidth=400
- row.addView(valueView)
- }
-
- return row
- }
-
- private fun populateCodecsTable(codecsTable: TableLayout) {
- val androidCodecs = MediaCodecList(MediaCodecList.REGULAR_CODECS)
- val video = mutableListOf()
- val audio = mutableListOf()
- for (codecInfo in androidCodecs.codecInfos.sortedBy { it.name }) {
- if (!codecInfo.isEncoder) {
- try {
- codecInfo.supportedTypes.forEach { type ->
- val caps = codecInfo.getCapabilitiesForType(type)
- val videoCaps =
- caps.videoCapabilities?.let {
- arrayOf(
- it.bitrateRange.toString(),
- it.supportedFrameRates.toString(),
- it.supportedWidths.toString(),
- it.supportedHeights.toString(),
- )
- } ?: arrayOf("", "", "", "")
- val profiles =
- caps.profileLevels.joinToString("\n") {
- "profile=0x${it.profile.toString(16)}, level=0x${
- it.level.toString(
- 16,
- )
- }"
- }
- val isSoftware =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- codecInfo.isSoftwareOnly
- } else {
- null
- }?.toString()
- val isHardware =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- codecInfo.isHardwareAccelerated
- } else {
- null
- }?.toString()
-
- val row =
- createRow(
- codecInfo.name,
- type,
- isSoftware,
- isHardware,
- profiles,
- *videoCaps,
- )
- if (caps.videoCapabilities != null) {
- video.add(row)
- } else {
- audio.add(row)
- }
- }
- } catch (_: Exception) {
- }
- }
- }
- codecsTable.addView(
- createRow(
- "Name",
- "Type",
- "SW",
- "HW Acc",
- "Profiles",
- "Bit rates",
- "Frame rates",
- "Widths",
- "Heights",
- ),
- )
- audio.forEach(codecsTable::addView)
- video.forEach(codecsTable::addView)
- }
-
- companion object {
- private const val TAG = "DebugFragment"
- private const val TABLE_TEXT_SIZE = 12F
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/DetailsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/DetailsFragment.kt
deleted file mode 100644
index 2de39ffef..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/DetailsFragment.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.util.TypedValue
-import android.view.View
-import android.widget.Button
-import android.widget.ImageView
-import android.widget.ScrollView
-import android.widget.TableLayout
-import android.widget.TableRow
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import com.github.damontecres.stashapp.util.MutationEngine
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.stashapp.util.onlyScrollIfNeeded
-import com.github.damontecres.stashapp.util.readOnlyModeEnabled
-import com.github.damontecres.stashapp.views.StashOnFocusChangeListener
-import com.github.damontecres.stashapp.views.StashRatingBar
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-
-/**
- * Simple details page with an image, favorite button, rating bar, and table for details
- */
-abstract class DetailsFragment : Fragment(R.layout.details_view) {
- protected val serverViewModel: ServerViewModel by activityViewModels()
- protected lateinit var imageView: ImageView
- protected lateinit var table: TableLayout
- protected lateinit var favoriteButton: Button
- protected lateinit var ratingBar: StashRatingBar
-
- protected lateinit var mutationEngine: MutationEngine
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- imageView = view.findViewById(R.id.details_image)
- table = view.findViewById(R.id.details_table)
- favoriteButton = view.findViewById(R.id.favorite_button)
- favoriteButton.onFocusChangeListener = StashOnFocusChangeListener(requireContext())
- favoriteButton.isFocusable = true
- ratingBar = view.findViewById(R.id.rating_bar)
- if (readOnlyModeEnabled()) {
- favoriteButton.isFocusable = false
- ratingBar.disable()
- }
-
- val server = StashServer.requireCurrentServer()
- mutationEngine = MutationEngine(server)
- }
-
- override fun onResume() {
- super.onResume()
- val scrollView = requireView().findViewById(R.id.details_scrollview)
- scrollView.onlyScrollIfNeeded()
- }
-
- protected fun addRow(
- key: Int,
- value: String?,
- allowNulLValue: Boolean = false,
- ) = addRow(getString(key), value, allowNulLValue, null)
-
- protected fun addRow(
- key: Int,
- value: String?,
- valueViewModifier: (TextView.() -> Unit)? = null,
- ) = addRow(getString(key), value, false, valueViewModifier)
-
- protected fun addRow(
- key: Int,
- value: String?,
- ) = addRow(getString(key), value, false, null)
-
- protected fun addRow(
- key: String,
- value: String?,
- allowNulLValue: Boolean = false,
- valueViewModifier: (TextView.() -> Unit)? = null,
- ) {
- if (value.isNullOrBlank() && !allowNulLValue) {
- return
- }
-
- val keyString = if (key.isNotNullOrBlank()) "$key:" else 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
- keyView.setTextSize(
- TypedValue.COMPLEX_UNIT_PX,
- resources.getDimension(R.dimen.table_text_size_large),
- )
-
- val valueView = row.findViewById(R.id.table_row_value)
- valueView.text = value
- valueView.setTextSize(
- TypedValue.COMPLEX_UNIT_PX,
- resources.getDimension(R.dimen.table_text_size_large),
- )
- valueViewModifier?.invoke(valueView)
-
- table.addView(row)
- }
-
- protected fun updateFavorite(favorite: Boolean) {
- if (favorite) {
- favoriteButton.setTextColor(
- resources.getColor(
- android.R.color.holo_red_light,
- requireActivity().theme,
- ),
- )
- } else {
- favoriteButton.setTextColor(
- resources.getColor(
- R.color.transparent_grey_25,
- requireActivity().theme,
- ),
- )
- }
- }
-
- companion object {
- private const val TAG = "DetailsFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/FilterDebugFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/FilterDebugFragment.kt
deleted file mode 100644
index 8097192f3..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/FilterDebugFragment.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.util.TypedValue
-import android.view.View
-import android.view.ViewGroup
-import android.widget.TableLayout
-import android.widget.TableRow
-import android.widget.TextView
-import androidx.activity.addCallback
-import androidx.core.view.get
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.commit
-import androidx.fragment.app.viewModels
-import com.github.damontecres.stashapp.api.fragment.StashData
-import com.github.damontecres.stashapp.filter.extractTitle
-import com.github.damontecres.stashapp.util.toReadableString
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import com.github.damontecres.stashapp.views.models.StashGridViewModel
-
-class FilterDebugFragment : Fragment(R.layout.filter_debug) {
- private val serverViewModel: ServerViewModel by activityViewModels()
- private val viewModel: StashGridViewModel by viewModels(ownerProducer = { requireParentFragment() })
-
- @SuppressLint("SetTextI18n")
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- val filterNameView = view.findViewById(R.id.filter_name)
- val filterDataTypeView = view.findViewById(R.id.filter_datatype)
- val filterFindFilterView = view.findViewById(R.id.filter_findfilter)
- val filterObjectFilterView = view.findViewById(R.id.filter_objectfilter)
-
- val itemListView = view.findViewById(R.id.item_list)
- val itemTextView = view.findViewById(R.id.items_text_view)
-
- val filter = viewModel.filterArgs.value
- filterNameView.text = filter?.name.toString()
- filterDataTypeView.text = filter?.dataType?.name.toString()
- filterFindFilterView.text = filter?.findFilter.toString()
- filterObjectFilterView.text = filter?.objectFilter?.toReadableString(true).toString()
-
- val ids = mutableSetOf()
- when (val status = viewModel.loadingStatus.value) {
- is StashGridViewModel.LoadingStatus.AdapterReady -> {
- val pages = status.pagingAdapter.getPages()
- itemTextView.text = "Items (${status.pagingAdapter.size()})"
- pages.values.sortedBy { it.number }.forEach { page ->
- val pageLayout = layoutInflater.inflate(R.layout.filter_debug_page, itemListView, false)
- val table = pageLayout.findViewById(R.id.page_table)
- itemListView.addView(pageLayout)
- val pageNumView = pageLayout.findViewById(R.id.page_number_text)
- pageNumView.text = "Page #${page.number}"
- page.items.forEachIndexed { index, item ->
- item as StashData
- val rowView = layoutInflater.inflate(R.layout.filter_debug_page_row, table, false) as TableRow
- val positionView = rowView[0] as TextView
- val idView = rowView[1] as TextView
- val titleView = rowView[2] as TextView
- positionView.text = ((page.number - 1) * status.pagingAdapter.pageSize + index + 1).toString()
- idView.text = item.id
- titleView.text = extractTitle(item)
-
- if (!ids.add(item.id)) {
- rowView.setBackgroundColor(resources.getColor(R.color.transparent_red_50, null))
- }
- table.addView(rowView)
- }
-// table.isStretchAllColumns = true
- }
- }
-
- else -> {
- val titleView = TextView(requireContext())
- titleView.text = "No items"
- titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24f)
- itemListView.addView(titleView)
- }
- }
- view.requestFocus()
-
- requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner) {
- requireParentFragment().childFragmentManager.commit {
- remove(this@FilterDebugFragment)
- }
- }
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/FilterFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/FilterFragment.kt
deleted file mode 100644
index ee0e70022..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/FilterFragment.kt
+++ /dev/null
@@ -1,410 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.content.Context
-import android.os.Bundle
-import android.util.Log
-import android.view.KeyEvent
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.AdapterView
-import android.widget.BaseAdapter
-import android.widget.Button
-import android.widget.TextView
-import android.widget.Toast
-import androidx.appcompat.widget.ListPopupWindow
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.viewModels
-import androidx.leanback.widget.SearchEditText
-import androidx.lifecycle.lifecycleScope
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.filter.FilterOptions
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.navigation.FilterAndPosition
-import com.github.damontecres.stashapp.presenters.NullPresenter
-import com.github.damontecres.stashapp.presenters.NullPresenterSelector
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.suppliers.toFilterArgs
-import com.github.damontecres.stashapp.util.FilterParser
-import com.github.damontecres.stashapp.util.QueryEngine
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.addExtraGridLongClicks
-import com.github.damontecres.stashapp.util.calculatePageSize
-import com.github.damontecres.stashapp.util.getDestination
-import com.github.damontecres.stashapp.util.getFilterArgs
-import com.github.damontecres.stashapp.util.getMaxMeasuredWidth
-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
-import com.github.damontecres.stashapp.views.TitleTransitionHelper
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import com.github.damontecres.stashapp.views.models.StashGridViewModel
-import kotlinx.coroutines.launch
-
-/**
- * Displays items of a single [DataType] in a [StashDataGridFragment].
- *
- * This fragment manages the sort (delegating to the [StashDataGridFragment]) and can also retrieve saved filters.
- */
-class FilterFragment :
- Fragment(R.layout.filter_list),
- KeyEvent.Callback {
- private val serverViewModel: ServerViewModel by activityViewModels()
- private val stashGridViewModel: StashGridViewModel by viewModels()
-
- private lateinit var buttonBar: View
- private lateinit var titleTextView: TextView
- private lateinit var filterButton: Button
- private lateinit var sortButton: Button
- private lateinit var playAllButton: Button
- private lateinit var searchEditText: SearchEditText
-
- private lateinit var sortButtonManager: SortButtonManager
- private lateinit var headerTransitionHelper: TitleTransitionHelper
-
- private lateinit var dataType: DataType
-
- private lateinit var fragment: StashDataGridFragment
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- val dest = requireArguments().getDestination()
- val startingFilter = dest.filterArgs
- dataType = startingFilter.dataType
- Log.d(TAG, "onCreate: dataType=$dataType")
-
- val presenterSelector = StashPresenter.defaultClassPresenterSelector()
- addExtraGridLongClicks(presenterSelector, dataType) {
- FilterAndPosition(
- stashGridViewModel.filterArgs.value!!,
- stashGridViewModel.currentPosition.value ?: -1,
- )
- }
- stashGridViewModel.init(
- NullPresenterSelector(
- presenterSelector,
- NullPresenter(dataType),
- ),
- calculatePageSize(requireContext(), dataType),
- )
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- val dest = requireArguments().getDestination()
-
- fragment =
- childFragmentManager.findFragmentById(R.id.list_fragment) as StashDataGridFragment
- Log.v(
- TAG,
- "filterArgs.isInitialized=${stashGridViewModel.filterArgs.isInitialized}, " +
- "savedInstanceState.isNull=${savedInstanceState == null}",
- )
- if (!stashGridViewModel.scrollToNextPage.isInitialized || stashGridViewModel.scrollToNextPage.value == null) {
- stashGridViewModel.scrollToNextPage.value = dest.scrollToNextPage
- }
- fragment.requestFocus = true
- fragment.init(dataType)
-
- serverViewModel.currentServer.observe(viewLifecycleOwner) {
- sortButtonManager =
- SortButtonManager(StashServer.getCurrentServerVersion()) { sortAndDirection ->
- stashGridViewModel.setFilter(sortAndDirection)
- }
- val filter =
- if (stashGridViewModel.filterArgs.isInitialized) {
- stashGridViewModel.filterArgs.value!!
- } else if (savedInstanceState != null) {
- val restoredFilter =
- savedInstanceState.getFilterArgs(STATE_FILTER) ?: dest.filterArgs
- stashGridViewModel.setFilter(restoredFilter)
- restoredFilter
- } else {
- stashGridViewModel.setFilter(dest.filterArgs)
- dest.filterArgs
- }
-
- filterButton = view.findViewById(R.id.filter_button)
- filterButton.setOnClickListener {
- Toast
- .makeText(requireContext(), "Filters not loaded yet!", Toast.LENGTH_SHORT)
- .show()
- }
- val onFocusChangeListener = StashOnFocusChangeListener(requireContext())
- filterButton.onFocusChangeListener = onFocusChangeListener
- lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- populateSavedFilters(dataType)
- }
-
- buttonBar = view.findViewById(R.id.button_bar)
-
- sortButton = view.findViewById(R.id.sort_button)
- sortButton.onFocusChangeListener = onFocusChangeListener
- playAllButton = view.findViewById(R.id.play_all_button)
- playAllButton.onFocusChangeListener = onFocusChangeListener
- titleTextView = view.findViewById(R.id.list_title)
- titleTextView.text = filter.name ?: getString(dataType.pluralStringId)
-
- searchEditText = view.findViewById(R.id.search_edit_text)
- stashGridViewModel.setupSearch(searchEditText)
-
- headerTransitionHelper = TitleTransitionHelper(view as ViewGroup, buttonBar)
- stashGridViewModel.currentPosition.observe(viewLifecycleOwner) { position ->
- val shouldShowTitle = position < fragment.numberOfColumns
- headerTransitionHelper.showTitle(shouldShowTitle)
- }
-
- sortButtonManager.setUpSortButton(
- sortButton,
- filter.dataType,
- filter.sortAndDirection,
- )
-
- val playAllListener =
- PlayAllOnClickListener(serverViewModel.navigationManager, dataType) {
- FilterAndPosition(stashGridViewModel.filterArgs.value!!, 0)
- }
- playAllButton.setOnClickListener(playAllListener)
-
- if (filter.dataType.supportsPlaylists) {
- playAllButton.visibility = View.VISIBLE
- } else if (filter.dataType == DataType.IMAGE) {
- playAllButton.visibility = View.VISIBLE
- playAllButton.text = getString(R.string.play_slideshow)
- }
-
- val initialRequestFocus = fragment.requestFocus
- if (initialRequestFocus) {
- stashGridViewModel.searchBarFocus.observe(viewLifecycleOwner) { hasFocus ->
- // If the search text has focus, then the fragment shouldn't take it
- fragment.requestFocus = !hasFocus
- }
- }
- }
- }
-
- private suspend fun populateSavedFilters(dataType: DataType) {
- val server = StashServer.requireCurrentServer()
- val savedFilters =
- QueryEngine(server).getSavedFilters(dataType)
-
- val createFilterSupported = dataType in FilterOptions.keys
-
- // Always show the list for data types supporting create filter
- if (savedFilters.isEmpty() && !createFilterSupported) {
- filterButton.setOnClickListener {
- Toast
- .makeText(
- requireContext(),
- "No saved filters found",
- Toast.LENGTH_SHORT,
- ).show()
- }
- } else {
- val listPopUp =
- ListPopupWindow(
- requireContext(),
- null,
- android.R.attr.listPopupWindowStyle,
- )
- val adapterItems =
- savedFilters.map { it.name.ifBlank { getString(dataType.pluralStringId) } }
- val adapter = SavedFilterAdapter(requireContext(), createFilterSupported, adapterItems)
-
- listPopUp.setAdapter(adapter)
- listPopUp.inputMethodMode = ListPopupWindow.INPUT_METHOD_NEEDED
- listPopUp.anchorView = filterButton
-
- listPopUp.width = getMaxMeasuredWidth(requireContext(), adapter)
- listPopUp.isModal = true
-
- 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 lookupPos =
- if (adapter.createEnabled) {
- position - 3
- } else {
- position
- }
- if (adapter.createEnabled && (position == 0 || position == 1)) {
- val destination =
- if (position == 1) {
- // Create from current
- Destination.CreateFilter(
- fragment.dataType,
- stashGridViewModel.filterArgs.value!!,
- )
- } else {
- Destination.CreateFilter(fragment.dataType, null)
- }
- serverViewModel.navigationManager.navigate(destination)
- } else {
- val savedFilter =
- savedFilters[lookupPos]
- try {
- val filterArgs =
- savedFilter
- .toFilterArgs(filterParser)
- .withResolvedRandom()
- fragment.cleanup()
- serverViewModel.navigationManager.navigate(Destination.Filter(filterArgs))
- } catch (ex: Exception) {
- Log.e(TAG, "Exception parsing filter ${savedFilter.id}", ex)
- Toast
- .makeText(
- requireContext(),
- "Error with filter ${savedFilter.id}! Probably a bug: ${ex.message}",
- Toast.LENGTH_LONG,
- ).show()
- }
- }
- }
-
- filterButton.setOnClickListener {
- listPopUp.show()
- listPopUp.listView?.requestFocus()
- }
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- if (stashGridViewModel.filterArgs.isInitialized) {
- outState.putFilterArgs(STATE_FILTER, stashGridViewModel.filterArgs.value!!)
- }
- }
-
- private class SavedFilterAdapter(
- context: Context,
- val createEnabled: Boolean,
- val filters: List,
- ) : BaseAdapter() {
- private val inflater = LayoutInflater.from(context)
-
- override fun getCount(): Int =
- 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 = position.toLong()
-
- override fun getView(
- position: Int,
- convertView: View?,
- parent: ViewGroup?,
- ): View =
- 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 = !createEnabled
-
- override fun isEnabled(position: Int): Boolean = !(createEnabled && position == 2)
-
- override fun getItemViewType(position: Int): Int =
- if (isEnabled(position)) {
- 0
- } else {
- 1
- }
-
- override fun getViewTypeCount(): Int =
- if (createEnabled) {
- 2
- } else {
- 1
- }
- }
-
- override fun onKeyUp(
- keyCode: Int,
- event: KeyEvent,
- ): Boolean {
- val fragment =
- childFragmentManager.findFragmentById(R.id.list_fragment) as StashDataGridFragment?
- return fragment?.onKeyUp(keyCode, event) ?: false
- }
-
- override fun onKeyDown(
- keyCode: Int,
- event: KeyEvent,
- ): Boolean {
- val fragment =
- childFragmentManager.findFragmentById(R.id.list_fragment) as StashDataGridFragment?
- return fragment?.onKeyDown(keyCode, event) ?: false
- }
-
- override fun onKeyLongPress(
- keyCode: Int,
- event: KeyEvent,
- ): Boolean {
- val fragment =
- childFragmentManager.findFragmentById(R.id.list_fragment) as StashDataGridFragment?
- return fragment?.onKeyLongPress(keyCode, event) ?: false
- }
-
- override fun onKeyMultiple(
- keyCode: Int,
- repeatCount: Int,
- event: KeyEvent,
- ): Boolean {
- val fragment =
- childFragmentManager.findFragmentById(R.id.list_fragment) as StashDataGridFragment?
- return fragment?.onKeyMultiple(keyCode, repeatCount, event) ?: false
- }
-
- companion object {
- private const val TAG = "FilterFragment"
- private const val STATE_FILTER = "currentFilter"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/GalleryDetailsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/GalleryDetailsFragment.kt
deleted file mode 100644
index 155a9c2ac..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/GalleryDetailsFragment.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.GalleryFilterType
-import com.github.damontecres.stashapp.api.type.StringCriterionInput
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashGlide
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.stashapp.util.showSetRatingToast
-import com.github.damontecres.stashapp.views.models.GalleryViewModel
-import kotlinx.coroutines.launch
-
-class GalleryDetailsFragment : DetailsFragment() {
- private val viewModel: GalleryViewModel by viewModels(ownerProducer = { requireParentFragment() })
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- favoriteButton.visibility = View.GONE
-
- viewModel.item.observe(viewLifecycleOwner) { galleryData ->
- if (galleryData == null) {
- return@observe
- }
- if (PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_show_playback_debug_info), false)
- ) {
- addRow(R.string.id, galleryData.id)
- }
- addRow(R.string.stashapp_date, galleryData.date)
- addRow(R.string.stashapp_scene_code, galleryData.code)
- addRow(R.string.stashapp_photographer, galleryData.photographer) {
- if (galleryData.photographer.isNotNullOrBlank()) {
- setTextColor(resources.getColorStateList(R.color.clickable_text, null))
- setOnClickListener {
- val objectFilter =
- GalleryFilterType(
- photographer =
- Optional.present(
- StringCriterionInput(
- value = galleryData.photographer,
- modifier = CriterionModifier.EQUALS,
- ),
- ),
- )
- val filterArgs =
- FilterArgs(
- dataType = DataType.GALLERY,
- name = galleryData.photographer,
- objectFilter = objectFilter,
- )
- serverViewModel.navigationManager.navigate(Destination.Filter(filterArgs))
- }
- }
- }
- addRow(R.string.stashapp_details, galleryData.details)
-
- if (galleryData.paths.cover.isNotNullOrBlank()) {
- StashGlide
- .with(requireContext(), galleryData.paths.cover)
- .optionalFitCenter()
- .error(StashPresenter.glideError(requireContext()))
- .into(imageView)
- } else {
- imageView.visibility = View.INVISIBLE
- }
-
- ratingBar.rating100 = galleryData.rating100 ?: 0
- ratingBar.setRatingCallback { newRating100 ->
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val newItem =
- mutationEngine.updateGallery(galleryData.id, rating100 = newRating100)
- if (newItem != null) {
- showSetRatingToast(requireContext(), newRating100)
- viewModel.update(newItem)
- }
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/GalleryFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/GalleryFragment.kt
deleted file mode 100644
index d9700e6f8..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/GalleryFragment.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import androidx.leanback.widget.ClassPresenterSelector
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.api.fragment.PerformerData
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.ImageFilterType
-import com.github.damontecres.stashapp.api.type.MultiCriterionInput
-import com.github.damontecres.stashapp.api.type.SceneFilterType
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.presenters.PerformerInScenePresenter
-import com.github.damontecres.stashapp.suppliers.DataSupplierOverride
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.PageFilterKey
-import com.github.damontecres.stashapp.util.StashFragmentPagerAdapter
-import com.github.damontecres.stashapp.util.getUiTabs
-import com.github.damontecres.stashapp.util.name
-import com.github.damontecres.stashapp.views.models.GalleryViewModel
-
-class GalleryFragment : TabbedFragment(DataType.GALLERY.name) {
- private val viewModel: GalleryViewModel by viewModels()
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- serverViewModel.currentServer.observe(viewLifecycleOwner) {
- viewModel.init(requireArguments())
- }
- serverViewModel
- .withLiveData(viewModel.item)
- .observe(viewLifecycleOwner) { (server, gallery) ->
- if (gallery == null) {
- Toast
- .makeText(
- requireContext(),
- "Gallery '${viewModel.itemId}' not found",
- Toast.LENGTH_LONG,
- ).show()
- serverViewModel.navigationManager.goBack()
- return@observe
- }
- tabViewModel.title.value = gallery.name
-
- val galleries =
- Optional.present(
- MultiCriterionInput(
- value = Optional.present(listOf(gallery.id)),
- modifier = CriterionModifier.INCLUDES_ALL,
- ),
- )
- if (!tabViewModel.tabs.isInitialized) {
- tabViewModel.tabs.value =
- listOf(
- StashFragmentPagerAdapter.PagerEntry(getString(R.string.stashapp_details)) {
- GalleryDetailsFragment()
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.IMAGE) {
- StashGridControlsFragment(
- dataType = DataType.IMAGE,
- findFilter =
- serverViewModel
- .requireServer()
- .serverPreferences
- .getDefaultPageFilter(
- PageFilterKey.GALLERY_IMAGES,
- ).findFilter,
- objectFilter = ImageFilterType(galleries = galleries),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.SCENE) {
- StashGridControlsFragment(
- dataType = DataType.SCENE,
- objectFilter = SceneFilterType(galleries = galleries),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.PERFORMER) {
- val presenter =
- ClassPresenterSelector().addClassPresenter(
- PerformerData::class.java,
- PerformerInScenePresenter(gallery.date),
- )
- val fragment =
- StashGridControlsFragment(
- filterArgs =
- FilterArgs(
- DataType.PERFORMER,
- override = DataSupplierOverride.GalleryPerformer(gallery.id),
- ),
- )
- fragment.presenterSelector = presenter
- fragment
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.TAG) {
- StashGridControlsFragment(
- filterArgs =
- FilterArgs(
- DataType.TAG,
- override = DataSupplierOverride.GalleryTag(gallery.id),
- ),
- )
- },
- ).filter { it.title in getUiTabs(requireContext(), DataType.GALLERY) }
- }
- }
- }
-
- companion object {
- private const val TAG = "GalleryFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/GroupDetailsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/GroupDetailsFragment.kt
deleted file mode 100644
index 7d84c1da5..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/GroupDetailsFragment.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.util.TypedValue
-import android.view.View
-import android.widget.ImageView
-import android.widget.TableLayout
-import android.widget.TableRow
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.util.StashGlide
-import com.github.damontecres.stashapp.views.models.GroupViewModel
-import com.github.damontecres.stashapp.views.parseTimeToString
-import kotlin.time.DurationUnit
-import kotlin.time.toDuration
-
-class GroupDetailsFragment : Fragment(R.layout.group_view) {
- private val viewModel: GroupViewModel by viewModels(ownerProducer = { requireParentFragment() })
-
- private lateinit var frontImage: ImageView
- private lateinit var backImage: ImageView
- private lateinit var table: TableLayout
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- frontImage = view.findViewById(R.id.group_front_image)
- backImage = view.findViewById(R.id.group_back_image)
-
- table = view.findViewById(R.id.group_table)
- viewModel.item.observe(viewLifecycleOwner) { groupData ->
- if (groupData == null) {
- return@observe
- }
-
- if (groupData.front_image_path != null) {
- StashGlide
- .with(requireActivity(), groupData.front_image_path)
- .error(StashPresenter.glideError(requireContext()))
- .into(frontImage)
- }
- if (groupData.back_image_path != null) {
- StashGlide
- .with(requireActivity(), groupData.back_image_path)
- .error(StashPresenter.glideError(requireContext()))
- .into(backImage)
- }
-
- addRow(
- R.string.stashapp_duration,
- groupData.duration?.toDuration(DurationUnit.MINUTES)?.toString(),
- )
- addRow(R.string.stashapp_date, groupData.date)
- addRow(R.string.stashapp_studio, groupData.studio?.name)
- addRow(R.string.stashapp_director, groupData.director)
- addRow(R.string.stashapp_synopsis, groupData.synopsis)
- addRow(R.string.stashapp_created_at, parseTimeToString(groupData.created_at))
- addRow(R.string.stashapp_updated_at, parseTimeToString(groupData.updated_at))
- if (PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_show_playback_debug_info), false)
- ) {
- addRow(R.string.id, groupData.id)
- }
- table.setColumnShrinkable(1, true)
- }
- }
-
- 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
- keyView.setTextSize(
- TypedValue.COMPLEX_UNIT_PX,
- resources.getDimension(R.dimen.table_text_size_large),
- )
-
- val valueView = row.findViewById(R.id.table_row_value)
- valueView.text = value
- valueView.setTextSize(
- TypedValue.COMPLEX_UNIT_PX,
- resources.getDimension(R.dimen.table_text_size_large),
- )
-
- table.addView(row)
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/GroupFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/GroupFragment.kt
deleted file mode 100644
index eb09b8a3d..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/GroupFragment.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.api.fragment.GroupRelationshipType
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.HierarchicalMultiCriterionInput
-import com.github.damontecres.stashapp.api.type.SceneFilterType
-import com.github.damontecres.stashapp.api.type.SceneMarkerFilterType
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.data.StashFindFilter
-import com.github.damontecres.stashapp.suppliers.DataSupplierOverride
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.PageFilterKey
-import com.github.damontecres.stashapp.util.StashFragmentPagerAdapter
-import com.github.damontecres.stashapp.util.getUiTabs
-import com.github.damontecres.stashapp.views.models.GroupViewModel
-
-class GroupFragment : TabbedFragment(DataType.GROUP.name) {
- private val viewModel: GroupViewModel by viewModels()
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- serverViewModel.currentServer.observe(viewLifecycleOwner) {
- viewModel.init(requireArguments())
- }
- serverViewModel
- .withLiveData(viewModel.item)
- .observe(viewLifecycleOwner) { (server, group) ->
- if (group == null) {
- Toast
- .makeText(
- requireContext(),
- "Group '${viewModel.itemId}' not found",
- Toast.LENGTH_LONG,
- ).show()
- serverViewModel.navigationManager.goBack()
- return@observe
- }
- tabViewModel.title.value = group.name
- val groupSceneFilter =
- serverViewModel.requireServer().serverPreferences.getDefaultPageFilter(
- PageFilterKey.GROUP_SCENES,
- )
- val subGroupFilter =
- serverViewModel.requireServer().serverPreferences.getDefaultPageFilter(
- PageFilterKey.GROUP_SUB_GROUPS,
- )
- tabViewModel.tabs.value =
- listOf(
- StashFragmentPagerAdapter.PagerEntry(getString(R.string.stashapp_details)) {
- GroupDetailsFragment()
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.SCENE) {
- createStashGridFragment(
- groupId = group.id,
- dataType = DataType.SCENE,
- defaultFindFilter = groupSceneFilter.findFilter,
- createObjectFilter = { SceneFilterType(groups = it) },
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.MARKER) {
- createStashGridFragment(
- groupId = group.id,
- dataType = DataType.MARKER,
- defaultFindFilter = null,
- createObjectFilter = {
- SceneMarkerFilterType(
- scene_filter =
- Optional.present(
- SceneFilterType(
- groups = it,
- ),
- ),
- )
- },
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.TAG) {
- StashGridControlsFragment(
- FilterArgs(
- DataType.TAG,
- override = DataSupplierOverride.GroupTags(group.id),
- ),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(getString(R.string.stashapp_containing_groups)) {
- StashGridControlsFragment(
- FilterArgs(
- DataType.GROUP,
- override =
- DataSupplierOverride.GroupRelationship(
- group.id,
- GroupRelationshipType.CONTAINING,
- ),
- ),
- )
- },
- // TODO use subgroup filter
- StashFragmentPagerAdapter.PagerEntry(getString(R.string.stashapp_sub_groups)) {
- StashGridControlsFragment(
- FilterArgs(
- DataType.GROUP,
- override =
- DataSupplierOverride.GroupRelationship(
- group.id,
- GroupRelationshipType.SUB,
- ),
- ),
- )
- },
- ).filter { it.title in getUiTabs(requireContext(), DataType.GROUP) }
- }
- }
-
- private fun createStashGridFragment(
- groupId: String,
- dataType: DataType,
- defaultFindFilter: StashFindFilter?,
- createObjectFilter: (Optional) -> StashDataFilter,
- ): StashGridControlsFragment {
- val fragment =
- StashGridControlsFragment(
- dataType = dataType,
- findFilter = defaultFindFilter,
- objectFilter = createObjectFilter(createCriterionInput(false, groupId)),
- )
- fragment.subContentSwitchInitialIsChecked = false
- fragment.subContentText = getString(R.string.stashapp_include_sub_group_content)
- fragment.subContentSwitchCheckedListener = { isChecked ->
- val newFilter =
- fragment.currentFilter.copy(
- objectFilter =
- createObjectFilter(
- createCriterionInput(isChecked, groupId),
- ),
- )
- fragment.currentFilter = newFilter
- }
- return fragment
- }
-
- private fun createCriterionInput(
- subTags: Boolean,
- groupId: String,
- ): Optional.Present =
- Optional.present(
- HierarchicalMultiCriterionInput(
- value = Optional.present(listOf(groupId)),
- modifier = CriterionModifier.INCLUDES_ALL,
- depth = Optional.present(if (subTags) -1 else 0),
- ),
- )
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/LicenseFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/LicenseFragment.kt
deleted file mode 100644
index 3c87dd38e..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/LicenseFragment.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import com.github.damontecres.stashapp.util.concatIfNotBlank
-import java.io.BufferedReader
-
-/**
- * Show various licenses for third party libraries included in the app
- */
-class LicenseFragment : Fragment(R.layout.license) {
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- val attributions = resources.getTextArray(R.array.stash_license_attributions)
- val licenseText =
- resources.assets
- .open("LICENSE")
- .bufferedReader()
- .use(BufferedReader::readText)
- val otherLicenses =
- resources.assets
- .list("licenses")!!
- .map {
- resources.assets
- .open("licenses/$it")
- .bufferedReader()
- .use(BufferedReader::readText)
- }.toTypedArray()
-
- val text =
- concatIfNotBlank(
- getString(R.string.license_separator),
- *attributions,
- licenseText,
- *otherLicenses,
- )
-
- view.findViewById(R.id.license_text).text = text
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt
new file mode 100644
index 000000000..a1c3aeec1
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt
@@ -0,0 +1,198 @@
+package com.github.damontecres.stashapp
+
+import android.os.Bundle
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.background
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.datastore.core.DataStore
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation3.runtime.NavBackStack
+import androidx.tv.material3.MaterialTheme
+import co.touchlab.kermit.Logger
+import com.github.damontecres.stashapp.di.AuthHttpClient
+import com.github.damontecres.stashapp.di.server.CurrentServer
+import com.github.damontecres.stashapp.di.server.ServerRepository
+import com.github.damontecres.stashapp.di.services.NavigationManager
+import com.github.damontecres.stashapp.navigation.Destination
+import com.github.damontecres.stashapp.proto.StashPreferences
+import com.github.damontecres.stashapp.ui.AppTheme
+import com.github.damontecres.stashapp.ui.GlobalContext
+import com.github.damontecres.stashapp.ui.LocalGlobalContext
+import com.github.damontecres.stashapp.ui.chooseColorScheme
+import com.github.damontecres.stashapp.ui.components.LoadingPage
+import com.github.damontecres.stashapp.ui.components.server.InitialSetup
+import com.github.damontecres.stashapp.ui.defaultColorSchemeSet
+import com.github.damontecres.stashapp.ui.nav.ApplicationContent
+import com.github.damontecres.stashapp.ui.nav.CoilConfig
+import com.github.damontecres.stashapp.ui.pages.PinEntryPage
+import com.github.damontecres.stashapp.ui.readThemeJson
+import com.github.damontecres.stashapp.util.isNotNullOrBlank
+import com.github.damontecres.stashapp.util.launchDefault
+import kotlinx.coroutines.flow.first
+import kotlinx.serialization.json.Json
+import okhttp3.OkHttpClient
+import org.koin.android.ext.android.get
+import org.koin.android.ext.android.inject
+import org.koin.core.qualifier.qualifier
+import timber.log.Timber
+
+class MainActivity : AppCompatActivity() {
+ private val navigationManager: NavigationManager by inject()
+ private val serverRepository: ServerRepository by inject()
+ private val preferences: DataStore by inject()
+
+ private val httpClient: OkHttpClient = get(qualifier())
+
+ private val json =
+ Json {
+ classDiscriminator = "_type"
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ window?.setFlags(
+ WindowManager.LayoutParams.FLAG_SECURE,
+ WindowManager.LayoutParams.FLAG_SECURE,
+ )
+
+ Logger.i { "onCreate: has savedInstanceState?=${savedInstanceState != null} " }
+
+ val backStackStr = savedInstanceState?.getString(KEY_BACK_STACK)
+ if (backStackStr != null) {
+ Timber.d("Restoring back stack")
+ val backStack = json.decodeFromString>(backStackStr)
+ navigationManager.backStack = NavBackStack(*backStack.toTypedArray())
+ } else {
+ navigationManager.backStack = NavBackStack(Destination.Main())
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Logger.i { "onResume" }
+ setContent {
+ LoadingPage(Modifier.fillMaxSize())
+ }
+
+ lifecycleScope.launchDefault {
+ val prefs = preferences.data.first()
+ val hasPin = prefs.pinPreferences.pin.isNotNullOrBlank()
+ serverRepository.restore()
+ showContent(hasPin)
+ }
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ Timber.d("onSaveInstanceState")
+ val str = json.encodeToString(navigationManager.backStack.toList())
+ outState.putString(KEY_BACK_STACK, str)
+ }
+
+ fun showContent(hasPin: Boolean) {
+ Logger.i { "showContent: hasPin=$hasPin" }
+ setContent {
+ val preferences by preferences.data.collectAsState(null)
+ preferences?.let { preferences ->
+ var pinActive by remember(hasPin) { mutableStateOf(hasPin) }
+ val isSystemInDarkTheme = isSystemInDarkTheme()
+ CoilConfig(httpClient, preferences)
+ var colorScheme by
+ remember {
+ mutableStateOf(
+ com.github.damontecres.stashapp.ui.getTheme(
+ this@MainActivity,
+ preferences.interfacePreferences.themeStyle,
+ preferences.interfacePreferences.theme,
+ isSystemInDarkTheme,
+ ),
+ )
+ }
+ AppTheme(colorScheme = colorScheme) {
+ if (pinActive) {
+ PinEntryPage(
+ requiredPin = preferences.pinPreferences.pin,
+ title = stringResource(R.string.enter_pin),
+ onCorrectPin = { pinActive = false },
+ preventBack = true,
+ autoSubmit = preferences.pinPreferences.autoSubmit,
+ modifier = Modifier.fillMaxSize(),
+ )
+ } else {
+ val currentServer by serverRepository.currentServer.collectAsState()
+ LaunchedEffect(currentServer) {
+ Logger.i { "Switched server to ${currentServer.server.url}" }
+ }
+ if (currentServer != CurrentServer.UNSET) {
+ key(currentServer) {
+ CompositionLocalProvider(
+ LocalGlobalContext provides
+ GlobalContext(
+ currentServer,
+ navigationManager,
+ preferences,
+ ),
+ ) {
+ ApplicationContent(
+ currentServer = currentServer,
+ preferences = preferences,
+ navigationManager = navigationManager,
+ onChangeTheme = { name ->
+ try {
+ colorScheme =
+ chooseColorScheme(
+ preferences.interfacePreferences.themeStyle,
+ isSystemInDarkTheme,
+ if (name.isNullOrBlank() || name == "default") {
+ defaultColorSchemeSet
+ } else {
+ readThemeJson(
+ this@MainActivity,
+ name,
+ )
+ },
+ )
+ Logger.i { "Updated theme" }
+ } catch (ex: Exception) {
+ Logger.e(ex) { "Exception changing theme" }
+ Toast
+ .makeText(
+ this@MainActivity,
+ "Error changing theme: ${ex.localizedMessage}",
+ Toast.LENGTH_LONG,
+ ).show()
+ }
+ },
+ modifier = Modifier.background(MaterialTheme.colorScheme.background),
+ // TODO could use onKeyEvent here to make focus/movement sounds everywhere
+ // But it wouldn't know if the focus would actually change
+ )
+ }
+ }
+ } else {
+ InitialSetup(Modifier.fillMaxSize())
+ }
+ }
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val KEY_BACK_STACK = "backStack"
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt
deleted file mode 100644
index 57980cb15..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt
+++ /dev/null
@@ -1,365 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.util.Log
-import android.view.KeyEvent
-import android.view.View
-import android.widget.Toast
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.addCallback
-import androidx.core.content.ContextCompat
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.commitNow
-import androidx.leanback.app.BrowseSupportFragment
-import androidx.leanback.widget.ArrayObjectAdapter
-import androidx.leanback.widget.BrowseFrameLayout
-import androidx.leanback.widget.HeaderItem
-import androidx.leanback.widget.ListRow
-import androidx.leanback.widget.ListRowPresenter
-import androidx.leanback.widget.SparseArrayObjectAdapter
-import androidx.lifecycle.lifecycleScope
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.navigation.FilterAndPosition
-import com.github.damontecres.stashapp.navigation.NavigationOnItemViewClickedListener
-import com.github.damontecres.stashapp.presenters.StashImageCardView
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.DefaultKeyEventCallback
-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.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.TestResult
-import com.github.damontecres.stashapp.util.Version
-import com.github.damontecres.stashapp.util.getCaseInsensitive
-import com.github.damontecres.stashapp.util.maybeStartPlayback
-import com.github.damontecres.stashapp.util.showToastOnMain
-import com.github.damontecres.stashapp.util.testStashConnection
-import com.github.damontecres.stashapp.views.LoadingFragment
-import com.github.damontecres.stashapp.views.MainTitleView
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-
-/**
- * Loads a grid of cards with groups to browse.
- */
-class MainFragment :
- BrowseSupportFragment(),
- DefaultKeyEventCallback {
- private val viewModel: ServerViewModel by activityViewModels()
-
- private val rowsAdapter = SparseArrayObjectAdapter(ListRowPresenter())
- private val adapters = ArrayList()
- private val filterList = ArrayList()
- private lateinit var backCallback: OnBackPressedCallback
-
- private var currentPosition: Position? = null
- private var dataFetchedFor: StashServer? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- headersState = HEADERS_DISABLED
- setupUIElements()
- adapter = rowsAdapter
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- setupEventListeners()
-
- // Override the focus search so that pressing up from the rows will move to search first
- val browseFrameLayout =
- view.findViewById(androidx.leanback.R.id.browse_frame)
- browseFrameLayout.onFocusSearchListener =
- BrowseFrameLayout.OnFocusSearchListener { focused: View?, direction: Int ->
- if (direction == View.FOCUS_UP) {
- requireActivity().findViewById(R.id.search_button)
- } else {
- null
- }
- }
-
- backCallback =
- requireActivity().onBackPressedDispatcher.addCallback(
- viewLifecycleOwner,
- currentPosition != null,
- ) {
- val pos = currentPosition!!
- if (pos.column > 0) {
- selectedPosition = pos.row
- (selectedRowViewHolder as ListRowPresenter.ViewHolder)
- .gridView.selectedPosition = 0
- } else if (pos.row > 0) {
- selectedPosition = 0
- }
- }
- (titleView as MainTitleView).focusListener.addListener { _, isFocused ->
- backCallback.isEnabled = !isFocused
- }
-
- setupObservers()
- setOnItemViewSelectedListener { itemViewHolder, item, rowViewHolder, row ->
- val rowNum = rowsAdapter.indexOf(row)
- val col =
- (rowViewHolder as ListRowPresenter.ViewHolder).gridView.selectedPosition
- val pos = Position(rowNum, col)
-// Log.v(TAG, "$pos")
- backCallback.isEnabled = pos.row > 0 || pos.column > 0
- currentPosition = pos
- }
- }
-
- private fun setupObservers() {
- viewModel.cardUiSettings.observe(viewLifecycleOwner) {
- // Refresh the cards
- Log.d(TAG, "Card UI settings changed")
- rowsAdapter.notifyItemRangeChanged(0, rowsAdapter.size())
- }
-
- viewModel.currentServer.observe(viewLifecycleOwner) { newServer ->
- if (newServer == null) {
- Log.w(TAG, "Null server")
- viewModel.navigationManager.navigate(Destination.ManageServers(true))
- return@observe
- }
- if (dataFetchedFor == newServer) {
- return@observe
- } else {
- dataFetchedFor = newServer
- }
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler { ex ->
- Toast.makeText(
- requireContext(),
- "Exception: ${ex.message}",
- Toast.LENGTH_LONG,
- )
- },
- ) {
- Log.d(TAG, "Server changed")
- showLoading()
- clearData()
- try {
- if (newServer != null) {
- val result =
- testStashConnection(
- requireContext(),
- false,
- newServer.apolloClient,
- )
- if (result is TestResult.Success) {
- val mainTitleView =
- requireActivity().findViewById(R.id.browse_title_group)
- mainTitleView.refreshMenuItems(newServer.serverPreferences)
- fetchData(newServer)
- } else if (result is TestResult.UnsupportedVersion) {
- Log.w(
- TAG,
- "Server version is not supported: ${result.serverVersion}",
- )
- Toast
- .makeText(
- requireContext(),
- "Server version ${result.serverVersion} is not supported!",
- Toast.LENGTH_LONG,
- ).show()
- } else {
- Log.w(TAG, "testStashConnection returned $result")
- requireActivity().findViewById(R.id.search_button).requestFocus()
- Toast
- .makeText(
- requireContext(),
- "Connection to Stash failed: $result",
- Toast.LENGTH_LONG,
- ).show()
- }
- } else {
- Log.e(TAG, "newServer is null")
- Toast
- .makeText(
- requireContext(),
- "Stash server not configured!",
- Toast.LENGTH_LONG,
- ).show()
- }
- } finally {
- hideLoading()
- }
- }
- }
- }
-
- private fun setupUIElements() {
- title = getString(R.string.browse_title)
- // over title
- isHeadersTransitionOnBackEnabled = true
-
- // set fastLane (or headers) background color
- brandColor = ContextCompat.getColor(requireActivity(), R.color.fastlane_background)
- }
-
- private fun setupEventListeners() {
- setOnSearchClickedListener {
- viewModel.navigationManager.navigate(Destination.Search)
- }
-
- onItemViewClickedListener =
- NavigationOnItemViewClickedListener(viewModel.navigationManager) {
- val position = currentPosition!!
- val filter = filterList[position.row]
- FilterAndPosition(filter, position.column)
- }
- }
-
- private fun clearData() {
- rowsAdapter.clear()
- adapters.forEach { it.clear() }
- }
-
- private suspend fun fetchData(server: StashServer) =
- withContext(Dispatchers.IO) {
- try {
- val serverVersion = server.serverPreferences.serverVersion
-
- if (!Version.isStashVersionSupported(serverVersion)) {
- val msg =
- "Stash server version $serverVersion is not supported!"
- Log.e(TAG, msg)
- withContext(Dispatchers.Main) {
- Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show()
- requireActivity().findViewById(R.id.search_button).requestFocus()
- }
- } else {
- val queryEngine = QueryEngine(server)
- val filterParser = FilterParser(serverVersion)
- val frontPageContent =
- server.serverPreferences.uiConfiguration?.getCaseInsensitive("frontPageContent") as List>?
- if (frontPageContent == null) {
- showToastOnMain(
- requireContext(),
- "Unable to find front page content! Check the Web UI.",
- Toast.LENGTH_LONG,
- )
- return@withContext
- }
- Log.d(TAG, "${frontPageContent.size} front page rows")
- val pageSize = viewModel.cardUiSettings.value!!.maxSearchResults
- val frontPageParser =
- FrontPageParser(requireContext(), queryEngine, filterParser, pageSize)
- val jobs = frontPageParser.parse(frontPageContent)
- jobs.forEachIndexed { index, job ->
- job.await().let { row ->
- if (row is FrontPageParser.FrontPageRow.Success) {
- filterList.add(row.filter)
-
- val adapter =
- ArrayObjectAdapter(StashPresenter.defaultClassPresenterSelector())
- adapter.addAll(0, row.data)
- adapter.add(row.filter)
- adapters.add(adapter)
- withContext(Dispatchers.Main) {
- rowsAdapter.set(
- index,
- ListRow(HeaderItem(row.name), adapter),
- )
- }
- } else if (row is FrontPageParser.FrontPageRow.Error) {
- Log.w(TAG, "Error on front page row $index")
- withContext(Dispatchers.Main) {
- Toast
- .makeText(
- requireContext(),
- "Error loading row ${index + 1} on front page",
- Toast.LENGTH_SHORT,
- ).show()
- }
- }
- }
- }
- }
- } catch (ex: QueryEngine.StashNotConfiguredException) {
- Log.e(TAG, "StashNotConfiguredException", ex)
- withContext(Dispatchers.Main) {
- Toast
- .makeText(
- requireContext(),
- "Stash not configured. Please enter the URL in settings!",
- Toast.LENGTH_LONG,
- ).show()
- requireActivity().findViewById(R.id.search_button).requestFocus()
- }
- } catch (ex: QueryEngine.QueryException) {
- Log.e(TAG, "QueryException", ex)
- withContext(Dispatchers.Main) {
- Toast
- .makeText(
- requireContext(),
- ex.message,
- Toast.LENGTH_LONG,
- ).show()
- requireActivity().findViewById(R.id.search_button).requestFocus()
- }
- } catch (ex: Exception) {
- Log.e(TAG, "Exception in fetchData", ex)
- showToastOnMain(
- requireContext(),
- "Error fetching data: ${ex.message}",
- Toast.LENGTH_LONG,
- )
- }
- }
-
- override fun onKeyUp(
- keyCode: Int,
- event: KeyEvent,
- ): Boolean {
- if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
- val item =
- currentPosition?.let {
- val row = rowsAdapter.get(it.row) as ListRow
- row.adapter.get(it.column)
- }
- if (item != null && requireActivity().currentFocus is StashImageCardView) {
- maybeStartPlayback(requireContext(), item)
- }
- }
- return super.onKeyUp(keyCode, event)
- }
-
- private suspend fun showLoading() =
- withContext(Dispatchers.Main) {
- childFragmentManager.commitNow {
- add(requireView().id, LoadingFragment(), "stash_loading")
- }
- }
-
- private suspend fun hideLoading() =
- withContext(Dispatchers.Main) {
- childFragmentManager.findFragmentByTag("stash_loading")?.let {
- childFragmentManager.commitNow {
- remove(it)
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- viewModel.updateServerPreferences()
- }
-
- data class Position(
- val row: Int,
- val column: Int,
- )
-
- companion object {
- private const val TAG = "MainFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/MarkerDetailsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/MarkerDetailsFragment.kt
deleted file mode 100644
index 2c8609da9..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/MarkerDetailsFragment.kt
+++ /dev/null
@@ -1,445 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.annotation.SuppressLint
-import android.graphics.Bitmap
-import android.graphics.drawable.Drawable
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Toast
-import androidx.core.content.ContextCompat
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.setFragmentResultListener
-import androidx.fragment.app.viewModels
-import androidx.leanback.app.DetailsSupportFragment
-import androidx.leanback.app.DetailsSupportFragmentBackgroundController
-import androidx.leanback.widget.AbstractDetailsDescriptionPresenter
-import androidx.leanback.widget.Action
-import androidx.leanback.widget.ArrayObjectAdapter
-import androidx.leanback.widget.ClassPresenterSelector
-import androidx.leanback.widget.DetailsOverviewRow
-import androidx.leanback.widget.FullWidthDetailsOverviewRowPresenter
-import androidx.leanback.widget.HeaderItem
-import androidx.leanback.widget.ListRow
-import androidx.leanback.widget.ListRowPresenter
-import androidx.leanback.widget.OnActionClickedListener
-import androidx.leanback.widget.SparseArrayObjectAdapter
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.bumptech.glide.request.target.CustomTarget
-import com.bumptech.glide.request.transition.Transition
-import com.github.damontecres.stashapp.actions.StashAction
-import com.github.damontecres.stashapp.actions.StashActionClickedListener
-import com.github.damontecres.stashapp.api.fragment.FullMarkerData
-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.navigation.Destination
-import com.github.damontecres.stashapp.navigation.NavigationOnItemViewClickedListener
-import com.github.damontecres.stashapp.playback.PlaybackMode
-import com.github.damontecres.stashapp.presenters.ActionPresenter
-import com.github.damontecres.stashapp.presenters.ScenePresenter
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.presenters.TagPresenter
-import com.github.damontecres.stashapp.util.ListRowManager
-import com.github.damontecres.stashapp.util.MutationEngine
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashGlide
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.convertDpToPixel
-import com.github.damontecres.stashapp.util.getDataType
-import com.github.damontecres.stashapp.util.getDestination
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.stashapp.util.readOnlyModeDisabled
-import com.github.damontecres.stashapp.views.ClassOnItemViewClickedListener
-import com.github.damontecres.stashapp.views.StashRatingBar
-import com.github.damontecres.stashapp.views.durationToString
-import com.github.damontecres.stashapp.views.models.MarkerDetailsViewModel
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import com.github.damontecres.stashapp.views.parseTimeToString
-import kotlinx.coroutines.launch
-import kotlin.time.DurationUnit
-import kotlin.time.toDuration
-
-class MarkerDetailsFragment : DetailsSupportFragment() {
- companion object {
- private const val TAG = "MarkerDetailsFragment"
-
- private const val REPLACE_PRIMARY_ID = 10012L
-
- private const val DETAILS_POS = 1
- private const val PRIMARY_TAG_POS = DETAILS_POS + 1
- private const val TAG_POS = PRIMARY_TAG_POS + 1
- private const val ACTIONS_POS = TAG_POS + 1
- }
-
- private val serverViewModel by activityViewModels()
- private val viewModel by viewModels()
-
- private val mDetailsBackground = DetailsSupportFragmentBackgroundController(this)
- private val mAdapter = SparseArrayObjectAdapter()
-
- private val primaryTagPresenter =
- TagPresenter()
- .addLongCLickAction(
- StashPresenter.PopUpItem(
- REPLACE_PRIMARY_ID,
- R.string.replace,
- ),
- { readOnlyModeDisabled() },
- ) { _, _ ->
- serverViewModel.navigationManager.navigate(
- Destination.SearchFor(
- MarkerDetailsFragment::class.simpleName!!,
- REPLACE_PRIMARY_ID,
- DataType.TAG,
- ),
- )
- }
- private val primaryTagRowManager =
- ListRowManager(
- DataType.TAG,
- ListRowManager.SparseArrayRowModifier(mAdapter, PRIMARY_TAG_POS),
- ArrayObjectAdapter(primaryTagPresenter),
- ) { tagIds ->
- // Kind of hacky
- val marker = viewModel.item.value!!
- val result =
- mutationEngine.setTagsOnMarker(
- marker.id,
- tagIds[1],
- marker.tags.map { it.tagData.id },
- )
- viewModel.setMarker(result!!)
- listOfNotNull(result.primary_tag.tagData)
- }
-
- private val tagsRowManager =
- ListRowManager(
- DataType.TAG,
- ListRowManager.SparseArrayRowModifier(mAdapter, TAG_POS),
- ArrayObjectAdapter(TagPresenter()),
- ) { tagIds ->
- val marker = viewModel.item.value!!
- val result =
- mutationEngine.setTagsOnMarker(marker.id, marker.primary_tag.tagData.id, tagIds)
- viewModel.setMarker(result!!)
- result.tags.map { it.tagData }
- }
-
- private val sceneActionsAdapter =
- SparseArrayObjectAdapter(
- ClassPresenterSelector().addClassPresenter(
- StashAction::class.java,
- ActionPresenter(),
- ),
- )
-
- private lateinit var mutationEngine: MutationEngine
-
- private lateinit var detailsPresenter: FullWidthDetailsOverviewRowPresenter
-
- private val actionClickListener =
- object : StashActionClickedListener {
- override fun onClicked(action: StashAction) {
- if (action == StashAction.ADD_TAG) {
- serverViewModel.navigationManager.navigate(
- Destination.SearchFor(
- MarkerDetailsFragment::class.simpleName!!,
- StashAction.ADD_TAG.id,
- DataType.TAG,
- ),
- )
- } else if (action == StashAction.SHIFT_MARKERS) {
- serverViewModel.navigationManager.navigate(
- Destination.UpdateMarker(viewModel.item.value!!.id),
- )
- }
- }
-
- override fun incrementOCounter(counter: OCounter): Unit = throw IllegalStateException()
- }
-
- @Deprecated("Deprecated in Java")
- override fun inflateTitle(
- inflater: LayoutInflater?,
- parent: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- primaryTagRowManager.name = getString(R.string.stashapp_primary_tag)
- mutationEngine = MutationEngine(StashServer.requireCurrentServer())
-
- setFragmentResultListener(MarkerDetailsFragment::class.simpleName!!) { _, bundle ->
- val sourceId = bundle.getLong(SearchForFragment.RESULT_ID_KEY)
- val dataType = bundle.getDataType()
- val newId = bundle.getString(SearchForFragment.RESULT_ITEM_ID_KEY)
- Log.v(TAG, "Adding $dataType: $newId")
- if (newId != null) {
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler { ex ->
- Toast.makeText(
- requireContext(),
- "Failed to update: ${ex.message}",
- Toast.LENGTH_LONG,
- )
- },
- ) {
- when (sourceId) {
- REPLACE_PRIMARY_ID -> {
- val newPrimaryTag = primaryTagRowManager.add(newId)
- if (newPrimaryTag != null) {
- Toast
- .makeText(
- requireContext(),
- "Changed primary tag to '${newPrimaryTag.name}'",
- Toast.LENGTH_LONG,
- ).show()
- }
- }
-
- StashAction.ADD_TAG.id -> {
- val newTagData = tagsRowManager.add(newId)
- if (newTagData != null) {
- Toast
- .makeText(
- requireContext(),
- "Added tag '${newTagData.name}' to marker",
- Toast.LENGTH_SHORT,
- ).show()
- }
- }
-
- else -> {
- throw IllegalArgumentException("Unknown action id $sourceId")
- }
- }
- }
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- val markerDetailsDest = requireArguments().getDestination()
- viewModel.init(serverViewModel.requireServer(), markerDetailsDest.markerId)
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- setupDetailsOverviewRowPresenter()
- adapter = mAdapter
-
- onItemViewClickedListener =
- ClassOnItemViewClickedListener(NavigationOnItemViewClickedListener(serverViewModel.navigationManager))
- .addListenerForClass(StashAction::class.java) { item ->
- actionClickListener.onClicked(item)
- }.addListenerForClass(Action::class.java) { _ ->
- // no-op, detailsPresenter.onActionClickedListener will handle
- }
-
- viewModel.item.observe(viewLifecycleOwner) { marker ->
- if (marker == null) {
- Toast.makeText(requireContext(), "Marker not found", Toast.LENGTH_LONG).show()
- serverViewModel.navigationManager.goBack()
- return@observe
- }
- val sceneId = marker.scene.videoSceneData.id
-
- initializeBackground(marker.screenshot)
- setupDetailsOverviewRow(marker)
-
- primaryTagRowManager.setItems(listOf(marker.primary_tag.tagData))
- tagsRowManager.setItems(marker.tags.map { it.tagData })
-
- detailsPresenter.onActionClickedListener =
- OnActionClickedListener { _ ->
- serverViewModel.navigationManager.navigate(
- Destination.Playback(
- sceneId,
- (viewModel.seconds.value!! * 1000).toLong(),
- PlaybackMode.Choose,
- ),
- )
- }
-
- sceneActionsAdapter.set(1, StashAction.ADD_TAG)
- sceneActionsAdapter.set(2, StashAction.SHIFT_MARKERS)
-
- if (readOnlyModeDisabled()) {
- mAdapter.set(
- ACTIONS_POS,
- ListRow(
- HeaderItem(getString(R.string.stashapp_actions_name)),
- sceneActionsAdapter,
- ),
- )
- }
- }
- }
-
- private fun setupDetailsOverviewRowPresenter() {
- detailsPresenter =
- FullWidthDetailsOverviewRowPresenter(
- object : AbstractDetailsDescriptionPresenter() {
- @SuppressLint("SetTextI18n")
- override fun onBindDescription(
- vh: ViewHolder,
- item: Any,
- ) {
- item as FullMarkerData
- val ratingBar = vh.view.findViewById(R.id.rating_bar)
- ratingBar.visibility = View.GONE
-
- vh.title.text =
- if (item.title.isNotNullOrBlank()) {
- item.title
- } else {
- item.primary_tag.tagData.name
- }
- val body = mutableListOf()
- if (item.end_seconds != null) {
- val endTime =
- item.end_seconds
- .toDuration(
- DurationUnit.SECONDS,
- )
- body.add(
- "${getString(R.string.stashapp_time_end)}: $endTime",
- )
- val duration = endTime - item.seconds.toDuration(DurationUnit.SECONDS)
- body.add(
- "${getString(R.string.stashapp_duration)}: $duration",
- )
- }
- if (PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(
- getString(R.string.pref_key_show_playback_debug_info),
- false,
- )
- ) {
- if (body.isNotEmpty()) {
- body.add("\n")
- }
- body.add("${getString(R.string.id)}: ${item.id}")
- }
- val createdAt =
- getString(R.string.stashapp_created_at) + ": " +
- parseTimeToString(
- item.created_at,
- )
- val updatedAt =
- getString(R.string.stashapp_updated_at) + ": " +
- parseTimeToString(
- item.updated_at,
- )
- body.add(createdAt)
- body.add(updatedAt)
- vh.body.text = body.joinToString("\n")
- }
- },
- )
- // Set detail background.
- detailsPresenter.backgroundColor =
- ContextCompat.getColor(requireActivity(), R.color.default_card_background)
-
- val presenterSelector =
- ClassPresenterSelector()
- .addClassPresenter(ListRow::class.java, ListRowPresenter())
- .addClassPresenter(DetailsOverviewRow::class.java, detailsPresenter)
- mAdapter.presenterSelector = presenterSelector
- }
-
- private fun setupDetailsOverviewRow(marker: FullMarkerData) {
- val row = DetailsOverviewRow(marker)
-
- val screenshotUrl = marker.screenshot
- if (screenshotUrl.isNotNullOrBlank()) {
- row.item = marker
- val width =
- convertDpToPixel(
- requireActivity(),
- ScenePresenter.CARD_WIDTH,
- )
- val height =
- convertDpToPixel(
- requireActivity(),
- ScenePresenter.CARD_HEIGHT,
- )
- StashGlide
- .with(requireActivity(), screenshotUrl)
- .optionalCenterCrop()
- .error(StashPresenter.glideError(requireContext()))
- .into>(
- object : CustomTarget(width, height) {
- override fun onResourceReady(
- resource: Drawable,
- transition: Transition?,
- ) {
- row.imageDrawable = resource
- mAdapter.notifyArrayItemRangeChanged(0, mAdapter.size())
- }
-
- override fun onLoadCleared(placeholder: Drawable?) {
- row.imageDrawable = null
- }
- },
- )
- }
-
- viewModel.seconds.observe(viewLifecycleOwner) { newSeconds ->
- Log.v(TAG, "Marker newSeconds=$newSeconds")
- val playActionsAdapter = ArrayObjectAdapter()
- playActionsAdapter.add(
- Action(
- 1L,
- resources.getString(R.string.play_scene),
- durationToString(newSeconds),
- ),
- )
- row.actionsAdapter = playActionsAdapter
- }
- if (!viewModel.seconds.isInitialized) {
- viewModel.seconds.value = marker.seconds
- }
-
- mAdapter.set(1, row)
- }
-
- private fun initializeBackground(screenshotUrl: String?) {
- if (mDetailsBackground.coverBitmap == null) {
- mDetailsBackground.enableParallax()
-
- if (screenshotUrl.isNotNullOrBlank()) {
- StashGlide
- .withBitmap(requireActivity(), screenshotUrl)
- .optionalCenterCrop()
- .error(R.drawable.baseline_camera_indoor_48)
- .into>(
- object : CustomTarget() {
- override fun onResourceReady(
- bitmap: Bitmap,
- transition: Transition?,
- ) {
- mDetailsBackground.coverBitmap = bitmap
- mAdapter.notifyArrayItemRangeChanged(0, mAdapter.size())
- }
-
- override fun onLoadCleared(placeholder: Drawable?) {
- mDetailsBackground.coverBitmap = null
- }
- },
- )
- }
- }
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/PerformerDetailsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/PerformerDetailsFragment.kt
deleted file mode 100644
index b6b95b2ab..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/PerformerDetailsFragment.kt
+++ /dev/null
@@ -1,149 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Build
-import android.os.Bundle
-import android.view.View
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.api.fragment.PerformerData
-import com.github.damontecres.stashapp.api.type.CircumcisedEnum
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashGlide
-import com.github.damontecres.stashapp.util.ageInYears
-import com.github.damontecres.stashapp.util.showSetRatingToast
-import com.github.damontecres.stashapp.views.careerString
-import com.github.damontecres.stashapp.views.models.PerformerViewModel
-import com.github.damontecres.stashapp.views.parseTimeToString
-import kotlinx.coroutines.launch
-import kotlin.math.floor
-import kotlin.math.roundToInt
-
-/**
- * Details for a performer
- */
-class PerformerDetailsFragment : DetailsFragment() {
- private val viewModel: PerformerViewModel by viewModels(ownerProducer = { requireParentFragment() })
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- viewModel.item.observe(viewLifecycleOwner) { performer ->
- if (performer != null) {
- updateUi(performer)
- }
- }
- }
-
- private fun updateUi(perf: PerformerData) {
- updateFavorite(perf.favorite)
-
- favoriteButton.setOnClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val newPerformer = mutationEngine.updatePerformer(perf.id, !perf.favorite)
- if (newPerformer != null) {
- if (newPerformer.favorite) {
- Toast
- .makeText(
- requireContext(),
- getString(R.string.stashapp_performer_favorite),
- Toast.LENGTH_SHORT,
- ).show()
- }
- viewModel.update(newPerformer)
- }
- }
- }
-
- ratingBar.rating100 = perf.rating100 ?: 0
- ratingBar.setRatingCallback { newRating100 ->
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val newPerformer = mutationEngine.updatePerformer(perf.id, rating100 = newRating100)
- if (newPerformer != null) {
- showSetRatingToast(requireContext(), newRating100)
- viewModel.update(newPerformer)
- }
- }
- }
-
- if (perf.image_path != null) {
- StashGlide
- .with(requireContext(), perf.image_path)
- .optionalFitCenter()
- .error(StashPresenter.glideError(requireContext()))
- .into(imageView)
- }
-
- table.removeAllViews()
-
- if (perf.alias_list.isNotEmpty()) {
- addRow(
- R.string.stashapp_aliases,
- perf.alias_list.joinToString(", "),
- )
- }
- if (!perf.birthdate.isNullOrBlank()) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val age = perf.ageInYears.toString()
- addRow(R.string.stashapp_age, "$age (${perf.birthdate})")
- }
- }
- addRow(R.string.stashapp_death_date, perf.death_date)
- addRow(R.string.stashapp_country, perf.country)
- addRow(R.string.stashapp_ethnicity, perf.ethnicity)
- addRow(R.string.stashapp_hair_color, perf.hair_color)
- addRow(R.string.stashapp_eye_color, perf.eye_color)
- if (perf.height_cm != null) {
- val feet = floor(perf.height_cm / 30.48).toInt()
- val inches = (perf.height_cm / 2.54 - feet * 12).roundToInt()
- addRow(R.string.stashapp_height, "${perf.height_cm} cm ($feet'$inches\")")
- }
- if (perf.weight != null) {
- val pounds = (perf.weight * 2.2).roundToInt()
- addRow(R.string.stashapp_weight, "${perf.weight} kg ($pounds lbs)")
- }
- if (perf.penis_length != null) {
- val inches = kotlin.math.round(perf.penis_length / 2.54 * 100) / 100
- addRow(R.string.stashapp_penis_length, "${perf.penis_length} cm ($inches\")")
- }
- val circString =
- when (perf.circumcised) {
- CircumcisedEnum.CUT -> getString(R.string.stashapp_circumcised_types_CUT)
- CircumcisedEnum.UNCUT -> getString(R.string.stashapp_circumcised_types_UNCUT)
- CircumcisedEnum.UNKNOWN__, null -> null
- }
- addRow(R.string.stashapp_circumcised, circString)
-
- addRow(R.string.stashapp_tattoos, perf.tattoos)
- addRow(R.string.stashapp_piercings, perf.piercings)
- perf.careerString?.let {
- addRow(R.string.stashapp_career_length, it)
- }
- addRow(R.string.stashapp_created_at, parseTimeToString(perf.created_at))
- addRow(R.string.stashapp_updated_at, parseTimeToString(perf.updated_at))
-
- val customFields = perf.custom_fields as Map
- if (customFields.isNotEmpty()) {
- addRow("", null, true)
- addRow(R.string.stashapp_custom_fields_title, null, true)
- customFields.forEach {
- addRow(it.key, it.value?.toString())
- }
- }
- if (PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_show_playback_debug_info), false)
- ) {
- addRow(R.string.id, perf.id)
- }
- table.setColumnShrinkable(1, true)
- }
-
- companion object {
- private const val TAG = "PerformerFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/PerformerFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/PerformerFragment.kt
deleted file mode 100644
index 2356ab054..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/PerformerFragment.kt
+++ /dev/null
@@ -1,309 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.graphics.Color
-import android.os.Bundle
-import android.text.SpannableString
-import android.text.style.ForegroundColorSpan
-import android.text.style.RelativeSizeSpan
-import android.view.View
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import androidx.leanback.widget.ClassPresenterSelector
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.api.fragment.PerformerData
-import com.github.damontecres.stashapp.api.fragment.StudioData
-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.GroupFilterType
-import com.github.damontecres.stashapp.api.type.HierarchicalMultiCriterionInput
-import com.github.damontecres.stashapp.api.type.ImageFilterType
-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.SceneMarkerFilterType
-import com.github.damontecres.stashapp.api.type.StudioFilterType
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.data.StashFindFilter
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.presenters.PerformerPresenter
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.presenters.StudioPresenter
-import com.github.damontecres.stashapp.presenters.TagPresenter
-import com.github.damontecres.stashapp.suppliers.DataSupplierOverride
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.PageFilterKey
-import com.github.damontecres.stashapp.util.StashFragmentPagerAdapter
-import com.github.damontecres.stashapp.util.getUiTabs
-import com.github.damontecres.stashapp.views.models.PerformerViewModel
-
-/**
- * Main [TabbedFragment] for a performers which includes [PerformerDetailsFragment] and other tabs
- */
-class PerformerFragment : TabbedFragment(DataType.PERFORMER.name) {
- private val viewModel: PerformerViewModel by viewModels()
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- serverViewModel.currentServer.observe(viewLifecycleOwner) {
- viewModel.init(requireArguments())
- }
-
- serverViewModel
- .withLiveData(viewModel.item)
- .observe(viewLifecycleOwner) { (server, performer) ->
- if (performer == null) {
- Toast
- .makeText(
- requireContext(),
- "Performer '${viewModel.itemId}' not found",
- Toast.LENGTH_LONG,
- ).show()
- serverViewModel.navigationManager.goBack()
- return@observe
- }
- tabViewModel.title.value =
- SpannableString("${performer.name} ${performer.disambiguation}").apply {
- val start = performer.name.length + 1
- val end = length
- setSpan(RelativeSizeSpan(.60f), start, end, 0)
- setSpan(ForegroundColorSpan(Color.GRAY), start, end, 0)
- }
-
- val performers =
- Optional.present(
- MultiCriterionInput(
- value = Optional.present(listOf(performer.id)),
- modifier = CriterionModifier.INCLUDES_ALL,
- ),
- )
-
- if (!tabViewModel.tabs.isInitialized) {
- tabViewModel.tabs.value =
- listOf(
- StashFragmentPagerAdapter.PagerEntry(getString(R.string.stashapp_details)) {
- PerformerDetailsFragment()
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.SCENE) {
- StashGridControlsFragment(
- dataType = DataType.SCENE,
- findFilter =
- server.serverPreferences
- .getDefaultPageFilter(
- PageFilterKey.PERFORMER_SCENES,
- ).findFilter,
- objectFilter = SceneFilterType(performers = performers),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.GALLERY) {
- StashGridControlsFragment(
- dataType = DataType.GALLERY,
- findFilter =
- server.serverPreferences
- .getDefaultPageFilter(
- PageFilterKey.PERFORMER_GALLERIES,
- ).findFilter,
- objectFilter = GalleryFilterType(performers = performers),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.IMAGE) {
- StashGridControlsFragment(
- dataType = DataType.IMAGE,
- findFilter =
- server.serverPreferences
- .getDefaultPageFilter(
- PageFilterKey.PERFORMER_IMAGES,
- ).findFilter,
- objectFilter = ImageFilterType(performers = performers),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.GROUP) {
- StashGridControlsFragment(
- dataType = DataType.GROUP,
- findFilter =
- server.serverPreferences
- .getDefaultPageFilter(
- PageFilterKey.PERFORMER_GROUPS,
- ).findFilter,
- objectFilter = GroupFilterType(performers = performers),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.TAG) {
- val presenter =
- ClassPresenterSelector()
- .addClassPresenter(
- TagData::class.java,
- TagPresenter(performersWithTagLongClickCallback()),
- )
- val fragment =
- StashGridControlsFragment(
- FilterArgs(
- dataType = DataType.TAG,
- override = DataSupplierOverride.PerformerTags(performer.id),
- ),
- )
- fragment.presenterSelector = presenter
- fragment
- },
- StashFragmentPagerAdapter.PagerEntry(getString(R.string.stashapp_appears_with)) {
- val presenter =
- ClassPresenterSelector()
- .addClassPresenter(
- PerformerData::class.java,
- PerformerPresenter(
- performTogetherLongClickCallback(
- performer,
- ),
- ),
- )
- val fragment =
- StashGridControlsFragment(
- dataType = DataType.PERFORMER,
- findFilter =
- server.serverPreferences
- .getDefaultPageFilter(
- PageFilterKey.PERFORMER_APPEARS_WITH,
- ).findFilter,
- objectFilter =
- PerformerFilterType(
- performers =
- Optional.present(
- MultiCriterionInput(
- value = Optional.present(listOf(performer.id)),
- modifier = CriterionModifier.INCLUDES_ALL,
- ),
- ),
- ),
- )
- fragment.presenterSelector = presenter
- fragment
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.MARKER) {
- StashGridControlsFragment(
- dataType = DataType.MARKER,
- objectFilter = SceneMarkerFilterType(performers = performers),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.STUDIO) {
- val presenter =
- ClassPresenterSelector()
- .addClassPresenter(
- StudioData::class.java,
- StudioPresenter(
- scenesWithStudioLongClickCallback(
- performer,
- ),
- ),
- )
- val fragment =
- StashGridControlsFragment(
- dataType = DataType.STUDIO,
- findFilter = StashFindFilter(sortAndDirection = DataType.STUDIO.defaultSort),
- objectFilter =
- StudioFilterType(
- scenes_filter =
- Optional.present(
- SceneFilterType(
- performers = performers,
- ),
- ),
- ),
- )
- fragment.presenterSelector = presenter
- fragment
- },
- ).filter { it.title in getUiTabs(requireContext(), DataType.PERFORMER) }
- }
- }
- }
-
- private fun performTogetherLongClickCallback(performer: PerformerData) =
- StashPresenter
- .LongClickCallBack(
- StashPresenter.PopUpItem.DEFAULT to StashPresenter.PopUpAction { cardView, _ -> cardView.performClick() },
- ).addAction(StashPresenter.PopUpItem(1L, "View scenes together")) { _, item ->
- val performerIds = listOf(performer.id, item.id)
- val name = "${performer.name} & ${item.name}"
- val filter =
- FilterArgs(
- dataType = DataType.SCENE,
- name = name,
- objectFilter =
- SceneFilterType(
- performers =
- Optional.present(
- MultiCriterionInput(
- value = Optional.present(performerIds),
- modifier = CriterionModifier.INCLUDES_ALL,
- ),
- ),
- ),
- )
- serverViewModel.navigationManager.navigate(Destination.Filter(filter))
- }
-
- private fun performersWithTagLongClickCallback() =
- StashPresenter
- .LongClickCallBack(
- StashPresenter.PopUpItem.DEFAULT to StashPresenter.PopUpAction { cardView, _ -> cardView.performClick() },
- ).addAction(
- StashPresenter.PopUpItem(
- 1,
- "View performers with this tag",
- ),
- ) { _, item ->
- val name = "Performers with ${item.name}"
- val filter =
- FilterArgs(
- dataType = DataType.PERFORMER,
- name = name,
- objectFilter =
- PerformerFilterType(
- tags =
- Optional.present(
- HierarchicalMultiCriterionInput(
- value = Optional.present(listOf(item.id)),
- modifier = CriterionModifier.INCLUDES_ALL,
- depth = Optional.absent(),
- ),
- ),
- ),
- )
- serverViewModel.navigationManager.navigate(Destination.Filter(filter))
- }
-
- private fun scenesWithStudioLongClickCallback(performer: PerformerData) =
- StashPresenter
- .LongClickCallBack(
- StashPresenter.PopUpItem.DEFAULT to StashPresenter.PopUpAction { cardView, _ -> cardView.performClick() },
- ).addAction(StashPresenter.PopUpItem(1L, "View scenes with")) { _, item ->
- val name = "${performer.name} scenes for ${item.name}"
- val filter =
- FilterArgs(
- dataType = DataType.SCENE,
- name = name,
- objectFilter =
- SceneFilterType(
- performers =
- Optional.present(
- MultiCriterionInput(
- value = Optional.present(listOf(performer.id)),
- modifier = CriterionModifier.INCLUDES,
- ),
- ),
- studios =
- Optional.present(
- HierarchicalMultiCriterionInput(
- value = Optional.present(listOf(item.id)),
- modifier = CriterionModifier.INCLUDES,
- ),
- ),
- ),
- )
- serverViewModel.navigationManager.navigate(Destination.Filter(filter))
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/PinFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/PinFragment.kt
deleted file mode 100644
index 4fc0bb259..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/PinFragment.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import android.view.inputmethod.InputMethodManager
-import android.widget.Button
-import android.widget.EditText
-import android.widget.Toast
-import androidx.core.content.ContextCompat.getSystemService
-import androidx.core.widget.doAfterTextChanged
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-
-class PinFragment : Fragment(R.layout.pin_dialog) {
- private val serverViewModel: ServerViewModel by activityViewModels()
-
- private lateinit var pinEditText: EditText
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val pinCode =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getString("pinCode", "")
- if (pinCode.isNullOrBlank()) {
- startMain()
- }
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- pinEditText = view.findViewById(R.id.pin_edit_text)
-
- val pinCode =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getString(getString(R.string.pref_key_pin_code), "")
-
- val submit = view.findViewById(R.id.pin_submit)
- submit.setOnClickListener {
- val enteredPin = pinEditText.text.toString()
- if (enteredPin == pinCode) {
- startMain()
- } else {
- Toast.makeText(requireContext(), "Wrong PIN", Toast.LENGTH_SHORT).show()
- }
- }
-
- val autoSubmitPin =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_pin_code_auto), false)
- if (autoSubmitPin) {
- submit.visibility = View.INVISIBLE
- pinEditText.doAfterTextChanged {
- val enteredPin = it.toString()
- if (enteredPin == pinCode) {
- startMain()
- }
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- pinEditText.requestFocus()
- val imm = getSystemService(requireContext(), InputMethodManager::class.java)!!
- imm.showSoftInput(pinEditText, InputMethodManager.SHOW_IMPLICIT)
- }
-
- override fun onStop() {
- super.onStop()
- val imm = getSystemService(requireContext(), InputMethodManager::class.java)!!
- imm.hideSoftInputFromWindow(pinEditText.windowToken, 0)
- }
-
- private fun startMain() {
- serverViewModel.navigationManager.clearPinFragment()
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/PinFragmentCompose.kt b/app/src/main/java/com/github/damontecres/stashapp/PinFragmentCompose.kt
deleted file mode 100644
index a07ab8762..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/PinFragmentCompose.kt
+++ /dev/null
@@ -1,88 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.util.Log
-import android.view.View
-import android.view.inputmethod.InputMethodManager
-import android.widget.Button
-import android.widget.EditText
-import android.widget.Toast
-import androidx.core.content.ContextCompat.getSystemService
-import androidx.core.widget.doAfterTextChanged
-import androidx.fragment.app.Fragment
-import androidx.preference.PreferenceManager
-
-class PinFragmentCompose : Fragment(R.layout.pin_dialog) {
- private lateinit var pinEditText: EditText
-
- var onCorrectPin: () -> Unit = {}
-
- override fun onCreate(savedInstanceState: Bundle?) {
- Log.i("RootPin", "onCreate")
- super.onCreate(savedInstanceState)
- val pinCode =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getString("pinCode", "")
- if (pinCode.isNullOrBlank()) {
- onCorrectPin.invoke()
- }
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- Log.i("RootPin", "onViewCreated")
- super.onViewCreated(view, savedInstanceState)
-
- pinEditText = view.findViewById(R.id.pin_edit_text)
- pinEditText.setText("")
-
- val pinCode =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getString(getString(R.string.pref_key_pin_code), "")
-
- val submit = view.findViewById(R.id.pin_submit)
- submit.setOnClickListener {
- val enteredPin = pinEditText.text.toString()
- if (enteredPin == pinCode) {
- onCorrectPin.invoke()
- } else {
- Toast.makeText(requireContext(), "Wrong PIN", Toast.LENGTH_SHORT).show()
- }
- }
-
- val autoSubmitPin =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_pin_code_auto), false)
- if (autoSubmitPin) {
- submit.visibility = View.INVISIBLE
- pinEditText.doAfterTextChanged {
- val enteredPin = it.toString()
- if (enteredPin == pinCode) {
- onCorrectPin.invoke()
- }
- }
- }
- }
-
- override fun onResume() {
- Log.i("RootPin", "onResume")
- super.onResume()
- requireView().visibility = View.VISIBLE
- pinEditText.requestFocus()
- val imm = getSystemService(requireContext(), InputMethodManager::class.java)!!
- imm.showSoftInput(pinEditText, InputMethodManager.SHOW_IMPLICIT)
- }
-
- override fun onStop() {
- Log.i("RootPin", "onStop")
- super.onStop()
- val imm = getSystemService(requireContext(), InputMethodManager::class.java)!!
- imm.hideSoftInputFromWindow(pinEditText.windowToken, 0)
- pinEditText.setText("")
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/RootActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/RootActivity.kt
deleted file mode 100644
index 71b6dcedf..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/RootActivity.kt
+++ /dev/null
@@ -1,433 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.os.Bundle
-import android.util.Log
-import android.view.KeyEvent
-import android.view.View
-import android.view.WindowManager
-import android.widget.ImageView
-import android.widget.Toast
-import androidx.activity.viewModels
-import androidx.annotation.OptIn
-import androidx.appcompat.app.AppCompatActivity
-import androidx.appcompat.app.AppCompatDelegate
-import androidx.appcompat.app.ViewPumpAppCompatDelegate
-import androidx.core.view.WindowCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.WindowInsetsControllerCompat
-import androidx.core.widget.ContentLoadingProgressBar
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.FragmentManager
-import androidx.media3.common.util.UnstableApi
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.navigation.NavigationListener
-import com.github.damontecres.stashapp.navigation.NavigationManager
-import com.github.damontecres.stashapp.navigation.NavigationManagerCompose
-import com.github.damontecres.stashapp.navigation.NavigationManagerLeanback
-import com.github.damontecres.stashapp.util.KeyEventDispatcher
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.animateToInvisible
-import com.github.damontecres.stashapp.util.composeEnabled
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.stashapp.util.maybeGetDestination
-import com.github.damontecres.stashapp.util.putDestination
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import dev.b3nedikt.restring.Restring
-import kotlin.properties.Delegates
-
-/**
- * The only activity in the app
- */
-class RootActivity :
- AppCompatActivity(),
- NavigationListener {
- private val serverViewModel by viewModels()
- private var useCompose by Delegates.notNull()
- private lateinit var navigationManager: NavigationManager
- private var currentFragment: Fragment? = null
- lateinit var rootFragmentView: View
-
- private var hasCheckedForUpdate = false
- private lateinit var loadingView: ContentLoadingProgressBar
- private lateinit var bgLogo: ImageView
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- useCompose = composeEnabled(this)
- navigationManager =
- if (useCompose) {
- NavigationManagerCompose(this, serverViewModel)
- } else {
- NavigationManagerLeanback(this)
- }
- navigationManager.addListener(this)
- StashApplication.navigationManager = navigationManager
- serverViewModel.navigationManager = navigationManager
-
- if (useCompose) {
- setContentView(R.layout.activity_root_compose)
- } else {
- setContentView(R.layout.activity_root)
- }
- rootFragmentView = findViewById(R.id.root_fragment)
-
- setUpLifeCycleListeners()
- Log.v(
- TAG,
- "onCreate: savedInstanceState==null:${savedInstanceState == null}, currentFragment==null:${currentFragment == null}",
- )
- window.setFlags(
- WindowManager.LayoutParams.FLAG_SECURE,
- WindowManager.LayoutParams.FLAG_SECURE,
- )
-
- val appHasPin = appHasPin()
-
- if (savedInstanceState != null) {
- navigationManager.previousDestination = savedInstanceState.maybeGetDestination()
- Log.d(TAG, "Restoring destination: ${navigationManager.previousDestination}")
- }
-
- loadingView = findViewById(R.id.loading_progress_bar)
- bgLogo = findViewById(R.id.background_logo)
-
- val currentServer = StashServer.findConfiguredStashServer(this)
- if (currentServer != null) {
- Log.i(TAG, "Server configured")
- StashServer.setCurrentStashServer(this, currentServer)
- serverViewModel.init(this, currentServer, useCompose)
-
- serverViewModel.serverConnection.observe(this) { result ->
- loadingView.hide()
- when (result) {
- is ServerViewModel.ServerConnection.Failure -> {
- Log.w(TAG, "Exception connecting to server", result.exception)
- Toast
- .makeText(
- this,
- "Error connecting to ${result.server.url}",
- Toast.LENGTH_LONG,
- ).show()
- if (!appHasPin) {
- navigationManager.navigate(Destination.ManageServers(true))
- }
- }
-
- ServerViewModel.ServerConnection.NotConfigured -> {
- Log.i(TAG, "No servers configured, starting setup")
- navigationManager.navigate(Destination.Setup)
- }
-
- ServerViewModel.ServerConnection.Pending -> {}
-
- ServerViewModel.ServerConnection.Success -> {}
- }
- }
-
- serverViewModel.currentServer.observe(this) { server ->
- if (server != null) {
- if (savedInstanceState == null) {
- if (!appHasPin) {
- serverViewModel.currentServer.removeObservers(this@RootActivity)
- if (useCompose) {
- // Workaround because compose goToMain is special
- navigationManager.navigate(Destination.Main)
- } else {
- navigationManager.goToMain()
- }
- }
- }
- }
- }
-
- serverViewModel.destination.observe(this) { destination ->
- val windowInsetsController =
- WindowCompat.getInsetsController(window, window.decorView)
- if (destination is Destination.Playback ||
- destination is Destination.Playlist ||
- destination is Destination.Slideshow ||
- destination is Destination.UpdateMarker
- ) {
- Log.v(TAG, "Hiding system bars for $destination")
- windowInsetsController.systemBarsBehavior =
- WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
- windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
- } else {
- Log.v(TAG, "Showing system bars for $destination")
- windowInsetsController.systemBarsBehavior =
- WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
- windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
- }
- }
- } else {
- loadingView.hide()
- Log.i(TAG, "No server, starting setup")
- // No server configured
- navigationManager.navigate(Destination.Setup)
- }
- }
-
- override fun onResume() {
- super.onResume()
- Log.v(TAG, "onResume")
- hasCheckedForUpdate = false
- if (appHasPin()) {
- navigationManager.navigate(Destination.Pin)
- } else {
- loadingView.hide()
- bgLogo.animateToInvisible(View.GONE)
- serverViewModel.updateServerPreferences()
- }
- }
-
- override fun onPause() {
- Log.v(TAG, "onPause")
- if (appHasPin()) {
- navigationManager.navigate(Destination.Pin)
- }
- super.onPause()
- }
-
- @OptIn(UnstableApi::class)
- override fun onStop() {
- Log.v(TAG, "onStop")
- StashExoPlayer.releasePlayer()
- super.onStop()
- }
-
- override fun onNavigate(
- previousDestination: Destination?,
- nextDestination: Destination,
- fragment: Fragment?,
- ) {
- serverViewModel.setCurrentDestination(nextDestination)
- loadingView.hide()
- bgLogo.animateToInvisible(View.GONE)
- Log.v(
- TAG,
- "onNavigate: $previousDestination=>$nextDestination",
- )
- currentFragment = fragment
- if (!useCompose && nextDestination == Destination.Main && !hasCheckedForUpdate) {
- serverViewModel.maybeShowUpdate(this)
- hasCheckedForUpdate = true
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- Log.v(TAG, "onSaveInstanceState")
- super.onSaveInstanceState(outState)
- val dest = navigationManager.previousDestination
- if (dest != null) {
- outState.putDestination(dest)
- } else {
- outState.putParcelable(NavigationManager.DESTINATION_ARG, null)
- }
- }
-
- private fun appHasPin(): Boolean =
- PreferenceManager
- .getDefaultSharedPreferences(this)
- .getString("pinCode", "")
- .isNotNullOrBlank()
-
- // Delegate key events to the current fragment
-
- @SuppressLint("RestrictedApi")
- override fun dispatchKeyEvent(event: KeyEvent): Boolean {
- val fragment = currentFragment
- if (fragment is KeyEventDispatcher && fragment.isAdded) {
- return fragment.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
- }
- return super.dispatchKeyEvent(event)
- }
-
- override fun onKeyDown(
- keyCode: Int,
- event: KeyEvent?,
- ): Boolean {
- val fragment = currentFragment
- return if (fragment != null && fragment.isAdded && fragment is KeyEvent.Callback) {
- fragment.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
- } else {
- super.onKeyDown(keyCode, event)
- }
- }
-
- override fun onKeyUp(
- keyCode: Int,
- event: KeyEvent?,
- ): Boolean {
- val fragment = currentFragment
- return if (fragment != null && fragment.isAdded && fragment is KeyEvent.Callback) {
- fragment.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event)
- } else {
- super.onKeyUp(keyCode, event)
- }
- }
-
- override fun onKeyLongPress(
- keyCode: Int,
- event: KeyEvent?,
- ): Boolean {
- val fragment = currentFragment
- return if (fragment != null && fragment.isAdded && fragment is KeyEvent.Callback) {
- fragment.onKeyLongPress(keyCode, event) || super.onKeyLongPress(keyCode, event)
- } else {
- super.onKeyLongPress(keyCode, event)
- }
- }
-
- override fun onKeyMultiple(
- keyCode: Int,
- repeatCount: Int,
- event: KeyEvent?,
- ): Boolean {
- val fragment = currentFragment
- return if (fragment != null && fragment.isAdded && fragment is KeyEvent.Callback) {
- fragment.onKeyMultiple(keyCode, repeatCount, event) ||
- super.onKeyMultiple(
- keyCode,
- repeatCount,
- event,
- )
- } else {
- super.onKeyMultiple(keyCode, repeatCount, event)
- }
- }
-
- private val appCompatDelegate: AppCompatDelegate by lazy {
- ViewPumpAppCompatDelegate(
- baseDelegate = super.getDelegate(),
- baseContext = this,
- wrapContext = Restring::wrapContext,
- )
- }
-
- override fun getDelegate(): AppCompatDelegate = appCompatDelegate
-
- private fun setUpLifeCycleListeners() {
- if (DEBUG) {
- supportFragmentManager.registerFragmentLifecycleCallbacks(
- object :
- FragmentManager.FragmentLifecycleCallbacks() {
- override fun onFragmentAttached(
- fm: FragmentManager,
- f: Fragment,
- context: Context,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentAttached: f=${f::class.simpleName} (${f.tag})",
- )
- }
-
- override fun onFragmentCreated(
- fm: FragmentManager,
- f: Fragment,
- savedInstanceState: Bundle?,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentCreated: f=${f::class.simpleName} (${f.tag})",
- )
- }
-
- override fun onFragmentViewCreated(
- fm: FragmentManager,
- f: Fragment,
- v: View,
- savedInstanceState: Bundle?,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentViewCreated: f=${f::class.simpleName} (${f.tag})",
- )
- }
-
- override fun onFragmentStarted(
- fm: FragmentManager,
- f: Fragment,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentStarted: f=${f::class.simpleName} (${f.tag})",
- )
- }
-
- override fun onFragmentResumed(
- fm: FragmentManager,
- f: Fragment,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentResumed: f=${f::class.simpleName} (${f.tag})",
- )
- }
-
- override fun onFragmentPaused(
- fm: FragmentManager,
- f: Fragment,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentPaused: f=${f::class.simpleName} (${f.tag})",
- )
- }
-
- override fun onFragmentStopped(
- fm: FragmentManager,
- f: Fragment,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentStopped: f=${f::class.simpleName} (${f.tag})",
- )
- }
-
- override fun onFragmentSaveInstanceState(
- fm: FragmentManager,
- f: Fragment,
- outState: Bundle,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentSaveInstanceState: f=${f::class.simpleName} (${f.tag})",
- )
- }
-
- override fun onFragmentViewDestroyed(
- fm: FragmentManager,
- f: Fragment,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentViewDestroyed: f=${f::class.simpleName} (${f.tag})",
- )
- }
-
- override fun onFragmentDestroyed(
- fm: FragmentManager,
- f: Fragment,
- ) {
- Log.v(
- TAG_LIFECYCLE,
- "onFragmentDestroyed: f=${f::class.simpleName} (${f.tag})",
- )
- }
- },
- false,
- )
- }
- }
-
- companion object {
- private const val TAG = "RootActivity"
- private const val TAG_LIFECYCLE = "LifecycleTracking"
-
- private const val DEBUG = false
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsFragment.kt
deleted file mode 100644
index 65d18630e..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/SceneDetailsFragment.kt
+++ /dev/null
@@ -1,800 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.drawable.Drawable
-import android.os.Bundle
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Toast
-import androidx.core.content.ContextCompat
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.setFragmentResultListener
-import androidx.fragment.app.viewModels
-import androidx.leanback.app.DetailsSupportFragment
-import androidx.leanback.app.DetailsSupportFragmentBackgroundController
-import androidx.leanback.widget.Action
-import androidx.leanback.widget.ArrayObjectAdapter
-import androidx.leanback.widget.ClassPresenterSelector
-import androidx.leanback.widget.DetailsOverviewRow
-import androidx.leanback.widget.FullWidthDetailsOverviewRowPresenter
-import androidx.leanback.widget.HeaderItem
-import androidx.leanback.widget.ListRow
-import androidx.leanback.widget.ListRowPresenter
-import androidx.leanback.widget.OnActionClickedListener
-import androidx.leanback.widget.OnItemViewClickedListener
-import androidx.leanback.widget.Presenter
-import androidx.leanback.widget.Row
-import androidx.leanback.widget.RowPresenter
-import androidx.leanback.widget.SinglePresenterSelector
-import androidx.leanback.widget.SparseArrayObjectAdapter
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.bumptech.glide.request.target.CustomTarget
-import com.bumptech.glide.request.transition.Transition
-import com.github.damontecres.stashapp.actions.CreateMarkerAction
-import com.github.damontecres.stashapp.actions.StashAction
-import com.github.damontecres.stashapp.actions.StashActionClickedListener
-import com.github.damontecres.stashapp.api.fragment.FullSceneData
-import com.github.damontecres.stashapp.api.fragment.GroupData
-import com.github.damontecres.stashapp.api.fragment.MarkerData
-import com.github.damontecres.stashapp.api.fragment.PerformerData
-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.navigation.Destination
-import com.github.damontecres.stashapp.navigation.NavigationOnItemViewClickedListener
-import com.github.damontecres.stashapp.playback.PlaybackMode
-import com.github.damontecres.stashapp.playback.findPossibleTranscodeLabels
-import com.github.damontecres.stashapp.playback.streamChoiceFromLabel
-import com.github.damontecres.stashapp.presenters.ActionPresenter
-import com.github.damontecres.stashapp.presenters.CreateMarkerActionPresenter
-import com.github.damontecres.stashapp.presenters.GalleryPresenter
-import com.github.damontecres.stashapp.presenters.GroupPresenter
-import com.github.damontecres.stashapp.presenters.MarkerPresenter
-import com.github.damontecres.stashapp.presenters.OCounterPresenter
-import com.github.damontecres.stashapp.presenters.PerformerInScenePresenter
-import com.github.damontecres.stashapp.presenters.SceneDetailsPresenter
-import com.github.damontecres.stashapp.presenters.ScenePresenter
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.presenters.StashPresenter.PopUpItem
-import com.github.damontecres.stashapp.presenters.StudioPresenter
-import com.github.damontecres.stashapp.presenters.TagPresenter
-import com.github.damontecres.stashapp.util.Constants
-import com.github.damontecres.stashapp.util.ListRowManager
-import com.github.damontecres.stashapp.util.MutationEngine
-import com.github.damontecres.stashapp.util.QueryEngine
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashDiffCallback
-import com.github.damontecres.stashapp.util.StashGlide
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.configRowManager
-import com.github.damontecres.stashapp.util.createOCounterLongClickCallBack
-import com.github.damontecres.stashapp.util.createRemoveLongClickListener
-import com.github.damontecres.stashapp.util.getDataType
-import com.github.damontecres.stashapp.util.getDestination
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.stashapp.util.readOnlyModeDisabled
-import com.github.damontecres.stashapp.util.showSetRatingToast
-import com.github.damontecres.stashapp.views.ClassOnItemViewClickedListener
-import com.github.damontecres.stashapp.views.models.SceneViewModel
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import com.github.damontecres.stashapp.views.showSimpleListPopupWindow
-import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
-
-/**
- * Fragment to show a scene's details and actions
- */
-class SceneDetailsFragment : DetailsSupportFragment() {
- private val serverViewModel: ServerViewModel by activityViewModels()
- private val viewModel by viewModels()
-
- private lateinit var sceneId: String
- private var sceneData: FullSceneData? = null
-
- private lateinit var queryEngine: QueryEngine
- private lateinit var mutationEngine: MutationEngine
-
- private val mAdapter = SparseArrayObjectAdapter()
-
- private val studioRowManager =
- ListRowManager(
- DataType.STUDIO,
- ListRowManager.SparseArrayRowModifier(mAdapter, STUDIO_POS),
- ArrayObjectAdapter(),
- StashApplication.getApplication().getString(DataType.STUDIO.stringId),
- ) { studioIds ->
- val newStudioId =
- if (studioIds.isEmpty()) {
- null
- } else if (studioIds.size == 1) {
- studioIds.first()
- } else {
- studioIds.last()
- }
-
- val result = mutationEngine.setStudioOnScene(sceneId, newStudioId)
- val newStudio = result?.studio?.studioData
- if (newStudio != null) {
- listOf(newStudio)
- } else {
- listOf()
- }
- }
-
- private val performersRowManager =
- ListRowManager(
- DataType.PERFORMER,
- ListRowManager.SparseArrayRowModifier(mAdapter, PERFORMER_POS),
- ArrayObjectAdapter(),
- ) { performerIds ->
- val result = mutationEngine.setPerformersOnScene(sceneId, performerIds)
- result?.performers?.map { it.performerData }.orEmpty()
- }
-
- private val tagsRowManager =
- ListRowManager(
- DataType.TAG,
- ListRowManager.SparseArrayRowModifier(mAdapter, TAG_POS),
- ArrayObjectAdapter(),
- ) { tagIds ->
- val result = mutationEngine.setTagsOnScene(sceneId, tagIds)
- result?.tags?.map { it.tagData }.orEmpty()
- }
-
- private val groupsRowManager =
- ListRowManager(
- DataType.GROUP,
- ListRowManager.SparseArrayRowModifier(mAdapter, GROUP_POS),
- ArrayObjectAdapter(),
- ) { groupIds ->
- val result = mutationEngine.setGroupsOnScene(sceneData!!.id, groupIds)
- result?.groups?.map { it.group.groupData }.orEmpty()
- }
-
- private val markersRowManager =
- ListRowManager(
- DataType.MARKER,
- ListRowManager.SparseArrayRowModifier(mAdapter, MARKER_POS),
- ArrayObjectAdapter(),
- ) { markerIds ->
- val removed = sceneData!!.scene_markers.map { it.id }.toSet() - markerIds.toSet()
- removed.forEach { mutationEngine.deleteMarker(it) }
- sceneData =
- sceneData!!.copy(scene_markers = sceneData!!.scene_markers.filter { it.id !in removed })
- queryEngine.findMarkersInScene(sceneData!!.id)
- }
-
- private val galleriesAdapter = ArrayObjectAdapter(GalleryPresenter())
-
- private val sceneActionsAdapter: SparseArrayObjectAdapter by lazy {
- SparseArrayObjectAdapter(
- ClassPresenterSelector()
- .addClassPresenter(
- StashAction::class.java,
- ActionPresenter(),
- ).addClassPresenter(
- OCounter::class.java,
- OCounterPresenter(
- createOCounterLongClickCallBack(
- DataType.SCENE,
- sceneId,
- mutationEngine,
- viewLifecycleOwner.lifecycleScope,
- ) { newCount ->
- sceneActionsAdapter.set(O_COUNTER_POS, newCount)
- sceneData = sceneData!!.copy(o_counter = newCount.count)
- },
- ),
- ).addClassPresenter(
- CreateMarkerAction::class.java,
- CreateMarkerActionPresenter(),
- ),
- )
- }
-
- private var detailsOverviewRow: DetailsOverviewRow? = null
- private lateinit var mDetailsBackground: DetailsSupportFragmentBackgroundController
-
- private val playActionsAdapter = SparseArrayObjectAdapter()
-
- private var scenePresenter: SceneDetailsPresenter? = null
- private var detailsPresenter: FullWidthDetailsOverviewRowPresenter? = null
-
- @Deprecated("Deprecated in Java")
- override fun inflateTitle(
- inflater: LayoutInflater?,
- parent: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- sceneId = requireArguments().getDestination().id
- Log.d(TAG, "onCreate: sceneId=$sceneId")
-
- queryEngine = QueryEngine(StashServer.requireCurrentServer())
- mutationEngine = MutationEngine(StashServer.requireCurrentServer())
-
- mDetailsBackground = DetailsSupportFragmentBackgroundController(this)
- mDetailsBackground.enableParallax()
-
- setFragmentResultListener(Constants.POSITION_REQUEST_KEY) { _, bundle ->
- val position = bundle.getLong(Constants.POSITION_REQUEST_KEY)
- Log.v(TAG, "setFragmentResultListener: position=$position")
- viewModel.currentPosition.value = position
- if (serverViewModel.requireServer().serverPreferences.trackActivity &&
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_playback_track_activity), true)
- ) {
- viewModel.saveCurrentPosition()
- }
- }
-
- setFragmentResultListener(SceneDetailsFragment::class.simpleName!!) { _, bundle ->
- val sourceId = bundle.getLong(SearchForFragment.RESULT_ID_KEY)
- val dataType = bundle.getDataType()
- val newId = bundle.getString(SearchForFragment.RESULT_ITEM_ID_KEY)
- if (newId != null) {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- when (dataType) {
- DataType.GROUP -> {
- val newGroup = groupsRowManager.add(newId)
- if (newGroup != null) {
- Toast
- .makeText(
- requireContext(),
- "Added to '${newGroup.name}'",
- Toast.LENGTH_SHORT,
- ).show()
- }
- }
-
- DataType.PERFORMER -> {
- val newPerformer = performersRowManager.add(newId)
- if (newPerformer != null) {
- Toast
- .makeText(
- requireContext(),
- "Added performer '${newPerformer.name}' to scene",
- Toast.LENGTH_SHORT,
- ).show()
- }
- }
-
- DataType.STUDIO -> {
- val newStudio = studioRowManager.add(newId)
- if (newStudio != null) {
- Toast
- .makeText(
- requireContext(),
- "Set studio to '${newStudio.name}'",
- Toast.LENGTH_SHORT,
- ).show()
- }
- }
-
- DataType.TAG -> {
- if (sourceId == StashAction.ADD_TAG.id) {
- val newTag = tagsRowManager.add(newId)
- if (newTag != null) {
- Toast
- .makeText(
- requireContext(),
- "Added tag '${newTag.name}' to scene",
- Toast.LENGTH_SHORT,
- ).show()
- }
- } else if (sourceId == StashAction.CREATE_MARKER.id) {
- val position = viewModel.currentPosition.value ?: 0L
- Log.d(
- TAG,
- "Adding marker at $position with tagId=$newId to scene ${sceneData?.id}",
- )
- val newMarker =
- mutationEngine.createMarker(
- sceneId,
- position,
- newId,
- )!!
- markersRowManager.add(newMarker.id)
- Toast
- .makeText(
- requireContext(),
- "Created a new marker with primary tag '${newMarker.primary_tag.tagData.name}'",
- Toast.LENGTH_SHORT,
- ).show()
- }
- }
-
- DataType.SCENE, DataType.MARKER, DataType.IMAGE, DataType.GALLERY -> {
- throw IllegalArgumentException()
- }
- }
- }
- }
- }
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View? {
- val actionListener = SceneActionListener()
- onItemViewClickedListener =
- ClassOnItemViewClickedListener(NavigationOnItemViewClickedListener(serverViewModel.navigationManager))
- .addListenerForClass(StashAction::class.java, actionListener)
- .addListenerForClass(CreateMarkerAction::class.java) { _ ->
- actionListener.onClicked(StashAction.CREATE_MARKER)
- }.addListenerForClass(Action::class.java) { _ ->
- // no-op, detailsPresenter.onActionClickedListener will handle
- }.addListenerForClass(OCounter::class.java) { oCounter ->
- actionListener.incrementOCounter(oCounter)
- }
- return super.onCreateView(inflater, container, savedInstanceState)
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- setupDetailsOverviewRowPresenter()
-
- if (mAdapter.lookup(ACTIONS_POS) == null) {
- if (readOnlyModeDisabled()) {
- sceneActionsAdapter.set(ADD_TAG_POS, StashAction.ADD_TAG)
- sceneActionsAdapter.set(ADD_PERFORMER_POS, StashAction.ADD_PERFORMER)
- sceneActionsAdapter.set(SET_STUDIO_POS, StashAction.SET_STUDIO)
- sceneActionsAdapter.set(ADD_GROUP_POS, StashAction.ADD_GROUP)
- }
- sceneActionsAdapter.set(FORCE_TRANSCODE_POS, StashAction.FORCE_TRANSCODE)
- sceneActionsAdapter.set(FORCE_DIRECT_PLAY_POS, StashAction.FORCE_DIRECT_PLAY)
- mAdapter.set(
- ACTIONS_POS,
- ListRow(HeaderItem(getString(R.string.stashapp_actions_name)), sceneActionsAdapter),
- )
- }
-
- configRowManager({ viewLifecycleOwner.lifecycleScope }, tagsRowManager, ::TagPresenter)
- configRowManager({ viewLifecycleOwner.lifecycleScope }, studioRowManager, ::StudioPresenter)
- configRowManager({ viewLifecycleOwner.lifecycleScope }, groupsRowManager, ::GroupPresenter)
-
- markersRowManager.adapter.presenterSelector =
- SinglePresenterSelector(
- MarkerPresenter(
- createRemoveLongClickListener(
- { viewLifecycleOwner.lifecycleScope },
- markersRowManager,
- ).addAction(MARKER_DETAILS_POPUP) { _, item ->
- serverViewModel.navigationManager.navigate(
- Destination.MarkerDetails(item.id),
- )
- }.addAction(
- PopUpItem(MARKER_DETAILS_POPUP.id + 1, R.string.shift_seconds),
- { readOnlyModeDisabled() },
- { _, item ->
- StashApplication.navigationManager.navigate(
- Destination.UpdateMarker(item.id),
- )
- },
- ),
- ),
- )
-
- setupObservers()
- }
-
- override fun onResume() {
- super.onResume()
- viewModel.init(sceneId, true)
- }
-
- private fun setupObservers() {
- viewModel.scene.observe(viewLifecycleOwner) { sceneData ->
- if (sceneData == null) {
- Toast
- .makeText(requireContext(), "Scene $sceneId not found", Toast.LENGTH_LONG)
- .show()
- serverViewModel.navigationManager.goBack()
- return@observe
- }
- Log.v(TAG, "sceneData.id=${sceneData.id}")
-
- configRowManager(
- { viewLifecycleOwner.lifecycleScope },
- performersRowManager,
- ) { callback ->
- PerformerInScenePresenter(sceneData.date, callback)
- }
-
- setupDetailsOverviewRow(sceneData)
-
- adapter = mAdapter
-
- initializeBackground(sceneData)
-
- if (readOnlyModeDisabled()) {
- sceneActionsAdapter.set(
- O_COUNTER_POS,
- OCounter(
- sceneId,
- sceneData.o_counter ?: 0,
- ),
- )
- }
-
- if (sceneData.studio?.studioData != null) {
- studioRowManager.setItems(listOf(sceneData.studio.studioData))
- }
-
- this@SceneDetailsFragment.sceneData = sceneData
- }
-
- viewModel.tags.observe(viewLifecycleOwner, tagsRowManager::setItems)
-
- viewModel.markers.observe(viewLifecycleOwner, markersRowManager::setItems)
-
- viewModel.groups.observe(viewLifecycleOwner, groupsRowManager::setItems)
-
- viewModel.performers.observe(viewLifecycleOwner, performersRowManager::setItems)
-
- viewModel.galleries.observe(viewLifecycleOwner) { galleries ->
- if (galleries.isNotEmpty()) {
- if (mAdapter.lookup(GALLERY_POS) == null) {
- mAdapter.set(
- GALLERY_POS,
- ListRow(
- HeaderItem(getString(R.string.stashapp_galleries)),
- galleriesAdapter,
- ),
- )
- }
- galleriesAdapter.setItems(galleries, StashDiffCallback)
- } else {
- mAdapter.clear(GALLERY_POS)
- }
- }
-
- viewModel.suggestedScenes.observe(viewLifecycleOwner) { items ->
- if (items.isEmpty()) {
- mAdapter.clear(SIMILAR_POS)
- } else {
- val adapter =
- ArrayObjectAdapter(StashPresenter.defaultClassPresenterSelector())
- adapter.addAll(0, items)
- mAdapter.set(
- SIMILAR_POS,
- ListRow(
- HeaderItem(getString(R.string.more_like_this_scene)),
- adapter,
- ),
- )
- }
- }
-
- viewModel.currentPosition.observe(viewLifecycleOwner) { localPosition ->
- if (localPosition >= 0 && readOnlyModeDisabled()) {
- sceneActionsAdapter.set(
- CREATE_MARKER_POS,
- CreateMarkerAction(localPosition),
- )
- }
-
- setupPlayActionsAdapter(localPosition)
- }
- }
-
- private fun initializeBackground(sceneData: FullSceneData) {
- val screenshotUrl = sceneData.paths.screenshot
- if (mDetailsBackground.coverBitmap == null && screenshotUrl.isNotNullOrBlank()) {
- StashGlide
- .withBitmap(requireActivity(), screenshotUrl)
- .optionalCenterCrop()
- .error(R.drawable.baseline_camera_indoor_48)
- .into>(
- object : CustomTarget() {
- override fun onResourceReady(
- bitmap: Bitmap,
- transition: Transition?,
- ) {
- mDetailsBackground.coverBitmap = bitmap
- mAdapter.notifyArrayItemRangeChanged(0, mAdapter.size())
- }
-
- override fun onLoadCleared(placeholder: Drawable?) {
- mDetailsBackground.coverBitmap = null
- }
- },
- )
- }
- }
-
- private fun setupDetailsOverviewRow(sceneData: FullSceneData) {
- val row =
- if (detailsOverviewRow != null) {
- detailsOverviewRow!!
- } else {
- val newRow = DetailsOverviewRow(sceneData)
- newRow.actionsAdapter = playActionsAdapter
- mAdapter.set(DETAILS_POS, newRow)
- newRow
- }
-
- val screenshotUrl = sceneData.paths.screenshot
- if (detailsOverviewRow == null && !screenshotUrl.isNullOrBlank()) {
- row.item = sceneData
- val width = convertDpToPixel(requireActivity(), DETAIL_THUMB_WIDTH)
- val height = convertDpToPixel(requireActivity(), DETAIL_THUMB_HEIGHT)
- StashGlide
- .with(requireActivity(), screenshotUrl)
- .optionalCenterCrop()
- .error(StashPresenter.glideError(requireContext()))
- .into>(
- object : CustomTarget(width, height) {
- override fun onResourceReady(
- resource: Drawable,
- transition: Transition?,
- ) {
- row.imageDrawable = resource
- mAdapter.notifyArrayItemRangeChanged(0, mAdapter.size())
- }
-
- override fun onLoadCleared(placeholder: Drawable?) {
- row.imageDrawable = null
- }
- },
- )
- }
- detailsOverviewRow = row
- }
-
- private fun setupDetailsOverviewRowPresenter() {
- scenePresenter =
- SceneDetailsPresenter { rating100: Int ->
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler(
- Toast.makeText(
- requireContext(),
- "Failed to set rating",
- Toast.LENGTH_SHORT,
- ),
- ),
- ) {
- mutationEngine.setRating(
- sceneId,
- rating100,
- )
- showSetRatingToast(requireContext(), rating100)
- }
- }
- detailsPresenter =
- FullWidthDetailsOverviewRowPresenter(scenePresenter).apply {
- // Set detail background.
- backgroundColor =
- ContextCompat.getColor(requireActivity(), R.color.default_card_background)
- isParticipatingEntranceTransition = false
-
- onActionClickedListener =
- OnActionClickedListener { action ->
- if (this@SceneDetailsFragment.sceneData != null) {
- if (action.id in
- longArrayOf(
- ACTION_PLAY_SCENE,
- ACTION_RESUME_SCENE,
- ACTION_TRANSCODE_RESUME_SCENE,
- ACTION_DIRECT_PLAY_RESUME_SCENE,
- )
- ) {
- val position =
- if (action.id == ACTION_RESUME_SCENE ||
- action.id == ACTION_TRANSCODE_RESUME_SCENE ||
- action.id == ACTION_DIRECT_PLAY_RESUME_SCENE
- ) {
- viewModel.currentPosition.value ?: 0L
- } else {
- 0L
- }
- val mode =
- when (action.id) {
- ACTION_DIRECT_PLAY_RESUME_SCENE -> PlaybackMode.ForcedDirectPlay
- else -> PlaybackMode.Choose
- }
-
- val playbackDest = Destination.Playback(sceneId, position, mode)
- serverViewModel.navigationManager.navigate(playbackDest)
- } else {
- throw IllegalArgumentException("Action $action (id=${action.id} is not supported!")
- }
- }
- }
- }
- val presenterSelector =
- ClassPresenterSelector()
- .addClassPresenter(ListRow::class.java, ListRowPresenter())
- .addClassPresenter(DetailsOverviewRow::class.java, detailsPresenter)
- mAdapter.presenterSelector = presenterSelector
- }
-
- private fun convertDpToPixel(
- context: Context,
- dp: Int,
- ): Int {
- val density = context.applicationContext.resources.displayMetrics.density
- return (dp.toFloat() * density).roundToInt()
- }
-
- private inner class SceneActionListener :
- StashActionClickedListener,
- OnItemViewClickedListener {
- override fun onClicked(action: StashAction) {
- if (action in StashAction.SEARCH_FOR_ACTIONS) {
- val dataType =
- when (action) {
- StashAction.ADD_TAG -> DataType.TAG
- StashAction.ADD_PERFORMER -> DataType.PERFORMER
- StashAction.ADD_GROUP -> DataType.GROUP
- StashAction.SET_STUDIO -> DataType.STUDIO
- StashAction.CREATE_MARKER -> DataType.TAG
- else -> throw RuntimeException("Unsupported search for type $action")
- }
- val title =
- if (action == StashAction.CREATE_MARKER) "for primary tag for scene marker" else null
- serverViewModel.navigationManager.navigate(
- Destination.SearchFor(
- SceneDetailsFragment::class.simpleName!!,
- action.id,
- dataType,
- title,
- ),
- )
- } else if (action == StashAction.FORCE_DIRECT_PLAY) {
- detailsPresenter!!.onActionClickedListener.onActionClicked(
- Action(
- ACTION_DIRECT_PLAY_RESUME_SCENE,
- ),
- )
- } else if (action == StashAction.FORCE_TRANSCODE) {
- Toast
- .makeText(
- requireContext(),
- "A bug occurred! Cannot play scene.",
- Toast.LENGTH_SHORT,
- ).show()
- Log.w(TAG, "This should not occur\n" + Log.getStackTraceString(Exception()))
- }
- }
-
- override fun incrementOCounter(counter: OCounter) {
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler(
- Toast.makeText(
- requireContext(),
- getString(R.string.failed_update),
- Toast.LENGTH_SHORT,
- ),
- ),
- ) {
- val newCounter = mutationEngine.incrementOCounter(counter.id)
- sceneData = sceneData!!.copy(o_counter = newCounter.count)
- sceneActionsAdapter.set(O_COUNTER_POS, newCounter)
- }
- }
-
- override fun onItemClicked(
- itemViewHolder: Presenter.ViewHolder,
- item: Any,
- rowViewHolder: RowPresenter.ViewHolder?,
- row: Row?,
- ) {
- if (item is StashAction) {
- if (item == StashAction.FORCE_TRANSCODE) {
- val format =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getString("stream_choice", "HLS")!!
- val options =
- findPossibleTranscodeLabels(
- requireContext(),
- streamChoiceFromLabel(format),
- viewModel.scene.value?.sceneStreams,
- )
- Log.d(TAG, "options=$options")
- showSimpleListPopupWindow(
- anchorView = itemViewHolder.view,
- options = options.map { it.readableName },
- ) { index ->
- val label = options[index].label
- Log.v(TAG, "Transcode as $label")
- val position = viewModel.currentPosition.value ?: 0L
- val playbackDest =
- Destination.Playback(
- sceneId,
- position,
- PlaybackMode.ForcedTranscode(label),
- )
- serverViewModel.navigationManager.navigate(playbackDest)
- }
- } else {
- onClicked(item)
- }
- }
- }
- }
-
- private fun setupPlayActionsAdapter(position: Long) {
- Log.v(TAG, "setupPlayActionsAdapter: position=$position")
- if (position >= 0L) {
- val serverPreferences = serverViewModel.requireServer().serverPreferences
- if (position <= 0L || serverPreferences.alwaysStartFromBeginning) {
- playActionsAdapter.set(
- 0,
- Action(ACTION_PLAY_SCENE, resources.getString(R.string.play_scene)),
- )
- playActionsAdapter.clear(1)
- } else {
- playActionsAdapter.set(0, Action(ACTION_RESUME_SCENE, getString(R.string.resume)))
- // Force focus to move to Resume
- playActionsAdapter.clear(1)
- playActionsAdapter.set(1, Action(ACTION_PLAY_SCENE, getString(R.string.restart)))
- }
- } else {
- playActionsAdapter.clear()
- }
- }
-
- override fun onDestroyView() {
- scenePresenter?.ratingCallback = null
- detailsPresenter?.onActionClickedListener = null
- detailsPresenter = null
- onItemViewClickedListener = null
- scenePresenter = null
- super.onDestroyView()
- }
-
- companion object {
- private const val TAG = "SceneDetailsFragment"
-
- private const val ACTION_PLAY_SCENE = 1L
- private const val ACTION_RESUME_SCENE = 2L
- private const val ACTION_TRANSCODE_RESUME_SCENE = 3L
- private const val ACTION_DIRECT_PLAY_RESUME_SCENE = 4L
-
- private const val DETAIL_THUMB_WIDTH = ScenePresenter.CARD_WIDTH
- private const val DETAIL_THUMB_HEIGHT = ScenePresenter.CARD_HEIGHT
-
- // Row order
- private const val DETAILS_POS = 1
- private const val MARKER_POS = DETAILS_POS + 1
- private const val GROUP_POS = MARKER_POS + 1
- private const val STUDIO_POS = GROUP_POS + 1
- private const val PERFORMER_POS = STUDIO_POS + 1
- private const val TAG_POS = PERFORMER_POS + 1
- private const val GALLERY_POS = TAG_POS + 1
- private const val ACTIONS_POS = GALLERY_POS + 1
- private const val SIMILAR_POS = ACTIONS_POS + 1
-
- // Actions row order
- private const val O_COUNTER_POS = 1
- private const val CREATE_MARKER_POS = O_COUNTER_POS + 1
- private const val ADD_TAG_POS = CREATE_MARKER_POS + 1
- private const val ADD_PERFORMER_POS = ADD_TAG_POS + 1
- private const val ADD_GROUP_POS = ADD_PERFORMER_POS + 1
- private const val SET_STUDIO_POS = ADD_GROUP_POS + 1
- private const val FORCE_TRANSCODE_POS = SET_STUDIO_POS + 1
- private const val FORCE_DIRECT_PLAY_POS = FORCE_TRANSCODE_POS + 1
-
- private val MARKER_DETAILS_POPUP =
- StashPresenter.PopUpItem(
- 177,
- StashApplication.getApplication().getString(R.string.stashapp_details),
- )
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/SearchForFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/SearchForFragment.kt
deleted file mode 100644
index 61bfff1d3..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/SearchForFragment.kt
+++ /dev/null
@@ -1,520 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.text.TextUtils
-import android.util.Log
-import android.widget.Toast
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.core.os.bundleOf
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.setFragmentResult
-import androidx.leanback.app.SearchSupportFragment
-import androidx.leanback.widget.ArrayObjectAdapter
-import androidx.leanback.widget.HeaderItem
-import androidx.leanback.widget.ListRow
-import androidx.leanback.widget.ListRowPresenter
-import androidx.leanback.widget.ObjectAdapter
-import androidx.leanback.widget.Presenter
-import androidx.leanback.widget.Row
-import androidx.leanback.widget.RowPresenter
-import androidx.leanback.widget.SparseArrayObjectAdapter
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.actions.StashAction
-import com.github.damontecres.stashapp.api.fragment.GroupData
-import com.github.damontecres.stashapp.api.fragment.PerformerData
-import com.github.damontecres.stashapp.api.fragment.StashData
-import com.github.damontecres.stashapp.api.fragment.StudioData
-import com.github.damontecres.stashapp.api.fragment.TagData
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.FindFilterType
-import com.github.damontecres.stashapp.api.type.GalleryFilterType
-import com.github.damontecres.stashapp.api.type.GroupCreateInput
-import com.github.damontecres.stashapp.api.type.PerformerCreateInput
-import com.github.damontecres.stashapp.api.type.SortDirectionEnum
-import com.github.damontecres.stashapp.api.type.StringCriterionInput
-import com.github.damontecres.stashapp.api.type.TagCreateInput
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.data.room.RecentSearchItem
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.presenters.StashImageCardView
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.util.MutationEngine
-import com.github.damontecres.stashapp.util.QueryEngine
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.defaultCardHeight
-import com.github.damontecres.stashapp.util.defaultCardWidth
-import com.github.damontecres.stashapp.util.getDestination
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.stashapp.util.putDataType
-import com.github.damontecres.stashapp.util.readOnlyModeDisabled
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import kotlinx.coroutines.CoroutineExceptionHandler
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlin.coroutines.CoroutineContext
-import kotlin.properties.Delegates
-
-/**
- * Search for an item of a specific [DataType]
- *
- * Will load recently used items via [com.github.damontecres.stashapp.data.room.RecentSearchItemsDao]
- */
-class SearchForFragment :
- SearchSupportFragment(),
- SearchSupportFragment.SearchResultProvider {
- private val serverViewModel by activityViewModels()
-
- private var taskJob: Job? = null
- private var query: String? = null
-
- private val adapter = SparseArrayObjectAdapter(ListRowPresenter())
- private val searchResultsAdapter = ArrayObjectAdapter()
- private var perPage by Delegates.notNull()
-
- private val exceptionHandler =
- CoroutineExceptionHandler { _: CoroutineContext, ex: Throwable ->
- Log.e(TAG, "Exception in search", ex)
- Toast
- .makeText(requireContext(), "Search failed: ${ex.message}", Toast.LENGTH_LONG)
- .show()
- }
-
- private val server = StashServer.requireCurrentServer()
- private val queryEngine = QueryEngine(server)
-
- private lateinit var searchFor: Destination.SearchFor
- private lateinit var dataType: DataType
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- searchFor = requireArguments().getDestination()
- dataType = searchFor.dataType
- perPage =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getInt("maxSearchResults", 25)
- title = searchFor.title ?: getString(dataType.pluralStringId)
-
- searchResultsAdapter.presenterSelector =
- StashPresenter
- .defaultClassPresenterSelector()
- .addClassPresenter(StashAction::class.java, CreateNewPresenter(dataType))
- adapter.set(
- RESULTS_POS,
- ListRow(HeaderItem(getString(R.string.waiting_for_query)), ArrayObjectAdapter()),
- )
-
- setSearchResultProvider(this)
- setOnItemViewClickedListener {
- itemViewHolder: Presenter.ViewHolder,
- item: Any,
- rowViewHolder: RowPresenter.ViewHolder?,
- row: Row?,
- ->
- if (item == StashAction.CREATE_NEW) {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- handleCreate(query)
- }
- } else if (item is StashData) {
- returnId(item)
- } else {
- throw IllegalStateException("Unknown item: $item")
- }
- }
- }
-
- private suspend fun handleCreate(query: String?) {
- if (query.isNotNullOrBlank()) {
- val name = query.replaceFirstChar(Char::titlecase)
- val mutationEngine = MutationEngine(server)
- val item =
- when (dataType) {
- DataType.TAG -> {
- mutationEngine.createTag(TagCreateInput(name = name))
- }
-
- DataType.PERFORMER -> {
- mutationEngine.createPerformer(PerformerCreateInput(name = name))
- }
-
- DataType.GROUP -> {
- mutationEngine.createGroup(GroupCreateInput(name = name))
- }
-
- DataType.STUDIO -> {
- mutationEngine.createStudio(name = name)
- }
-
- else -> {
- throw IllegalArgumentException("Unsupported datatype $dataType")
- }
- }
- if (item != null) {
- Toast
- .makeText(
- requireContext(),
- "Created new ${getString(dataType.stringId)}: $name",
- Toast.LENGTH_LONG,
- ).show()
- returnId(item)
- }
- }
- }
-
- private fun returnId(item: StashData?) {
- if (item != null) {
- val currentServer = serverViewModel.currentServer.value
- if (dataType in DATA_TYPE_SUGGESTIONS && currentServer != null) {
- viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO + StashCoroutineExceptionHandler()) {
- StashApplication
- .getDatabase()
- .recentSearchItemsDao()
- .insert(RecentSearchItem(currentServer.url, item.id, dataType))
- }
- }
- setFragmentResult(
- searchFor.requestKey,
- bundleOf(
- RESULT_ID_KEY to searchFor.sourceId,
- RESULT_ITEM_ID_KEY to item.id,
- ).putDataType(dataType),
- )
- serverViewModel.navigationManager.goBack()
- }
- }
-
- override fun onResume() {
- super.onResume()
- if (dataType in DATA_TYPE_SUGGESTIONS) {
- val presenterSelector = StashPresenter.defaultClassPresenterSelector()
-
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler {
- Toast.makeText(
- requireContext(),
- "Error loading suggestions: ${it.message}",
- Toast.LENGTH_LONG,
- )
- },
- ) {
- val resultsAdapter = ArrayObjectAdapter(presenterSelector)
- val sortBy =
- when (dataType) {
- DataType.GALLERY -> "images_count"
- else -> "scenes_count"
- }
- val filter =
- FindFilterType(
- direction = Optional.present(SortDirectionEnum.DESC),
- per_page = Optional.present(perPage),
- sort = Optional.present(sortBy),
- )
- val results =
- when (dataType) {
- DataType.GALLERY -> {
- // Cannot add an image to a zip/folder gallery, so exclude them
- queryEngine.findGalleries(
- filter,
- GalleryFilterType(
- path =
- Optional.present(
- StringCriterionInput(
- value = "",
- modifier = CriterionModifier.IS_NULL,
- ),
- ),
- ),
- )
- }
-
- else -> {
- queryEngine.find(dataType, filter)
- }
- }
- resultsAdapter.addAll(0, results)
- adapter.set(
- SUGGESTIONS_POS,
- ListRow(HeaderItem(getString(R.string.suggestions)), resultsAdapter),
- )
- }
- getMostRecent()
- }
- }
-
- private fun getMostRecent(count: Int = 0) {
- if (count > 25) {
- // Just in case
- return
- }
- val currentServer = serverViewModel.requireServer()
- val handler =
- CoroutineExceptionHandler { _, ex ->
- if (ex !is QueryEngine.QueryException || ex.cause != null || ex.message.isNullOrBlank()) {
- Log.e(TAG, "Exception during recent items check")
- } else {
- val match = Regex("with id (\\d+) not found").find(ex.message!!)
- if (match != null) {
- val id = match.groupValues[1]
- val toDelete =
- RecentSearchItem(
- serverUrl = currentServer.url,
- id = id,
- dataType = dataType,
- )
- Log.i(TAG, "Deleting recent item ${toDelete.dataType} id=${toDelete.id}")
- StashApplication
- .getDatabase()
- .recentSearchItemsDao()
- .delete(toDelete)
- getMostRecent(count + 1)
- }
- }
- }
- viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO + handler) {
- val mostRecent =
- StashApplication
- .getDatabase()
- .recentSearchItemsDao()
- .getMostRecent(perPage, currentServer.url, dataType)
- val mostRecentIds = mostRecent.map { it.id }
- Log.v(TAG, "Got ${mostRecentIds.size} recent items")
- if (mostRecentIds.isNotEmpty()) {
- val items =
- when (dataType) {
- DataType.PERFORMER -> {
- queryEngine.findPerformers(performerIds = mostRecentIds)
- }
-
- DataType.TAG -> {
- queryEngine.getTags(mostRecentIds)
- }
-
- DataType.STUDIO -> {
- queryEngine.findStudios(studioIds = mostRecentIds)
- }
-
- DataType.GALLERY -> {
- queryEngine.findGalleries(galleryIds = mostRecentIds)
- }
-
- DataType.GROUP -> {
- queryEngine.findGroups(groupIds = mostRecentIds)
- }
-
- else -> {
- listOf()
- }
- }
-
- if (items.isNotEmpty()) {
- val results =
- ArrayObjectAdapter(StashPresenter.defaultClassPresenterSelector())
- Log.v(
- TAG,
- "${mostRecentIds.size} recent items resolved to ${results.size()} items",
- )
- results.addAll(0, items)
- withContext(Dispatchers.Main) {
- val headerName =
- getString(
- R.string.format_recently_used,
- getString(dataType.pluralStringId).lowercase(),
- )
- adapter.set(RECENT_POS, ListRow(HeaderItem(headerName), results))
- }
- }
- }
- }
- }
-
- override fun getResultsAdapter(): ObjectAdapter = adapter
-
- override fun onQueryTextChange(newQuery: String): Boolean {
- taskJob?.cancel()
- taskJob =
- viewLifecycleOwner.lifecycleScope.launch(exceptionHandler) {
- val searchDelay =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getInt("searchDelay", 500)
- delay(searchDelay.toLong())
- search(newQuery)
- }
- return true
- }
-
- override fun onQueryTextSubmit(query: String): Boolean {
- taskJob?.cancel()
- taskJob =
- viewLifecycleOwner.lifecycleScope.launch(exceptionHandler) {
- search(query)
- }
- return true
- }
-
- private fun search(query: String) {
- searchResultsAdapter.clear()
-
- this.query = query
- if (!TextUtils.isEmpty(query)) {
- adapter.set(
- RESULTS_POS,
- ListRow(
- HeaderItem(getString(R.string.stashapp_loading_generic)),
- ArrayObjectAdapter(),
- ),
- )
- val filter =
- FindFilterType(
- q = Optional.present(query),
- per_page = Optional.present(perPage),
- )
- viewLifecycleOwner.lifecycleScope.launch(exceptionHandler) {
- val results =
- when (dataType) {
- DataType.GALLERY -> {
- // Cannot add an image to a zip gallery, so exclude them
- queryEngine.findGalleries(
- filter,
- GalleryFilterType(
- path =
- Optional.present(
- StringCriterionInput(
- value = "",
- modifier = CriterionModifier.IS_NULL,
- ),
- ),
- ),
- )
- }
-
- else -> {
- queryEngine.find(dataType, filter)
- }
- }
- val createAllowed = allowCreate(dataType, query, results)
- if (createAllowed && readOnlyModeDisabled()) {
- searchResultsAdapter.add(StashAction.CREATE_NEW)
- }
- if (results.isNotEmpty()) {
- searchResultsAdapter.addAll(0, results)
- adapter.set(
- RESULTS_POS,
- ListRow(HeaderItem(getString(R.string.results)), searchResultsAdapter),
- )
- } else {
- if (createAllowed && readOnlyModeDisabled()) {
- adapter.set(
- RESULTS_POS,
- ListRow(
- HeaderItem(getString(R.string.stashapp_component_tagger_results_match_failed_no_result)),
- searchResultsAdapter,
- ),
- )
- } else {
- adapter.set(
- RESULTS_POS,
- ListRow(
- HeaderItem(getString(R.string.stashapp_component_tagger_results_match_failed_no_result)),
- ArrayObjectAdapter(),
- ),
- )
- }
- }
- }
- } else {
- adapter.set(
- RESULTS_POS,
- ListRow(HeaderItem(getString(R.string.waiting_for_query)), ArrayObjectAdapter()),
- )
- }
- }
-
- private inner class CreateNewPresenter(
- val dataType: DataType,
- ) : StashPresenter() {
- override fun doOnBindViewHolder(
- cardView: StashImageCardView,
- item: StashAction,
- ) {
- cardView.setMainImageDimensions(dataType.defaultCardWidth, dataType.defaultCardHeight)
- cardView.titleText = "Create new ${getString(dataType.stringId)}"
- cardView.contentText = query?.replaceFirstChar(Char::titlecase)
- cardView.mainImage =
- AppCompatResources.getDrawable(requireContext(), R.drawable.baseline_add_box_24)
- }
- }
-
- companion object {
- const val TAG = "SearchForFragment"
-
- const val ID_KEY = "id"
- const val RESULT_ID_KEY = "$TAG.sourceId"
- const val RESULT_ITEM_ID_KEY = "$TAG.resultId"
-
- private const val RESULTS_POS = 0
- private const val SUGGESTIONS_POS = RESULTS_POS + 1
- private const val RECENT_POS = SUGGESTIONS_POS + 1
-
- // List of data types that support querying for suggestions
- val DATA_TYPE_SUGGESTIONS =
- setOf(
- DataType.TAG,
- DataType.PERFORMER,
- DataType.STUDIO,
- DataType.GALLERY,
- DataType.GROUP,
- )
-
- fun allowCreate(
- dataType: DataType,
- query: String,
- items: List,
- ): Boolean {
- val q = query.lowercase()
- return when (dataType) {
- DataType.GROUP -> {
- items as List
- items.none { it.name.lowercase() == q || it.aliases?.lowercase() == q }
- }
-
- DataType.PERFORMER -> {
- items as List
- items.none { it.name.lowercase() == q || it.alias_list.any { it.lowercase() == q } }
- }
-
- DataType.TAG -> {
- items as List
- items.none { it.name.lowercase() == q || it.aliases.any { it.lowercase() == q } }
- }
-
- DataType.STUDIO -> {
- items as List
- items.none { it.name.lowercase() == q || it.aliases.any { it.lowercase() == q } }
- }
-
- DataType.SCENE -> {
- false
- }
-
- DataType.MARKER -> {
- false
- }
-
- DataType.IMAGE -> {
- false
- }
-
- DataType.GALLERY -> {
- false
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt
deleted file mode 100644
index db5e885b2..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt
+++ /dev/null
@@ -1,786 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.DialogInterface
-import android.os.Bundle
-import android.text.InputType
-import android.util.Log
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.EditText
-import android.widget.Toast
-import androidx.annotation.XmlRes
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.leanback.preference.LeanbackEditTextPreferenceDialogFragmentCompat
-import androidx.leanback.preference.LeanbackPreferenceFragmentCompat
-import androidx.leanback.preference.LeanbackSettingsFragmentCompat
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.EditTextPreference
-import androidx.preference.Preference
-import androidx.preference.PreferenceDialogFragmentCompat
-import androidx.preference.PreferenceFragmentCompat
-import androidx.preference.PreferenceManager
-import androidx.preference.PreferenceScreen
-import androidx.preference.SeekBarPreference
-import androidx.preference.SwitchPreference
-import coil3.annotation.DelicateCoilApi
-import coil3.imageLoader
-import com.bumptech.glide.Glide
-import com.bumptech.glide.load.engine.cache.DiskCache
-import com.github.damontecres.stashapp.api.fragment.StashJob
-import com.github.damontecres.stashapp.api.type.JobStatus
-import com.github.damontecres.stashapp.api.type.JobStatusUpdateType
-import com.github.damontecres.stashapp.data.JobResult
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.setup.readonly.ReadOnlyPinConfigFragment
-import com.github.damontecres.stashapp.util.Constants
-import com.github.damontecres.stashapp.util.LongClickPreference
-import com.github.damontecres.stashapp.util.MutationEngine
-import com.github.damontecres.stashapp.util.QueryEngine
-import com.github.damontecres.stashapp.util.ServerPreferences
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.SubscriptionEngine
-import com.github.damontecres.stashapp.util.UpdateChecker
-import com.github.damontecres.stashapp.util.cacheDurationPrefToDuration
-import com.github.damontecres.stashapp.util.getDestination
-import com.github.damontecres.stashapp.util.getStringNotNull
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.stashapp.util.joinNotNullOrBlank
-import com.github.damontecres.stashapp.util.launchIO
-import com.github.damontecres.stashapp.util.plugin.CompanionPlugin
-import com.github.damontecres.stashapp.util.testStashConnection
-import com.github.damontecres.stashapp.views.dialog.ConfirmationDialogFragment
-import com.github.damontecres.stashapp.views.formatBytes
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.Serializable
-import okhttp3.Cache
-import java.io.File
-
-@Serializable
-enum class PreferenceScreenOption(
- @XmlRes val preferenceRes: Int,
-) {
- BASIC(R.xml.root_preferences),
- ADVANCED(R.xml.advanced_preferences),
- USER_INTERFACE(R.xml.ui_preferences),
- ;
-
- companion object {
- fun fromString(name: String?) = entries.firstOrNull { it.name == name } ?: BASIC
- }
-}
-
-class SettingsFragment : LeanbackSettingsFragmentCompat() {
- override fun onPreferenceStartInitialScreen() {
- // Always reset player when user may change settings
- StashExoPlayer.releasePlayer()
- val destination = requireArguments().getDestination()
- // PREFERENCE_FRAGMENT_TAG is private, so hardcoded here
- val prevFragment =
- childFragmentManager
- .findFragmentByTag("androidx.leanback.preference.LeanbackSettingsFragment.PREFERENCE_FRAGMENT")
- // If the previous fragment was not a preference, then the current one should be, so do not start a new one
- if (prevFragment !is LeanbackPreferenceFragmentCompat) {
- startPreferenceFragment(
- PreferencesFragment(
- ::startPreferenceFragment,
- ::startImmersiveFragment,
- ),
- )
- }
- }
-
- override fun onPreferenceStartFragment(
- caller: PreferenceFragmentCompat,
- pref: Preference,
- ): Boolean {
- val args: Bundle = pref.extras
- val f: Fragment =
- childFragmentManager.fragmentFactory.instantiate(
- requireActivity().classLoader,
- pref.fragment!!,
- )
- f.arguments = args
- f.setTargetFragment(caller, 0)
- if (f is PreferenceFragmentCompat ||
- f is PreferenceDialogFragmentCompat
- ) {
- startPreferenceFragment(f)
- } else {
- startImmersiveFragment(f)
- }
- return true
- }
-
- override fun onPreferenceStartScreen(
- caller: PreferenceFragmentCompat,
- pref: PreferenceScreen,
- ): Boolean {
- val fragment: Fragment =
- PreferencesFragment(::startPreferenceFragment, ::startImmersiveFragment)
- val args = Bundle(1)
- args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.key)
- fragment.arguments = args
- startPreferenceFragment(fragment)
- return true
- }
-
- override fun onPreferenceDisplayDialog(
- caller: PreferenceFragmentCompat,
- pref: Preference,
- ): Boolean =
- if (pref.key == getString(R.string.pref_key_pin_code)) {
- val f = NumberEditTextPreferencesDialog(pref.key)
- f.setTargetFragment(caller, 0)
- startPreferenceFragment(f)
- true
- } else {
- super.onPreferenceDisplayDialog(caller, pref)
- }
-
- class PreferencesFragment(
- val startPreferenceFragmentFunc: (fragment: LeanbackPreferenceFragmentCompat) -> Unit,
- val startImmersiveFunc: (fragment: Fragment) -> Unit,
- ) : LeanbackPreferenceFragmentCompat() {
- private val serverViewModel: ServerViewModel by activityViewModels()
- private var subscriptionJob: Job? = null
-
- override fun onCreatePreferences(
- savedInstanceState: Bundle?,
- rootKey: String?,
- ) {
- setPreferencesFromResource(R.xml.root_preferences, rootKey)
-
- findPreference(PREF_STASH_URL)!!.setOnPreferenceClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- testStashConnection(
- requireContext(),
- true,
- serverViewModel.requireServer().apolloClient,
- )
- }
- true
- }
-
- val manageServers = findPreference("manageServers")
- manageServers!!.setOnPreferenceClickListener {
- serverViewModel.navigationManager.navigate(Destination.ManageServers(true))
- true
- }
-
- val pinCodePref = findPreference("pinCode")!!
- pinCodePref.summaryProvider =
- Preference.SummaryProvider { preference ->
- if (preference.text.isNullOrBlank()) {
- "No PIN is set"
- } else {
- "PIN is set"
- }
- }
-
- val readOnlyModePref =
- findPreference(getString(R.string.pref_key_read_only_mode))!!
- readOnlyModePref.setOnPreferenceChangeListener { _, newValue ->
- if (newValue == true) {
- startImmersiveFunc(ReadOnlyPinConfigFragment())
- }
- newValue == false
- }
-
- val installedVersion = UpdateChecker.getInstalledVersion(requireActivity())
- val versionPref = findPreference("versionName")!!
- versionPref.summary = installedVersion.toString()
- var clickCount = 0
- versionPref.setOnPreferenceClickListener {
- if (clickCount > 2) {
- clickCount = 0
- serverViewModel.navigationManager.navigate(Destination.Debug)
- } else {
- clickCount++
- }
- true
- }
-
- val updateUrl =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getStringNotNull(
- "updateCheckUrl",
- requireContext().getString(R.string.app_update_url),
- )
-
- val checkForUpdatePref = findPreference("checkForUpdate")!!
- val installUpdate = findPreference("installUpdate")!!
- listOf(checkForUpdatePref, installUpdate).forEach { pref ->
- pref.setOnPreferenceClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val release = UpdateChecker.getLatestRelease(requireContext(), updateUrl)
- if (release != null) {
- if (release.version.isGreaterThan(installedVersion)) {
- serverViewModel.navigationManager.navigate(
- Destination.UpdateApp(
- release,
- ),
- )
- } else {
- Toast
- .makeText(
- requireContext(),
- "No update available!",
- Toast.LENGTH_SHORT,
- ).show()
- }
- } else {
- Toast
- .makeText(
- requireContext(),
- "Failed to check for updates",
- Toast.LENGTH_LONG,
- ).show()
- }
- }
- true
- }
- pref.setOnLongClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val release = UpdateChecker.getLatestRelease(requireContext(), updateUrl)
- if (release != null) {
- serverViewModel.navigationManager.navigate(Destination.UpdateApp(release))
- } else {
- Toast
- .makeText(
- requireContext(),
- "Failed to check for updates",
- Toast.LENGTH_LONG,
- ).show()
- }
- }
- true
- }
- }
-
- findPreference("skip_back_time")!!.min = 5
- findPreference("skip_forward_time")!!.min = 5
-
- val advancedPreferences = findPreference("advancedPreferences")!!
- advancedPreferences.setOnPreferenceClickListener {
- startPreferenceFragmentFunc(AdvancedPreferencesFragment())
- true
- }
- val advancedUiPreferences = findPreference("advancedUiPreferences")!!
- advancedUiPreferences.setOnPreferenceClickListener {
- startPreferenceFragmentFunc(SettingsUiFragment())
- true
- }
- }
-
- @OptIn(DelicateCoilApi::class)
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- Log.v(TAG, "onViewCreated: savedInstanceState==null: ${savedInstanceState == null}")
- super.onViewCreated(view, savedInstanceState)
- setTitle(getString(R.string.stashapp_settings))
- view.requestFocus()
- serverViewModel.currentServer.observe(viewLifecycleOwner) {
- if (it != null) {
- refresh(it)
- }
- }
-
- if (PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean("autoCheckForUpdates", true)
- ) {
- val checkForUpdatePref = findPreference("checkForUpdate")
- val installUpdate = findPreference("installUpdate")
- val updateCategory = findPreference("updateCategory")
-
- val updatePrefs = listOf(checkForUpdatePref, installUpdate)
-
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val updateUrl =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getStringNotNull(
- "updateCheckUrl",
- requireContext().getString(R.string.app_update_url),
- )
- val release = UpdateChecker.getLatestRelease(requireContext(), updateUrl)
- val installedVersion = UpdateChecker.getInstalledVersion(requireActivity())
- if (release != null) {
- if (release.version.isGreaterThan(installedVersion)) {
- updatePrefs.forEach {
- it?.title = "Install update"
- it?.summary =
- getString(R.string.stashapp_package_manager_latest_version) + ": ${release.version}"
- }
- updateCategory?.isVisible = true
- } else {
- updatePrefs.forEach {
- it?.title = "No update available"
- it?.summary = null
- }
- updateCategory?.isVisible = false
- }
- }
- }
- }
- }
-
- /**
- * Handle updating the UI for job progress
- */
- private fun handleJobUpdate(
- updateType: JobStatusUpdateType,
- job: StashJob,
- serverPrefs: ServerPreferences,
- ) {
- val triggerScan = findPreference("triggerScan")!!
- val generatePref = findPreference("triggerGenerate")!!
-
- val scanJobId = serverPrefs.scanJobId
- val generateJobId = serverPrefs.generateJobId
-
- if (job.id == scanJobId || job.id == generateJobId) {
- if (updateType == JobStatusUpdateType.REMOVE || !job.isRunning()) {
- // Job complete
- if (job.id == scanJobId) {
- serverPrefs.scanJobId = null
- triggerScan.summary =
- getString(R.string.stashapp_config_tasks_scan_for_content_desc)
- } else {
- serverPrefs.generateJobId = null
- generatePref.summary =
- getString(R.string.stashapp_config_tasks_generate_desc)
- }
- } else {
- val message =
- if (job.progress != null) {
- val progress = (job.progress * 1000).toInt() / 10.0
- if (!job.subTasks.isNullOrEmpty() &&
- job.subTasks.first().isNotNullOrBlank()
- ) {
- "${job.description} $progress%\n${job.subTasks.first()}"
- } else {
- "${job.description} $progress%"
- }
- } else {
- job.description
- }
-
- if (job.id == scanJobId) {
- triggerScan.summary = message
- } else {
- generatePref.summary = message
- }
- }
- }
- }
-
- private fun StashJob.isRunning() = status == JobStatus.RUNNING || status == JobStatus.READY || status == JobStatus.STOPPING
-
- private fun refresh(currentServer: StashServer) {
- Log.v(TAG, "refresh")
- findPreference(PREF_STASH_URL)!!.summary = currentServer.url
-
- findPreference(getString(R.string.pref_key_read_only_mode))!!.isChecked =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_read_only_mode), false)
-
- val queryEngine = QueryEngine(currentServer)
-
- val triggerScan = findPreference("triggerScan")!!
- val pendingScanJobId = currentServer.serverPreferences.scanJobId
- if (pendingScanJobId != null) {
- // Check if a job is already running
- // Such as when a user triggers it, navigates away, and then back
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val job = queryEngine.getJob(pendingScanJobId)
- if (job == null || !job.isRunning()) {
- currentServer.serverPreferences.scanJobId = null
- triggerScan.summary =
- getString(R.string.stashapp_config_tasks_scan_for_content_desc)
- } else {
- handleJobUpdate(
- JobStatusUpdateType.UPDATE,
- job,
- currentServer.serverPreferences,
- )
- }
- }
- }
-
- val generatePref = findPreference("triggerGenerate")!!
- val pendingGenJobId = currentServer.serverPreferences.generateJobId
- if (pendingGenJobId != null) {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val job = queryEngine.getJob(pendingGenJobId)
- if (job == null || !job.isRunning()) {
- currentServer.serverPreferences.generateJobId = null
- generatePref.summary =
- getString(R.string.stashapp_config_tasks_generate_desc)
- } else {
- handleJobUpdate(
- JobStatusUpdateType.UPDATE,
- job,
- currentServer.serverPreferences,
- )
- }
- }
- }
-
- val serverPrefs = currentServer.serverPreferences
- val subscriptionEngine = SubscriptionEngine(currentServer)
- // Cancel previous subscription
- subscriptionJob?.cancel()
- subscriptionJob =
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- subscriptionEngine.subscribeToJobs { update ->
- val type = update.jobsSubscribe.type
- val jobData = update.jobsSubscribe.job.stashJob
- Log.v(TAG, "job subscription update: $type ${jobData.id}")
- handleJobUpdate(type, jobData, serverPrefs)
- }
- }
-
- triggerScan.setOnPreferenceClickListener {
- if (currentServer.serverPreferences.scanJobId == null) {
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler(
- autoToast = true,
- ),
- ) {
- currentServer.updateServerPrefs()
- val jobId = MutationEngine(currentServer).triggerScan()
- // TODO: job could finish between these two lines of code
- currentServer.serverPreferences.scanJobId = jobId
- Toast
- .makeText(
- requireContext(),
- "Triggered a library scan",
- Toast.LENGTH_LONG,
- ).show()
- }
- }
- true
- }
-
- generatePref.setOnPreferenceClickListener {
- if (currentServer.serverPreferences.generateJobId == null) {
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler(
- autoToast = true,
- ),
- ) {
- currentServer.updateServerPrefs()
- val jobId = MutationEngine(currentServer).triggerGenerate()
- currentServer.serverPreferences.generateJobId = jobId
- Toast
- .makeText(
- requireContext(),
- "Triggered a generate task",
- Toast.LENGTH_LONG,
- ).show()
- }
- }
- true
- }
-
- val sendLogsPref = findPreference("sendLogs")!!
- sendLogsPref.title = "Checking for companion plugin..."
- sendLogsPref.summary = null
-
- val setupSendLogsPref = {
- sendLogsPref.title = "Send Logs"
- sendLogsPref.summary = "Send a copy of recent app logs to your current server"
- sendLogsPref.setOnPreferenceClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- CompanionPlugin.sendLogCat(
- requireContext(),
- serverViewModel.requireServer(),
- false,
- )
- }
- true
- }
- sendLogsPref.setOnLongClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- CompanionPlugin.sendLogCat(
- requireContext(),
- serverViewModel.requireServer(),
- true,
- )
- }
- true
- }
- }
-
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val serverPrefs = StashServer.requireCurrentServer().updateServerPrefs()
- if (serverPrefs.companionPluginInstalled) {
- setupSendLogsPref()
- } else {
- sendLogsPref.title = "Install companion plugin"
- sendLogsPref.summary =
- "Install StashAppAndroid TV Companion plugin on the current server"
- sendLogsPref.setOnPreferenceClickListener {
- viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO + StashCoroutineExceptionHandler()) {
- val jobId =
- CompanionPlugin.installPlugin(
- MutationEngine(serverViewModel.currentServer.value!!),
- )
- val queryEngine =
- QueryEngine(serverViewModel.currentServer.value!!)
- val result = queryEngine.waitForJob(jobId)
- withContext(Dispatchers.Main) {
- if (result is JobResult.Failure) {
- Toast
- .makeText(
- requireContext(),
- "Error installing plugin: ${result.message}",
- Toast.LENGTH_LONG,
- ).show()
- } else if (result is JobResult.NotFound) {
- Toast
- .makeText(
- requireContext(),
- "Error installing plugin",
- Toast.LENGTH_LONG,
- ).show()
- } else {
- val serverPrefs =
- StashServer.requireCurrentServer().updateServerPrefs()
- if (serverPrefs.companionPluginInstalled) {
- Toast
- .makeText(
- requireContext(),
- "Companion plugin installed!",
- Toast.LENGTH_SHORT,
- ).show()
- setupSendLogsPref()
- } else {
- Toast
- .makeText(
- requireContext(),
- "Error installing plugin",
- Toast.LENGTH_LONG,
- ).show()
- }
- }
- }
- }
- true
- }
- }
- }
- }
-
- companion object {
- private const val TAG = "SettingsFragment"
- }
- }
-
- class AdvancedPreferencesFragment : LeanbackPreferenceFragmentCompat() {
- private val serverViewModel: ServerViewModel by activityViewModels()
-
- override fun onCreatePreferences(
- savedInstanceState: Bundle?,
- rootKey: String?,
- ) {
- setPreferencesFromResource(R.xml.advanced_preferences, rootKey)
-
- findPreference("maxSearchResults")!!.min = 5
- findPreference("searchDelay")!!.min = 50
-
- val cacheSizePref =
- findPreference(getString(R.string.pref_key_network_cache_size))!!
- cacheSizePref.min = 25
- val cache = Constants.getNetworkCache(requireContext())
- setUsedCachedSummary(cacheSizePref, cache)
-
- findPreference("clearCache")!!.setOnPreferenceClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- clearCaches(requireContext())
- withContext(Dispatchers.Main) {
- setUsedCachedSummary(cacheSizePref, cache)
- }
- }
- true
- }
-
- val cacheDurationPref = findPreference("networkCacheDuration")!!
- setCacheDurationSummary(cacheDurationPref, cacheDurationPref.value)
- cacheDurationPref.setOnPreferenceChangeListener { _, newValue ->
- setCacheDurationSummary(cacheDurationPref, newValue)
- true
- }
-
- findPreference("license")!!.setOnPreferenceClickListener {
- serverViewModel.navigationManager.navigate(Destination.LicenseInfo)
- true
- }
-
- val networkTimeoutPref = findPreference("networkTimeout")!!
- networkTimeoutPref.setOnPreferenceClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- testStashConnection(
- requireContext(),
- true,
- serverViewModel.requireServer().apolloClient,
- )
- }
- true
- }
- networkTimeoutPref.setOnPreferenceChangeListener { _, newValue ->
- if (newValue == 0) {
- Toast
- .makeText(
- requireContext(),
- "Warning! A zero network timeout will wait forever!",
- Toast.LENGTH_LONG,
- ).show()
- }
- true
- }
-
- val persistPlaybackEffectsPref =
- findPreference(getString(R.string.pref_key_playback_save_effects))!!
- persistPlaybackEffectsPref.setOnPreferenceChangeListener { preference, newValue ->
- if (newValue == false) {
- viewLifecycleOwner.lifecycleScope.launchIO {
- StashApplication.getDatabase().playbackEffectsDao().deleteAll()
- }
- }
- true
- }
-
- val videoEffectsPref =
- findPreference(getString(R.string.pref_key_video_filters))!!
- videoEffectsPref.setOnPreferenceChangeListener { _, newValue ->
- if (newValue == true) {
- ConfirmationDialogFragment(
- "Some device do not support video filters!\n\nWould you like to enable it?",
- ) { _, which ->
- if (which == DialogInterface.BUTTON_POSITIVE) {
- videoEffectsPref.isChecked = true
- }
- }.show(childFragmentManager, null)
- }
- newValue == false
- }
-
- val cpus = Runtime.getRuntime().availableProcessors()
- val imageThreads =
- findPreference(getString(R.string.pref_key_image_loading_threads))!!
- imageThreads.min = 1
- imageThreads.max = cpus * 3
- imageThreads.setDefaultValue(cpus)
- imageThreads.value =
- PreferenceManager.getDefaultSharedPreferences(requireContext()).getInt(
- getString(R.string.pref_key_image_loading_threads),
- Runtime.getRuntime().availableProcessors(),
- )
- imageThreads.summary = "Requires restart, default is $cpus"
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- setTitle(getString(R.string.advanced_settings))
- view.requestFocus()
- }
-
- private fun setCacheDurationSummary(
- pref: SeekBarPreference,
- value: Any,
- ) {
- val duration = cacheDurationPrefToDuration(value.toString().toInt())
- pref.summary = duration?.toString() ?: "Always request from server"
- }
-
- @SuppressLint("DefaultLocale")
- private fun setUsedCachedSummary(
- cacheSizePref: Preference,
- cache: Cache,
- ) {
- val glideCacheSize =
- File(
- requireContext().cacheDir,
- DiskCache.Factory.DEFAULT_DISK_CACHE_DIR,
- ).walkTopDown()
- .filter { it.isFile }
- .map { it.length() }
- .sum()
- val cacheSizeFormatted = formatBytes(cache.size())
- val glideCacheSizeFormatted = formatBytes(glideCacheSize)
-
- val useCompose =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_use_compose_ui), true)
- val composeCacheUsed =
- if (useCompose) {
- val diskUsed =
- "D:" + formatBytes(requireContext().imageLoader.diskCache?.size ?: 0L)
- val memoryUsed =
- "M:" + formatBytes(requireContext().imageLoader.memoryCache?.size ?: 0L)
- " (Compose: " + listOf(diskUsed, memoryUsed).joinNotNullOrBlank(" / ") + ")"
- } else {
- ""
- }
-
- cacheSizePref.summary =
- "Using $cacheSizeFormatted (Images $glideCacheSizeFormatted)$composeCacheUsed"
- }
- }
-
- class NumberEditTextPreferencesDialog(
- key: String,
- ) : LeanbackEditTextPreferenceDialogFragmentCompat() {
- init {
- val args = Bundle(1)
- args.putString(ARG_KEY, key)
- arguments = args
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View? {
- val root = super.onCreateView(inflater, container, savedInstanceState)
- val editTextView = root?.findViewById(android.R.id.edit)
- editTextView?.inputType = InputType.TYPE_CLASS_NUMBER
- return root
- }
- }
-
- companion object {
- const val PREF_STASH_URL = "stashUrl"
- const val PREF_STASH_API_KEY = "stashApiKey"
-
- suspend fun clearCaches(context: Context) =
- withContext(Dispatchers.IO) {
- withContext(Dispatchers.Main) {
- Constants.getNetworkCache(context).evictAll()
- Glide.get(context).clearMemory()
- }
- Glide.get(context).clearDiskCache()
- context.imageLoader.memoryCache?.clear()
- context.imageLoader.diskCache?.clear()
- }
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/SettingsUiFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/SettingsUiFragment.kt
deleted file mode 100644
index 2ea05482f..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/SettingsUiFragment.kt
+++ /dev/null
@@ -1,153 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.content.Intent
-import android.os.Bundle
-import android.view.View
-import android.widget.Toast
-import androidx.leanback.preference.LeanbackPreferenceFragmentCompat
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.Preference
-import androidx.preference.PreferenceManager
-import androidx.preference.SeekBarPreference
-import androidx.preference.SwitchPreference
-import coil3.SingletonImageLoader
-import coil3.annotation.DelicateCoilApi
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.composeEnabled
-import com.github.damontecres.stashapp.views.dialog.ConfirmationDialogFragment
-import kotlinx.coroutines.launch
-import java.util.Locale
-
-class SettingsUiFragment : LeanbackPreferenceFragmentCompat() {
- @OptIn(DelicateCoilApi::class)
- override fun onCreatePreferences(
- savedInstanceState: Bundle?,
- rootKey: String?,
- ) {
- setPreferencesFromResource(R.xml.ui_preferences, rootKey)
-
- val videoDelayPref =
- findPreference(getString(R.string.pref_key_ui_card_overlay_delay))!!
- setVideoDelaySummary(videoDelayPref, videoDelayPref.value)
- videoDelayPref.setOnPreferenceChangeListener { _, newValue ->
- setVideoDelaySummary(videoDelayPref, newValue)
- true
- }
-
- val slideshowDurationPref =
- findPreference(getString(R.string.pref_key_slideshow_duration))!!
- slideshowDurationPref.min = 1
-
- val slideShowDurationPref =
- findPreference(getString(R.string.pref_key_slideshow_duration))!!
- setVideoDelaySummary(slideShowDurationPref, slideShowDurationPref.value * 1000, 0)
- slideShowDurationPref.setOnPreferenceChangeListener { _, newValue ->
- setVideoDelaySummary(slideShowDurationPref, newValue.toString().toInt() * 1000, 0)
- true
- }
-
- val imageClipDelayPref =
- findPreference(getString(R.string.pref_key_slideshow_duration_image_clip))!!
- setVideoDelaySummary(imageClipDelayPref, imageClipDelayPref.value)
- imageClipDelayPref.setOnPreferenceChangeListener { _, newValue ->
- setVideoDelaySummary(imageClipDelayPref, newValue, 2)
- true
- }
-
- val composeEnabledPref =
- findPreference(getString(R.string.pref_key_use_compose_ui))!!
- composeEnabledPref.setOnPreferenceChangeListener { preference, newValue ->
- if (newValue == true) {
- ConfirmationDialogFragment.show(
- childFragmentManager,
- "Do you want to switch to the new UI?\n\nNote: the app will restart.",
- ) {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- SettingsFragment.clearCaches(requireContext())
- composeEnabledPref.isChecked = true
- // Clear coil singleton
- SingletonImageLoader.reset()
- requireActivity().startActivity(
- Intent(
- requireActivity(),
- RootActivity::class.java,
- ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
- )
- }
- }
- } else if (newValue == false) {
- ConfirmationDialogFragment.show(
- childFragmentManager,
- "Please report any issues you encountered with the new UI!\n\nDo you want switch back to the old UI?\n\nNote: the app will restart.",
- ) {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- SettingsFragment.clearCaches(requireContext())
- composeEnabledPref.isChecked = false
- // Clear coil singleton
- SingletonImageLoader.reset()
- requireActivity().startActivity(
- Intent(
- requireActivity(),
- RootActivity::class.java,
- ).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK),
- )
- }
- }
- }
- false
- }
-
- val chooseThemePref =
- findPreference(getString(R.string.pref_key_ui_theme_file))
- chooseThemePref?.summary =
- PreferenceManager.getDefaultSharedPreferences(requireContext()).getString(
- getString(R.string.pref_key_ui_theme_file),
- "default",
- )
- if (composeEnabled(requireContext())) {
- chooseThemePref?.setOnPreferenceClickListener {
- StashApplication.navigationManager.navigate(Destination.ChooseTheme)
- true
- }
- } else {
- chooseThemePref?.setOnPreferenceClickListener {
- Toast
- .makeText(
- requireContext(),
- "Must enable compose to change this",
- Toast.LENGTH_SHORT,
- ).show()
- true
- }
- }
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- setTitle(getString(R.string.ui_settings))
- view.requestFocus()
- }
-
- private fun setVideoDelaySummary(
- pref: SeekBarPreference,
- value: Any,
- decimals: Int = 1,
- ) {
- val newValue = value.toString().toInt()
- pref.summary =
- if (newValue > 0) {
- String.format(
- Locale.getDefault(),
- "%.${decimals}f %s",
- newValue / 1000.0,
- getString(R.string.stashapp_seconds),
- )
- } else {
- "No delay"
- }
- }
-}
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 5c0810a49..5b2603907 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt
@@ -15,13 +15,8 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.preference.PreferenceManager
-import androidx.room.Room
-import com.github.damontecres.stashapp.data.room.AppDatabase
-import com.github.damontecres.stashapp.data.room.MIGRATION_4_TO_5
-import com.github.damontecres.stashapp.navigation.NavigationManager
+import com.github.damontecres.stashapp.di.AppModule
import com.github.damontecres.stashapp.util.AppUpgradeHandler
-import com.github.damontecres.stashapp.util.QueryEngine
-import com.github.damontecres.stashapp.util.StashServer
import com.github.damontecres.stashapp.util.Version
import dev.b3nedikt.restring.Restring
import org.acra.ACRA
@@ -29,8 +24,14 @@ import org.acra.ReportField
import org.acra.config.dialog
import org.acra.data.StringFormat
import org.acra.ktx.initAcra
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.core.annotation.KoinApplication
+import org.koin.core.context.startKoin
+import org.koin.plugin.module.dsl.modules
import timber.log.Timber
+@KoinApplication
class StashApplication : Application() {
@OptIn(ExperimentalComposeRuntimeApi::class)
override fun onCreate() {
@@ -109,6 +110,12 @@ class StashApplication : Application() {
ProcessLifecycleOwner.get().lifecycle.addObserver(LifecycleObserverImpl())
+ startKoin {
+ androidLogger()
+ androidContext(this@StashApplication)
+ modules(AppModule::class)
+ }
+
val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val currentVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null)
val currentVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1)
@@ -142,22 +149,10 @@ class StashApplication : Application() {
}
}
}
-
- setupDB()
}
override fun getResources(): Resources = Restring.wrapResources(applicationContext, super.getResources())
- private fun setupDB() {
- val dbName = getString(R.string.app_name)
- database =
- Room
- .databaseBuilder(this, AppDatabase::class.java, dbName)
- .addMigrations(MIGRATION_4_TO_5)
- .fallbackToDestructiveMigration()
- .build()
- }
-
@Deprecated("Deprecated in Java")
override fun onLowMemory() {
Log.w(TAG, "onLowMemory")
@@ -172,32 +167,24 @@ class StashApplication : Application() {
inner class LifecycleObserverImpl : DefaultLifecycleObserver {
override fun onPause(owner: LifecycleOwner) {
Log.v(TAG, "LifecycleObserverImpl.onPause")
- StashExoPlayer.releasePlayer()
}
override fun onStop(owner: LifecycleOwner) {
Log.v(TAG, "LifecycleObserverImpl.onStop")
- StashExoPlayer.releasePlayer()
}
override fun onDestroy(owner: LifecycleOwner) {
Log.v(TAG, "LifecycleObserverImpl.onDestroy")
- StashExoPlayer.releasePlayer()
}
}
companion object {
private lateinit var application: StashApplication
- private lateinit var database: AppDatabase
- lateinit var navigationManager: NavigationManager
- var currentServer: StashServer? = null
private val fontCache = mutableMapOf()
fun getApplication(): StashApplication = application
- fun requireCurrentServer(): StashServer = currentServer ?: throw QueryEngine.StashNotConfiguredException()
-
fun getFont(
@FontRes fontId: Int,
): Typeface {
@@ -206,8 +193,6 @@ class StashApplication : Application() {
}
}
- fun getDatabase(): AppDatabase = database
-
const val TAG = "StashApplication"
const val VERSION_NAME_PREVIOUS_KEY = "VERSION_NAME_PREVIOUS_NAME"
const val VERSION_CODE_PREVIOUS_KEY = "VERSION_CODE_PREVIOUS_NAME"
diff --git a/app/src/main/java/com/github/damontecres/stashapp/StashDataGridFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/StashDataGridFragment.kt
deleted file mode 100644
index 93be1eaba..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/StashDataGridFragment.kt
+++ /dev/null
@@ -1,694 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.util.Log
-import android.view.KeyEvent
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import android.widget.LinearLayout
-import android.widget.TextView
-import androidx.activity.OnBackPressedCallback
-import androidx.activity.addCallback
-import androidx.core.view.contains
-import androidx.core.view.forEach
-import androidx.core.view.get
-import androidx.core.widget.ContentLoadingProgressBar
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.commit
-import androidx.fragment.app.viewModels
-import androidx.leanback.system.Settings
-import androidx.leanback.transition.TransitionHelper
-import androidx.leanback.widget.ArrayObjectAdapter
-import androidx.leanback.widget.BrowseFrameLayout
-import androidx.leanback.widget.FocusHighlight
-import androidx.leanback.widget.FocusHighlightHelper
-import androidx.leanback.widget.ItemBridgeAdapter
-import androidx.leanback.widget.ItemBridgeAdapterShadowOverlayWrapper
-import androidx.leanback.widget.OnItemViewClickedListener
-import androidx.leanback.widget.OnItemViewSelectedListener
-import androidx.leanback.widget.Presenter
-import androidx.leanback.widget.ShadowOverlayHelper
-import androidx.leanback.widget.VerticalGridView
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.api.type.SortDirectionEnum
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.data.SortOption
-import com.github.damontecres.stashapp.navigation.FilterAndPosition
-import com.github.damontecres.stashapp.navigation.NavigationOnItemViewClickedListener
-import com.github.damontecres.stashapp.presenters.ScenePresenter
-import com.github.damontecres.stashapp.presenters.StashImageCardView
-import com.github.damontecres.stashapp.suppliers.DataSupplierFactory
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.AlphabetSearchUtils
-import com.github.damontecres.stashapp.util.DefaultKeyEventCallback
-import com.github.damontecres.stashapp.util.PagingObjectAdapter
-import com.github.damontecres.stashapp.util.QueryEngine
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.animateToInvisible
-import com.github.damontecres.stashapp.util.animateToVisible
-import com.github.damontecres.stashapp.util.defaultCardWidth
-import com.github.damontecres.stashapp.util.getDataType
-import com.github.damontecres.stashapp.util.getInt
-import com.github.damontecres.stashapp.util.maybeStartPlayback
-import com.github.damontecres.stashapp.util.putDataType
-import com.github.damontecres.stashapp.views.StashOnFocusChangeListener
-import com.github.damontecres.stashapp.views.formatNumber
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import com.github.damontecres.stashapp.views.models.StashGridViewModel
-import kotlinx.coroutines.launch
-import kotlin.math.abs
-import kotlin.properties.Delegates
-
-/**
- * A [Fragment] that shows a grid of [StashData] of the same [DataType].
- *
- * The items are derived from a [FilterArgs] and queried via [DataSupplierFactory].
- */
-class StashDataGridFragment :
- Fragment(),
- DefaultKeyEventCallback {
- private lateinit var mShadowOverlayHelper: ShadowOverlayHelper
- private val serverViewModel: ServerViewModel by activityViewModels()
- private val viewModel: StashGridViewModel by viewModels(ownerProducer = { requireParentFragment() })
-
- // Views
- private lateinit var footerLayout: View
- private lateinit var positionTextView: TextView
- private lateinit var totalCountTextView: TextView
- private lateinit var noResultsTextView: TextView
- private lateinit var gridView: VerticalGridView
- private lateinit var itemBridgeAdapter: ItemBridgeAdapter
- private var pagingAdapter: PagingObjectAdapter? = null
- private lateinit var alphabetFilterLayout: VerticalGridView
- private lateinit var loadingProgressBar: ContentLoadingProgressBar
- private lateinit var jumpButtonLayout: LinearLayout
-
- private var remoteButtonPaging: Boolean = true
- private var prefBackPressScrollEnabled: Boolean = true
-
- // State
- private var previousPosition = -1
- private var selectedPosition = -1
-
- private var onBackPressedCallback: OnBackPressedCallback? = null
-
- // Modifiable properties
-
- /**
- * Request that the grid receive focus in [onStart]
- */
- var requestFocus: Boolean = false
-
- /**
- * The item clicked listener, will default to [NavigationOnItemViewClickedListener] in [onViewCreated] if not specified before
- */
- var onItemViewClickedListener: OnItemViewClickedListener? = null
-
- // Unmodifiable properties, current state
-
- /**
- * Type of items being displayed
- */
- lateinit var dataType: DataType
-
- var numberOfColumns by Delegates.notNull()
- private set
-
- fun init(dataType: DataType) {
- this.dataType = dataType
- }
-
- private val mViewSelectedListener =
- OnItemViewSelectedListener { itemViewHolder, item, rowViewHolder, row ->
- val position = gridView.selectedPosition
- onSelectedOrJump(position)
- }
-
- private val backPressFocusChangeListener: View.OnFocusChangeListener by lazy {
- object : StashOnFocusChangeListener(requireContext(), R.fraction.alphabet_zoom) {
- override fun onFocusChange(
- v: View,
- hasFocus: Boolean,
- ) {
- super.onFocusChange(v, hasFocus)
- if (hasFocus) {
- onBackPressedCallback?.isEnabled = false
- }
- }
- }
- }
-
- private fun jumpTo(newPosition: Int) {
- Log.v(TAG, "jumpTo $newPosition")
- if (gridView.adapter != null) {
- if (abs(selectedPosition - newPosition) < numberOfColumns * 10) {
- // If new position is close to the current, smooth scroll
- gridView.setSelectedPositionSmooth(newPosition)
- onSelectedOrJump(newPosition)
- } else {
- // If not, just jump without smooth scrolling
-
- viewLifecycleOwner.lifecycleScope.launch {
- pagingAdapter?.prefetch(newPosition)?.join()
- gridView.selectedPosition = newPosition
- onSelectedOrJump(newPosition)
- }
- }
- }
- }
-
- private fun onSelectedOrJump(position: Int) {
- if (position != selectedPosition) {
- if (DEBUG) Log.v(TAG, "newPosition=$position, previousPosition=$previousPosition")
- previousPosition = selectedPosition
- selectedPosition = position
- viewModel.position = position
- positionTextView.text = formatNumber(position + 1, false)
- // If on the second row & the back callback exists, enable it
- onBackPressedCallback?.isEnabled = selectedPosition >= numberOfColumns
- pagingAdapter?.maybePrefetch(position)
- }
- }
-
- @SuppressLint("RestrictedApi")
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- if (savedInstanceState != null) {
- dataType = savedInstanceState.getDataType()
- }
- Log.v(TAG, "onCreateView: dataType=$dataType")
- val root =
- inflater.inflate(
- R.layout.stash_grid,
- container,
- false,
- ) as ViewGroup
- footerLayout = root.findViewById(R.id.footer_layout)
-
- val cardSize =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getInt("cardSize", requireContext().getString(R.string.card_size_default))
- numberOfColumns =
- (cardSize * (ScenePresenter.CARD_WIDTH.toDouble() / dataType.defaultCardWidth)).toInt()
-
- if (onItemViewClickedListener == null) {
- onItemViewClickedListener =
- NavigationOnItemViewClickedListener(serverViewModel.navigationManager) {
- FilterAndPosition(viewModel.filterArgs.value!!, selectedPosition)
- }
- }
-
- remoteButtonPaging =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_remote_page_buttons), true)
-
- val mUseFocusDimmer = false
- mShadowOverlayHelper =
- ShadowOverlayHelper
- .Builder()
- .needsOverlay(mUseFocusDimmer)
- .needsShadow(true)
- .needsRoundedCorner(false)
- .preferZOrder(!Settings.getInstance(requireContext()).preferStaticShadows())
- .keepForegroundDrawable(true)
- .options(ShadowOverlayHelper.Options.DEFAULT)
- .build(requireContext())
-
- gridView = root.findViewById(R.id.browse_grid)
- gridView.setNumColumns(numberOfColumns)
- // gridView.setColumnWidth(0)
- if (gridView.adapter == null) {
- Log.v(TAG, "gridView.adapter == null")
- itemBridgeAdapter = VerticalGridItemBridgeAdapter()
- gridView.adapter = itemBridgeAdapter
-
- if (mShadowOverlayHelper.needsWrapper()) {
- val mShadowOverlayWrapper =
- ItemBridgeAdapterShadowOverlayWrapper(
- mShadowOverlayHelper,
- )
- itemBridgeAdapter.setWrapper(mShadowOverlayWrapper)
- }
- mShadowOverlayHelper.prepareParentForShadow(gridView)
- gridView.setFocusDrawingOrderEnabled(
- mShadowOverlayHelper.getShadowType()
- != ShadowOverlayHelper.SHADOW_DYNAMIC,
- )
- FocusHighlightHelper.setupBrowseItemFocusHighlight(
- itemBridgeAdapter,
- FocusHighlight.ZOOM_FACTOR_MEDIUM,
- mUseFocusDimmer,
- )
- } else {
- Log.v(TAG, "gridView.adapter != null")
- itemBridgeAdapter = gridView.adapter as ItemBridgeAdapter
- }
- gridView.setOnChildSelectedListener { parent, view, position, id ->
- val ibn =
- if (view == null) null else gridView.getChildViewHolder(view) as ItemBridgeAdapter.ViewHolder
- if (ibn != null) {
- mViewSelectedListener.onItemSelected(ibn.viewHolder, ibn.item, null, null)
- } else {
- mViewSelectedListener.onItemSelected(null, null, null, null)
- }
- }
-
- jumpButtonLayout = root.findViewById(R.id.jump_layout)
- loadingProgressBar = root.findViewById(R.id.loading_progress_bar)
-
- alphabetFilterLayout = root.findViewById(R.id.alphabet_scrollview)
- if (!SortOption.isJumpSupported(dataType)) {
- alphabetFilterLayout.visibility = View.GONE
- } else {
- val alphabetAdapter =
- ArrayObjectAdapter(
- object : Presenter() {
- override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
- val button =
- inflater.inflate(
- R.layout.alphabet_button,
- alphabetFilterLayout,
- false,
- ) as Button
- button.onFocusChangeListener = backPressFocusChangeListener
- return ViewHolder(button)
- }
-
- override fun onBindViewHolder(
- viewHolder: ViewHolder,
- item: Any?,
- ) {
- val letter = item as Char
- (viewHolder.view as Button).text = letter.toString()
- viewHolder.view.setOnClickListener {
- loadingProgressBar.show()
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler(
- autoToast = true,
- ),
- ) {
- val server = StashServer.requireCurrentServer()
- val factory =
- DataSupplierFactory(server.serverPreferences.serverVersion)
- val letterPosition =
- AlphabetSearchUtils.findPosition(
- letter,
- viewModel.filterArgs.value!!,
- QueryEngine(server),
- factory,
- )
- Log.v(TAG, "Found position for $letter: $letterPosition")
- val jumpPosition =
- if (viewModel.filterArgs.value
- ?.sortAndDirection
- ?.direction == SortDirectionEnum.DESC
- ) {
- // Reverse if sorting descending
- pagingAdapter!!.size() - letterPosition - 1
- } else {
- letterPosition
- }
-
- jumpTo(jumpPosition)
- gridView.requestFocus()
- loadingProgressBar.hide()
- }
- }
- }
-
- override fun onUnbindViewHolder(viewHolder: ViewHolder) {
- (viewHolder.view as Button).text = null
- viewHolder.view.setOnClickListener(null)
- }
- },
- )
- alphabetAdapter.addAll(0, AlphabetSearchUtils.LETTERS.toList())
- alphabetFilterLayout.adapter = ItemBridgeAdapter(alphabetAdapter)
- }
- return root
- }
-
- @SuppressLint("SetTextI18n")
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- Log.v(TAG, "onViewCreated")
- super.onViewCreated(view, savedInstanceState)
-
- positionTextView = view.findViewById(R.id.position_text)
- totalCountTextView = view.findViewById(R.id.total_count_text)
- noResultsTextView = view.findViewById(R.id.no_results_text)
-
- val previousPosition =
- if (savedInstanceState == null) {
- Log.v(TAG, "onViewCreated savedInstanceState is null")
- -1
- } else {
- Log.v(TAG, "onViewCreated restoring")
- savedInstanceState.getInt("mSelectedPosition")
- }
- Log.v(TAG, "previousPosition=$previousPosition")
-
- if (pagingAdapter != null) {
- Log.v(TAG, "pagingAdapter.isInitialized")
- itemBridgeAdapter.setAdapter(pagingAdapter)
- if (selectedPosition != -1) {
- gridView.selectedPosition = selectedPosition
- }
- showOrHideViews()
- }
-
- viewModel.loadingStatus.observe(viewLifecycleOwner) { status ->
- when (status) {
- is StashGridViewModel.LoadingStatus.AdapterReady -> {
- val scrollToNextPage = viewModel.scrollToNextPage.value ?: false
- Log.v(
- TAG,
- "LoadingStatus.AdapterReady: previousPosition=$previousPosition, scrollToNextPage=$scrollToNextPage",
- )
- pagingAdapter = status.pagingAdapter
- itemBridgeAdapter.setAdapter(status.pagingAdapter)
- if (previousPosition > 0) {
- jumpTo(previousPosition)
- } else if (previousPosition < 0 && scrollToNextPage) {
- val page =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getInt("maxSearchResults", 25)
- jumpTo(page)
- // Only scroll the first time
- viewModel.scrollToNextPage.value = false
- }
- loadingProgressBar.hide()
- if (requestFocus) {
- gridView.requestFocus()
- }
- showOrHideViews()
- }
-
- StashGridViewModel.LoadingStatus.FirstPageLoaded -> {
- Log.v(TAG, "LoadingStatus.FirstPageLoaded")
- if (requestFocus) {
- gridView.requestFocus()
- }
-// firstPageListener?.invoke()
- }
-
- StashGridViewModel.LoadingStatus.Start -> {
- Log.v(TAG, "LoadingStatus.Start")
- loadingProgressBar.show()
- }
-
- StashGridViewModel.LoadingStatus.NoOp -> {
- Log.v(TAG, "LoadingStatus.NoOp")
-// viewModel.loadingStatus.removeObservers(viewLifecycleOwner)
- }
- }
- }
-
- prefBackPressScrollEnabled =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_back_button_scroll), true)
-
- if (prefBackPressScrollEnabled) {
- onBackPressedCallback =
- requireActivity().onBackPressedDispatcher.addCallback(
- viewLifecycleOwner,
- selectedPosition >= numberOfColumns,
- ) {
- jumpTo(0)
- isEnabled = false
- }
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- Log.v(TAG, "onSaveInstanceState")
- super.onSaveInstanceState(outState)
- outState.putInt("mSelectedPosition", selectedPosition)
- outState.putDataType(dataType)
- }
-
- override fun onViewStateRestored(savedInstanceState: Bundle?) {
- Log.v(TAG, "onViewStateRestored")
- super.onViewStateRestored(savedInstanceState)
- }
-
- override fun onDestroyView() {
- Log.v(TAG, "onDestroyView")
- super.onDestroyView()
- }
-
- private fun showOrHideViews() {
- val count = pagingAdapter?.size() ?: -1
- if (count <= 0) {
- positionTextView.text = getString(R.string.zero)
- noResultsTextView.animateToVisible()
- loadingProgressBar.hide()
- jumpButtonLayout.animateToInvisible(View.GONE)
- } else {
- setupJumpButtons(count)
- jumpButtonLayout.animateToVisible()
- noResultsTextView.animateToInvisible(View.GONE)
- }
- if (count > 0 && selectedPosition >= 0) {
- positionTextView.text = formatNumber(selectedPosition + 1, false)
- positionTextView
- }
- totalCountTextView.text =
- formatNumber(
- count,
- serverViewModel.requireServer().serverPreferences.abbreviateCounters,
- )
-
- if (count > 0 &&
- SortOption.isJumpSupported(
- dataType,
- viewModel.filterArgs.value!!
- .sortAndDirection.sort,
- )
- ) {
- alphabetFilterLayout.animateToVisible(400L)
- } else if (SortOption.isJumpSupported(dataType)) {
- // If the data type supports jump, let the buttons occupy space
- alphabetFilterLayout.animateToInvisible(View.INVISIBLE, 400L)
- } else {
- // If the data type doesn't support jumping, use the space for the grid
- alphabetFilterLayout.animateToInvisible(View.GONE, 400L)
- }
-
- val showFooter =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_show_grid_footer), true)
-
- if (showFooter) {
- footerLayout.animateToVisible()
- } else {
- footerLayout.visibility = View.GONE
- }
- }
-
- private fun setupJumpButtons(count: Int) {
- val columns = numberOfColumns
- val jump2 =
- if (count >= 25_000) {
- columns * 2000
- } else if (count >= 7_000) {
- columns * 200
- } else if (count >= 2_000) {
- columns * 50
- } else {
- columns * 20
- }
- val jump1 =
- if (count >= 25_000) {
- columns * 500
- } else if (count >= 7_000) {
- columns * 50
- } else if (count >= 2_000) {
- columns * 15
- } else {
- columns * 6
- }
- Log.v(TAG, "Setup jumps: $jump1 & $jump2, count=$count")
- jumpButtonLayout[0].setOnClickListener {
- jumpTo((selectedPosition - jump2).coerceIn(0, count - 1))
- }
- jumpButtonLayout[1].setOnClickListener {
- jumpTo((selectedPosition - jump1).coerceIn(0, count - 1))
- }
- jumpButtonLayout[2].setOnClickListener {
- jumpTo((selectedPosition + jump1).coerceIn(0, count - 1))
- }
- jumpButtonLayout[3].setOnClickListener {
- jumpTo((selectedPosition + jump2).coerceIn(0, count - 1))
- }
- jumpButtonLayout.forEach {
- it.onFocusChangeListener = backPressFocusChangeListener
- }
- }
-
- override fun onStart() {
- super.onStart()
-
- val browseFrameLayout =
- requireView().findViewById(androidx.leanback.R.id.grid_frame)
- browseFrameLayout.onFocusSearchListener =
- BrowseFrameLayout.OnFocusSearchListener { focused: View?, direction: Int ->
- if (focused != null && focused in alphabetFilterLayout) {
- if (direction == View.FOCUS_LEFT) {
- gridView
- } else {
- null
- }
- } else if (focused != null && focused in jumpButtonLayout) {
- if (direction == View.FOCUS_RIGHT) {
- gridView
- } else {
- null
- }
- } else if (direction == View.FOCUS_UP) {
- val filterButton = requireActivity().findViewById(R.id.filter_button)
- filterButton
- } else {
- null
- }
- }
- }
-
- fun cleanup() {
- pagingAdapter?.clearCache()
- }
-
- override fun onKeyLongPress(
- keyCode: Int,
- event: KeyEvent,
- ): Boolean {
- if (keyCode == KeyEvent.KEYCODE_BACK && prefBackPressScrollEnabled) {
- if (DEBUG) Log.d(TAG, "Long press back, maybe $selectedPosition=>$previousPosition")
- if (previousPosition >= 0 &&
- previousPosition != selectedPosition &&
- requireActivity().currentFocus is StashImageCardView
- ) {
- jumpTo(previousPosition)
- return true
- }
- }
- return super.onKeyLongPress(keyCode, event)
- }
-
- override fun onKeyUp(
- keyCode: Int,
- event: KeyEvent,
- ): Boolean {
- if (keyCode == KeyEvent.KEYCODE_D && event.isCtrlPressed) {
- requireParentFragment().childFragmentManager.commit {
- add(requireView().id, FilterDebugFragment())
- }
- return true
- }
- // If play is pressed and the page contains scenes or markers and user is focused on a card (ie not a button)
- if ((keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) &&
- (dataType == DataType.SCENE || dataType == DataType.MARKER) &&
- requireActivity().currentFocus is StashImageCardView
- ) {
- val position = selectedPosition
- val item = pagingAdapter?.get(position)
- if (item != null) {
- maybeStartPlayback(requireContext(), item)
- return true
- } else {
- return false
- }
- } else if (remoteButtonPaging &&
- keyCode in
- setOf(
- KeyEvent.KEYCODE_PAGE_UP,
- KeyEvent.KEYCODE_CHANNEL_UP,
- KeyEvent.KEYCODE_MEDIA_PREVIOUS,
- KeyEvent.KEYCODE_MEDIA_REWIND,
- KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD,
- ) &&
- requireActivity().currentFocus is StashImageCardView
- ) {
- jumpButtonLayout[1].callOnClick()
- return true
- } else if (remoteButtonPaging &&
- keyCode in
- setOf(
- KeyEvent.KEYCODE_PAGE_DOWN,
- KeyEvent.KEYCODE_CHANNEL_DOWN,
- KeyEvent.KEYCODE_MEDIA_NEXT,
- KeyEvent.KEYCODE_MEDIA_FAST_FORWARD,
- KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD,
- ) &&
- requireActivity().currentFocus is StashImageCardView
- ) {
- jumpButtonLayout[2].callOnClick()
- return true
- } else {
- return super.onKeyUp(keyCode, event)
- }
- }
-
- companion object {
- private const val TAG = "StashDataGridFragment"
-
- private const val DEBUG = false
- }
-
- inner class VerticalGridItemBridgeAdapter : ItemBridgeAdapter() {
- @SuppressLint("RestrictedApi")
- override fun onCreate(viewHolder: ViewHolder) {
- if (viewHolder.itemView is ViewGroup) {
- TransitionHelper.setTransitionGroup(
- viewHolder.itemView as ViewGroup,
- true,
- )
- }
- mShadowOverlayHelper.onViewCreated(viewHolder.itemView)
- }
-
- public override fun onBind(itemViewHolder: ViewHolder) {
- // Only when having an OnItemClickListener, we attach the OnClickListener.
- if (onItemViewClickedListener != null) {
- val itemView = itemViewHolder.viewHolder.view
- itemView.setOnClickListener {
- if (onItemViewClickedListener != null) {
- // Row is always null
- onItemViewClickedListener!!.onItemClicked(
- itemViewHolder.viewHolder,
- itemViewHolder.item,
- null,
- null,
- )
- }
- }
- }
- }
-
- public override fun onUnbind(viewHolder: ViewHolder) {
- if (onItemViewClickedListener != null) {
- viewHolder.viewHolder.view.setOnClickListener(null)
- }
- }
-
- public override fun onAttachedToWindow(viewHolder: ViewHolder) {
- viewHolder.itemView.isActivated = true
- }
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/StashExoPlayer.kt b/app/src/main/java/com/github/damontecres/stashapp/StashExoPlayer.kt
deleted file mode 100644
index 4abc242ef..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/StashExoPlayer.kt
+++ /dev/null
@@ -1,299 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.content.Context
-import android.util.Log
-import androidx.annotation.OptIn
-import androidx.media3.common.Player
-import androidx.media3.common.TrackSelectionParameters
-import androidx.media3.common.util.UnstableApi
-import androidx.media3.datasource.DefaultHttpDataSource
-import androidx.media3.datasource.okhttp.OkHttpDataSource
-import androidx.media3.exoplayer.DefaultRenderersFactory
-import androidx.media3.exoplayer.ExoPlayer
-import androidx.media3.exoplayer.analytics.AnalyticsListener
-import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
-import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
-import androidx.media3.exoplayer.util.EventLogger
-import androidx.media3.extractor.DefaultExtractorsFactory
-import androidx.media3.extractor.ts.TsExtractor
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.StashExoPlayer.Companion.getInstance
-import com.github.damontecres.stashapp.proto.PlaybackBackend
-import com.github.damontecres.stashapp.proto.PlaybackHttpClient
-import com.github.damontecres.stashapp.proto.PlaybackPreferences
-import com.github.damontecres.stashapp.proto.copy
-import com.github.damontecres.stashapp.util.Constants
-import com.github.damontecres.stashapp.util.SkipParams
-import com.github.damontecres.stashapp.util.StashClient
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.wholphin.mpv.MpvPlayer
-import timber.log.Timber
-
-/**
- * Manages a static [ExoPlayer] which might be reused between views
- */
-class StashExoPlayer private constructor() {
- companion object {
- private const val TAG = "StashExoPlayer"
-
- private val analyticsListeners: MutableList = mutableListOf()
- private val listeners: MutableList = mutableListOf()
-
- @Volatile
- private var instance: Player? = null // Volatile modifier is necessary
-
- @Volatile
- private var playbackPreferences: PlaybackPreferences? = null
-
- @OptIn(UnstableApi::class)
- fun getInstanceForCard(
- context: Context,
- server: StashServer,
- ): Player =
- getInstance(
- context,
- server,
- PlaybackPreferences
- .newBuilder()
- .apply {
- playbackBackend = PlaybackBackend.EXO_PLAYER
- skipForwardMs = 30_000
- skipBackwardMs = 30_000
- }.build(),
- )
-
- @OptIn(UnstableApi::class)
- fun getInstance(
- context: Context,
- server: StashServer,
- playbackPreferences: PlaybackPreferences,
- ): Player {
- if (instance == null || playbackPreferences != this.playbackPreferences) {
- synchronized(this) {
- // synchronized to avoid concurrency problem
- if (instance == null || playbackPreferences != this.playbackPreferences) {
- instance =
- createInstance(
- context,
- server,
- playbackPreferences,
- )
- }
- }
- }
- return instance!!
- }
-
- /**
- * Create a new [ExoPlayer] instance. [getInstance] should be preferred where possible.
- */
- @OptIn(UnstableApi::class)
- fun createInstance(
- context: Context,
- server: StashServer,
- playbackPreferences: PlaybackPreferences,
- ): Player {
- releasePlayer()
- Timber.i("backend=%s", playbackPreferences.playbackBackend)
- val skipParams =
- playbackPreferences.let {
- SkipParams.Values(
- it.skipForwardMs,
- it.skipBackwardMs,
- )
- }
- val httpClient = playbackPreferences.playbackHttpClient
- val debugLogging = playbackPreferences.debugLoggingEnabled
- this.playbackPreferences = playbackPreferences.copy {}
- return if (playbackPreferences.playbackBackend == PlaybackBackend.MPV) {
- MpvPlayer(
- context,
- playbackPreferences.mpvPreferences.hardwareDecoding,
- playbackPreferences.mpvPreferences.gpuNext,
- )
- } else {
- val dataSourceFactory =
- when (httpClient) {
- PlaybackHttpClient.OKHTTP -> {
- OkHttpDataSource
- .Factory(server.streamingOkHttpClient)
- }
-
- else -> {
- DefaultHttpDataSource
- .Factory()
- .setConnectTimeoutMs(5_000)
- .setReadTimeoutMs(30_000)
- .setUserAgent(StashClient.createUserAgent(context))
- .apply {
- if (server.apiKey.isNotNullOrBlank()) {
- setDefaultRequestProperties(mapOf(Constants.STASH_API_HEADER to server.apiKey))
- }
- }
- }
- }
- Log.d(TAG, "createInstance")
- val preferences = PreferenceManager.getDefaultSharedPreferences(context)
- val skipForward =
- when (skipParams) {
- is SkipParams.Default -> {
- preferences.getInt(
- context.getString(R.string.pref_key_skip_forward_time),
- 30,
- ) * 1000L
- }
-
- is SkipParams.Values -> {
- skipParams.skipForward
- }
- }
- val skipBack =
- when (skipParams) {
- is SkipParams.Default -> {
- preferences.getInt(
- context.getString(R.string.pref_key_skip_back_time),
- 10,
- ) * 1000L
- }
-
- is SkipParams.Values -> {
- skipParams.skipBack
- }
- }
- val trackSelector = DefaultTrackSelector(context)
- trackSelector.parameters =
- trackSelector
- .buildUponParameters()
- .setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
- .setAudioOffloadPreferences(
- TrackSelectionParameters.AudioOffloadPreferences
- .Builder()
- .setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
- .build(),
- ).build()
- val extractorsFactory =
- DefaultExtractorsFactory().apply {
- setTsExtractorTimestampSearchBytes(TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES * 3)
- setConstantBitrateSeekingEnabled(true)
- setConstantBitrateSeekingAlwaysEnabled(true)
- }
- ExoPlayer
- .Builder(context)
-// .setLoadControl(
-// DefaultLoadControl
-// .Builder()
-// .setBufferDurationsMs(
-// 5_000,
-// 30_000,
-// DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
-// DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
-// ).setTargetBufferBytes(64_000_000)
-// .setPrioritizeTimeOverSizeThresholds(false)
-// .build(),
-// )
- .setMediaSourceFactory(
- DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory),
- ).setRenderersFactory(
- DefaultRenderersFactory(context)
- .setEnableDecoderFallback(true)
- .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON),
- ).setSeekBackIncrementMs(skipBack)
- .setSeekForwardIncrementMs(skipForward)
- .setTrackSelector(trackSelector)
- .build()
- .also {
- if (debugLogging) {
- it.addAnalyticsListener(EventLogger())
- }
- }
- }
- }
-
- @OptIn(UnstableApi::class)
- fun releasePlayer() {
- if (instance != null) {
- synchronized(this) {
- // synchronized to avoid concurrency problem
- if (instance != null) {
- Log.d(TAG, "releasePlayer")
- removeListeners()
- instance!!.stop()
- if (!instance!!.isReleased) {
- instance!!.release()
- }
- instance = null
- playbackPreferences = null
- }
- }
- }
- }
-
- fun addListener(listener: Player.Listener) {
- if (instance == null) {
- Log.w(TAG, "Cannot add listener to null instance: $listener")
- } else if (listeners.contains(listener)) {
- Log.w(TAG, "Listener already added: $listener")
- } else {
- Log.v(TAG, "Added listener: $listener")
- listeners.add(listener)
- instance?.addListener(listener)
- }
- }
-
- fun addListener(listener: AnalyticsListener) {
- if (instance == null) {
- Log.w(TAG, "Cannot add listener to null instance: $listener")
- } else if (analyticsListeners.contains(listener)) {
- Log.w(TAG, "Listener already added: $listener")
- } else {
- Log.v(TAG, "Added listener: $listener")
- analyticsListeners.add(listener)
- (instance as? ExoPlayer)?.addAnalyticsListener(listener)
- }
- }
-
- fun removeListeners() {
- Log.v(
- TAG,
- "Removing ${listeners.size} listeners & ${analyticsListeners.size} analytics listeners",
- )
- listeners.forEach {
- instance?.removeListener(it)
- }
- listeners.clear()
- analyticsListeners.forEach {
- (instance as? ExoPlayer)?.removeAnalyticsListener(it)
- }
- analyticsListeners.clear()
- }
-
- fun removeListener(listener: Player.Listener) {
- if (listeners.remove(listener)) {
- instance?.removeListener(listener)
- Log.v(TAG, "Removed listener: $listener")
- } else {
- Log.w(TAG, "Listener was not added previously: $listener")
- }
- }
-
- fun removeListener(listener: AnalyticsListener) {
- if (analyticsListeners.remove(listener)) {
- (instance as? ExoPlayer)?.removeAnalyticsListener(listener)
- Log.v(TAG, "Removed listener: $listener")
- } else {
- Log.w(TAG, "Listener was not added previously: $listener")
- }
- }
- }
-}
-
-val Player.isReleased: Boolean
- @OptIn(UnstableApi::class)
- get() {
- return when (this) {
- is ExoPlayer -> isReleased
- is MpvPlayer -> isReleased
- else -> throw IllegalStateException("Unknown Player type: ${this::class.qualifiedName}")
- }
- }
diff --git a/app/src/main/java/com/github/damontecres/stashapp/StashGridControlsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/StashGridControlsFragment.kt
deleted file mode 100644
index c851611a0..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/StashGridControlsFragment.kt
+++ /dev/null
@@ -1,326 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.annotation.SuppressLint
-import android.os.Bundle
-import android.util.Log
-import android.view.KeyEvent
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.Button
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.viewModels
-import androidx.leanback.widget.OnItemViewClickedListener
-import androidx.leanback.widget.PresenterSelector
-import androidx.leanback.widget.SearchEditText
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.data.StashFindFilter
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.navigation.FilterAndPosition
-import com.github.damontecres.stashapp.navigation.NavigationOnItemViewClickedListener
-import com.github.damontecres.stashapp.presenters.ClassPresenterSelector
-import com.github.damontecres.stashapp.presenters.NullPresenter
-import com.github.damontecres.stashapp.presenters.NullPresenterSelector
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.DelegateKeyEventCallback
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.addExtraGridLongClicks
-import com.github.damontecres.stashapp.util.calculatePageSize
-import com.github.damontecres.stashapp.util.getFilterArgs
-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
-import com.github.damontecres.stashapp.views.TitleTransitionHelper
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import com.github.damontecres.stashapp.views.models.StashGridViewModel
-import com.google.android.material.switchmaterial.SwitchMaterial
-
-/**
- * A [Fragment] that shows a [StashDataGridFragment] along with controls for sorting, etc
- */
-class StashGridControlsFragment() :
- Fragment(),
- DelegateKeyEventCallback {
- private val serverViewModel: ServerViewModel by activityViewModels()
- private val viewModel: StashGridViewModel by viewModels()
-
- var currentFilter: FilterArgs
- get() = viewModel.filterArgs.value!!
- set(newFilter) = viewModel.setFilter(newFilter)
-
- // Views
- private lateinit var sortButton: Button
- private lateinit var playAllButton: Button
- private lateinit var filterButton: Button
- private lateinit var subContentSwitch: SwitchMaterial
- private lateinit var searchEditText: SearchEditText
-
- private lateinit var fragment: StashDataGridFragment
-
- override val keyEventDelegate: KeyEvent.Callback?
- get() = if (this::fragment.isInitialized) fragment else null
-
- private var remoteButtonPaging: Boolean = true
-
- // Arguments
- private lateinit var initialFilter: FilterArgs
-
- // State
- private lateinit var gridHeaderTransitionHelper: TitleTransitionHelper
-
- // Modifiable properties
-
- /**
- * Request that the grid receive focus in [onStart]
- */
- var requestFocus: Boolean = false
-
- /**
- * An optional name for this fragment, not used in this View
- */
- var name: String? = null
-
- /**
- * The presenter for the items, defaults to [StashPresenter.defaultClassPresenterSelector]
- */
- var presenterSelector: PresenterSelector = StashPresenter.defaultClassPresenterSelector()
-
- /**
- * The item clicked listener, will default to [NavigationOnItemViewClickedListener] in [onViewCreated] if not specified before
- */
- var onItemViewClickedListener: OnItemViewClickedListener? = null
-
- /**
- * Callback for when the sub content switch state is updated. Also ensures the switch will be displayed if not null
- */
- var subContentSwitchCheckedListener: ((isChecked: Boolean) -> Unit)? = null
-
- /**
- * Sets whether the sub content switch should start checked or not
- */
- var subContentSwitchInitialIsChecked: Boolean = false
-
- /**
- * The initial text on the sub content switch
- */
- var subContentText: CharSequence? = null
-
- /**
- * Notify when the grid is showing/hiding the header
- */
- var headerVisibilityListener: HeaderVisibilityListener? = null
-
- // Unmodifiable properties, current state
-
- /**
- * Type of items being displayed
- */
- val dataType: DataType
- get() = initialFilter.dataType
-
- var headerShowing: Boolean = true
- private set
-
- constructor(
- filterArgs: FilterArgs,
- ) : this() {
- this.initialFilter = filterArgs
- }
-
- constructor(
- dataType: DataType,
- findFilter: StashFindFilter? = null,
- objectFilter: StashDataFilter? = null,
- ) : this(FilterArgs(dataType, null, findFilter, objectFilter))
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- if (savedInstanceState != null) {
- name = savedInstanceState.getString(STATE_NAME)
- initialFilter = savedInstanceState.getFilterArgs(STATE_FILTER)!!
- Log.v(TAG, "sortAndDirection=${initialFilter.sortAndDirection}")
- }
-
- Log.v(TAG, "onCreate: dataType=$dataType")
-
- viewModel.init(
- NullPresenterSelector(presenterSelector, NullPresenter(dataType)),
- calculatePageSize(requireContext(), dataType),
- )
-
- remoteButtonPaging =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_remote_page_buttons), true)
- }
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
- val root =
- inflater.inflate(
- R.layout.stash_grid_controls,
- container,
- false,
- ) as ViewGroup
- val onFocusChangeListener = StashOnFocusChangeListener(requireContext())
- sortButton = root.findViewById(R.id.sort_button)
- sortButton.onFocusChangeListener = onFocusChangeListener
- playAllButton = root.findViewById(R.id.play_all_button)
- playAllButton.onFocusChangeListener = onFocusChangeListener
- filterButton = root.findViewById(R.id.filter_button)
- filterButton.onFocusChangeListener = onFocusChangeListener
- subContentSwitch = root.findViewById(R.id.sub_content_switch)
- subContentSwitch.onFocusChangeListener = onFocusChangeListener
- searchEditText = root.findViewById(R.id.search_edit_text)
-
- if (name == null) {
- name = getString(dataType.pluralStringId)
- }
-
- fragment =
- childFragmentManager.findFragmentById(R.id.grid_fragment) as StashDataGridFragment
- fragment.init(dataType)
- if (onItemViewClickedListener != null) {
- fragment.onItemViewClickedListener = onItemViewClickedListener
- }
- val gridHeader = root.findViewById(R.id.grid_header)
- gridHeaderTransitionHelper = TitleTransitionHelper(root, gridHeader)
-
- return root
- }
-
- @SuppressLint("SetTextI18n")
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- viewModel.currentPosition.observe(viewLifecycleOwner) { position ->
- val shouldShowTitle = position < fragment.numberOfColumns
- if (DEBUG) {
- Log.v(
- TAG,
- "showOrHideTitle: $shouldShowTitle, position=$position",
- )
- }
- showTitle(shouldShowTitle)
- }
-
- val filter =
- if (viewModel.filterArgs.isInitialized) {
- viewModel.filterArgs.value!!
- } else if (savedInstanceState != null) {
- initialFilter = savedInstanceState.getFilterArgs(STATE_FILTER)!!
- viewModel.setFilter(initialFilter)
- initialFilter
- } else {
- viewModel.setFilter(initialFilter)
- initialFilter
- }
-
- sortButton.nextFocusUpId = R.id.tab_layout
- SortButtonManager(StashServer.getCurrentServerVersion()) {
- viewModel.setFilter(currentFilter.with(it))
- }.setUpSortButton(sortButton, dataType, filter.sortAndDirection)
-
- val playAllListener =
- PlayAllOnClickListener(serverViewModel.navigationManager, dataType) {
- FilterAndPosition(viewModel.filterArgs.value!!, 0)
- }
- playAllButton.setOnClickListener(playAllListener)
-
- if (dataType.supportsPlaylists) {
- playAllButton.visibility = View.VISIBLE
- playAllButton.nextFocusUpId = R.id.tab_layout
- } else if (dataType == DataType.IMAGE) {
- playAllButton.visibility = View.VISIBLE
- playAllButton.nextFocusUpId = R.id.tab_layout
- playAllButton.text = getString(R.string.play_slideshow)
- }
-
- if (presenterSelector is ClassPresenterSelector) {
- addExtraGridLongClicks(presenterSelector as ClassPresenterSelector, dataType) {
- FilterAndPosition(
- viewModel.filterArgs.value!!,
- viewModel.currentPosition.value ?: -1,
- )
- }
- }
-
- filterButton.nextFocusUpId = R.id.tab_layout
- filterButton.setOnClickListener {
- serverViewModel.navigationManager.navigate(
- Destination.CreateFilter(
- dataType,
- viewModel.filterArgs.value!!,
- ),
- )
- }
-
- subContentSwitch.nextFocusUpId = R.id.tab_layout
- if (subContentSwitchCheckedListener != null) {
- subContentSwitch.isChecked = subContentSwitchInitialIsChecked
- subContentSwitch.text = subContentText
- subContentSwitch.visibility = View.VISIBLE
- subContentSwitch.setOnCheckedChangeListener { _, isChecked ->
- subContentSwitchCheckedListener?.invoke(isChecked)
- }
- }
-
- viewModel.setupSearch(searchEditText)
-
- val initialRequestFocus = fragment.requestFocus
- if (initialRequestFocus) {
- viewModel.searchBarFocus.observe(viewLifecycleOwner) { hasFocus ->
- // If the search text has focus, then the fragment shouldn't take it
- fragment.requestFocus = !hasFocus
- }
- }
- }
-
- override fun onSaveInstanceState(outState: Bundle) {
- super.onSaveInstanceState(outState)
- if (viewModel.filterArgs.isInitialized) {
- outState.putFilterArgs(STATE_FILTER, currentFilter)
- } else {
- outState.putFilterArgs(STATE_FILTER, initialFilter)
- }
- outState.putString(STATE_NAME, name)
- }
-
- private fun showTitle(show: Boolean) {
- headerShowing = show
- gridHeaderTransitionHelper.showTitle(show)
- headerVisibilityListener?.onHeaderVisibilityChanged(this, show)
- }
-
- override fun onDestroyView() {
- super.onDestroyView()
-// viewModel.clearCache()
- }
-
- companion object {
- private const val TAG = "StashGridControlsFragment"
-
- private const val STATE_FILTER = "currentFilter"
- private const val STATE_NAME = "name"
-
- private const val DEBUG = false
- }
-
- fun interface HeaderVisibilityListener {
- fun onHeaderVisibilityChanged(
- fragment: StashGridControlsFragment,
- headerShowing: Boolean,
- )
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/StashSearchFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/StashSearchFragment.kt
deleted file mode 100644
index 471f2b921..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/StashSearchFragment.kt
+++ /dev/null
@@ -1,146 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.text.TextUtils
-import android.widget.Toast
-import androidx.fragment.app.activityViewModels
-import androidx.leanback.app.SearchSupportFragment
-import androidx.leanback.widget.ArrayObjectAdapter
-import androidx.leanback.widget.HeaderItem
-import androidx.leanback.widget.ListRow
-import androidx.leanback.widget.ListRowPresenter
-import androidx.leanback.widget.ObjectAdapter
-import androidx.leanback.widget.SparseArrayObjectAdapter
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.MainFragment.Position
-import com.github.damontecres.stashapp.api.type.FindFilterType
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.data.StashFindFilter
-import com.github.damontecres.stashapp.data.toStashFindFilter
-import com.github.damontecres.stashapp.navigation.FilterAndPosition
-import com.github.damontecres.stashapp.navigation.NavigationOnItemViewClickedListener
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.QueryEngine
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-
-/**
- * Search for items regardless of data type
- */
-class StashSearchFragment :
- SearchSupportFragment(),
- SearchSupportFragment.SearchResultProvider {
- private val serverViewModel: ServerViewModel by activityViewModels()
-
- private var taskJob: Job? = null
-
- private val rowsAdapter = SparseArrayObjectAdapter(ListRowPresenter())
- private var currentPosition: Position = Position(0, 0)
- private var currentQuery: String? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setSearchResultProvider(this)
- setOnItemViewSelectedListener { itemViewHolder, item, rowViewHolder, row ->
- val rowNum = rowsAdapter.indexOf(row)
- val col =
- (rowViewHolder as ListRowPresenter.ViewHolder).gridView.selectedPosition
- val pos = Position(rowNum, col)
- currentPosition = pos
- }
- setOnItemViewClickedListener(
- NavigationOnItemViewClickedListener(serverViewModel.navigationManager) {
- val filter =
- FilterArgs(
- dataType = DataType.IMAGE,
- findFilter = StashFindFilter(q = currentQuery),
- )
- FilterAndPosition(filter, currentPosition.column)
- },
- )
- }
-
- override fun getResultsAdapter(): ObjectAdapter = rowsAdapter
-
- override fun onQueryTextChange(newQuery: String): Boolean {
- taskJob?.cancel()
- taskJob =
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val searchDelay =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getInt("searchDelay", 500)
- delay(searchDelay.toLong())
- search(newQuery)
- }
- return true
- }
-
- override fun onQueryTextSubmit(query: String): Boolean {
- taskJob?.cancel()
- taskJob =
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- search(query)
- }
- return true
- }
-
- private fun search(query: String) {
- if (!TextUtils.isEmpty(query) && query != currentQuery) {
- rowsAdapter.clear()
- currentQuery = query
- val perPage =
- PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getInt("maxSearchResults", 25)
- val filter =
- FindFilterType(
- q = Optional.present(query),
- per_page = Optional.present(perPage),
- page = Optional.present(1),
- )
- val queryEngine = QueryEngine(StashServer.requireCurrentServer())
- DataType.entries.forEach {
- val adapter = ArrayObjectAdapter(StashPresenter.defaultClassPresenterSelector())
- rowsAdapter.set(
- it.ordinal,
- ListRow(HeaderItem(getString(it.pluralStringId)), adapter),
- )
-
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler { ex ->
- Toast.makeText(
- requireContext(),
- "Search for ${getString(it.pluralStringId)} failed: ${ex.message}",
- Toast.LENGTH_LONG,
- )
- },
- ) {
- val results = queryEngine.find(it, filter)
- if (results.isNotEmpty()) {
- adapter.addAll(0, results)
- val viewAll =
- FilterArgs(
- dataType = it,
- findFilter = filter.toStashFindFilter(),
- )
- adapter.add(viewAll)
- } else {
- rowsAdapter.clear(it.ordinal)
- }
- }
- }
- }
- }
-
- companion object {
- const val TAG = "StashSearchFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/StudioDetailsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/StudioDetailsFragment.kt
deleted file mode 100644
index 546deaf16..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/StudioDetailsFragment.kt
+++ /dev/null
@@ -1,117 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.api.fragment.StudioData
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashGlide
-import com.github.damontecres.stashapp.util.showSetRatingToast
-import com.github.damontecres.stashapp.views.models.StudioViewModel
-import com.github.damontecres.stashapp.views.parseTimeToString
-import kotlinx.coroutines.launch
-
-/**
- * Details for a studio
- */
-class StudioDetailsFragment : DetailsFragment() {
- private val viewModel: StudioViewModel by viewModels(ownerProducer = { requireParentFragment() })
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- viewModel.item.observe(viewLifecycleOwner) { studio ->
- if (studio == null) {
- return@observe
- } else {
- updateUi(studio)
- }
- }
- }
-
- private fun updateUi(studio: StudioData) {
- updateFavorite(studio.favorite)
-
- ratingBar.rating100 = studio.rating100 ?: 0
- ratingBar.setRatingCallback { newRating100 ->
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val newStudio = mutationEngine.updateStudio(studio.id, rating100 = newRating100)
- if (newStudio != null) {
- showSetRatingToast(requireContext(), newRating100)
- viewModel.update(newStudio)
- }
- }
- }
-
- favoriteButton.setOnClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val newStudio = mutationEngine.updateStudio(studio.id, !studio.favorite)
- if (newStudio != null) {
- if (newStudio.favorite) {
- Toast
- .makeText(
- requireContext(),
- "Studio favorited",
- Toast.LENGTH_SHORT,
- ).show()
- }
- viewModel.update(newStudio)
- }
- }
- }
- if (studio.image_path != null) {
- StashGlide
- .with(requireContext(), studio.image_path)
- .optionalFitCenter()
- .error(StashPresenter.glideError(requireContext()))
- .into(imageView)
- }
-
- table.removeAllViews()
-
- addRow(R.string.stashapp_details, studio.details)
- addRow(R.string.stashapp_parent_studio, studio.parent_studio?.name) {
- if (studio.parent_studio != null) {
- setTextColor(resources.getColorStateList(R.color.clickable_text, null))
- setOnClickListener {
- serverViewModel.navigationManager.navigate(
- Destination.Item(
- DataType.STUDIO,
- studio.parent_studio.id,
- ),
- )
- }
- }
- }
-
- if (studio.aliases.isNotEmpty()) {
- addRow(
- R.string.stashapp_aliases,
- studio.aliases.joinToString(", "),
- )
- }
-
- addRow(R.string.stashapp_created_at, parseTimeToString(studio.created_at))
- addRow(R.string.stashapp_updated_at, parseTimeToString(studio.updated_at))
- if (PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_show_playback_debug_info), false)
- ) {
- addRow(R.string.id, studio.id)
- }
- table.setColumnShrinkable(1, true)
- }
-
- companion object {
- private const val TAG = "StudioDetailsFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/StudioFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/StudioFragment.kt
deleted file mode 100644
index badb7c8f1..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/StudioFragment.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.GalleryFilterType
-import com.github.damontecres.stashapp.api.type.GroupFilterType
-import com.github.damontecres.stashapp.api.type.HierarchicalMultiCriterionInput
-import com.github.damontecres.stashapp.api.type.ImageFilterType
-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.SceneMarkerFilterType
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.api.type.StudioFilterType
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.data.StashFindFilter
-import com.github.damontecres.stashapp.suppliers.DataSupplierOverride
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.PageFilterKey
-import com.github.damontecres.stashapp.util.StashFragmentPagerAdapter
-import com.github.damontecres.stashapp.util.getUiTabs
-import com.github.damontecres.stashapp.views.models.StudioViewModel
-
-class StudioFragment : TabbedFragment(DataType.STUDIO.name) {
- private val viewModel: StudioViewModel by viewModels()
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- serverViewModel.currentServer.observe(viewLifecycleOwner) {
- viewModel.init(requireArguments())
- }
- serverViewModel
- .withLiveData(viewModel.item)
- .observe(viewLifecycleOwner) { (server, studio) ->
- if (studio == null) {
- Toast
- .makeText(
- requireContext(),
- "Studio '${viewModel.itemId}' not found",
- Toast.LENGTH_LONG,
- ).show()
- serverViewModel.navigationManager.goBack()
- return@observe
- }
- tabViewModel.title.value = studio.name
-
- if (!tabViewModel.tabs.isInitialized) {
- tabViewModel.tabs.value =
- listOf(
- StashFragmentPagerAdapter.PagerEntry(getString(R.string.stashapp_details)) {
- StudioDetailsFragment()
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.SCENE) {
- createStashGridFragment(
- studio.id,
- DataType.SCENE,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.STUDIO_SCENES).findFilter,
- ) { studios ->
- SceneFilterType(studios = studios)
- }
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.GALLERY) {
- createStashGridFragment(
- studio.id,
- DataType.GALLERY,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.STUDIO_GALLERIES).findFilter,
- ) { studios ->
- GalleryFilterType(studios = studios)
- }
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.IMAGE) {
- createStashGridFragment(
- studio.id,
- DataType.IMAGE,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.STUDIO_IMAGES).findFilter,
- ) { studios ->
- ImageFilterType(studios = studios)
- }
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.PERFORMER) {
- createStashGridFragment(
- studio.id,
- DataType.PERFORMER,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.STUDIO_PERFORMERS).findFilter,
- ) { studios ->
- PerformerFilterType(studios = studios)
- }
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.GROUP) {
- createStashGridFragment(
- studio.id,
- DataType.GROUP,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.STUDIO_GROUPS).findFilter,
- ) { studios ->
- GroupFilterType(studios = studios)
- }
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.TAG) {
- StashGridControlsFragment(
- FilterArgs(
- DataType.TAG,
- override = DataSupplierOverride.StudioTags(studio.id),
- ),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(getString(R.string.stashapp_subsidiary_studios)) {
- StashGridControlsFragment(
- dataType = DataType.STUDIO,
- findFilter =
- server.serverPreferences
- .getDefaultPageFilter(
- PageFilterKey.STUDIO_CHILDREN,
- ).findFilter,
- objectFilter =
- StudioFilterType(
- parents =
- Optional.present(
- MultiCriterionInput(
- value = Optional.present(listOf(studio.id)),
- modifier = CriterionModifier.INCLUDES,
- ),
- ),
- ),
- )
- },
- StashFragmentPagerAdapter.PagerEntry(DataType.MARKER) {
- createStashGridFragment(
- studio.id,
- DataType.MARKER,
- null,
- ) { studios ->
- SceneMarkerFilterType(
- scene_filter =
- Optional.present(
- SceneFilterType(studios = studios),
- ),
- )
- }
- },
- ).filter { it.title in getUiTabs(requireContext(), DataType.STUDIO) }
- }
- }
- }
-
- private fun createStashGridFragment(
- studioId: String,
- dataType: DataType,
- defaultFindFilter: StashFindFilter?,
- createObjectFilter: (Optional) -> StashDataFilter,
- ): StashGridControlsFragment {
- val fragment =
- StashGridControlsFragment(
- dataType = dataType,
- findFilter = defaultFindFilter,
- objectFilter = createObjectFilter(createCriterionInput(false, studioId)),
- )
- fragment.subContentSwitchInitialIsChecked = false
- fragment.subContentText = getString(R.string.stashapp_include_sub_studio_content)
- fragment.subContentSwitchCheckedListener = { isChecked ->
- val newFilter =
- fragment.currentFilter.copy(
- objectFilter =
- createObjectFilter(
- createCriterionInput(isChecked, studioId),
- ),
- )
- fragment.currentFilter = newFilter
- }
- return fragment
- }
-
- private fun createCriterionInput(
- includeSub: Boolean,
- studioId: String,
- ): Optional.Present =
- Optional.present(
- HierarchicalMultiCriterionInput(
- value = Optional.present(listOf(studioId)),
- modifier = CriterionModifier.INCLUDES,
- depth = Optional.present(if (includeSub) -1 else 0),
- ),
- )
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/TabbedFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/TabbedFragment.kt
deleted file mode 100644
index b7bd05dc9..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/TabbedFragment.kt
+++ /dev/null
@@ -1,151 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.util.Log
-import android.view.KeyEvent
-import android.view.View
-import android.widget.TextView
-import androidx.core.content.edit
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.fragment.app.viewModels
-import androidx.leanback.tab.LeanbackTabLayout
-import androidx.leanback.tab.LeanbackViewPager
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.util.DelegateKeyEventCallback
-import com.github.damontecres.stashapp.util.StashFragmentPagerAdapter
-import com.github.damontecres.stashapp.util.animateToInvisible
-import com.github.damontecres.stashapp.util.animateToVisible
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import com.github.damontecres.stashapp.views.models.TabbedGridViewModel
-import com.google.android.material.tabs.TabLayout
-
-/**
- * A [Fragment] that displays multiple tabs
- */
-abstract class TabbedFragment(
- private val tabKey: String,
-) : Fragment(R.layout.tabbed_grid_view),
- DelegateKeyEventCallback,
- StashGridControlsFragment.HeaderVisibilityListener {
- private lateinit var viewPager: LeanbackViewPager
- protected val serverViewModel by activityViewModels()
- protected val tabViewModel by viewModels()
-
- private lateinit var tabLayout: LeanbackTabLayout
- private lateinit var adapter: StashFragmentPagerAdapter
- private var currentTabPosition = 0
- private var firstTime = true
-
- private val fragments = mutableMapOf()
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
- val rememberTab =
- preferences.getBoolean(getString(R.string.pref_key_ui_remember_tab), false)
- val rememberTabKey = getString(R.string.pref_key_ui_remember_tab) + ".$tabKey"
- val rememberedTabIndex = if (rememberTab) preferences.getInt(rememberTabKey, 0) else 0
-
- tabViewModel.tabs.observe(viewLifecycleOwner) { pages ->
- adapter = StashFragmentPagerAdapter(pages, childFragmentManager)
- adapter.fragmentCreatedListener = { fragment, position ->
- if (fragment is StashGridControlsFragment) {
- fragment.headerVisibilityListener = this@TabbedFragment
- }
- fragments[position] = fragment
- }
- viewPager.adapter = adapter
- if (savedInstanceState == null && tabLayout.childCount > 0 && firstTime) {
- tabLayout.getChildAt(0).requestFocus()
- }
-
- tabLayout.addOnTabSelectedListener(
- object : TabLayout.OnTabSelectedListener {
- override fun onTabSelected(tab: TabLayout.Tab) {
- for (i in 0..(R.id.grid_title)
- tabViewModel.title.observe(viewLifecycleOwner) {
- gridTitle.text = it
- }
-
- viewPager = view.findViewById(R.id.view_pager)
- tabLayout = view.findViewById(R.id.tab_layout)
- tabLayout.setupWithViewPager(viewPager)
- // Hide tabLayout if needed
- fragments.values.forEach {
- if (it is StashGridControlsFragment) {
- it.headerVisibilityListener = this
- if (!it.headerShowing) {
- Log.v(TAG, "Making tabLayout gone")
- tabLayout.visibility = View.GONE
- }
- }
- }
- }
-
- override val keyEventDelegate: KeyEvent.Callback?
- get() {
- val tabIndex = currentTabPosition
- val fragment = fragments[tabIndex]
- return if (fragment != null && fragment is KeyEvent.Callback) {
- fragment
- } else {
- null
- }
- }
-
- override fun onHeaderVisibilityChanged(
- fragment: StashGridControlsFragment,
- headerShowing: Boolean,
- ) {
- if (fragment == fragments[currentTabPosition]) {
-// Log.v(TAG, "onHeaderVisibilityChanged: headerShowing=$headerShowing")
- if (headerShowing) {
- tabLayout.animateToVisible()
- } else {
- tabLayout.animateToInvisible(View.GONE)
- }
- }
- }
-
- companion object {
- private const val TAG = "TabbedFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/TagDetailsFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/TagDetailsFragment.kt
deleted file mode 100644
index 1c8d56625..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/TagDetailsFragment.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.PreferenceManager
-import com.github.damontecres.stashapp.api.fragment.TagData
-import com.github.damontecres.stashapp.presenters.StashPresenter
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashGlide
-import com.github.damontecres.stashapp.views.models.TagViewModel
-import com.github.damontecres.stashapp.views.parseTimeToString
-import kotlinx.coroutines.launch
-
-/**
- * Details for a tag
- */
-class TagDetailsFragment : DetailsFragment() {
- private val viewModel: TagViewModel by viewModels(ownerProducer = { requireParentFragment() })
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- // Tags do not have a rating
- ratingBar.visibility = View.GONE
-
- viewModel.item.observe(viewLifecycleOwner) { tag ->
- if (tag == null) {
- return@observe
- } else {
- updateUi(tag)
- }
- }
- }
-
- private fun updateUi(tag: TagData) {
- updateFavorite(tag.favorite)
-
- favoriteButton.setOnClickListener {
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler()) {
- val newTag = mutationEngine.setTagFavorite(tag.id, !tag.favorite)
- if (newTag != null) {
- if (newTag.favorite) {
- Toast
- .makeText(
- requireContext(),
- "Tag favorited",
- Toast.LENGTH_SHORT,
- ).show()
- }
- viewModel.update(newTag)
- }
- }
- }
- if (tag.image_path != null) {
- StashGlide
- .with(requireContext(), tag.image_path)
- .optionalFitCenter()
- .error(StashPresenter.glideError(requireContext()))
- .into(imageView)
- }
-
- table.removeAllViews()
-
- addRow(R.string.stashapp_sort_name, tag.sort_name)
- addRow(R.string.stashapp_description, tag.description)
-
- if (tag.aliases.isNotEmpty()) {
- addRow(
- R.string.stashapp_aliases,
- tag.aliases.joinToString(", "),
- )
- }
-
- addRow(R.string.stashapp_created_at, parseTimeToString(tag.created_at))
- addRow(R.string.stashapp_updated_at, parseTimeToString(tag.updated_at))
- if (PreferenceManager
- .getDefaultSharedPreferences(requireContext())
- .getBoolean(getString(R.string.pref_key_show_playback_debug_info), false)
- ) {
- addRow(R.string.id, tag.id)
- }
- table.setColumnShrinkable(1, true)
- }
-
- companion object {
- private const val TAG = "TagDetailsFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/TagFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/TagFragment.kt
deleted file mode 100644
index 9cf4a9edf..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/TagFragment.kt
+++ /dev/null
@@ -1,181 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import android.widget.Toast
-import androidx.fragment.app.viewModels
-import com.apollographql.apollo.api.Optional
-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.StashDataFilter
-import com.github.damontecres.stashapp.api.type.StudioFilterType
-import com.github.damontecres.stashapp.api.type.TagFilterType
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.data.StashFindFilter
-import com.github.damontecres.stashapp.util.PageFilterKey
-import com.github.damontecres.stashapp.util.StashFragmentPagerAdapter.PagerEntry
-import com.github.damontecres.stashapp.util.getUiTabs
-import com.github.damontecres.stashapp.views.models.StashGridViewModel
-import com.github.damontecres.stashapp.views.models.TagViewModel
-
-class TagFragment : TabbedFragment(DataType.TAG.name) {
- private val viewModel: TagViewModel by viewModels()
- private val stashGridViewModel: StashGridViewModel by viewModels()
-
- private var includeSubTags = false
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
-
- serverViewModel.currentServer.observe(viewLifecycleOwner) {
- viewModel.init(requireArguments())
- }
- serverViewModel
- .withLiveData(viewModel.item)
- .observe(viewLifecycleOwner) { (server, tag) ->
- if (tag == null) {
- Toast
- .makeText(
- requireContext(),
- "Tag '${viewModel.itemId}' not found",
- Toast.LENGTH_LONG,
- ).show()
- serverViewModel.navigationManager.goBack()
- return@observe
- }
- tabViewModel.title.value = tag.name
-
- if (!tabViewModel.tabs.isInitialized) {
- tabViewModel.tabs.value =
- listOf(
- PagerEntry(getString(R.string.stashapp_details)) {
- TagDetailsFragment()
- },
- PagerEntry(DataType.SCENE) {
- createStashGridFragment(
- tag.id,
- dataType = DataType.SCENE,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.TAG_SCENES).findFilter,
- ) { tags ->
- SceneFilterType(tags = tags)
- }
- },
- PagerEntry(DataType.GALLERY) {
- createStashGridFragment(
- tag.id,
- dataType = DataType.GALLERY,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.TAG_GALLERIES).findFilter,
- ) { tags ->
- GalleryFilterType(tags = tags)
- }
- },
- PagerEntry(DataType.IMAGE) {
- createStashGridFragment(
- tag.id,
- dataType = DataType.IMAGE,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.TAG_IMAGES).findFilter,
- ) { tags ->
- ImageFilterType(tags = tags)
- }
- },
- PagerEntry(DataType.MARKER) {
- createStashGridFragment(
- tag.id,
- dataType = DataType.MARKER,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.TAG_MARKERS).findFilter,
- ) { tags ->
- SceneMarkerFilterType(tags = tags)
- }
- },
- PagerEntry(DataType.PERFORMER) {
- createStashGridFragment(
- tag.id,
- dataType = DataType.PERFORMER,
- server.serverPreferences.getDefaultPageFilter(PageFilterKey.TAG_PERFORMERS).findFilter,
- ) { tags ->
- PerformerFilterType(tags = tags)
- }
- },
- PagerEntry(DataType.STUDIO) {
- createStashGridFragment(
- tag.id,
- dataType = DataType.STUDIO,
- null,
- ) { tags ->
- StudioFilterType(tags = tags)
- }
- },
- PagerEntry(getString(R.string.stashapp_sub_tags)) {
- createStashGridFragment(
- tag.id,
- dataType = DataType.TAG,
- null,
- ) { tags ->
- TagFilterType(parents = tags)
- }
- },
- PagerEntry(getString(R.string.stashapp_parent_tags)) {
- StashGridControlsFragment(
- dataType = DataType.TAG,
- objectFilter =
- TagFilterType(
- children =
- createCriterionInput(
- false,
- tag.id,
- ),
- ),
- )
- },
- ).filter { it.title in getUiTabs(requireContext(), DataType.TAG) }
- }
- }
- }
-
- private fun createStashGridFragment(
- tagId: String,
- dataType: DataType,
- defaultFindFilter: StashFindFilter?,
- createObjectFilter: (Optional) -> StashDataFilter,
- ): StashGridControlsFragment {
- val fragment =
- StashGridControlsFragment(
- dataType = dataType,
- findFilter = defaultFindFilter,
- objectFilter = createObjectFilter(createCriterionInput(includeSubTags, tagId)),
- )
- fragment.subContentSwitchInitialIsChecked = includeSubTags
- fragment.subContentText = getString(R.string.stashapp_include_sub_tag_content)
- fragment.subContentSwitchCheckedListener = { isChecked ->
- val newFilter =
- fragment.currentFilter.copy(
- objectFilter =
- createObjectFilter(
- createCriterionInput(isChecked, tagId),
- ),
- )
- fragment.currentFilter = newFilter
- }
- return fragment
- }
-
- private fun createCriterionInput(
- subTags: Boolean,
- tagId: String,
- ): Optional.Present =
- Optional.present(
- HierarchicalMultiCriterionInput(
- value = Optional.present(listOf(tagId)),
- modifier = CriterionModifier.INCLUDES_ALL,
- depth = Optional.present(if (subTags) -1 else 0),
- ),
- )
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/UpdateAppFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/UpdateAppFragment.kt
deleted file mode 100644
index 84c349cf3..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/UpdateAppFragment.kt
+++ /dev/null
@@ -1,118 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.widget.Toast
-import androidx.appcompat.content.res.AppCompatResources
-import androidx.fragment.app.activityViewModels
-import androidx.leanback.app.GuidedStepSupportFragment
-import androidx.leanback.widget.GuidanceStylist
-import androidx.leanback.widget.GuidedAction
-import androidx.lifecycle.lifecycleScope
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.ui.pages.DownloadCallback
-import com.github.damontecres.stashapp.util.Release
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.UpdateChecker
-import com.github.damontecres.stashapp.util.Version
-import com.github.damontecres.stashapp.util.getDestination
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.stashapp.util.joinNotNullOrBlank
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import kotlinx.coroutines.launch
-
-class UpdateAppFragment : GuidedStepSupportFragment() {
- private val serverViewModel: ServerViewModel by activityViewModels()
-
- private lateinit var release: Release
-
- override fun onCreate(savedInstanceState: Bundle?) {
- release = requireArguments().getDestination().release
- super.onCreate(savedInstanceState)
- }
-
- override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance {
- val installedVersion = UpdateChecker.getInstalledVersion(requireActivity())
- val serverVersion = StashServer.getCurrentServerVersion()
- val description =
- buildList {
- add("${getString(R.string.stashapp_package_manager_installed_version)}: $installedVersion")
- if (!Version.isServerSupportedByAppVersion(serverVersion, release.version)) {
- add("Warning!! This update does not support the current server's version $serverVersion!!")
- }
- addAll(release.notes)
- }.joinNotNullOrBlank("\n\n")
-
- return GuidanceStylist.Guidance(
- "${getString(R.string.stashapp_package_manager_latest_version)}: ${release.version}",
- description,
- null,
- AppCompatResources.getDrawable(requireContext(), R.drawable.stash_logo),
- )
- }
-
- override fun onCreateActions(
- actions: MutableList,
- savedInstanceState: Bundle?,
- ) {
- super.onCreateActions(actions, savedInstanceState)
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(GuidedAction.ACTION_ID_YES)
- .title("Download & Install")
- .description("${release.version}")
- .hasNext(false)
- .build(),
- )
- if (release.body.isNotNullOrBlank()) {
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(1000L)
- .title("See changelog")
- .hasNext(false)
- .build(),
- )
- }
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .clickAction(GuidedAction.ACTION_ID_CANCEL)
- .hasNext(false)
- .build(),
- )
- }
-
- override fun onGuidedActionClicked(action: GuidedAction) {
- if (action.id == GuidedAction.ACTION_ID_YES) {
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler {
- Toast.makeText(
- requireContext(),
- "Update failed: ${it.message}",
- Toast.LENGTH_LONG,
- )
- },
- ) {
- UpdateChecker.installRelease(
- requireActivity(),
- release,
- object : DownloadCallback {
- override fun contentLength(contentLength: Long) {
- }
-
- override fun bytesDownloaded(bytes: Long) {
- }
- },
- )
- }
- } else if (action.id == 1000L) {
- serverViewModel.navigationManager.navigate(Destination.ReleaseChangelog(release))
- } else {
- finishGuidedStepSupportFragments()
- }
- }
-
- override fun onProvideTheme(): Int = R.style.Theme_StashAppAndroidTV_GuidedStep
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/UpdateChangelogFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/UpdateChangelogFragment.kt
deleted file mode 100644
index f3eb4c242..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/UpdateChangelogFragment.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.github.damontecres.stashapp
-
-import android.os.Bundle
-import android.view.View
-import android.widget.TextView
-import androidx.fragment.app.Fragment
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.util.getDestination
-import io.noties.markwon.Markwon
-
-class UpdateChangelogFragment : Fragment(R.layout.changelog) {
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- val release = requireArguments().getDestination().release
- val changelogText =
- "# ${release.version}\n\n${release.body}"
- // Convert PR urls to number
- .replace(
- Regex("https://github.com/damontecres/StashAppAndroidTV/pull/(\\d+)"),
- "#$1",
- )
- // Remove the last line for full changelog since its just a link
- .replace(Regex("\\*\\*Full Changelog\\*\\*.*"), "")
- val textView = view.findViewById(R.id.changelog_text)
- val markdown = Markwon.create(requireContext())
- markdown.setMarkdown(textView, changelogText)
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/data/Scene.kt b/app/src/main/java/com/github/damontecres/stashapp/data/Scene.kt
index e7396736d..779692a82 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/data/Scene.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/data/Scene.kt
@@ -28,6 +28,7 @@ data class Scene(
val oCounter: Int?,
val captionUrl: String?,
val captions: List,
+ val studioImageUrl: String?,
) {
val durationPosition get() = duration?.times(1000L)?.toLong()
@@ -61,6 +62,7 @@ data class Scene(
oCounter = data.o_counter,
captionUrl = data.paths.caption,
captions = data.captions?.map { it.caption }.orEmpty(),
+ studioImageUrl = data.studio?.studioData?.image_path,
)
}
@@ -91,6 +93,7 @@ data class Scene(
oCounter = data.o_counter,
captionUrl = data.paths.caption,
captions = data.captions?.map { it.caption }.orEmpty(),
+ studioImageUrl = data.studio?.image_path,
)
}
@@ -129,6 +132,7 @@ data class Scene(
oCounter = video.o_counter,
captionUrl = video.paths.caption,
captions = video.captions?.map { it.caption }.orEmpty(),
+ studioImageUrl = video.studio?.image_path,
)
}
}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/AppModule.kt b/app/src/main/java/com/github/damontecres/stashapp/di/AppModule.kt
new file mode 100644
index 000000000..e8ef1308a
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/AppModule.kt
@@ -0,0 +1,95 @@
+package com.github.damontecres.stashapp.di
+
+import android.app.Application
+import android.content.Context
+import android.os.Build
+import androidx.datastore.core.DataStore
+import androidx.room.Room
+import co.touchlab.kermit.Logger
+import com.github.damontecres.stashapp.R
+import com.github.damontecres.stashapp.data.room.AppDatabase
+import com.github.damontecres.stashapp.data.room.MIGRATION_4_TO_5
+import com.github.damontecres.stashapp.di.server.ServerInterceptor
+import com.github.damontecres.stashapp.di.server.ServerRepository
+import com.github.damontecres.stashapp.proto.StashPreferences
+import com.github.damontecres.stashapp.util.joinNotNullOrBlank
+import com.github.damontecres.stashapp.util.joinValueNotNull
+import com.github.damontecres.stashapp.util.preferences
+import okhttp3.OkHttpClient
+import org.koin.core.annotation.ComponentScan
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Named
+import org.koin.core.annotation.Single
+
+@Module
+@ComponentScan("com.github.damontecres.stashapp")
+class AppModule
+
+@Named
+annotation class StandardHttpClient
+
+@Named
+annotation class AuthHttpClient
+
+@Single
+@StandardHttpClient
+fun provideStandardHttpClient(context: Context): OkHttpClient {
+ val userAgent = createUserAgent(context)
+ Logger.v { "User-Agent=$userAgent" }
+ return OkHttpClient
+ .Builder()
+ .addInterceptor {
+ it.proceed(
+ it
+ .request()
+ .newBuilder()
+ .header("User-Agent", userAgent)
+ .build(),
+ )
+ }.build()
+}
+
+@Single
+@AuthHttpClient
+fun provideAuthHttpClient(
+ @StandardHttpClient httpClient: OkHttpClient,
+ serverRepository: ServerRepository,
+): OkHttpClient =
+ httpClient
+ .newBuilder()
+ .addInterceptor {
+ val server = serverRepository.currentServer.value.server
+ ServerInterceptor(server).intercept(it)
+ }.build()
+
+fun createUserAgent(context: Context): String {
+ val appName = context.getString(R.string.app_name)
+ val versionStr = context.packageManager.getPackageInfo(context.packageName, 0).versionName
+ val comments =
+ listOf(
+ joinValueNotNull("os", Build.VERSION.BASE_OS),
+ joinValueNotNull("release", Build.VERSION.RELEASE),
+ "sdk/${Build.VERSION.SDK_INT}",
+ ).joinNotNullOrBlank("; ")
+ val device =
+ listOf(
+ Build.MANUFACTURER,
+ Build.MODEL,
+ if (Build.MODEL != Build.PRODUCT) Build.PRODUCT else null,
+ Build.DEVICE,
+ ).joinNotNullOrBlank("; ")
+ return "$appName/$versionStr ($comments) ($device)"
+}
+
+@Single
+fun provideDataStore(context: Context): DataStore = context.preferences
+
+@Single
+fun provideDatabase(context: Application): AppDatabase {
+ val dbName = context.getString(R.string.app_name)
+ return Room
+ .databaseBuilder(context, AppDatabase::class.java, dbName)
+ .addMigrations(MIGRATION_4_TO_5)
+ .fallbackToDestructiveMigration()
+ .build()
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/MutationEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt
similarity index 82%
rename from app/src/main/java/com/github/damontecres/stashapp/util/MutationEngine.kt
rename to app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt
index bdec9c15f..363356708 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/util/MutationEngine.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt
@@ -1,7 +1,6 @@
-package com.github.damontecres.stashapp.util
+package com.github.damontecres.stashapp.di.server
-import android.util.Log
-import com.apollographql.apollo.ApolloClient
+import co.touchlab.kermit.Logger
import com.apollographql.apollo.api.ApolloResponse
import com.apollographql.apollo.api.Mutation
import com.apollographql.apollo.api.Optional
@@ -60,20 +59,19 @@ import com.github.damontecres.stashapp.api.type.StudioUpdateInput
import com.github.damontecres.stashapp.api.type.TagCreateInput
import com.github.damontecres.stashapp.api.type.TagUpdateInput
import com.github.damontecres.stashapp.data.OCounter
+import com.github.damontecres.stashapp.util.toSeconds
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import java.util.concurrent.atomic.AtomicInteger
+import org.koin.core.annotation.Single
/**
* Class for sending graphql mutations
*/
+@Single
class MutationEngine(
- apolloClient: ApolloClient,
- server: StashServer?,
-) : StashEngine(apolloClient, server) {
- constructor(server: StashServer) : this(server.apolloClient, server)
-
- private val readOnlyMode = readOnlyModeEnabled()
+ api: StashApi,
+) : StashEngine(api) {
+ var readOnlyMode = false
suspend fun executeMutation(
mutation: Mutation,
@@ -84,12 +82,12 @@ class MutationEngine(
throw IllegalStateException("Read only mode enabled!")
}
val mutationName = mutation.name()
- val id = MUTATION_ID.getAndIncrement()
+ val id = getRequestId()
- Log.v(TAG, "executeMutation $id $mutationName start")
+ Logger.v { "executeMutation $id $mutationName start" }
val response = client.mutation(mutation).execute()
if (response.data != null) {
- Log.v(TAG, "executeMutation $id $mutationName successful")
+ Logger.v { "executeMutation $id $mutationName successful" }
return@withContext response
} else if (response.exception != null) {
throw createException(id, mutationName, response.exception!!) { msg, ex ->
@@ -97,7 +95,7 @@ class MutationEngine(
}
} else {
val errorMsgs = response.errors!!.joinToString("\n") { it.message }
- Log.e(TAG, "Errors in $id $mutationName: ${response.errors}")
+ Logger.e { "Errors in $id $mutationName: ${response.errors}" }
throw MutationException(id, mutationName, "Error in $mutationName: $errorMsgs")
}
}
@@ -116,10 +114,9 @@ class MutationEngine(
): Boolean {
val resumeTime = position / 1000.0
val playDuration = if (duration != null && duration >= 1) duration / 1000.0 else null
- Log.v(
- TAG,
- "SceneSaveActivity sceneId=$sceneId, position=$position, playDuration=$playDuration",
- )
+ Logger.v {
+ "SceneSaveActivity sceneId=$sceneId, position=$position, playDuration=$playDuration"
+ }
val mutation =
SceneSaveActivityMutation(
scene_id = sceneId,
@@ -131,59 +128,48 @@ class MutationEngine(
}
suspend fun incrementPlayCount(sceneId: String): Int {
- Log.v(TAG, "incrementPlayCount on $sceneId")
+ Logger.v { "incrementPlayCount on $sceneId" }
val mutation = SceneAddPlayCountMutation(sceneId, emptyList())
val result = executeMutation(mutation, true)
return result.data!!.sceneAddPlay.count
}
- private fun getServerBoolean(
- preferenceKey: String,
- defValue: Boolean = false,
- ): Optional =
- Optional.presentIfNotNull(
- serverPreferences?.preferences?.getBoolean(
- preferenceKey,
- defValue,
- ),
- )
-
- suspend fun triggerScan(): String {
+ suspend fun triggerScan(serverPreferences: ServerPreferences): String {
+ val config = serverPreferences.scanConfig
val mutation =
MetadataScanMutation(
ScanMetadataInput(
- scanGenerateClipPreviews = getServerBoolean(ServerPreferences.PREF_SCAN_GENERATE_CLIP_PREVIEWS),
- scanGenerateCovers =
- getServerBoolean(
- ServerPreferences.PREF_SCAN_GENERATE_COVERS,
- true,
- ),
- scanGeneratePhashes = getServerBoolean(ServerPreferences.PREF_SCAN_GENERATE_PHASHES),
- scanGeneratePreviews = getServerBoolean(ServerPreferences.PREF_SCAN_GENERATE_PREVIEWS),
- scanGenerateSprites = getServerBoolean(ServerPreferences.PREF_SCAN_GENERATE_SPRITES),
- scanGenerateThumbnails = getServerBoolean(ServerPreferences.PREF_SCAN_GENERATE_THUMBNAILS),
- scanGenerateImagePreviews = getServerBoolean(ServerPreferences.PREF_SCAN_GENERATE_IMAGE_PREVIEWS),
+ scanGenerateCovers = Optional.present(config.scanGenerateCovers),
+ scanGeneratePreviews = Optional.present(config.scanGeneratePreviews),
+ scanGenerateImagePreviews = Optional.present(config.scanGenerateImagePreviews),
+ scanGenerateSprites = Optional.present(config.scanGenerateSprites),
+ scanGeneratePhashes = Optional.present(config.scanGeneratePhashes),
+ scanGenerateImagePhashes = Optional.present(config.scanGeneratePhashes),
+ scanGenerateThumbnails = Optional.present(config.scanGenerateThumbnails),
+ scanGenerateClipPreviews = Optional.present(config.scanGenerateClipPreviews),
),
)
return executeMutation(mutation).data!!.metadataScan
}
- suspend fun triggerGenerate(): String {
+ suspend fun triggerGenerate(serverPreferences: ServerPreferences): String {
+ val config = serverPreferences.generateConfig
val mutation =
MetadataGenerateMutation(
GenerateMetadataInput(
- clipPreviews = getServerBoolean(ServerPreferences.PREF_GEN_CLIP_PREVIEWS),
- covers = getServerBoolean(ServerPreferences.PREF_GEN_COVERS, true),
- imagePreviews = getServerBoolean(ServerPreferences.PREF_GEN_IMAGE_PREVIEWS),
- interactiveHeatmapsSpeeds = getServerBoolean(ServerPreferences.PREF_GEN_INTERACTIVE_HEATMAPS_SPEEDS),
- markerImagePreviews = getServerBoolean(ServerPreferences.PREF_GEN_MARKER_IMAGE_PREVIEWS),
- markers = getServerBoolean(ServerPreferences.PREF_GEN_MARKERS, true),
- markerScreenshots = getServerBoolean(ServerPreferences.PREF_GEN_MARKER_SCREENSHOTS),
- phashes = getServerBoolean(ServerPreferences.PREF_GEN_PHASHES, true),
- previews = getServerBoolean(ServerPreferences.PREF_GEN_PREVIEWS, true),
- sprites = getServerBoolean(ServerPreferences.PREF_GEN_SPRITES, true),
- transcodes = getServerBoolean(ServerPreferences.PREF_GEN_TRANSCODES),
- imageThumbnails = getServerBoolean(ServerPreferences.PREF_GEN_IMAGE_THUMBNAILS),
+ covers = Optional.present(config.covers),
+ sprites = Optional.present(config.sprites),
+ previews = Optional.present(config.previews),
+ imagePreviews = Optional.present(config.imagePreviews),
+ markers = Optional.present(config.markers),
+ markerImagePreviews = Optional.present(config.markerImagePreviews),
+ markerScreenshots = Optional.present(config.markerScreenshots),
+ transcodes = Optional.present(config.transcodes),
+ phashes = Optional.present(config.phashes),
+ interactiveHeatmapsSpeeds = Optional.present(config.interactiveHeatmapsSpeeds),
+ imagePhashes = Optional.present(config.phashes),
+ imageThumbnails = Optional.present(config.imageThumbnails),
+ clipPreviews = Optional.present(config.clipPreviews),
),
)
return executeMutation(mutation).data!!.metadataGenerate
@@ -193,7 +179,7 @@ class MutationEngine(
sceneId: String,
tagIds: List,
): SceneUpdateMutation.SceneUpdate? {
- Log.v(TAG, "setTagsOnScene sceneId=$sceneId, tagIds=$tagIds")
+ Logger.v { "setTagsOnScene sceneId=$sceneId, tagIds=$tagIds" }
val mutation =
SceneUpdateMutation(
input =
@@ -210,7 +196,7 @@ class MutationEngine(
sceneId: String,
groupIds: List,
): SceneUpdateMutation.SceneUpdate? {
- Log.v(TAG, "setGroupsOnScene sceneId=$sceneId, tagIds=$groupIds")
+ Logger.v { "setGroupsOnScene sceneId=$sceneId, tagIds=$groupIds" }
val mutation =
SceneUpdateMutation(
input =
@@ -242,7 +228,7 @@ class MutationEngine(
primaryTagId: String,
tagIds: List,
): FullMarkerData? {
- Log.v(TAG, "setTagsOnMarker markerId=$markerId, primaryTagId=$primaryTagId, tagIds=$tagIds")
+ Logger.v { "setTagsOnMarker markerId=$markerId, primaryTagId=$primaryTagId, tagIds=$tagIds" }
return updateMarker(
SceneMarkerUpdateInput(
id = markerId,
@@ -256,7 +242,7 @@ class MutationEngine(
sceneId: String,
performerIds: List,
): SceneUpdateMutation.SceneUpdate? {
- Log.v(TAG, "setPerformersOnScene sceneId=$sceneId, performerIds=$performerIds")
+ Logger.v { "setPerformersOnScene sceneId=$sceneId, performerIds=$performerIds" }
val mutation =
SceneUpdateMutation(
input =
@@ -273,7 +259,7 @@ class MutationEngine(
sceneId: String,
studioId: String?,
): SceneUpdateMutation.SceneUpdate? {
- Log.v(TAG, "setStudioOnScene sceneId=$sceneId, studioId=$studioId")
+ Logger.v { "setStudioOnScene sceneId=$sceneId, studioId=$studioId" }
val mutation =
SceneUpdateMutation(
input =
@@ -290,7 +276,7 @@ class MutationEngine(
sceneId: String,
galleryIds: List,
): SceneUpdateMutation.SceneUpdate? {
- Log.v(TAG, "setGalleriesOnScene sceneId=$sceneId, galleryIds=$galleryIds")
+ Logger.v { "setGalleriesOnScene sceneId=$sceneId, galleryIds=$galleryIds" }
val mutation =
SceneUpdateMutation(
input =
@@ -349,7 +335,7 @@ class MutationEngine(
sceneId: String,
rating100: Int,
): SceneUpdateMutation.SceneUpdate? {
- Log.v(TAG, "setRating sceneId=$sceneId, rating=$rating100")
+ Logger.v { "setRating sceneId=$sceneId, rating=$rating100" }
val mutation =
SceneUpdateMutation(
input =
@@ -366,10 +352,9 @@ class MutationEngine(
deleteFiles: Boolean,
deleteGenerated: Boolean,
): Boolean {
- Log.v(
- TAG,
- "deleteScene: sceneId=$sceneId, deleteFiles=$deleteFiles, deleteGenerated=$deleteGenerated",
- )
+ Logger.v {
+ "deleteScene: sceneId=$sceneId, deleteFiles=$deleteFiles, deleteGenerated=$deleteGenerated"
+ }
val mutation =
DeleteSceneMutation(
input =
@@ -564,12 +549,6 @@ class MutationEngine(
return executeMutation(mutation).data?.groupUpdate?.groupData
}
- companion object {
- const val TAG = "MutationEngine"
-
- private val MUTATION_ID = AtomicInteger(0)
- }
-
open class MutationException(
id: Int,
mutationName: String,
diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/QueryEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt
similarity index 95%
rename from app/src/main/java/com/github/damontecres/stashapp/util/QueryEngine.kt
rename to app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt
index 5ff9bd357..89ce1d54a 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/util/QueryEngine.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt
@@ -1,8 +1,6 @@
-package com.github.damontecres.stashapp.util
+package com.github.damontecres.stashapp.di.server
-import android.util.Log
import com.apollographql.apollo.ApolloCall
-import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.api.ApolloResponse
import com.apollographql.apollo.api.Operation
import com.apollographql.apollo.api.Optional
@@ -61,10 +59,12 @@ import com.github.damontecres.stashapp.api.type.StudioFilterType
import com.github.damontecres.stashapp.api.type.TagFilterType
import com.github.damontecres.stashapp.data.DataType
import com.github.damontecres.stashapp.data.JobResult
+import com.github.damontecres.stashapp.util.getRandomSort
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
-import java.util.concurrent.atomic.AtomicInteger
+import org.koin.core.annotation.Single
+import timber.log.Timber
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@@ -72,21 +72,19 @@ import kotlin.time.toDuration
/**
* Handles making graphql queries to the server
*/
+@Single
class QueryEngine(
- apolloClient: ApolloClient,
- server: StashServer?,
-) : StashEngine(apolloClient, server) {
- constructor(server: StashServer) : this(server.apolloClient, server)
-
+ api: StashApi,
+) : StashEngine(api) {
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")
+ val id = getRequestId()
+ Timber.v("executeQuery $id $queryName")
val response = query.execute()
if (response.data != null) {
- Log.v(TAG, "executeQuery $id $queryName successful")
+ Timber.v("executeQuery $id $queryName successful")
return@withContext response
} else if (response.exception != null) {
throw createException(id, queryName, response.exception!!) { msg, ex ->
@@ -94,7 +92,7 @@ class QueryEngine(
}
} else {
val errorMessages = response.errors!!.joinToString("\n") { it.message }
- Log.e(TAG, "Errors in $id $queryName: ${response.errors}")
+ Timber.e("Errors in $id $queryName: ${response.errors}")
throw QueryException(id, queryName, "Error in $queryName: $errorMessages")
}
}
@@ -417,7 +415,7 @@ class QueryEngine(
DataType.MARKER -> findMarkers(findFilter, useRandom = useRandom)
DataType.IMAGE -> findImages(findFilter, useRandom = useRandom)
DataType.GALLERY -> findGalleries(findFilter, useRandom = useRandom)
- }
+ } as List
/**
* Search for a type of data with the given query. Users will need to cast the returned List.
@@ -437,8 +435,8 @@ class QueryEngine(
DataType.GROUP -> findGroups(groupIds = ids)
DataType.IMAGE -> findImages(ids = ids)
DataType.GALLERY -> findGalleries(galleryIds = ids)
- DataType.MARKER -> throw UnsupportedOperationException("Cannot query markers by ID") // TODO: Unsupported by the server
- }
+ DataType.MARKER -> findMarkers(markerIds = ids)
+ } as List
}
suspend fun getSavedFilter(filterId: String): SavedFilter? {
@@ -500,7 +498,7 @@ class QueryEngine(
): FindFilterType? =
if (filter != null) {
if (useRandom && filter.sort.getOrNull()?.startsWith("random") == true) {
- Log.v(TAG, "Updating random filter")
+ Timber.v("Updating random filter")
filter.copy(sort = Optional.present("random_" + getRandomSort()))
} else {
filter
@@ -511,8 +509,6 @@ class QueryEngine(
companion object {
private const val TAG = "QueryEngine"
-
- private val QUERY_ID = AtomicInteger(0)
}
open class QueryException(
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerInterceptor.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerInterceptor.kt
new file mode 100644
index 000000000..bfb04c762
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerInterceptor.kt
@@ -0,0 +1,34 @@
+package com.github.damontecres.stashapp.di.server
+
+import com.github.damontecres.stashapp.util.Constants
+import okhttp3.Interceptor
+import okhttp3.Response
+
+class ServerInterceptor(
+ private val server: StashServer,
+) : Interceptor {
+ override fun intercept(chain: Interceptor.Chain): Response {
+ val request =
+ if (server.apiKey != null) {
+ val isStashUrl =
+ chain
+ .request()
+ .url
+ .toString()
+ .startsWith(server.serverRoot)
+ if (isStashUrl) {
+ // Only set the API Key if the target URL is the stash server
+ chain
+ .request()
+ .newBuilder()
+ .addHeader(Constants.STASH_API_HEADER, server.cleanedApiKey!!)
+ .build()
+ } else {
+ chain.request()
+ }
+ } else {
+ chain.request()
+ }
+ return chain.proceed(request)
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerPreferences.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerPreferences.kt
new file mode 100644
index 000000000..33109c27d
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerPreferences.kt
@@ -0,0 +1,376 @@
+package com.github.damontecres.stashapp.di.server
+
+import android.content.Context
+import co.touchlab.kermit.Logger
+import com.github.damontecres.stashapp.R
+import com.github.damontecres.stashapp.api.ConfigurationQuery
+import com.github.damontecres.stashapp.data.DataType
+import com.github.damontecres.stashapp.suppliers.FilterArgs
+import com.github.damontecres.stashapp.util.FilterParser
+import com.github.damontecres.stashapp.util.PageFilterKey
+import com.github.damontecres.stashapp.util.Version
+import com.github.damontecres.stashapp.util.clear
+import com.github.damontecres.stashapp.util.getCaseInsensitive
+import com.github.damontecres.stashapp.util.isNotNullOrBlank
+import com.github.damontecres.stashapp.util.parseDictionary
+import com.github.damontecres.stashapp.util.plugin.CompanionPluginService
+import dev.b3nedikt.restring.Restring
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.jsonObject
+import java.util.EnumSet
+
+data class ServerPreferences(
+ val version: Version = Version(0, 0, 0),
+ val companionPluginVersion: Version? = null,
+ val menuItems: EnumSet = EnumSet.allOf(DataType::class.java),
+// val uiConfiguration: Map<*, *> = emptyMap(),
+ val trackActivity: Boolean = true,
+ val showStudioAsText: Boolean = false,
+ val minimumPlayPercent: Int = 0,
+ val ratingsAsStars: Boolean = true,
+ val starPrecision: Float = 1f,
+ val alwaysStartFromBeginning: Boolean = false,
+ val abbreviateCounters: Boolean = false,
+ val sfwMode: Boolean = false,
+ val customLocalesEnabled: Boolean = false,
+ val customLocales: String? = null,
+ val defaultFilters: Map = emptyMap(),
+ val defaultPageFilters: Map = emptyMap(),
+ val frontPageContent: List> = emptyList(),
+ val scanConfig: ConfigurationQuery.Scan =
+ ConfigurationQuery.Scan(
+ scanGenerateClipPreviews = false,
+ scanGenerateCovers = false,
+ scanGenerateImagePreviews = false,
+ scanGeneratePhashes = false,
+ scanGeneratePreviews = false,
+ scanGenerateSprites = false,
+ scanGenerateThumbnails = false,
+ ),
+ val generateConfig: ConfigurationQuery.Generate =
+ ConfigurationQuery.Generate(
+ clipPreviews = false,
+ covers = false,
+ imagePreviews = false,
+ interactiveHeatmapsSpeeds = false,
+ markerImagePreviews = false,
+ markers = false,
+ markerScreenshots = false,
+ phashes = false,
+ previews = false,
+ sprites = false,
+ transcodes = false,
+ imageThumbnails = false,
+ ),
+) {
+ val companionPluginInstalled: Boolean get() = companionPluginVersion != null
+
+ companion object {
+ fun createServerPreferences(config: ConfigurationQuery.Data): ServerPreferences {
+ val serverVersion = Version.tryFromString(config.version.version) ?: Version(0, 0, 0)
+ Logger.i { "updatePreferences for server version $serverVersion" }
+
+ val companionPluginVersion =
+ config.plugins
+ ?.firstOrNull { it.id == CompanionPluginService.PLUGIN_ID }
+ ?.version
+ ?.let { Version.tryFromString(it) }
+ val menuItems =
+ config.configuration.`interface`.menuItems
+ ?.map(String::lowercase)
+ ?.mapNotNull {
+ when (it) {
+ "scenes" -> DataType.SCENE
+ "groups", "movies" -> DataType.GROUP
+ "markers" -> DataType.MARKER
+ "performers" -> DataType.PERFORMER
+ "studios" -> DataType.STUDIO
+ "tags" -> DataType.TAG
+ "images" -> DataType.IMAGE
+ "galleries" -> DataType.GALLERY
+ else -> null
+ }
+ }?.let { EnumSet.copyOf(it) } ?: EnumSet.allOf(DataType::class.java)
+ val showStudioAsText = config.configuration.`interface`.showStudioAsText ?: false
+ val customLocalesEnabled =
+ config.configuration.`interface`.customLocalesEnabled == true &&
+ config.configuration.`interface`.customLocales
+ .isNotNullOrBlank()
+ val customLocales = config.configuration.`interface`.customLocales
+ val sfwMode = config.configuration.`interface`.sfwContentMode
+
+ var serverPreferences =
+ ServerPreferences(
+ version = serverVersion,
+ companionPluginVersion = companionPluginVersion,
+ menuItems = menuItems,
+ showStudioAsText = showStudioAsText,
+ customLocalesEnabled = customLocalesEnabled,
+ customLocales = customLocales,
+ sfwMode = sfwMode,
+ )
+
+ val ui = config.configuration.ui
+ if (ui !is Map<*, *>) {
+ Logger.w { "config.configuration.ui is not a map" }
+ } else {
+ val taskDefaults = ui.getCaseInsensitive("taskDefaults") as Map?
+ try {
+ val scanConfig = parseScan(config.configuration.defaults.scan, taskDefaults)
+ serverPreferences = serverPreferences.copy(scanConfig = scanConfig)
+ } catch (ex: Exception) {
+ Logger.e(ex) { "Exception during scan parsing" }
+ }
+
+ try {
+ val generateConfig = parseGenerate(config.configuration.defaults.generate, taskDefaults)
+ serverPreferences = serverPreferences.copy(generateConfig = generateConfig)
+ } catch (ex: Exception) {
+ Logger.e(ex) { "Exception during generate parsing" }
+ }
+
+ serverPreferences = readUIConfig(serverPreferences, ui)
+ }
+ return serverPreferences
+ }
+
+ private fun readUIConfig(
+ serverPreferences: ServerPreferences,
+ ui: Map<*, *>,
+ ): ServerPreferences {
+ val frontPageContent = ui.getCaseInsensitive("frontPageContent") as? List>
+ var serverPreferences = serverPreferences
+ val trackActivity =
+ ui
+ .getCaseInsensitive("trackActivity")
+ ?.toString()
+ ?.toBoolean() ?: if (serverPreferences.version.isAtLeast(Version.V0_26_0)) {
+ // If server is >=0.26.0 and doesn't provide a value, default to true
+ // See https://github.com/stashapp/stash/pull/4710
+ true
+ } else {
+ // Server <0.26.0 default to false
+ false
+ }
+
+ val minimumPlayPercent =
+ ui.getCaseInsensitive("minimumPlayPercent")?.toString()?.toIntOrNull() ?: 0
+ val ratingSystemOptionsRaw = ui.getCaseInsensitive("ratingSystemOptions")
+
+ try {
+ val ratingSystemOptions = ratingSystemOptionsRaw as Map?
+ val ratingType = ratingSystemOptions?.getCaseInsensitive("type") ?: "stars"
+ val starPrecision =
+ when (
+ ratingSystemOptions?.getCaseInsensitive("starPrecision")?.lowercase()
+ ) {
+ "full" -> 1.0f
+ "half" -> 0.5f
+ "quarter" -> 0.25f
+ "tenth" -> 0.1f
+ else -> 1.0f
+ }
+ serverPreferences =
+ serverPreferences.copy(
+ ratingsAsStars = ratingType == "stars",
+ starPrecision = starPrecision,
+ )
+ } catch (ex: Exception) {
+ Logger.e(ex) { "Exception parsing ratingSystemOptions: $ratingSystemOptionsRaw" }
+ }
+ val alwaysStartFromBeginning = ui.getCaseInsensitive("alwaysStartFromBeginning")?.toString()?.toBoolean() ?: false
+ val abbreviateCounters = ui.getCaseInsensitive("abbreviateCounters") as Boolean? ?: false
+
+ val defaultFilters = ui.getCaseInsensitive("defaultFilters") as Map?
+ val filterParser = FilterParser(serverPreferences.version)
+ val filters =
+ DataType.entries.associateWith { dataType ->
+ val filterMap =
+ defaultFilters?.getCaseInsensitive(dataType.filterMode.name) as Map?
+ val filter =
+ if (filterMap != null) {
+ try {
+ filterParser.convertFilterMap(dataType, filterMap)
+ } catch (ex: Exception) {
+ Logger.w(ex) { "default filter parse error for $dataType" }
+ FilterArgs(dataType)
+ }
+ } else {
+ FilterArgs(dataType)
+ }
+ filter
+ }
+
+ val pageFilters =
+ PageFilterKey.entries.associateWith { key ->
+ val filterMap =
+ defaultFilters?.getCaseInsensitive(key.prefKey) as Map?
+ if (filterMap != null) {
+ try {
+ filterParser.convertFilterMap(key.dataType, filterMap, false)
+ } catch (ex: Exception) {
+ Logger.w(ex) { "default filter parse error for $key" }
+ FilterArgs(key.dataType)
+ }
+ } else {
+ FilterArgs(key.dataType)
+ }
+ }
+
+ return serverPreferences.copy(
+ frontPageContent = frontPageContent.orEmpty(),
+ trackActivity = trackActivity,
+ minimumPlayPercent = minimumPlayPercent,
+ alwaysStartFromBeginning = alwaysStartFromBeginning,
+ abbreviateCounters = abbreviateCounters,
+ defaultFilters = filters,
+ defaultPageFilters = pageFilters,
+ )
+ }
+
+ private fun parseScan(
+ defaultScan: ConfigurationQuery.Scan?,
+ uiTaskDefaults: Map?,
+ ): ConfigurationQuery.Scan {
+ val scan = uiTaskDefaults?.getCaseInsensitive("scan") as Map?
+ val scanGenerateCovers =
+ scan.getBoolean("scanGenerateCovers", defaultScan?.scanGenerateCovers, false)
+ val scanGeneratePreviews =
+ scan.getBoolean("scanGeneratePreviews", defaultScan?.scanGeneratePreviews, false)
+ val scanGenerateImagePreviews =
+ scan.getBoolean(
+ "scanGenerateImagePreviews",
+ defaultScan?.scanGenerateImagePreviews,
+ false,
+ )
+ val scanGenerateSprites =
+ scan.getBoolean("scanGenerateSprites", defaultScan?.scanGenerateSprites, false)
+ val scanGeneratePhashes =
+ scan.getBoolean("scanGeneratePhashes", defaultScan?.scanGeneratePhashes, false)
+ val scanGenerateThumbnails =
+ scan.getBoolean(
+ "scanGenerateThumbnails",
+ defaultScan?.scanGenerateThumbnails,
+ false,
+ )
+ val scanGenerateClipPreviews =
+ scan.getBoolean(
+ "scanGenerateClipPreviews",
+ defaultScan?.scanGenerateClipPreviews,
+ false,
+ )
+ return ConfigurationQuery.Scan(
+ scanGenerateClipPreviews = scanGenerateClipPreviews,
+ scanGenerateCovers = scanGenerateCovers,
+ scanGenerateImagePreviews = scanGenerateImagePreviews,
+ scanGeneratePhashes = scanGeneratePhashes,
+ scanGeneratePreviews = scanGeneratePreviews,
+ scanGenerateSprites = scanGenerateSprites,
+ scanGenerateThumbnails = scanGenerateThumbnails,
+ )
+ }
+
+ private fun parseGenerate(
+ defaultGenerate: ConfigurationQuery.Generate?,
+ uiTaskDefaults: Map?,
+ ): ConfigurationQuery.Generate {
+ val generate = uiTaskDefaults?.getCaseInsensitive("generate") as Map?
+ val clipPreviews =
+ generate.getBoolean("clipPreviews", defaultGenerate?.clipPreviews, false)
+ val covers = generate.getBoolean("covers", defaultGenerate?.covers, false)
+ val imagePreviews =
+ generate.getBoolean("imagePreviews", defaultGenerate?.imagePreviews, false)
+ val interactiveHeatmapsSpeeds =
+ generate.getBoolean(
+ "interactiveHeatmapsSpeeds",
+ defaultGenerate?.interactiveHeatmapsSpeeds,
+ false,
+ )
+ val markerImagePreviews =
+ generate.getBoolean(
+ "markerImagePreviews",
+ defaultGenerate?.markerImagePreviews,
+ false,
+ )
+ val markers = generate.getBoolean("markers", defaultGenerate?.markers, false)
+ val markerScreenshots =
+ generate.getBoolean("markerScreenshots", defaultGenerate?.markerScreenshots, false)
+ val phashes = generate.getBoolean("phashes", defaultGenerate?.phashes, false)
+ val previews = generate.getBoolean("previews", defaultGenerate?.previews, false)
+ val sprites = generate.getBoolean("sprites", defaultGenerate?.sprites, false)
+ val transcodes = generate.getBoolean("transcodes", defaultGenerate?.transcodes, false)
+ val imageThumbnails =
+ generate.getBoolean("imageThumbnails", defaultGenerate?.imageThumbnails, false)
+ return ConfigurationQuery.Generate(
+ clipPreviews = clipPreviews,
+ covers = covers,
+ imagePreviews = imagePreviews,
+ interactiveHeatmapsSpeeds = interactiveHeatmapsSpeeds,
+ markerImagePreviews = markerImagePreviews,
+ markers = markers,
+ markerScreenshots = markerScreenshots,
+ phashes = phashes,
+ previews = previews,
+ sprites = sprites,
+ transcodes = transcodes,
+ imageThumbnails = imageThumbnails,
+ )
+ }
+
+ private fun Map?.getBoolean(
+ key: String,
+ defaultsValue: Boolean?,
+ default: Boolean,
+ ): Boolean = this?.getCaseInsensitive(key)?.toString()?.toBoolean() ?: defaultsValue ?: default
+ }
+
+ fun restring(context: Context) {
+ val useRestring = sfwMode || (customLocalesEnabled && customLocales.isNotNullOrBlank())
+ if (useRestring) {
+ Restring.clear()
+ try {
+ if (sfwMode) {
+ Restring.putStrings(
+ Restring.locale,
+ mapOf(
+ "stashapp_stats_total_o_count" to
+ context.getString(R.string.stashapp_stats_total_o_count_sfw),
+ "stashapp_o_count" to
+ context.getString(R.string.stashapp_o_count_sfw),
+ "stashapp_last_o_at" to
+ context.getString(R.string.stashapp_last_o_at_sfw),
+ ),
+ )
+ }
+ if (customLocalesEnabled && customLocales.isNotNullOrBlank()) {
+ val root = Json.parseToJsonElement(customLocales)
+ if (root is JsonObject) {
+ val map =
+ parseDictionary(
+ root.jsonObject,
+ listOf("stashapp"),
+ false,
+ ).associate { it.key to it.value }
+ Restring.putStrings(Restring.locale, map)
+
+ if (root.containsKey("StashAppAndroidTV") && root.jsonObject["StashAppAndroidTV"] is JsonObject) {
+ val appMap =
+ parseDictionary(
+ root.jsonObject["StashAppAndroidTV"]!!.jsonObject,
+ listOf(),
+ true,
+ ).associate { it.key to it.value }
+ Restring.putStrings(Restring.locale, appMap)
+ }
+ }
+ }
+ } catch (ex: Exception) {
+ Logger.w(ex) { "Error parsing custom locales" }
+ Restring.clear()
+ }
+ } else {
+ Restring.clear()
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerRepository.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerRepository.kt
new file mode 100644
index 000000000..2dc6310d6
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerRepository.kt
@@ -0,0 +1,151 @@
+package com.github.damontecres.stashapp.di.server
+
+import android.content.Context
+import androidx.core.content.edit
+import androidx.datastore.core.DataStore
+import androidx.preference.PreferenceManager
+import com.github.damontecres.stashapp.proto.StashPreferences
+import com.github.damontecres.stashapp.util.isNotNullOrBlank
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.update
+import org.koin.core.annotation.Single
+import java.util.concurrent.ConcurrentHashMap
+
+@Single
+class ServerRepository(
+ private val context: Context,
+ private val api: StashApi,
+ private val preferences: DataStore,
+) {
+ val currentServer = MutableStateFlow(CurrentServer.UNSET)
+ val currentServerVersion get() = currentServer.value.serverPreferences.version
+
+ private val servers = ConcurrentHashMap()
+
+ suspend fun restore(): Boolean {
+ val manager = PreferenceManager.getDefaultSharedPreferences(context)
+ val url = manager.getString(PREF_STASH_URL, null)
+ val apiKey = manager.getString(PREF_STASH_API_KEY, null)
+ return if (url.isNotNullOrBlank()) {
+ addAndSwitchServer(StashServer(url, apiKey))
+ true
+ } else {
+ false
+ }
+ }
+
+ suspend fun findConfiguredStashServer(context: Context): StashServer? {
+ val manager = PreferenceManager.getDefaultSharedPreferences(context)
+ val url = manager.getString(PREF_STASH_URL, null)
+ val apiKey = manager.getString(PREF_STASH_API_KEY, null)
+ return if (url.isNotNullOrBlank()) {
+ servers.getOrPut(url) { StashServer(url, apiKey) }
+ } else {
+ null
+ }
+ }
+
+ suspend fun setCurrentStashServer(server: StashServer) {
+ addAndSwitchServer(server)
+ }
+
+ suspend fun removeStashServer(server: StashServer) {
+ val manager = PreferenceManager.getDefaultSharedPreferences(context)
+ val serverKey = SERVER_PREF_PREFIX + server.url
+ val apiKeyKey = SERVER_APIKEY_PREF_PREFIX + server.url
+ manager.edit(true) {
+ remove(serverKey)
+ remove(apiKeyKey)
+ }
+ }
+
+ suspend fun addServer(newServer: StashServer) {
+ val manager = PreferenceManager.getDefaultSharedPreferences(context)
+ val newServerKey = SERVER_PREF_PREFIX + newServer.url
+ val newApiKeyKey = SERVER_APIKEY_PREF_PREFIX + newServer.url
+ manager.edit(true) {
+ putString(newServerKey, newServer.url)
+ putString(newApiKeyKey, newServer.apiKey)
+ }
+ }
+
+ suspend fun addAndSwitchServer(newServer: StashServer) {
+ val manager = PreferenceManager.getDefaultSharedPreferences(context)
+ val current = findConfiguredStashServer(context)
+ val currentServerKey = SERVER_PREF_PREFIX + current?.url
+ val currentApiKeyKey =
+ SERVER_APIKEY_PREF_PREFIX + current?.url
+ val newServerKey = SERVER_PREF_PREFIX + newServer.url
+ val newApiKeyKey =
+ SERVER_APIKEY_PREF_PREFIX + newServer.url
+ manager.edit(true) {
+ if (current != null) {
+ putString(currentServerKey, current.url)
+ putString(currentApiKeyKey, current.apiKey)
+ }
+ putString(newServerKey, newServer.url)
+ putString(newApiKeyKey, newServer.apiKey)
+ putString(PREF_STASH_URL, newServer.url)
+ putString(PREF_STASH_API_KEY, newServer.apiKey)
+ }
+ api.changeServer(preferences.data.first(), newServer)
+ val config = QueryEngine(api).getServerConfiguration()
+ val serverPreferences = ServerPreferences.createServerPreferences(config)
+ currentServer.update {
+ CurrentServer(newServer, serverPreferences)
+ }
+ }
+
+ suspend fun updateServerPreferences() {
+ val config = QueryEngine(api).getServerConfiguration()
+ val serverPreferences = ServerPreferences.createServerPreferences(config)
+ currentServer.update {
+ it.copy(serverPreferences = serverPreferences)
+ }
+ }
+
+ suspend fun getAll(): List {
+ val manager = PreferenceManager.getDefaultSharedPreferences(context)
+ val keys =
+ manager.all.keys
+ .filter { it.startsWith(SERVER_PREF_PREFIX) }
+ .sorted()
+ .toList()
+ return keys
+ .map {
+ val url = it.replace(SERVER_PREF_PREFIX, "")
+ val apiKeyKey =
+ it.replace(
+ SERVER_PREF_PREFIX,
+ SERVER_APIKEY_PREF_PREFIX,
+ )
+ val apiKey =
+ manager.all[apiKeyKey]
+ ?.toString()
+ ?.replace(SERVER_APIKEY_PREF_PREFIX, "")
+ StashServer(url, apiKey)
+ }.sortedBy { it.url }
+ }
+
+ companion object {
+ private const val SERVER_PREF_PREFIX = "server_"
+ private const val SERVER_APIKEY_PREF_PREFIX = "apikey_"
+
+ const val PREF_STASH_URL = "stashUrl"
+ const val PREF_STASH_API_KEY = "stashApiKey"
+ }
+}
+
+data class CurrentServer(
+ val server: StashServer,
+ val serverPreferences: ServerPreferences,
+) {
+ companion object {
+ val UNSET =
+ CurrentServer(
+ StashServer.UNSET,
+ ServerPreferences(),
+ )
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/StashApi.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashApi.kt
new file mode 100644
index 000000000..bd2394040
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashApi.kt
@@ -0,0 +1,255 @@
+package com.github.damontecres.stashapp.di.server
+
+import android.annotation.SuppressLint
+import android.content.Context
+import androidx.core.net.toUri
+import co.touchlab.kermit.Logger
+import com.apollographql.apollo.ApolloClient
+import com.apollographql.apollo.annotations.ApolloExperimental
+import com.apollographql.apollo.network.http.DefaultHttpEngine
+import com.apollographql.apollo.network.websocket.GraphQLWsProtocol
+import com.apollographql.apollo.network.websocket.WebSocketEngine
+import com.apollographql.apollo.network.websocket.WebSocketNetworkTransport
+import com.github.damontecres.stashapp.di.StandardHttpClient
+import com.github.damontecres.stashapp.proto.StashPreferences
+import com.github.damontecres.stashapp.util.Constants.OK_HTTP_CACHE_DIR
+import com.github.damontecres.stashapp.util.cacheDurationPrefToDuration
+import com.github.damontecres.stashapp.util.isNotNullOrBlank
+import okhttp3.Cache
+import okhttp3.CacheControl
+import okhttp3.Call
+import okhttp3.EventListener
+import okhttp3.OkHttpClient
+import okhttp3.Response
+import org.koin.core.annotation.Single
+import timber.log.Timber
+import java.io.File
+import java.security.SecureRandom
+import java.security.cert.X509Certificate
+import java.util.concurrent.TimeUnit
+import javax.net.ssl.SSLContext
+import javax.net.ssl.X509TrustManager
+import kotlin.time.DurationUnit
+
+@Single
+class StashApi(
+ private val context: Context,
+ @param:StandardHttpClient private val httpClient: OkHttpClient,
+) {
+ @OptIn(ApolloExperimental::class)
+ var server: StashServer = StashServer.UNSET
+ private set
+
+ lateinit var apolloClient: ApolloClient
+ private set
+
+ suspend fun changeServer(
+ preferences: StashPreferences,
+ server: StashServer,
+ ) {
+ Timber.i("Switching server to %s", server.url)
+ this.apolloClient = createApolloClient(preferences, server)
+ this.server = server
+ }
+
+ fun createFor(
+ server: StashServer,
+ client: OkHttpClient,
+ ) = StashApi(context, client).apply {
+ this.server = server
+ apolloClient = createApolloClient(server, client)
+ }
+
+ @OptIn(ApolloExperimental::class)
+ suspend fun createApolloClient(
+ preferences: StashPreferences,
+ server: StashServer,
+ ): ApolloClient {
+ val client = createOkHttpClient(preferences, server)
+ return createApolloClient(server, client)
+ }
+
+ private suspend fun createOkHttpClient(
+ preferences: StashPreferences,
+ server: StashServer,
+ ): OkHttpClient {
+ val trustAll = preferences.advancedPreferences.trustSelfSignedCertificates
+ val cacheDuration =
+ cacheDurationPrefToDuration(preferences.cachePreferences.cacheExpirationTime)
+ val cacheLogging = preferences.cachePreferences.logCacheHits
+ val networkTimeout = preferences.advancedPreferences.networkTimeoutMs
+
+ var builder =
+ httpClient
+ .newBuilder()
+ .readTimeout(networkTimeout, TimeUnit.SECONDS)
+ .writeTimeout(networkTimeout, TimeUnit.SECONDS)
+
+ if (trustAll) {
+ val sslContext = SSLContext.getInstance("SSL")
+ sslContext.init(null, arrayOf(TRUST_ALL_CERTS), SecureRandom())
+ builder =
+ builder
+ .sslSocketFactory(
+ sslContext.socketFactory,
+ TRUST_ALL_CERTS,
+ ).hostnameVerifier { _, _ ->
+ true
+ }
+ }
+ if (cacheLogging) {
+ Logger.d {
+ "cacheDuration in hours: ${
+ cacheDuration?.toInt(
+ DurationUnit.HOURS,
+ )
+ }"
+ }
+ builder =
+ builder.eventListener(
+ object : EventListener() {
+ override fun cacheHit(
+ call: Call,
+ response: Response,
+ ) {
+ Logger.v {
+ "cacheHit: ${call.request().url} => ${response.code}"
+ }
+ }
+
+ override fun cacheMiss(call: Call) {
+ Logger.v { "cacheMiss: ${call.request().url}" }
+ }
+
+ override fun cacheConditionalHit(
+ call: Call,
+ cachedResponse: Response,
+ ) {
+ Logger.v {
+ "cacheConditionalHit: ${call.request().url} => ${cachedResponse.code}"
+ }
+ }
+ },
+ )
+ }
+ val cacheControl =
+ if (cacheDuration != null) {
+ CacheControl
+ .Builder()
+ .maxAge(
+ cacheDuration.toInt(DurationUnit.HOURS),
+ TimeUnit.HOURS,
+ ).build()
+ } else {
+ CacheControl.Builder().noCache().build()
+ }
+ builder =
+ builder
+ .addInterceptor(ServerInterceptor(server))
+ .addNetworkInterceptor {
+ val request =
+ it
+ .request()
+ .newBuilder()
+ .cacheControl(cacheControl)
+ .build()
+ it.proceed(request)
+ }
+
+ val cacheSize = preferences.cachePreferences.networkCacheSize * 1024 * 1024
+ builder.cache(Cache(File(context.cacheDir, OK_HTTP_CACHE_DIR), cacheSize))
+
+ return builder.build()
+ }
+
+ companion object {
+ @OptIn(ApolloExperimental::class)
+ fun createApolloClient(
+ server: StashServer,
+ httpClient: OkHttpClient,
+ ): ApolloClient {
+ val url = cleanServerUrl(server.url)
+ val client =
+ httpClient
+ .newBuilder()
+ .addInterceptor(ServerInterceptor(server))
+ .build()
+ val apolloClient =
+ ApolloClient
+ .Builder()
+ .serverUrl(url)
+ .httpEngine(DefaultHttpEngine(client))
+ .subscriptionNetworkTransport(
+ WebSocketNetworkTransport
+ .Builder()
+ .serverUrl(url)
+ .wsProtocol(GraphQLWsProtocol())
+ .webSocketEngine(WebSocketEngine(client))
+ .build(),
+ ).build()
+ return apolloClient
+ }
+
+ /**
+ * Cleans up the URL that a user enters
+ *
+ * Tries to add protocol (http) and endpoint (/graphql)
+ */
+ fun cleanServerUrl(stashUrl: String): String {
+ var cleanedStashUrl = stashUrl.trim()
+ if (!cleanedStashUrl.startsWith("http://") && !cleanedStashUrl.startsWith("https://")) {
+ // Assume http
+ cleanedStashUrl = "http://$cleanedStashUrl"
+ }
+ var url = cleanedStashUrl.toUri()
+ val pathSegments = url.pathSegments.toMutableList()
+ if (url.host.isNotNullOrBlank() && (pathSegments.isEmpty() || pathSegments.last() != "graphql")) {
+ pathSegments.add("graphql")
+ }
+ url =
+ url
+ .buildUpon()
+ .path(pathSegments.joinToString("/")) // Ensure the URL is the graphql endpoint
+ .build()
+ return url.toString()
+ }
+
+ fun createLoginUrl(stashUrl: String): String {
+ val cleanedStashUrl = cleanServerUrl(stashUrl)
+ var url = cleanedStashUrl.toUri()
+ val pathSegments = url.pathSegments.toMutableList()
+ if (url.host.isNotNullOrBlank() && pathSegments.isNotEmpty() && pathSegments.last() == "graphql") {
+ pathSegments.removeLastOrNull()
+ }
+ if (url.host.isNotNullOrBlank()) {
+ pathSegments.add("login")
+ }
+ url =
+ url
+ .buildUpon()
+ .path(pathSegments.joinToString("/")) // Ensure the URL is the graphql endpoint
+ .build()
+ return url.toString()
+ }
+ }
+}
+
+private val TRUST_ALL_CERTS: X509TrustManager =
+ @SuppressLint("CustomX509TrustManager")
+ object : X509TrustManager {
+ @SuppressLint("TrustAllX509TrustManager")
+ override fun checkClientTrusted(
+ chain: Array,
+ authType: String,
+ ) {
+ }
+
+ @SuppressLint("TrustAllX509TrustManager")
+ override fun checkServerTrusted(
+ chain: Array,
+ authType: String,
+ ) {
+ }
+
+ override fun getAcceptedIssuers(): Array = arrayOf()
+ }
diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/StashEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashEngine.kt
similarity index 64%
rename from app/src/main/java/com/github/damontecres/stashapp/util/StashEngine.kt
rename to app/src/main/java/com/github/damontecres/stashapp/di/server/StashEngine.kt
index 8d697d769..7d6ae23e2 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/util/StashEngine.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashEngine.kt
@@ -1,22 +1,24 @@
-package com.github.damontecres.stashapp.util
+package com.github.damontecres.stashapp.di.server
-import android.util.Log
-import com.apollographql.apollo.ApolloClient
import com.apollographql.apollo.exception.ApolloException
import com.apollographql.apollo.exception.ApolloHttpException
import com.apollographql.apollo.exception.ApolloNetworkException
+import com.github.damontecres.stashapp.util.isNotNullOrBlank
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import timber.log.Timber
/**
* Super class for "engines" that interact with the server
*/
abstract class StashEngine(
- protected val client: ApolloClient,
- protected val server: StashServer?,
+ protected val api: StashApi,
) {
- constructor(server: StashServer) : this(server.apolloClient, server)
+ protected val client get() = api.apolloClient
+ private var requestId: Int = 0
+ protected val mutex = Mutex()
- protected val serverPreferences = server?.serverPreferences
- protected val serverVersion = serverPreferences?.serverVersion
+ protected suspend fun getRequestId() = mutex.withLock { requestId++ }
protected fun createException(
id: Int,
@@ -32,22 +34,22 @@ abstract class StashEngine(
}
return when (ex) {
is ApolloNetworkException -> {
- Log.e(TAG, "Network error in $id $operationName", ex)
+ Timber.e(ex, "Network error in $id $operationName")
rethrow("Network error ($operationName): $causeMsg", ex)
}
is ApolloHttpException -> {
- Log.e(TAG, "HTTP ${ex.statusCode} error in $id $operationName", ex)
+ Timber.e(ex, "HTTP ${ex.statusCode} error in $id $operationName")
rethrow("HTTP ${ex.statusCode} ($operationName): $causeMsg", ex)
}
is ApolloException -> {
- Log.e(TAG, "ApolloException in $id $operationName", ex)
+ Timber.e(ex, "ApolloException in $id $operationName")
rethrow("Error with server ($operationName): $causeMsg", ex)
}
else -> {
- Log.e(TAG, "Exception in $id $operationName", ex)
+ Timber.e(ex, "Exception in $id $operationName")
rethrow("Error with $operationName: $causeMsg", ex)
}
}
@@ -59,8 +61,4 @@ abstract class StashEngine(
message: String? = null,
cause: Exception? = null,
) : Exception(message, cause)
-
- companion object {
- private const val TAG = "StashEngine"
- }
}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/StashServer.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashServer.kt
new file mode 100644
index 000000000..0a4daa8ff
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashServer.kt
@@ -0,0 +1,50 @@
+package com.github.damontecres.stashapp.di.server
+
+import androidx.core.net.toUri
+import com.github.damontecres.stashapp.util.isNotNullOrBlank
+
+/**
+ * Represents a server
+ */
+data class StashServer(
+ val url: String,
+ val apiKey: String?,
+) {
+ override fun toString(): String = "StashServer(url=$url, apiKey?=${apiKey.isNotNullOrBlank()})"
+
+ val cleanedApiKey by lazy { apiKey?.trim() }
+ val serverRoot by lazy { getServerRoot(url) }
+
+ val serverKey get() = url.replace(Regex("[^\\w.]"), "_")
+
+ companion object {
+ private const val SERVER_PREF_PREFIX = "server_"
+ private const val SERVER_APIKEY_PREF_PREFIX = "apikey_"
+
+ val UNSET = StashServer("UNSET", null)
+
+ /**
+ * Get the server URL excluding the (unlikely) `/graphql` last path segment
+ *
+ * Basically, if a user is using a reverse proxy routes the path ending with `/graphql`, this will remove that
+ */
+ private fun getServerRoot(stashUrl: String): String {
+ var cleanedStashUrl = stashUrl.trim()
+ if (!cleanedStashUrl.startsWith("http://") && !cleanedStashUrl.startsWith("https://")) {
+ // Assume http
+ cleanedStashUrl = "http://$cleanedStashUrl"
+ }
+ var url = cleanedStashUrl.toUri()
+ val pathSegments = url.pathSegments.toMutableList()
+ if (pathSegments.isNotEmpty() && pathSegments.last() == "graphql") {
+ pathSegments.removeAt(pathSegments.size - 1)
+ }
+ url =
+ url
+ .buildUpon()
+ .path(pathSegments.joinToString("/"))
+ .build()
+ return url.toString()
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/SubscriptionEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt
similarity index 93%
rename from app/src/main/java/com/github/damontecres/stashapp/util/SubscriptionEngine.kt
rename to app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt
index afd462786..9cfef88a4 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/util/SubscriptionEngine.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt
@@ -1,4 +1,4 @@
-package com.github.damontecres.stashapp.util
+package com.github.damontecres.stashapp.di.server
import android.util.Log
import com.apollographql.apollo.api.Subscription
@@ -6,21 +6,23 @@ import com.github.damontecres.stashapp.api.JobProgressSubscription
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import org.koin.core.annotation.Single
import java.util.concurrent.atomic.AtomicInteger
/**
* Engine for handling graphql subscriptions
*
- * @param server the stash server to connect to
+ * @param api the stash API to use
* @param client the client to use, defaults to one for the server
* @param ioDispatcher the dispatcher to use for general I/O operations
* @param callbackDispatcher the dispatcher to use for running callbacks from subscription results
*/
+@Single
class SubscriptionEngine(
- server: StashServer,
+ api: StashApi,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private val callbackDispatcher: CoroutineDispatcher = Dispatchers.Main,
-) : StashEngine(server) {
+) : StashEngine(api) {
private suspend fun executeSubscription(
subscription: Subscription,
consumer: (D) -> Unit,
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/services/ItemClicker.kt b/app/src/main/java/com/github/damontecres/stashapp/di/services/ItemClicker.kt
new file mode 100644
index 000000000..bd80b9f7a
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/services/ItemClicker.kt
@@ -0,0 +1,61 @@
+package com.github.damontecres.stashapp.di.services
+
+import android.app.Application
+import android.widget.Toast
+import co.touchlab.kermit.Logger
+import com.github.damontecres.stashapp.api.fragment.ImageData
+import com.github.damontecres.stashapp.api.fragment.StashData
+import com.github.damontecres.stashapp.navigation.Destination
+import com.github.damontecres.stashapp.navigation.FilterAndPosition
+import com.github.damontecres.stashapp.suppliers.FilterArgs
+import com.github.damontecres.stashapp.ui.components.ItemOnClicker
+import org.koin.core.annotation.Single
+
+@Single
+class ItemClicker(
+ private val context: Application,
+ private val navigationManager: NavigationManager,
+) : ItemOnClicker {
+ override fun onClick(
+ item: Any,
+ filterAndPosition: FilterAndPosition?,
+ ) {
+ when (item) {
+ is FilterArgs -> {
+ navigationManager.navigate(
+ Destination.Filter(
+ item,
+ true,
+ ),
+ )
+ }
+
+ is ImageData -> {
+ val (filter, position) = filterAndPosition!!
+ navigationManager.navigate(
+ Destination.Slideshow(
+ filter,
+ position,
+ false,
+ ),
+ )
+ }
+
+ is StashData -> {
+ navigationManager.navigate(
+ Destination.fromStashData(item),
+ )
+ }
+
+ else -> {
+ Toast
+ .makeText(
+ context,
+ "Unknown item. This is probably a bug",
+ Toast.LENGTH_SHORT,
+ ).show()
+ Logger.e { "Unknown item type: ${item::class.qualifiedName}" }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/services/NavigationManager.kt b/app/src/main/java/com/github/damontecres/stashapp/di/services/NavigationManager.kt
new file mode 100644
index 000000000..b6f0c8811
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/services/NavigationManager.kt
@@ -0,0 +1,85 @@
+package com.github.damontecres.stashapp.di.services
+
+import androidx.navigation3.runtime.NavBackStack
+import co.touchlab.kermit.Logger
+import com.github.damontecres.stashapp.navigation.Destination
+import org.acra.ACRA
+import org.koin.core.annotation.Single
+
+@Single
+class NavigationManager {
+ var backStack: MutableList = NavBackStack(Destination.Main())
+
+ /**
+ * Go to the specified [Destination]
+ */
+ fun navigate(destination: Destination) {
+ backStack.add(destination)
+ log()
+ }
+
+ /**
+ * Go to the specified [Destination], but reset the back stack to Home first
+ */
+ fun navigateToFromDrawer(destination: Destination) {
+ goToMain()
+ backStack.add(destination)
+ log()
+ }
+
+ /**
+ * Go to the previous page
+ */
+ fun goBack() {
+ synchronized(this) {
+ if (backStack.size > 1) {
+ backStack.removeLastOrNull()
+ }
+ }
+ log()
+ }
+
+ /**
+ * Go all the way back to the home page
+ */
+ fun goToMain() {
+ while (backStack.size > 1) {
+ backStack.removeLastOrNull()
+ }
+ if (backStack[0] !is Destination.Main) {
+ backStack[0] = Destination.Main()
+ }
+ log()
+ }
+
+ /**
+ * Go all the way back to the home page, and reload it from scratch
+ */
+ fun reloadMain() {
+ goToMain()
+ val id = (backStack[0] as Destination.Main).id + 1
+ backStack[0] = Destination.Main(id)
+ log()
+ }
+
+ /**
+ * Resets the backstack to the specified destination
+ */
+ fun replace(destination: Destination) {
+ while (backStack.size > 1) {
+ backStack.removeLastOrNull()
+ }
+ if (backStack.isEmpty()) {
+ backStack.add(0, destination)
+ } else {
+ backStack[0] = destination
+ }
+ log()
+ }
+
+ private fun log() {
+ val dest = backStack.lastOrNull().toString()
+ Logger.i { "Current Destination: $dest" }
+ ACRA.errorReporter.putCustomData("destination", dest)
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/services/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/stashapp/di/services/PlayerFactory.kt
new file mode 100644
index 000000000..63f0e4068
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/services/PlayerFactory.kt
@@ -0,0 +1,191 @@
+package com.github.damontecres.stashapp.di.services
+
+import android.app.Application
+import androidx.annotation.OptIn
+import androidx.media3.common.Player
+import androidx.media3.common.TrackSelectionParameters
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.DefaultHttpDataSource
+import androidx.media3.datasource.okhttp.OkHttpDataSource
+import androidx.media3.exoplayer.DefaultRenderersFactory
+import androidx.media3.exoplayer.ExoPlayer
+import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
+import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
+import androidx.media3.exoplayer.util.EventLogger
+import androidx.media3.extractor.DefaultExtractorsFactory
+import androidx.media3.extractor.ts.TsExtractor
+import com.github.damontecres.stashapp.di.AuthHttpClient
+import com.github.damontecres.stashapp.di.server.ServerRepository
+import com.github.damontecres.stashapp.di.server.StashServer
+import com.github.damontecres.stashapp.proto.PlaybackBackend
+import com.github.damontecres.stashapp.proto.PlaybackHttpClient
+import com.github.damontecres.stashapp.proto.PlaybackPreferences
+import com.github.damontecres.stashapp.util.Constants
+import com.github.damontecres.stashapp.util.SkipParams
+import com.github.damontecres.stashapp.util.StashClient
+import com.github.damontecres.stashapp.util.isNotNullOrBlank
+import com.github.damontecres.wholphin.mpv.MpvPlayer
+import okhttp3.OkHttpClient
+import org.koin.core.annotation.Single
+import timber.log.Timber
+
+@Single
+class PlayerFactory(
+ private val context: Application,
+ @param:AuthHttpClient private val okHttpClient: OkHttpClient,
+ private val serverRepository: ServerRepository,
+) {
+ private var currentPlayer: Player? = null
+ private var currentCardPlayer: Player? = null
+
+ /**
+ * Create a new [ExoPlayer] instance. [getInstance] should be preferred where possible.
+ */
+ @OptIn(UnstableApi::class)
+ fun createPlayer(playbackPreferences: PlaybackPreferences): Player {
+ currentPlayer?.release()
+
+ val server = serverRepository.currentServer.value.server
+
+ Timber.i("backend=%s", playbackPreferences.playbackBackend)
+ val skipParams =
+ playbackPreferences.let {
+ SkipParams.Values(
+ it.skipForwardMs,
+ it.skipBackwardMs,
+ )
+ }
+ val httpClient = playbackPreferences.playbackHttpClient
+ val debugLogging = playbackPreferences.debugLoggingEnabled
+ val player =
+ if (playbackPreferences.playbackBackend == PlaybackBackend.MPV) {
+ MpvPlayer(
+ context,
+ playbackPreferences.mpvPreferences.hardwareDecoding,
+ playbackPreferences.mpvPreferences.gpuNext,
+ )
+ } else {
+ val dataSourceFactory =
+ when (httpClient) {
+ PlaybackHttpClient.OKHTTP -> {
+ OkHttpDataSource
+ .Factory(okHttpClient)
+ }
+
+ else -> {
+ DefaultHttpDataSource
+ .Factory()
+ .setConnectTimeoutMs(5_000)
+ .setReadTimeoutMs(30_000)
+ .setUserAgent(StashClient.createUserAgent(context))
+ .apply {
+ if (server.apiKey.isNotNullOrBlank()) {
+ setDefaultRequestProperties(mapOf(Constants.STASH_API_HEADER to server.apiKey))
+ }
+ }
+ }
+ }
+ val skipForward = skipParams.skipForward
+ val skipBack = skipParams.skipBack
+ val trackSelector = DefaultTrackSelector(context)
+ trackSelector.parameters =
+ trackSelector
+ .buildUponParameters()
+ .setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
+ .setAudioOffloadPreferences(
+ TrackSelectionParameters.AudioOffloadPreferences
+ .Builder()
+ .setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
+ .build(),
+ ).build()
+ val extractorsFactory =
+ DefaultExtractorsFactory().apply {
+ setTsExtractorTimestampSearchBytes(TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES * 3)
+ setConstantBitrateSeekingEnabled(true)
+ setConstantBitrateSeekingAlwaysEnabled(true)
+ }
+ ExoPlayer
+ .Builder(context)
+// .setLoadControl(
+// DefaultLoadControl
+// .Builder()
+// .setBufferDurationsMs(
+// 5_000,
+// 30_000,
+// DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
+// DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS,
+// ).setTargetBufferBytes(64_000_000)
+// .setPrioritizeTimeOverSizeThresholds(false)
+// .build(),
+// )
+ .setMediaSourceFactory(
+ DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory),
+ ).setRenderersFactory(
+ DefaultRenderersFactory(context)
+ .setEnableDecoderFallback(true)
+ .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON),
+ ).setSeekBackIncrementMs(skipBack)
+ .setSeekForwardIncrementMs(skipForward)
+ .setTrackSelector(trackSelector)
+ .build()
+ .also {
+ if (debugLogging) {
+ it.addAnalyticsListener(EventLogger())
+ }
+ }
+ }
+
+ this@PlayerFactory.currentPlayer = player
+ return player
+ }
+
+ @OptIn(UnstableApi::class)
+ fun createPlayerForCard(server: StashServer): ExoPlayer {
+ this.currentCardPlayer?.release()
+ val dataSourceFactory =
+ DefaultHttpDataSource
+ .Factory()
+ .setConnectTimeoutMs(5_000)
+ .setReadTimeoutMs(30_000)
+ .setUserAgent(StashClient.createUserAgent(context))
+ .apply {
+ if (server.apiKey.isNotNullOrBlank()) {
+ setDefaultRequestProperties(mapOf(Constants.STASH_API_HEADER to server.apiKey))
+ }
+ }
+ val trackSelector = DefaultTrackSelector(context)
+ trackSelector.parameters =
+ trackSelector
+ .buildUponParameters()
+ .setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
+ .setAudioOffloadPreferences(
+ TrackSelectionParameters.AudioOffloadPreferences
+ .Builder()
+ .setAudioOffloadMode(TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED)
+ .build(),
+ ).build()
+ val extractorsFactory =
+ DefaultExtractorsFactory().apply {
+ setTsExtractorTimestampSearchBytes(TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES * 3)
+ setConstantBitrateSeekingEnabled(true)
+ setConstantBitrateSeekingAlwaysEnabled(true)
+ }
+ val player =
+ ExoPlayer
+ .Builder(context)
+ .setMediaSourceFactory(
+ DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory),
+ ).setRenderersFactory(
+ DefaultRenderersFactory(context)
+ .setEnableDecoderFallback(true)
+ .setExtensionRendererMode(DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON),
+ ).setTrackSelector(trackSelector)
+ .build()
+ .apply {
+ repeatMode = Player.REPEAT_MODE_ONE
+ playWhenReady = true
+ }
+ this.currentCardPlayer = player
+ return player
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/services/ServerLogger.kt b/app/src/main/java/com/github/damontecres/stashapp/di/services/ServerLogger.kt
new file mode 100644
index 000000000..cf5bde2d7
--- /dev/null
+++ b/app/src/main/java/com/github/damontecres/stashapp/di/services/ServerLogger.kt
@@ -0,0 +1,39 @@
+package com.github.damontecres.stashapp.di.services
+
+import androidx.datastore.core.DataStore
+import com.github.damontecres.stashapp.di.server.ServerRepository
+import com.github.damontecres.stashapp.proto.StashPreferences
+import com.github.damontecres.stashapp.util.plugin.CompanionPluginService
+import kotlinx.coroutines.flow.first
+import org.koin.core.annotation.Single
+
+@Single
+class ServerLogger(
+ private val preferences: DataStore,
+ private val serverRepository: ServerRepository,
+ private val companionPluginService: CompanionPluginService,
+) {
+ suspend fun logException(
+ exception: Throwable,
+ message: String? = null,
+ ) {
+ if (preferences.data
+ .first()
+ .advancedPreferences.logErrorsToServer
+ ) {
+ if (serverRepository.currentServer.value.serverPreferences.companionPluginInstalled) {
+ companionPluginService.sendLogMessage(
+ "mesg=$message\n${exception.stackTraceToString()}",
+ true,
+ )
+ }
+ }
+ }
+
+ suspend fun T.run(block: T.() -> R): Result =
+ runCatching {
+ block.invoke(this)
+ }.onFailure { ex ->
+ logException(ex, null)
+ }
+}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterFragment.kt
deleted file mode 100644
index d4c768734..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterFragment.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package com.github.damontecres.stashapp.filter
-
-import android.os.Bundle
-import android.view.View
-import androidx.fragment.app.Fragment
-import androidx.fragment.app.activityViewModels
-import androidx.leanback.app.GuidedStepSupportFragment
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.util.getDestination
-
-class CreateFilterFragment : Fragment(R.layout.frame) {
- private val viewModel by activityViewModels()
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- val dest = requireArguments().getDestination()
- val startingFilter = dest.startingFilter
- viewModel.initialize(
- dest.dataType,
- startingFilter,
- )
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- viewModel.ready.observe(viewLifecycleOwner) { ready ->
- if (ready) {
- GuidedStepSupportFragment.add(
- childFragmentManager,
- CreateFilterStep(),
- R.id.frame_content,
- )
- }
- }
- }
-
- companion object {
- private const val TAG = "CreateFilterFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterGuidedStepFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterGuidedStepFragment.kt
deleted file mode 100644
index da6d763ca..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterGuidedStepFragment.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-package com.github.damontecres.stashapp.filter
-
-import androidx.fragment.app.activityViewModels
-import androidx.leanback.app.GuidedStepSupportFragment
-import androidx.leanback.widget.GuidedAction
-import androidx.leanback.widget.GuidedActionsStylist
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.views.getString
-
-abstract class CreateFilterGuidedStepFragment : GuidedStepSupportFragment() {
- protected val viewModel: CreateFilterViewModel by activityViewModels()
-
- fun nextStep(step: GuidedStepSupportFragment) {
- add(requireActivity().supportFragmentManager, step, R.id.root_fragment)
- }
-
- override fun onProvideTheme(): Int = R.style.Theme_StashAppAndroidTV_GuidedStep
-
- override fun onCreateActionsStylist(): GuidedActionsStylist = StashGuidedActionsStylist()
-
- /**
- * Create a [GuidedAction] for a [CriterionModifier]
- */
- protected fun modifierAction(modifier: CriterionModifier): GuidedAction =
- GuidedAction
- .Builder(requireContext())
- .id(MODIFIER_OFFSET + modifier.ordinal)
- .hasNext(false)
- .title(modifier.getString(requireContext()))
- .build()
-
- /**
- * Enable or disable the "finish" [GuidedAction].
- *
- * The step must define a [GuidedAction] with ID=[GuidedAction.ACTION_ID_FINISH] or this will throw an exception.
- */
- protected fun enableFinish(enabled: Boolean) {
- findActionById(GuidedAction.ACTION_ID_FINISH)!!.isEnabled = enabled
- notifyActionChanged(findActionPositionById(GuidedAction.ACTION_ID_FINISH))
- }
-
- /**
- * Handle cancel & remove actions
- */
- protected fun onStandardActionClicked(
- action: GuidedAction,
- filterOption: FilterOption,
- ) {
- if (action.id == GuidedAction.ACTION_ID_CANCEL) {
- parentFragmentManager.popBackStack()
- } else if (action.id == ACTION_ID_REMOVE) {
- viewModel.updateFilter(filterOption, null)
- parentFragmentManager.popBackStack()
- }
- }
-
- /**
- * Add save, remove, & cancel actions
- */
- protected fun addStandardActions(
- actions: MutableList,
- filterOption: FilterOption,
- finishEnabled: Boolean? = null,
- ) {
- val currVal = viewModel.getValue(filterOption)
-
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(GuidedAction.ACTION_ID_FINISH)
- .hasNext(true)
- .title(getString(com.github.damontecres.stashapp.R.string.stashapp_actions_save))
- .enabled(finishEnabled ?: (currVal != null))
- .build(),
- )
-
- if (currVal != null) {
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(ACTION_ID_REMOVE)
- .hasNext(true)
- .title(getString(com.github.damontecres.stashapp.R.string.stashapp_actions_remove))
- .build(),
- )
- }
-
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(GuidedAction.ACTION_ID_CANCEL)
- .hasNext(true)
- .title(getString(com.github.damontecres.stashapp.R.string.stashapp_actions_cancel))
- .build(),
- )
- }
-
- fun CriterionModifier.hasTwoValues(): Boolean = this == CriterionModifier.BETWEEN || this == CriterionModifier.NOT_BETWEEN
-
- fun CriterionModifier.isNullModifier(): Boolean = this == CriterionModifier.IS_NULL || this == CriterionModifier.NOT_NULL
-
- companion object {
- const val MODIFIER_OFFSET = 3_000_000L
- const val ACTION_ID_REMOVE = -234L
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterStep.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterStep.kt
deleted file mode 100644
index a0604d2a4..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterStep.kt
+++ /dev/null
@@ -1,242 +0,0 @@
-package com.github.damontecres.stashapp.filter
-
-import android.os.Bundle
-import android.text.InputType
-import android.util.Log
-import android.view.View
-import androidx.core.content.ContextCompat
-import androidx.fragment.app.activityViewModels
-import androidx.leanback.widget.GuidanceStylist
-import androidx.leanback.widget.GuidedAction
-import androidx.lifecycle.lifecycleScope
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.api.type.SaveFilterInput
-import com.github.damontecres.stashapp.navigation.Destination
-import com.github.damontecres.stashapp.suppliers.FilterArgs
-import com.github.damontecres.stashapp.util.MutationEngine
-import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-import com.github.damontecres.stashapp.util.readOnlyModeDisabled
-import com.github.damontecres.stashapp.views.dialog.ConfirmationDialogFragment
-import com.github.damontecres.stashapp.views.formatNumber
-import com.github.damontecres.stashapp.views.models.ServerViewModel
-import kotlinx.coroutines.launch
-
-/**
- * The first step to create a new filter
- *
- * Assumes that [CreateFilterViewModel.initialize] has been called already
- */
-class CreateFilterStep : CreateFilterGuidedStepFragment() {
- private val serverViewModel: ServerViewModel by activityViewModels()
-
- override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance {
- val text =
- filterSummary(
- requireContext(),
- viewModel.dataType.value!!,
- viewModel.dataType.value!!.filterType,
- viewModel.objectFilter.value!!,
- viewModel::lookupIds,
- ).joinToString("\n").ifBlank { "No filters set" }
- val typeStr = getString(viewModel.dataType.value!!.stringId)
- return GuidanceStylist.Guidance(
- "Create $typeStr filter",
- text,
- null,
- ContextCompat.getDrawable(requireContext(), R.drawable.stash_logo),
- )
- }
-
- private fun updateActions(actions: MutableList) {
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(FILTER_NAME)
- .hasNext(false)
- .title(getString(R.string.stashapp_filter_name))
- .descriptionEditInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_WORDS)
- .descriptionEditable(true)
- .description(viewModel.filterName.value)
- .build(),
- )
-
- val sortDesc =
- findFilterSummary(
- requireContext(),
- viewModel.dataType.value!!,
- viewModel.findFilter.value!!,
- )
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(SORT_OPTION)
- .hasNext(true)
- .title(getString(R.string.sort_by))
- .description(sortDesc)
- .build(),
- )
-
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(FILTER_OPTIONS)
- .hasNext(true)
- .title(getString(R.string.stashapp_filters))
- .build(),
- )
- val count = viewModel.resultCount.value ?: -1
- val countStr = formatNumber(count, viewModel.abbreviateCounters)
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(SUBMIT)
- .hasNext(true)
- .title("Submit without saving")
- .description(if (count >= 0) "$countStr results" else "Querying...")
- .build(),
- )
- if (readOnlyModeDisabled()) {
- val submitAction =
- GuidedAction
- .Builder(requireContext())
- .id(SAVE_SUBMIT)
- .hasNext(true)
- .title("Save and submit")
- .build()
- updateSaveAndSubmit(submitAction, viewModel.filterName.value)
- actions.add(submitAction)
- }
- }
-
- override fun onCreateActions(
- actions: MutableList,
- savedInstanceState: Bundle?,
- ) {
- updateActions(actions)
- }
-
- override fun onResume() {
- super.onResume()
- val refreshActions = mutableListOf()
- updateActions(refreshActions)
- actions = refreshActions
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- if (savedInstanceState == null) {
- viewModel.updateCount()
- viewModel.resultCount.observe(viewLifecycleOwner) { count ->
- val countStr = formatNumber(count, viewModel.abbreviateCounters)
- if (count >= 0) {
- findActionById(SUBMIT)!!.description = "$countStr results"
- notifyActionChanged(findActionPositionById(SUBMIT))
- } else {
- findActionById(SUBMIT)!!.description = "Querying..."
- notifyActionChanged(findActionPositionById(SUBMIT))
- }
- }
- }
- }
-
- override fun onGuidedActionClicked(action: GuidedAction) {
- val dataType = viewModel.dataType.value!!
- if (action.id == FILTER_OPTIONS) {
- nextStep(CreateObjectFilterStep())
- } else if (action.id == SORT_OPTION) {
- nextStep(CreateFindFilterFragment(dataType, viewModel.findFilter.value!!))
- } else if (action.id == SUBMIT || action.id == SAVE_SUBMIT) {
- // Ready to load the filter!
- val filterArgs = viewModel.createFilterArgs()
- viewLifecycleOwner.lifecycleScope.launch(StashCoroutineExceptionHandler(autoToast = true)) {
- // If there is a name, try to save it to the server
- if (action.id == SAVE_SUBMIT && filterArgs.name.isNotNullOrBlank()) {
- val newFilterInput = viewModel.createSaveFilterInput()
- val existingId = newFilterInput.id.getOrNull()
- if (existingId.isNotNullOrBlank()) {
- // Filter exists, so prompt for confirmation
- ConfirmationDialogFragment.show(
- childFragmentManager,
- getString(
- R.string.stashapp_dialogs_overwrite_filter_warning,
- filterArgs.name,
- ),
- ) {
- viewLifecycleOwner.lifecycleScope.launch(
- StashCoroutineExceptionHandler(
- autoToast = true,
- ),
- ) {
- saveAndFinish(filterArgs, newFilterInput)
- }
- }
- } else {
- saveAndFinish(filterArgs, newFilterInput)
- }
- } else {
- // Just show the results without saving
- saveAndFinish(filterArgs, null)
- }
- }
- }
- }
-
- private suspend fun saveAndFinish(
- filterArgs: FilterArgs,
- newFilterInput: SaveFilterInput?,
- ) {
- if (newFilterInput != null) {
- val mutationEngine = MutationEngine(StashServer.requireCurrentServer())
- val newSavedFilter = mutationEngine.saveFilter(newFilterInput)
- Log.i(TAG, "New SavedFilter: ${newSavedFilter.id}")
- }
-
- serverViewModel.navigationManager.goBack()
- serverViewModel.navigationManager.navigate(Destination.Filter(filterArgs))
- }
-
- override fun onGuidedActionEditedAndProceed(action: GuidedAction): Long {
- if (action.id == FILTER_NAME) {
- val filterName = action.description?.toString()?.ifBlank { null }
- viewModel.filterName.value = filterName
- val submitAction = findActionById(SAVE_SUBMIT)
- if (submitAction != null) {
- // In read-only, this action doesn't exist
- updateSaveAndSubmit(submitAction, filterName)
- notifyActionChanged(findActionPositionById(SAVE_SUBMIT))
- }
- }
- return GuidedAction.ACTION_ID_NEXT
- }
-
- private fun updateSaveAndSubmit(
- submitAction: GuidedAction,
- filterName: String?,
- ) {
- val id = viewModel.getSavedFilterId(filterName)
- submitAction.isEnabled = filterName.isNotNullOrBlank()
- submitAction.description =
- if (submitAction.isEnabled && id.isNotNullOrBlank()) {
- getString(R.string.save_and_submit_overwrite)
- } else if (submitAction.isEnabled) {
- getString(R.string.stashapp_actions_save_filter)
- } else {
- getString(R.string.save_and_submit_no_name_desc)
- }
- }
-
- companion object {
- private val TAG = CreateFilterStep::class.simpleName
-
- private const val SAVE_SUBMIT = -1L
- private const val SUBMIT = -2L
- private const val FILTER_NAME = -3L
- private const val FILTER_OPTIONS = -4L
- private const val SORT_OPTION = -5L
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterViewModel.kt
index c0d135793..048c9a803 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterViewModel.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterViewModel.kt
@@ -13,29 +13,39 @@ import com.github.damontecres.stashapp.api.type.SaveFilterInput
import com.github.damontecres.stashapp.api.type.StashDataFilter
import com.github.damontecres.stashapp.data.DataType
import com.github.damontecres.stashapp.data.StashFindFilter
+import com.github.damontecres.stashapp.di.server.MutationEngine
+import com.github.damontecres.stashapp.di.server.QueryEngine
+import com.github.damontecres.stashapp.di.server.ServerRepository
+import com.github.damontecres.stashapp.di.services.NavigationManager
+import com.github.damontecres.stashapp.di.services.ServerLogger
import com.github.damontecres.stashapp.filter.output.FilterWriter
import com.github.damontecres.stashapp.suppliers.DataSupplierFactory
import com.github.damontecres.stashapp.suppliers.FilterArgs
import com.github.damontecres.stashapp.suppliers.StashPagingSource
-import com.github.damontecres.stashapp.util.MutationEngine
-import com.github.damontecres.stashapp.util.QueryEngine
import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler
-import com.github.damontecres.stashapp.util.StashServer
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
+import org.koin.core.annotation.InjectedParam
+import org.koin.core.annotation.KoinViewModel
import kotlin.reflect.cast
import kotlin.reflect.full.createInstance
/**
* Tracks state while the user builds a new filter
*/
-class CreateFilterViewModel : ViewModel() {
- val server = MutableLiveData(StashServer.requireCurrentServer())
- val abbreviateCounters: Boolean get() = server.value!!.serverPreferences.abbreviateCounters
- val queryEngine = QueryEngine(server.value!!)
+@KoinViewModel
+class CreateFilterViewModel(
+ private val serverRepository: ServerRepository,
+ private val serverLogger: ServerLogger,
+ private val queryEngine: QueryEngine,
+ private val mutationEngine: MutationEngine,
+ val navigationManager: NavigationManager,
+ @InjectedParam private val dataType: DataType,
+ @InjectedParam private val initialFilter: FilterArgs?,
+) : ViewModel() {
+ val currentServer get() = serverRepository.currentServer
val filterName = MutableLiveData(null)
- val dataType = MutableLiveData()
val objectFilter = MutableLiveData()
val findFilter = MutableLiveData()
@@ -51,13 +61,9 @@ class CreateFilterViewModel : ViewModel() {
/**
* Initialize the state
*/
- fun initialize(
- dataType: DataType,
- initialFilter: FilterArgs?,
- ) {
+ fun initialize() {
ready.value = false
- this.dataType.value = dataType
this.objectFilter.value =
initialFilter?.objectFilter ?: dataType.filterType.createInstance()
this.findFilter.value =
@@ -94,7 +100,7 @@ class CreateFilterViewModel : ViewModel() {
val currFilter = objectFilter.value!!
val newFilter =
filterOption.setter(
- dataType.value!!.filterType.cast(currFilter),
+ dataType.filterType.cast(currFilter),
Optional.presentIfNotNull(newItem),
)
objectFilter.value = newFilter
@@ -117,15 +123,18 @@ class CreateFilterViewModel : ViewModel() {
},
) {
val supplier =
- DataSupplierFactory(server.value!!.version).create(
+ DataSupplierFactory(serverRepository.currentServerVersion).create(
FilterArgs(
- dataType = dataType.value!!,
+ dataType = dataType,
findFilter = findFilter.value,
objectFilter = objectFilter.value,
),
)
val pagingSource =
- StashPagingSource(queryEngine, supplier)
+ StashPagingSource(
+ queryEngine,
+ supplier,
+ )
resultCount.value = pagingSource.getCount()
}
}
@@ -135,7 +144,7 @@ class CreateFilterViewModel : ViewModel() {
*/
fun getValue(filterOption: FilterOption): ValueType? {
val currFilter = objectFilter.value!!
- val value = filterOption.getter(dataType.value!!.filterType.cast(currFilter))
+ val value = filterOption.getter(dataType.filterType.cast(currFilter))
return value.getOrNull()
}
@@ -183,17 +192,16 @@ class CreateFilterViewModel : ViewModel() {
fun createFilterArgs(): FilterArgs =
FilterArgs(
- dataType = dataType.value!!,
+ dataType = dataType,
name = filterName.value,
findFilter = findFilter.value,
objectFilter = objectFilter.value,
).withResolvedRandom()
suspend fun createSaveFilterInput(): SaveFilterInput {
- val queryEngine = QueryEngine(server.value!!)
// Save it
val filterWriter =
- FilterWriter(dataType.value!!) { dataType, ids ->
+ FilterWriter(dataType) { dataType, ids ->
queryEngine
.getByIds(dataType, ids)
.associate { it.id to extractTitle(it) }
@@ -201,13 +209,13 @@ class CreateFilterViewModel : ViewModel() {
val findFilter =
findFilter.value ?: StashFindFilter(
null,
- dataType.value!!.defaultSort,
+ dataType.defaultSort,
)
val objectFilterMap = filterWriter.convertFilter(objectFilter.value!!)
val existingId = getSavedFilterId(filterName.value)
return SaveFilterInput(
id = Optional.presentIfNotNull(existingId),
- mode = dataType.value!!.filterMode,
+ mode = dataType.filterMode,
name = filterName.value!!,
find_filter =
Optional.presentIfNotNull(
@@ -219,7 +227,6 @@ class CreateFilterViewModel : ViewModel() {
}
suspend fun saveFilter() {
- val mutationEngine = MutationEngine(server.value!!)
val input = createSaveFilterInput()
mutationEngine.saveFilter(input)
}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFindFilterFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFindFilterFragment.kt
deleted file mode 100644
index af4019f17..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateFindFilterFragment.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-package com.github.damontecres.stashapp.filter
-
-import android.os.Bundle
-import android.text.InputType
-import androidx.core.content.ContextCompat
-import androidx.leanback.widget.GuidanceStylist
-import androidx.leanback.widget.GuidedAction
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.api.type.SortDirectionEnum
-import com.github.damontecres.stashapp.data.DataType
-import com.github.damontecres.stashapp.data.StashFindFilter
-import com.github.damontecres.stashapp.util.isNotNullOrBlank
-
-class CreateFindFilterFragment(
- val dataType: DataType,
- private var currFindFilter: StashFindFilter,
-) : CreateFilterGuidedStepFragment() {
- override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance =
- GuidanceStylist.Guidance(
- getString(R.string.sort_by),
- "",
- null,
- ContextCompat.getDrawable(requireContext(), R.drawable.stash_logo),
- )
-
- override fun onCreateActions(
- actions: MutableList,
- savedInstanceState: Bundle?,
- ) {
- val sortOptions =
- dataType.sortOptions
- .mapIndexed { index, sortOption ->
- GuidedAction
- .Builder(requireContext())
- .id(SORT_OFFSET + index)
- .hasNext(false)
- .title(sortOption.getString(requireContext()))
- .build()
- }.sortedBy { it.title.toString() }
-
- val sortDesc = currFindFilter.sortAndDirection?.sort?.getString(requireContext())
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(SORT)
- .hasNext(true)
- .subActions(sortOptions)
- .title(getString(R.string.sort_by))
- .description(sortDesc)
- .build(),
- )
-
- val directionOptions =
- buildList {
- add(
- GuidedAction
- .Builder(requireContext())
- .id(DIRECTION_OFFSET + SortDirectionEnum.ASC.ordinal)
- .hasNext(false)
- .title(getString(R.string.stashapp_ascending))
- .build(),
- )
- add(
- GuidedAction
- .Builder(requireContext())
- .id(DIRECTION_OFFSET + SortDirectionEnum.DESC.ordinal)
- .hasNext(false)
- .title(getString(R.string.stashapp_descending))
- .build(),
- )
- }
-
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(DIRECTION)
- .hasNext(true)
- .subActions(directionOptions)
- .title(getString(R.string.stashapp_config_ui_image_wall_direction))
- .description(getDirectionString(currFindFilter.sortAndDirection?.direction))
- .build(),
- )
-
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(QUERY)
- .hasNext(true)
- .title(getString(R.string.stashapp_component_tagger_noun_query))
- .descriptionEditable(true)
- .descriptionEditInputType(InputType.TYPE_CLASS_TEXT)
- .description(currFindFilter.q)
- .build(),
- )
-
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(GuidedAction.ACTION_ID_FINISH)
- .hasNext(true)
- .title(getString(R.string.stashapp_actions_save))
- .build(),
- )
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(GuidedAction.ACTION_ID_CANCEL)
- .hasNext(true)
- .title(getString(R.string.stashapp_actions_cancel))
- .build(),
- )
- }
-
- override fun onSubGuidedActionClicked(action: GuidedAction): Boolean {
- if (action.id >= SORT_OFFSET) {
- val newSort = dataType.sortOptions[(action.id - SORT_OFFSET).toInt()]
- currFindFilter = currFindFilter.withSort(newSort.key)
- findActionById(SORT)!!.description = newSort.getString(requireContext())
- notifyActionChanged(findActionPositionById(SORT))
- } else if (action.id >= DIRECTION_OFFSET) {
- val newDirection = SortDirectionEnum.entries[(action.id - DIRECTION_OFFSET).toInt()]
- currFindFilter = currFindFilter.withDirection(newDirection, dataType)
- findActionById(DIRECTION)!!.description = getDirectionString(newDirection)
- notifyActionChanged(findActionPositionById(DIRECTION))
- }
- return true
- }
-
- override fun onGuidedActionClicked(action: GuidedAction) {
- if (action.id == GuidedAction.ACTION_ID_FINISH) {
- val newQuery = findActionById(QUERY)!!.description?.toString()
- val newValue =
- if (newQuery.isNotNullOrBlank()) {
- currFindFilter.copy(q = newQuery)
- } else {
- currFindFilter
- }
- viewModel.findFilter.value = newValue
- viewModel.updateCount()
- parentFragmentManager.popBackStack()
- } else if (action.id == GuidedAction.ACTION_ID_CANCEL) {
- parentFragmentManager.popBackStack()
- }
- }
-
- private fun getDirectionString(direction: SortDirectionEnum?): String? =
- when (direction) {
- SortDirectionEnum.ASC -> getString(R.string.stashapp_ascending)
- SortDirectionEnum.DESC -> getString(R.string.stashapp_descending)
- else -> null
- }
-
- companion object {
- private const val TAG = "CreateFindFilterFragment"
- private const val DIRECTION = 2L
- private const val SORT = 3L
- private const val QUERY = 4L
-
- private const val DIRECTION_OFFSET = 1_000L
- private const val SORT_OFFSET = 2_000L
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateObjectFilterStep.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/CreateObjectFilterStep.kt
deleted file mode 100644
index 2d55ade08..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/CreateObjectFilterStep.kt
+++ /dev/null
@@ -1,228 +0,0 @@
-package com.github.damontecres.stashapp.filter
-
-import android.os.Bundle
-import android.view.View
-import androidx.annotation.StringRes
-import androidx.core.content.ContextCompat
-import androidx.leanback.widget.GuidanceStylist
-import androidx.leanback.widget.GuidedAction
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.api.type.CircumcisionCriterionInput
-import com.github.damontecres.stashapp.api.type.DateCriterionInput
-import com.github.damontecres.stashapp.api.type.FloatCriterionInput
-import com.github.damontecres.stashapp.api.type.GenderCriterionInput
-import com.github.damontecres.stashapp.api.type.HierarchicalMultiCriterionInput
-import com.github.damontecres.stashapp.api.type.IntCriterionInput
-import com.github.damontecres.stashapp.api.type.MultiCriterionInput
-import com.github.damontecres.stashapp.api.type.OrientationCriterionInput
-import com.github.damontecres.stashapp.api.type.ResolutionCriterionInput
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.api.type.StringCriterionInput
-import com.github.damontecres.stashapp.filter.picker.BooleanPickerFragment
-import com.github.damontecres.stashapp.filter.picker.CircumcisionPickerFragment
-import com.github.damontecres.stashapp.filter.picker.DatePickerFragment
-import com.github.damontecres.stashapp.filter.picker.DurationPickerFragment
-import com.github.damontecres.stashapp.filter.picker.FloatPickerFragment
-import com.github.damontecres.stashapp.filter.picker.GenderPickerFragment
-import com.github.damontecres.stashapp.filter.picker.HierarchicalMultiCriterionFragment
-import com.github.damontecres.stashapp.filter.picker.IntPickerFragment
-import com.github.damontecres.stashapp.filter.picker.MultiCriterionFragment
-import com.github.damontecres.stashapp.filter.picker.OrientationPickerFragment
-import com.github.damontecres.stashapp.filter.picker.RatingPickerFragment
-import com.github.damontecres.stashapp.filter.picker.ResolutionPickerFragment
-import com.github.damontecres.stashapp.filter.picker.StringPickerFragment
-import com.github.damontecres.stashapp.views.formatNumber
-
-class CreateObjectFilterStep : CreateFilterGuidedStepFragment() {
- private fun createActionList(): List {
- val dataType = viewModel.dataType.value!!
- return getFilterOptions(dataType)
- .mapIndexed { index, filterOption ->
- filterOption as FilterOption
- val value = viewModel.getValue(filterOption)
- val description =
- if (value != null) {
- filterSummary(filterOption.name, dataType, value, viewModel::lookupIds)
- } else {
- null
- }
- createAction(index, filterOption.nameStringId, description)
- }.sortedBy { it.title.toString() }
- }
-
- override fun onResume() {
- super.onResume()
- actions = createActionList()
- viewModel.updateCount()
- }
-
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- viewModel.resultCount.observe(viewLifecycleOwner) { count ->
- val countStr = formatNumber(count, viewModel.abbreviateCounters)
- if (count >= 0) {
- findButtonActionById(SUBMIT)!!.description = "$countStr results"
- notifyButtonActionChanged(findButtonActionPositionById(SUBMIT))
- } else {
- findButtonActionById(SUBMIT)!!.description = "Querying..."
- notifyButtonActionChanged(findButtonActionPositionById(SUBMIT))
- }
- }
- }
-
- override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance {
- val text =
- filterSummary(
- requireContext(),
- viewModel.dataType.value!!,
- viewModel.dataType.value!!.filterType,
- viewModel.objectFilter.value!!,
- viewModel::lookupIds,
- )
- return GuidanceStylist.Guidance(
- "Create filter",
- text.joinToString("\n"),
- null,
- ContextCompat.getDrawable(requireContext(), R.drawable.stash_logo),
- )
- }
-
- override fun onCreateActions(
- actions: MutableList,
- savedInstanceState: Bundle?,
- ) {
- actions.addAll(createActionList())
- }
-
- override fun onCreateButtonActions(
- actions: MutableList,
- savedInstanceState: Bundle?,
- ) {
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(SUBMIT)
- .hasNext(true)
- .title(getString(R.string.stashapp_actions_continue))
- .build(),
- )
- }
-
- private fun createAction(
- index: Int,
- @StringRes nameId: Int,
- description: String?,
- ): GuidedAction =
- GuidedAction
- .Builder(requireContext())
- .id(index.toLong())
- .hasNext(true)
- .title(nameId)
- .description(description)
- .build()
-
- override fun onGuidedActionClicked(action: GuidedAction) {
- val dataType = viewModel.dataType.value!!
-
- if (action.id == SUBMIT) {
- parentFragmentManager.popBackStack()
- } else {
- val filterOption = getFilterOptions(dataType)[action.id.toInt()]
- when (filterOption.nameStringId) {
- // Rating needs a special picker for its sub-filter type
- R.string.stashapp_rating -> {
- filterOption as FilterOption
- nextStep(RatingPickerFragment(filterOption))
- }
-
- R.string.stashapp_duration -> {
- filterOption as FilterOption
- nextStep(DurationPickerFragment(filterOption))
- }
-
- else -> {
- // Get the picker for the sub-filter type
- when (filterOption.type) {
- IntCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(IntPickerFragment(filterOption))
- }
-
- FloatCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(FloatPickerFragment(filterOption))
- }
-
- Boolean::class -> {
- filterOption as FilterOption
- nextStep(BooleanPickerFragment(filterOption))
- }
-
- StringCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(StringPickerFragment(filterOption))
- }
-
- DateCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(DatePickerFragment(filterOption))
- }
-
- MultiCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(
- MultiCriterionFragment(
- filterOption.dataType!!,
- filterOption,
- ),
- )
- }
-
- HierarchicalMultiCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(
- HierarchicalMultiCriterionFragment(
- filterOption.dataType!!,
- filterOption,
- ),
- )
- }
-
- GenderCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(GenderPickerFragment(filterOption))
- }
-
- ResolutionCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(ResolutionPickerFragment(filterOption))
- }
-
- OrientationCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(OrientationPickerFragment(filterOption))
- }
-
- CircumcisionCriterionInput::class -> {
- filterOption as FilterOption
- nextStep(CircumcisionPickerFragment(filterOption))
- }
-
- else -> {
- throw UnsupportedOperationException("$filterOption")
- }
- }
- }
- }
- }
- }
-
- companion object {
- private val TAG = CreateObjectFilterStep::class.simpleName
-
- private const val SUBMIT = GuidedAction.ACTION_ID_OK
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/DescriptionExtractors.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/DescriptionExtractors.kt
index a2b9c4cc8..fd20d6528 100644
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/DescriptionExtractors.kt
+++ b/app/src/main/java/com/github/damontecres/stashapp/filter/DescriptionExtractors.kt
@@ -39,7 +39,7 @@ import com.github.damontecres.stashapp.data.DataType
import com.github.damontecres.stashapp.data.StashFindFilter
import com.github.damontecres.stashapp.filter.output.FilterWriter
import com.github.damontecres.stashapp.filter.output.getAllIds
-import com.github.damontecres.stashapp.util.StashServer
+import com.github.damontecres.stashapp.ui.ComposeUiConfig
import com.github.damontecres.stashapp.util.joinNotNullOrBlank
import com.github.damontecres.stashapp.util.name
import com.github.damontecres.stashapp.util.titleOrFilename
@@ -255,9 +255,11 @@ fun filterSummary(f: StringCriterionInput): String {
}
}
-fun filterSummaryRating(f: IntCriterionInput): String {
+fun filterSummaryRating(
+ f: IntCriterionInput,
+ ratingsAsStars: Boolean,
+): String {
val context = StashApplication.getApplication()
- val ratingsAsStars = StashServer.requireCurrentServer().serverPreferences.ratingsAsStars
val modStr = f.modifier.getString(context)
val value = f.value
val value2 = f.value2.getOrNull()
@@ -530,10 +532,14 @@ fun filterSummary(
name: String,
filterDataType: DataType,
value: Any,
+ uiConfig: ComposeUiConfig,
idLookup: (DataType, List) -> Map,
): String =
if (name == "rating100") {
- filterSummaryRating(value as IntCriterionInput)
+ filterSummaryRating(
+ value as IntCriterionInput,
+ uiConfig.ratingAsStars,
+ )
} else if (name == "duration") {
filterSummaryDuration(value as IntCriterionInput)
} else {
@@ -619,6 +625,7 @@ fun filterSummary(
dataType: DataType,
type: KClass,
f: StashDataFilter,
+ uiConfig: ComposeUiConfig,
idLookup: (DataType, List) -> Map,
): List {
val filterOptionNames = FilterOptions[dataType]!!.associateBy { it.name }
@@ -629,7 +636,7 @@ fun filterSummary(
val value = obj.getOrNull()
if (value != null) {
val nameStringId = filterOptionNames[param.name]?.nameStringId
- val valueStr = filterSummary(param.name, dataType, value, idLookup)
+ val valueStr = filterSummary(param.name, dataType, value, uiConfig, idLookup)
val key =
if (nameStringId != null) {
context.getString(nameStringId)
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/StashGuidedActionsStylist.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/StashGuidedActionsStylist.kt
deleted file mode 100644
index 7c071b22d..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/StashGuidedActionsStylist.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.github.damontecres.stashapp.filter
-
-import androidx.leanback.widget.GuidedAction
-import androidx.leanback.widget.GuidedActionsStylist
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.filter.picker.GuidedDurationPickerAction
-import com.github.damontecres.stashapp.views.DurationPicker
-
-/**
- * Style the [GuidedAction]s for creating a filter. Adds support for additional pickers
- */
-class StashGuidedActionsStylist : GuidedActionsStylist() {
- override fun onProvideItemLayoutId(viewType: Int): Int {
- return if (viewType == DURATION_VIEW_TYPE) {
- R.layout.duration_guided_action
- } else {
- return super.onProvideItemLayoutId(viewType)
- }
- }
-
- override fun getItemViewType(action: GuidedAction): Int {
- return if (action is GuidedDurationPickerAction) {
- DURATION_VIEW_TYPE
- } else {
- return super.getItemViewType(action)
- }
- }
-
- override fun onBindActivatorView(
- vh: ViewHolder,
- action: GuidedAction,
- ) {
- if (action is GuidedDurationPickerAction) {
- val picker = vh.itemView.findViewById(R.id.guidedactions_activator_item)
- picker.duration = action.duration
- } else {
- super.onBindActivatorView(vh, action)
- }
- }
-
- override fun onUpdateActivatorView(
- vh: ViewHolder,
- action: GuidedAction,
- ): Boolean =
- if (action is GuidedDurationPickerAction) {
- val picker = vh.itemView.findViewById(R.id.guidedactions_activator_item)
- if (action.duration != picker.duration) {
- action.duration = picker.duration
- true
- } else {
- false
- }
- } else {
- super.onUpdateActivatorView(vh, action)
- }
-
- companion object {
- const val DURATION_VIEW_TYPE = 1000
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/BooleanPickerFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/picker/BooleanPickerFragment.kt
deleted file mode 100644
index 30545d928..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/BooleanPickerFragment.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.github.damontecres.stashapp.filter.picker
-
-import android.os.Bundle
-import androidx.core.content.ContextCompat
-import androidx.leanback.widget.GuidanceStylist
-import androidx.leanback.widget.GuidedAction
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.filter.CreateFilterGuidedStepFragment
-import com.github.damontecres.stashapp.filter.FilterOption
-
-/**
- * Select a boolean for a filter or remove it altogether
- */
-class BooleanPickerFragment(
- private val filterOption: FilterOption,
-) : CreateFilterGuidedStepFragment() {
- override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance =
- GuidanceStylist.Guidance(
- getString(filterOption.nameStringId),
- "",
- null,
- ContextCompat.getDrawable(requireContext(), R.drawable.stash_logo),
- )
-
- override fun onCreateActions(
- actions: MutableList,
- savedInstanceState: Bundle?,
- ) {
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(0L)
- .hasNext(true)
- .title(getString(R.string.stashapp_true))
- .build(),
- )
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(1L)
- .hasNext(true)
- .title(getString(R.string.stashapp_false))
- .build(),
- )
- if (viewModel.getValue(filterOption) != null) {
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(2L)
- .hasNext(true)
- .title(getString(R.string.stashapp_actions_remove))
- .build(),
- )
- }
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(GuidedAction.ACTION_ID_CANCEL)
- .hasNext(true)
- .title(getString(R.string.stashapp_actions_cancel))
- .build(),
- )
- }
-
- override fun onGuidedActionClicked(action: GuidedAction) {
- if (action.id != GuidedAction.ACTION_ID_CANCEL) {
- val newValue =
- when (action.id) {
- 0L -> true
- 1L -> false
- else -> null
- }
- viewModel.updateFilter(filterOption, newValue)
- }
- parentFragmentManager.popBackStack()
- }
-
- companion object {
- private const val TAG = "BooleanPickerFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/CircumcisionPickerFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/picker/CircumcisionPickerFragment.kt
deleted file mode 100644
index 61a8fd479..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/CircumcisionPickerFragment.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package com.github.damontecres.stashapp.filter.picker
-
-import android.os.Bundle
-import androidx.core.content.ContextCompat
-import androidx.leanback.widget.GuidanceStylist
-import androidx.leanback.widget.GuidedAction
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.api.type.CircumcisedEnum
-import com.github.damontecres.stashapp.api.type.CircumcisionCriterionInput
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.filter.CreateFilterGuidedStepFragment
-import com.github.damontecres.stashapp.filter.FilterOption
-import com.github.damontecres.stashapp.views.getString
-
-class CircumcisionPickerFragment(
- val filterOption: FilterOption,
-) : CreateFilterGuidedStepFragment() {
- private var currentModifier = CriterionModifier.INCLUDES
-
- override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance =
- GuidanceStylist.Guidance(
- getString(filterOption.nameStringId),
- null,
- null,
- ContextCompat.getDrawable(requireContext(), R.drawable.stash_logo),
- )
-
- override fun onCreateActions(
- actions: MutableList,
- savedInstanceState: Bundle?,
- ) {
- val curVal = viewModel.getValue(filterOption)
- val values = curVal?.value?.getOrNull().orEmpty()
-
- if (curVal?.modifier != null) {
- currentModifier = curVal.modifier
- }
-
- val modifierOptions =
- buildList {
- add(modifierAction(CriterionModifier.INCLUDES))
- add(modifierAction(CriterionModifier.EXCLUDES))
- add(modifierAction(CriterionModifier.IS_NULL))
- add(modifierAction(CriterionModifier.NOT_NULL))
- }
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(MODIFIER)
- .hasNext(false)
- .title("Modifier")
- .description(currentModifier.getString(requireContext()))
- .subActions(modifierOptions)
- .build(),
- )
-
- CircumcisedEnum.entries
- .mapIndexedNotNull { index, circ ->
- if (circ != CircumcisedEnum.UNKNOWN__) {
- val name =
- when (circ) {
- CircumcisedEnum.CUT -> getString(R.string.stashapp_circumcised_types_CUT)
- CircumcisedEnum.UNCUT -> getString(R.string.stashapp_circumcised_types_UNCUT)
- CircumcisedEnum.UNKNOWN__ -> "Unknown"
- }
- val action =
- GuidedAction
- .Builder(requireContext())
- .id(CIRC_OFFSET + index)
- .hasNext(false)
- .title(name)
- .checked(values.contains(circ))
- .checkSetId(GuidedAction.CHECKBOX_CHECK_SET_ID)
- .build()
- Pair(name, action)
- } else {
- null
- }
- }.sortedBy { it.first }
- .forEach { actions.add(it.second) }
-
- addStandardActions(actions, filterOption)
- }
-
- override fun onSubGuidedActionClicked(action: GuidedAction): Boolean {
- if (action.id >= MODIFIER_OFFSET) {
- currentModifier = CriterionModifier.entries[(action.id - MODIFIER_OFFSET).toInt()]
- findActionById(MODIFIER)!!.description = currentModifier.getString(requireContext())
- notifyActionChanged(findActionPositionById(MODIFIER))
- setFinish()
- }
- return true
- }
-
- override fun onGuidedActionClicked(action: GuidedAction) {
- if (action.id == GuidedAction.ACTION_ID_FINISH) {
- val values = getValues()
- val newFilter =
- CircumcisionCriterionInput(
- value = Optional.presentIfNotNull(values),
- modifier = currentModifier,
- )
- viewModel.updateFilter(filterOption, newFilter)
- parentFragmentManager.popBackStack()
- } else if (action.id in CIRC_OFFSET..? =
- actions
- .filter { it.id >= CIRC_OFFSET && it.isChecked }
- .map { CircumcisedEnum.entries[(it.id - CIRC_OFFSET).toInt()] }
- .ifEmpty { null }
-
- private fun setFinish() {
- if (currentModifier.isNullModifier() || getValues() != null) {
- enableFinish(true)
- } else {
- enableFinish(false)
- }
- }
-
- companion object {
- private const val TAG = "HierarchicalMultiCriterionFragment"
-
- private const val MODIFIER = 1L
-
- private const val CIRC_OFFSET = 1_000_000L
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/DatePickerFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/picker/DatePickerFragment.kt
deleted file mode 100644
index b534702c8..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/DatePickerFragment.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-package com.github.damontecres.stashapp.filter.picker
-
-import android.os.Bundle
-import android.util.Log
-import androidx.core.content.ContextCompat
-import androidx.leanback.widget.GuidanceStylist
-import androidx.leanback.widget.GuidedAction
-import androidx.leanback.widget.GuidedDatePickerAction
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.DateCriterionInput
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.filter.FilterOption
-import com.github.damontecres.stashapp.views.getString
-import java.text.ParseException
-import java.text.SimpleDateFormat
-import java.util.Calendar
-import java.util.Date
-import java.util.Locale
-
-/**
- * Pick a date value
- */
-class DatePickerFragment(
- filterOption: FilterOption,
-) : TwoValuePicker(filterOption) {
- private val format = SimpleDateFormat("yyyy-MM-dd", Locale.US)
- private val defaultDate =
- if (filterOption.nameStringId == R.string.stashapp_birthdate) {
- val cal = Calendar.getInstance()
- cal.time = Date()
- cal.add(Calendar.YEAR, -18)
- cal.time
- } else {
- Date()
- }
-
- override val modifierOptions: List
- get() =
- super.modifierOptions +
- listOf(
- CriterionModifier.IS_NULL,
- CriterionModifier.NOT_NULL,
- )
-
- override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance =
- GuidanceStylist.Guidance(
- getString(filterOption.nameStringId),
- "",
- null,
- ContextCompat.getDrawable(requireContext(), R.drawable.stash_logo),
- )
-
- override fun onCreateActions(
- actions: MutableList,
- savedInstanceState: Bundle?,
- ) {
- val curVal = filterOption.getter(viewModel.objectFilter.value!!).getOrNull()
- value1 = curVal?.value
- value2 = curVal?.value2?.getOrNull()
- modifier = curVal?.modifier ?: CriterionModifier.EQUALS
- createActionList(actions)
- }
-
- override fun createActionList(actions: MutableList): List {
- Log.v(TAG, "createActionList: actions.size=${actions.size}")
- val dateLong =
- if (value1 != null) {
- try {
- format.parse(value1!!) ?: defaultDate
- } catch (ex: ParseException) {
- Log.w(TAG, "Parse error ($value1)", ex)
- defaultDate
- }
- } else {
- defaultDate
- }.time
-
- val dateLong2 =
- if (value2 != null) {
- try {
- format.parse(value2!!) ?: defaultDate
- } catch (ex: ParseException) {
- Log.w(TAG, "Parse error ($value2)", ex)
- defaultDate
- }
- } else {
- defaultDate
- }.time
-
- val modifierOptions = this.modifierOptions.map(::modifierAction)
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(MODIFIER)
- .hasNext(false)
- .title("Modifier")
- .description(modifier.getString(requireContext()))
- .subActions(modifierOptions)
- .build(),
- )
-
- val valueText =
- if (modifier.hasTwoValues()) {
- getString(R.string.stashapp_criterion_greater_than)
- } else {
- getString(R.string.stashapp_criterion_value)
- }
- actions.add(
- GuidedDatePickerAction
- .Builder(requireContext())
- .id(VALUE_1)
- .hasNext(true)
- .title(valueText)
- .date(dateLong)
- .build(),
- )
-
- if (modifier.hasTwoValues()) {
- actions.add(
- GuidedDatePickerAction
- .Builder(requireContext())
- .id(VALUE_2)
- .hasNext(true)
- .title(getString(R.string.stashapp_criterion_less_than))
- .date(dateLong2)
- .build(),
- )
- }
-
- addStandardActions(actions, filterOption)
-
- return actions
- }
-
- override fun parseAction(action: GuidedAction?): String? {
- if (action is GuidedDatePickerAction) {
- return format.format(action.date)
- }
- return null
- }
-
- override fun createCriterionInput(
- value1: String?,
- value2: String?,
- modifier: CriterionModifier,
- ): DateCriterionInput? =
- if (value1 != null) {
- DateCriterionInput(
- value = value1,
- value2 = Optional.presentIfNotNull(value2),
- modifier = modifier,
- )
- } else if (modifier.isNullModifier()) {
- DateCriterionInput(value = "", modifier = modifier)
- } else {
- null
- }
-
- override val valueInputType: Int
- get() = throw IllegalStateException("Should not call valueInputType")
-
- override fun parseValue(v: String?): String = throw IllegalStateException("Should not call parseValue")
-
- companion object {
- private const val TAG = "DatePickerFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/DurationPickerFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/picker/DurationPickerFragment.kt
deleted file mode 100644
index 1aa73fdb4..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/DurationPickerFragment.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-package com.github.damontecres.stashapp.filter.picker
-
-import android.os.Bundle
-import android.util.Log
-import androidx.core.content.ContextCompat
-import androidx.leanback.widget.GuidanceStylist
-import androidx.leanback.widget.GuidedAction
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.R
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.IntCriterionInput
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.filter.FilterOption
-import com.github.damontecres.stashapp.views.getString
-
-/**
- * Pick a duration value with [com.github.damontecres.stashapp.views.DurationPicker]
- */
-class DurationPickerFragment(
- filterOption: FilterOption,
-) : TwoValuePicker(filterOption) {
- override fun onCreateGuidance(savedInstanceState: Bundle?): GuidanceStylist.Guidance =
- GuidanceStylist.Guidance(
- getString(filterOption.nameStringId),
- "",
- null,
- ContextCompat.getDrawable(requireContext(), R.drawable.stash_logo),
- )
-
- override fun onCreateActions(
- actions: MutableList,
- savedInstanceState: Bundle?,
- ) {
- val curVal = filterOption.getter(viewModel.objectFilter.value!!).getOrNull()
- value1 = curVal?.value
- value2 = curVal?.value2?.getOrNull()
- modifier = curVal?.modifier ?: CriterionModifier.EQUALS
- createActionList(actions)
- }
-
- override fun createActionList(actions: MutableList): List {
- Log.v(TAG, "createActionList: actions.size=${actions.size}")
-
- val modifierOptions = this.modifierOptions.map(::modifierAction)
- actions.add(
- GuidedAction
- .Builder(requireContext())
- .id(MODIFIER)
- .hasNext(false)
- .title("Modifier")
- .description(modifier.getString(requireContext()))
- .subActions(modifierOptions)
- .build(),
- )
-
- val valueText =
- if (modifier.hasTwoValues()) {
- getString(R.string.stashapp_criterion_greater_than)
- } else {
- getString(R.string.stashapp_criterion_value)
- }
- actions.add(
- GuidedDurationPickerAction
- .Builder(requireContext())
- .id(VALUE_1)
- .hasNext(true)
- .title(valueText)
- .duration(value1 ?: 0)
- .build(),
- )
-
- if (modifier.hasTwoValues()) {
- actions.add(
- GuidedDurationPickerAction
- .Builder(requireContext())
- .id(VALUE_2)
- .hasNext(true)
- .title(getString(R.string.stashapp_criterion_less_than))
- .duration(value2 ?: 0)
- .build(),
- )
- }
-
- addStandardActions(actions, filterOption)
-
- return actions
- }
-
- override fun parseAction(action: GuidedAction?): Int? {
- if (action is GuidedDurationPickerAction) {
- return action.duration
- }
- return null
- }
-
- override fun createCriterionInput(
- value1: Int?,
- value2: Int?,
- modifier: CriterionModifier,
- ): IntCriterionInput? =
- if (value1 != null) {
- IntCriterionInput(
- value = value1,
- value2 = Optional.presentIfNotNull(value2),
- modifier = modifier,
- )
- } else {
- null
- }
-
- override val valueInputType: Int
- get() = throw IllegalStateException("Should not call valueInputType")
-
- override fun parseValue(v: String?): Int? = v?.toIntOrNull()
-
- companion object {
- private const val TAG = "DatePickerFragment"
- }
-}
diff --git a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/FloatPickerFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/filter/picker/FloatPickerFragment.kt
deleted file mode 100644
index a9c309796..000000000
--- a/app/src/main/java/com/github/damontecres/stashapp/filter/picker/FloatPickerFragment.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.github.damontecres.stashapp.filter.picker
-
-import android.os.Bundle
-import android.text.InputType
-import androidx.leanback.widget.GuidedAction
-import com.apollographql.apollo.api.Optional
-import com.github.damontecres.stashapp.api.type.CriterionModifier
-import com.github.damontecres.stashapp.api.type.FloatCriterionInput
-import com.github.damontecres.stashapp.api.type.StashDataFilter
-import com.github.damontecres.stashapp.filter.FilterOption
-
-/**
- * Pick a decimal/float value
- */
-class FloatPickerFragment(
- filterOption: FilterOption