Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4d2a5ae
Initial koin setup
damontecres May 18, 2026
141f604
Add server prefs & initial compose conversions
damontecres May 18, 2026
a8e3486
Checkpoint
damontecres May 18, 2026
3d7571d
Checkpoint 2
damontecres May 18, 2026
4799f0b
Checkpoint 3
damontecres May 18, 2026
3aa4a09
Checkpoint 4
damontecres May 18, 2026
e7165ee
Checkpoint player
damontecres May 18, 2026
3499a39
WIP remove leanback/old UI
damontecres May 19, 2026
8072f30
Checkpoint app launches
damontecres May 19, 2026
8d55a59
Various fixes
damontecres May 19, 2026
03da1a7
Remove unused dependencies
damontecres May 19, 2026
0ac3148
Add back pin protection
damontecres May 19, 2026
29ecb0d
Fix adding server w/ auth
damontecres May 19, 2026
d2290d8
Add license & attribution info back
damontecres May 19, 2026
80be557
Add settings/read-only PIN back
damontecres May 19, 2026
c754bf6
Todos
damontecres May 19, 2026
ae87243
Fix compile issues
damontecres May 19, 2026
8df2714
Fix test compile issues
damontecres May 19, 2026
00b6a6b
Merge branch 'main' into develop/di
damontecres Jul 13, 2026
a5e93b4
Better focus on grid by lifting focus requester
damontecres Jul 13, 2026
518a9b1
Refactor cards into individual components
damontecres Jul 26, 2026
7a5458d
Use better modal nav drawer
damontecres Jul 26, 2026
f5125f6
Add go to studio from scene more menu
damontecres Jul 26, 2026
60b3e1f
Improvements to home page, header for all data types
damontecres Jul 26, 2026
c163cdc
Downgrade tv-material
damontecres Jul 26, 2026
29513eb
Merge branch 'main' into develop/di
damontecres Jul 26, 2026
0aa03d6
Remove unused versions
damontecres Jul 26, 2026
a61fb8e
Update playback overlay w/ better animation & show studio logo
damontecres Jul 26, 2026
23a00c3
Fix a few bugs & animations
damontecres Jul 27, 2026
a0f6597
Get download url by ABI
damontecres Jul 27, 2026
ebc6ef3
Merge branch 'main' into develop/di
damontecres Jul 28, 2026
30ac891
Better destinations for setup & pin
damontecres Jul 29, 2026
2f2d2b7
Pin improvements
damontecres Jul 29, 2026
96a7fa5
Fix issues with nav drawer scrolling & focus
damontecres Jul 29, 2026
25d93f8
Fix marker card previews
damontecres Jul 30, 2026
1019c09
Adjustments to padding on scene details
damontecres Jul 30, 2026
682ed80
Update manifest
damontecres Aug 3, 2026
a245232
Update developer.md
damontecres Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
29 changes: 13 additions & 16 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -32,18 +41,14 @@ 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
- Don't over-fetch data
- 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<DestinationClassName>()`).

### 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).
Expand All @@ -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.
Expand Down
36 changes: 20 additions & 16 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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"))
Expand Down
32 changes: 16 additions & 16 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<activity
android:name=".RootActivity"
android:name=".MainActivity"
android:banner="@mipmap/ic_banner"
android:configChanges="keyboard|keyboardHidden|navigation|orientation|screenSize|screenLayout|smallestScreenSize"
android:exported="true"
android:icon="@mipmap/ic_launcher"
android:logo="@mipmap/ic_launcher"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|screenLayout">
android:launchMode="singleTask"
android:logo="@mipmap/ic_launcher">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
Expand All @@ -57,23 +59,21 @@
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>

<!-- <receiver-->
<!-- android:name=".UpdateBroadcastReceiver"-->
<!-- android:exported="true">-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />-->
<!-- </intent-filter>-->
<!-- </receiver>-->

</provider> <!-- <receiver -->
<!-- android:name=".UpdateBroadcastReceiver" -->
<!-- android:exported="true"> -->
<!-- <intent-filter> -->
<!-- <action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> -->
<!-- </intent-filter> -->
<!-- </receiver> -->
<service
android:name=".util.StashDreamService"
android:exported="false"
android:label="@string/app_name"
android:permission="android.permission.BIND_DREAM_SERVICE">
<intent-filter>
<action android:name="android.service.dreams.DreamService" />

<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
Expand Down
96 changes: 0 additions & 96 deletions app/src/main/assets/licenses/glide.txt

This file was deleted.

4 changes: 4 additions & 0 deletions app/src/main/graphql/FindSceneMarkers.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ fragment VideoSceneData on Scene {
captions {
...Caption
}
studio {
id
image_path
}
}

fragment MarkerData on SceneMarker {
Expand Down
Loading
Loading