diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 934642e85..e98a1b0a7 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -6,9 +6,18 @@ See also the [Contributing](CONTRIBUTING.md) guide for general information on co This project is an Android TV client for Stash. It is written in Kotlin and uses the Apollo GraphQL client to interact with the server. -The app uses Android's [JetPack Compose](https://developer.android.com/jetpack/compose) for the UI, but it also has a legacy UI that uses Android's [Leanback](https://developer.android.com/training/tv/playback/leanback/) library. There are custom implementations for paging and navigation. +### Tech stack -The app uses a single Activity (`RootActivity`). On the legacy UI, each page is a `Fragment` that may contain other fragments. The app generally uses a MVVM architecture. +* [JetPack Compose](https://developer.android.com/jetpack/compose) for the UI +* [Koin](https://insert-koin.io/) for dependency injection +* [Navigation 3](https://developer.android.com/guide/navigation/navigation-3) for displaying app pages +* [Room](https://developer.android.com/training/data-storage/room) & [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) for local data storage +* [Media3/ExoPlayer](https://developer.android.com/media/media3/exoplayer) for media playback +* [MPV/libmpv](https://github.com/mpv-player/mpv) for media playback +* [Coil](https://coil-kt.github.io/coil/) for image loading +* [OkHttp](https://square.github.io/okhttp/) for HTTP requests + +There is a single Activity, `MainActivity`. The app generally uses a MVVM architecture. ### Modules @@ -32,7 +41,7 @@ The app uses [Apollo Kotlin](https://www.apollographql.com/docs/kotlin) to autom All interactions with the server are done via GraphQL using one of the `StashEngine` subclasses: `QueryEngine`, `MutationEngine`, or `SubscriptionEngine`. Each query/mutation/subscription roughly translates to a function in one of those classes. -If lower level network access is needed, the `StashServer` has fields for an `ApolloClient` for graphql and an `OkHttpClient` for other requests. `StashClient` can used to create new clients if further customization is needed. +If lower level network access is needed, the `StashApi` has an `ApolloClient` for graphql. For general HTTP requests an `OkHttpClient` (with or without authentication) can be injected. Tips: - Install the Apollo plugin in Android Studio @@ -40,10 +49,6 @@ Tips: - Use or create "slim" versions of types when possible - Avoid fetching counts or getting lists unless necessary for the UI -### Navigation - -Navigating the app is handled by the `NavigationManager` class. This class takes a `Destination` object and creates/displays the appropriate content. Destinations can have arguments that are passed to the destination's `Composable` or `Fragment` (retrieved via `requireArguments().getDestination()`). - ### Paging Paging is handled by wrapping a `StashPagingSource` in a `ComposePager` or `PagingObjectAdapter` (for paging items in the UI) or `StashSparseFilterFetcher` (for fetching arbitrary indexes not necessarily for the UI). @@ -64,20 +69,12 @@ In order to support both TV and phone/tablet UIs, there are a few compatibility The UI is contained in the `com.github.damontecres.stashapp.ui` package. -#### Legacy UI - -The UI is built using Leanback's `Presenters` to create cards or "full width" displays. - -`StashPresenter` is main abstract class for card presenters and implementations are responsible for binding data to views. The `StashPresenter.defaultClassPresenterSelector()` function returns a `Presenter` that can used for most types of data displayed as Cards. - ### Playback -The app uses [media3's `ExoPlayer`](https://github.com/androidx/media) for playback. The composbles for playback are in the `com.github.damontecres.stashapp.ui.components.playback` package with most logic in `PlaybackPageContent` and `PlaybackViewModel`. +The app uses [media3's `ExoPlayer`](https://github.com/androidx/media) for playback. The composables for playback are in the `com.github.damontecres.stashapp.ui.components.playback` package with most logic in `PlaybackPageContent` and `PlaybackViewModel`. `CodecSupport` determines which codecs are supported by the device. `StreamUtils` uses that information to determine if the stream from the server can be directly played or if it needs to be transcoded. -In the legacy UI, the `PlaybackFragment` abstract class handles most of the playback logic. - #### Extensions There are several native components for extra playback compatibility. This includes Media3 ffmpeg/av1 decoders and `libmpv`. These extensions are not required to build the app, but without them some functionality will not work. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a2ef1fb91..6a3efe7e6 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..d6154e458 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:launchMode="singleTask" + 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